sequence

Guest love code 3.0 began to develop fast for a whole year has now passed, although I put in the time only four months, but after the initial end almost only I a person in cases, can say something is also studied, wade the muddy water two or three times, back and forth to turn five or six times structure and also a lot of time on tenterhooks, Of course, I’m not gonna end up with anything like 90. But, these are not important, the matter flick clothes to go, deep work (xin) and (acid). Now looking back, just a brief record of some of the exploration process at that time, when the experience of drawing an ellipsis…

broken

Childhood sweethearts

Aicode is a Node application. In ali economy at that time, when it came to the framework of Node application, egg.js was known by everyone. As an important open source product of Ali fame outside, in recent years it is also a trend that comes out on top in the group. So egg.js is certainly our first choice. I had worked with it in both the Turing Project and UTT, and when I met it again, it must have been very easy to put the whole framework together. So I did it immediately. According to the specification of egg.js, I sorted out a code framework and made the first report.

In Egypt

At the first report, the supervisor naturally wanted to raise his head first, so under the supervisor’s instructions, I summed up two points that needed improvement, and knew what the supervisor ultimately wanted: a standardized, but highly scalable service framework. Leaving aside the final thought, let’s see what these two pain points are.

First, egg.js is a framework that is more convention than configuration

Egg follows a “convention over configuration” approach to application development that follows a uniform set of conventions. This approach within the team reduces the cost of learning for developers and allows them to flow instead of being “nails”.

Because of this, there is a constraint on the directory specification in egg.js. A basic egg.js project has the following directory structure:

An egg - project ├ ─ ─ package. Json ├ ─ ─ app. Js (optional) ├ ─ ─ agent. The js (optional) ├ ─ ─ app | ├ ─ ─ the router. The js │ ├ ─ ─ controller │ | └ ─ ─ home. Js │ │ ├ ─ ─ service (optional) | └ ─ ─ the user. The js │ ├ ─ ─ middleware (optional) │ | └ ─ ─ response_time. Js │ ├ ─ ─ the schedule (optional) │ | └ ─ ─ my_task. Js │ │ ├ ─ ─ public (optional) | └ ─ ─ reset. CSS │ ├ ─ ─ the view (optional) │ | └ ─ ─ home. TPL │ └ ─ ─ the extend (optional) │ ├ ─ ─ helper. Js (optional) │ ├ ─ ─ Request. Js (optional) │ ├ ─ ─ the response. The js (optional) │ ├ ─ ─ the context, js (optional) │ ├ ─ ─ application. Js (optional) │ └ ─ ─ agent. The js (optional) ├ ─ ─ the config | ├ ─ ─ plugin. Js | ├ ─ ─ config. The default. The js │ ├ ─ ─ config. Prod. Js | ├ ─ ─ config. The test. The js (optional) | ├ ─ ─ config. Local, js (optional) | └ ─ ─ Config.unittest.js (optional) ├ ─test├ ─ ─ middleware | └ ─ ─ response_time. Test. The js └ ─ ─ controller └ ─ ─ home. Test. JsCopy the code

As you can see, in our code directory app, all code files are grouped by function. For example, all controller code will be in the same directory, and all service code will be in the Service directory. Admittedly, this is a reasonable classification. But sometimes for some development teams, in the case of a large number of modules, the development needs to switch back and forth between the files in different directories, which brings inconvenience to the development, and the same module code dispersion will also lead to the obstacles to read the project. So, can we get egg.js to support a modular directory structure like the one below?

An egg - project ├ ─ ─ package. Json ├ ─ ─ app. Js (optional) ├ ─ ─ agent. The js (optional) ├ ─ ─ the SRC │ ├ ─ ─ the router. The js │ ├ ─ ─ home │ │ ├ ─ ─ │ ├── Home.service. Ts │ ├─ ├─ home.tpl │ ├─ User │ ├── user.controller. Ts │ ├─ user ├ ─ ─ the config | ├ ─ ─ plugin. Js | ├ ─ ─ config. The default. The js │ ├ ─ ─ config. Prod. Js | ├ ─ ─ config. The test. The js (optional) | ├ ─ ─ config. Local. Js (optional) | └ ─ ─ config. The unittest, js (optional)Copy the code

After studying the egg.js document and the source code of egg-core, it is found that it provides ** a way to extend Loader ** by defining the behavior of the loading directory. However, due to the following constraints, if we want to customize Loader, we must build a new framework based on Egg. And then build on that framework.

Egg implements AppWorkerLoader and AgentWorkerLoader based on Loader. The upper-layer framework is based on these two classes to extend the Loader. The extension of Loader can only be carried out in the framework.

So, what we need to do is roughly:

  1. usenpm init egg --type=frameworkCreate a framework
  2. inlib/loaderTo write your own loader
  3. Just specify the framework of the Egg in our project
'use strict';
const fs = require('fs');
const path = require('path');
const egg = require('egg');
const extend = require('extend2');

class AiMakeAppWorkerLoader extends egg.AppWorkerLoader {
  constructor(opt) {
    super(opt);
    this.opt = opt;
  }

  loadControllers() {
    super.loadController({
      directory: [ path.join(this.opt.baseDir, 'src/')].match: '**/*.controller.(js|ts)'.caseStyle: filepath= > {
        return customCamelize(filepath, '.controller'); }}); }loadServices() {
    super.loadService({
      directory: [ path.join(this.opt.baseDir, 'src/')].match: '**/*.service.(js|ts)'.caseStyle: filepath= > {
        return customCamelize(filepath, '.service'); }}); }load() {
    this.loadApplicationExtend();
    this.loadRequestExtend();
    this.loadResponseExtend();
    this.loadContextExtend();
    this.loadHelperExtend();

    this.loadCustomLoader();

    // app > plugin
    this.loadCustomApp();
    // app > plugin
    this.loadServices();
    // app > plugin > core
    this.loadMiddleware();
    // app
    this.loadControllers();
    // app
    this.loadRouter(); // Dependent on controllers}}/ /... Skip the utility code

module.exports = AiMakeAppWorkerLoader;

Copy the code

At this point, we’ve cracked the first pain point. Second, Egg. Js is a javascript-based development framework, but it’s 2019, and TypeScript, as a superset of JavaScript, gives us all the benefits of a strong typing system and provides a more sophisticated implementation of object-oriented programming. There is no reason why we should not choose TypeScript when developing a general service framework. Egg.js, however, does not provide TypeScript support natively. There may be historical reasons for this, but it is unacceptable to us. So, after a bit of searching, I found a way to use TypeScript in egg.js once again, following the thread in this Issue. The specific steps in the link have been very detailed, in fact, the main two points:

  1. When initializing the Egg project, add--type=tsparameter
  2. Use **egg-ts-helper ** during development to help automatically generate d.ts files

You can happily code egg.js in TypeScript.

Finally, I basically solved the two pain points, so I am happy, do not know heaven and earth to run to the second report.

Reproduction of

The second presentation was less relaxing, with a devastating critique of my depth of thinking. It also made me realize that although using a custom Loader can solve my superficial problems, the fundamental constraints still remain. Besides, this method has no flexibility, and users cannot write a new framework based on egg.js in order to adapt our service framework to their own habit of organizing files. In addition, egg.js is inherently disabled in TypeScript support. Even with the use of egg-ts-Helper to write TS code, the support for various third-party libraries is not controlled, and the user is still at great risk.

There is no way, egg.js, each other, as the phase forget in the river’s lake.

Full of beauty, suddenly alone with my eyes into

Since the breakup, I’ve been scouring Github for frameworks, and I’ve found a couple of back-up ones, but none have impressed me. When I was struggling with anxiety, my friends from the Beijing team mentioned it, NestJS. After I took a closer look at the Github page, I felt like I was in control. Well, yes, that’s it!

Now that there is a new love, it must be introduced to everyone, let’s listen to it first:

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, has built-in and full support for TypeScript (but still allows developers to write code in pure JavaScript) and combines elements of OOP (object-oriented programming), FP (functional programming), and FRP (functional responsive programming). Underneath, Nest uses powerful HTTP Server frameworks such as Express (the default) and Fastify. Nest provides a degree of abstraction on top of these frameworks, while exposing its API directly to developers. This makes it easy to use the myriad third-party modules for each platform.

Note that it’s a TypeScript native framework, which means that NestJS, and all the plugins in its ecosystem, are necessarily TypeScript, which solves my second problem in a flash. Is there a solution to the first question? Don’t worry, let me break it down to you.

At first glance, NestJS may seem unfamiliar to all of us, which is quite normal. For those of us in Vue and React technology stack, the way of thinking of NestJS is indeed not so easy to understand. But if you’ve ever worked with AngularJS, it might sound familiar. If you’ve ever been a backend developer and are comfortable with Java and Spring, you might jump up and shout: Spring Boot!

Your intuition is right. NestJS, like AngularJS and Spring, is designed based on Inversion of Control (IoC) principles. Both use DI = Dependency Injection to solve the coupling problem.

What is dependency injection? For a simple example, suppose we have a class Car and a class Engine. We organize the code as follows:

/ / the engine
export class Engine {
  public cylinders = 'Engine engine 1';
}

export class Car {
  public engine: Engine;
  public description = 'No DI';

  constructor() {
    this.engine = new Engine();
  }

  drive() {
    return `The ${this.description} car with ` +
      `The ${this.engine.cylinders} cylinders`; }}/ / sample reference https://juejin.im/post/6844903740953067534
Copy the code

At this point our engine initializes itself in an instance of Car. So suppose one day the engine is upgraded and a new parameter is added to the constructor:

/ / the engine
export class Engine {
  public cylinders = ' ';
  constructor(_cylinders:string) {
    this.cylinders = _cylinders; }}Copy the code

A Car using the engine would have to modify the constructor code in the Car class to accommodate the engine changes. This makes no sense, because cars are supposed to care less about the implementation details of the engine. At this point we say that the Car class relies on Engine.

Let’s say we implement Car using dependency injection:

export class Engine {
  public cylinders = 'Engine engine 1';
}

export class Car {
  public description = 'DI'; 

  // Inject Engine and Tires through constructors
  constructor(public engine: Engine) {}  

  drive() {
    return `The ${this.description} car with ` +
      `The ${this.engine.cylinders} cylinders`; }}Copy the code

The Car class no longer creates the Engine itself. It simply receives and consumes an instance of the Engine. An instance of Engine is injected through the constructor when the Car class is instantiated. The Car and Engine classes are decoupled. If we want to upgrade the Engine class, we only need to make a change in the Car instantiation statement.

export class Engine {
  public cylinders = ' ';
  constructor(_cylinders:string) {
    this.cylinders = _cylinders; }}export class Car {
  public description = 'DI'; 

  // Inject Engine and Tires through constructors
  constructor(public engine: Engine) {}  

  drive() {
    return `The ${this.description} car with ` +
      `The ${this.engine.cylinders} cylinders`; }}main(){
    const car = new Car(new Engine('Engine starter 2'), new Tires1());
    car.drive();
}
Copy the code

This is called dependency injection.

Of course, this is only the simplest example; in practice, class instantiation in NestJS is delegated to the IoC container (that is, the NestJS runtime system). We don’t need to manually inject it every time.

So having said all that, does DEPENDENCY injection have anything to do with our first question? B: of course! We know why egg.js needs to specify the directory structure, because in the Loader code of egg-core, the loading of Controller, Service, Config and so on is done by different load functions looking for the specified directory. So if they are not found in the specified place, egg.js will not be able to get them and mount them under CTX. NestJS, on the other hand, registers its dependencies in the container itself. In other words, NestJS does not need to find the dependencies in the specified location. We just need to inject the required Controller, Service and so on into the module, the module can get them and use them.

// app.controller.ts
import { Controller, Get, Inject } from '@nestjs/common';

@Controller()
export class AppController {
  @Inject('appService')
  private readonly appService;

  @Get()
  getHello(): string {
    return this.appService.getHello(); }}// app.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World! '; }}// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app/app.controller';
import { AppService } from './app/app.service';

@Module({
  imports: [].controllers: [AppController],
  providers: [{
    provide: 'appService'.useClass: AppService,
  }],
})
export class AppModule {}

Copy the code

As you can see above, we can use @inject (‘appService’) to Inject the Service instance directly into the property of app.controller.ts after registering the Service with @Injectable. At this point, we don’t need to care about where app.service.ts is. The directory can be organized as much as we like. The only requirement is to register it in the container. With DEPENDENCY injection, we have the flexibility to inject configurations at development time, and testability is enhanced by decoupling dependencies.

Of course, these are not the only advantages of NestJS. As a framework for the Node side to standard Java Spring, its design philosophy and development constraints can be of great help in larger projects. Also, it has built-in support for microservices, making it easier to scale up for larger projects. Combined with the above advantages, we finally resolutely chose NestJS.

Heaven pays off, this time the director did not beat the mandarin duck, finally finished the selection of this road.

urgent

Suddenly look back, the man is in the lights dim

A long time has passed and the battle between egg.js and NestJS has long been settled. Aike has been in full swing for more than half a year. One evening, an email from Midway sent egg.js the message that he had finally fulfilled his historic mission, and Midway picked up the baton and became the standard framework for the group. When I was doing research, I saw Midway and consulted with the great God in charge. Of course, in the end, I had no choice because I was not familiar with it and I thought that egg.js was the main part of the group. Now that the burden of selection is gone, I took a break to study the current Midway. It’s really a framework that’s up to date.

Midway with native TypeScript support is no longer the bane of egg.js. The numerous plugins compatible with Egg.js also make it easy to use in the development of various scenarios within the group. DI based design, let it in the architecture of a completely different. Even more radically, Midway’s automatic scanning of dependencies eliminates the need to manually register dependencies, which was a surprise to me compared to NestJS.

Midway uses an internal automatic scan mechanism that scans all files before the application is initialized, and files that contain decorators are automatically bound to the container.

Some of our pain points would have been solved by using Midway, and the code would have been much simpler. At this point I can not help but think of hindsight. However, since history has led me to choose NestJS, I’ll stick with it.

// app/controller/user.ts
import { Context, controller, get, inject, provide } from '@ali/midway';

@provide()
@controller('/user')
export class UserController {

  @inject()
  ctx: Context;

  @inject('userService')
  service;

  @get('/:id')
  async getUser(): Promise<void> {
    const id: number = this.ctx.params.id;
    const user = await this.service.getUser({id});
    this.ctx.body = {success: true.message: 'OK'.data: user}; }}// service/user.ts
import { provide } from '@ali/midway';
import { IUserService, IUserOptions, IUserResult } from '.. /interface';

@provide('userService')
export class UserService implements IUserService {

  async getUser(options: IUserOptions): Promise<IUserResult> {
    return {
      id: options.id,
      username: 'mockedName'.phone: '12345678901'.email: '[email protected]'}; }}Copy the code

Wuling people

It’s been almost a year since development, the front end is evolving rapidly, and there’s no right or wrong choice of technology. Far away from Node for more than half a year, I have not known the Wei and Jin dynasties. Record is just record, write for you is a story, write for me is a read. The author’s language cloud: insufficient for external humanity

The article can be reproduced at will, but please keep the original link.

We welcome you to join ES2049 Studio. Please send your resume to [email protected].