Observabily-other-type

Introduction to the

  1. Inversion of control (INVERSION of control) and dependency injection (DI) are common design patterns that have deep application scenarios in both front and back ends

  2. If you’ve been in Angular development before, you’ve used Injectable and Component decorators that provide inversion of control and dependency injection

  3. For node backend, there are also many libraries based on IoC and DI, such as NestJs, InversifyJs and so on

  4. Today, we will talk about the principle and simple implementation of decorators under the dependency injection framework. For those who do not know decorators, see here: ES6 Introduction: Decorators

The introduction of

I. Express development

You’ll often see code like this in Express development. In order to obtain core data to write some code unrelated to business logic, a lot of data, the code will be jumbled

const app = express();

app.use('/users', (req, res) => {
  const id = req.query.id;
  const uid = req.cookies.get('uid');
  const auth = req.header['authorization'];
  // Business logic...res.send(...) ; });Copy the code

Second, nestJS development

With Nest’s powerful decorator, we can write like this. Split routes into a class for more focused and specific maintenance; Route parameters are captured using a decorator, taking the core data directly, which makes the code more readable

@Controller('/users')
export class UserController {
  constructor(private userService: UserService) {}

  @Get('/')
  getUserById(@Query('id') id: string, @Headers('authorization') auth: string) {
    return this.userService.getUserBtyId(id, auth); }}Copy the code

Reflect Metadata

  1. Reflect Metadata is a proposal in ES7 for adding and reading Metadata at declaration time

  2. This is how inversion of control and dependency injection are implemented in versions of Angular 2+

  3. NestJs also incorporates Angular dependency injection ideas in its creation

  4. To use it, please refer to the Reflect Metadata API

  5. Configure in the project:

/ / download
yarn add reflect-metadata

// add to tsconfig.json
"compilerOptions": {
  "types": ["reflect-metadata"."node"]."emitDecoratorMetadata": true,}// index.ts in the root file
import 'reflect-metadata';
Copy the code

Open lu

Two sets of implementation schemes are designed. The first one is to use global variables to record decorative objects for learning. The second set is implemented with Reflect Metadata

Use global variables

Before Reflect Metadata came out, how did dependency injection work in Node

The idea is to maintain a global list, collect dependencies through decorator calls when the Controller is initialized, and intercept and modify the data when the client requests the resource

1) Define the decorator

  1. Simple class decorator

This is simply collecting dependencies and doing nothing, right

export const controllerList: ControllerType[] = [];

export function Controller(path = ' ') :ClassDecorator {
  // target: controller class, not instance
  return (target: object) = > {
    controllerList.push({ path, target });
  };
}
Copy the code
  1. HTTP request decorator

It’s just another layer of encapsulation based on HTTP methods

export type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch';

export const routeList: RouteType[] = [];

export function createMethodDecorator(method: HttpMethod = 'get') {
  return (path = '/') :MethodDecorator= >
    // Target: indicates the current class instance. Name: indicates the current function name. Descriptor: indicates the descriptor of the current attribute (function)
    (target: object, name: string, descriptor: any) => {
      routeList.push({ type: method, target, name, path, func: descriptor.value });
    };
}

/ / use
export const Get = createMethodDecorator('get');
export const Post = createMethodDecorator('post');
Copy the code
  1. Parameter decorator

A layer is encapsulated according to the parameters

export type Param = 'params' | 'query' | 'body' | 'headers' | 'cookies';

export const paramList: ParamType[] = [];

export function createParamDecorator(type: Param) {
  return(key? : string):ParameterDecorator= >
    // target: indicates the current class instance. Name: indicates the current function name. Index: indicates the sequence of the current function parameters
    (target: object, name: string, index: number) => {
      paramList.push({ key, index, type, name });
    };
}

/ / use
export const Query = createParamDecorator('query');
export const Body = createParamDecorator('body');
export const Headers = createParamDecorator('headers');
Copy the code
  1. Type decorator

This type of decorator is an optimization and is used in the same way as decorators such as Query

export type Parse = 'number' | 'string' | 'boolean';

export const parseList: ParseType[] = [];

export function Parse(type: Parse) :ParameterDecorator {
  return (target: object, name: string, index: number) = > {
    parseList.push({ type, index, name });
  };
}
Copy the code

2) Decorator injection (Register)

  1. Three-level traversal injection

Controller traversal, configure all root routes

Route traversal, configure the child routes under the current root route

Param and parse traversal to configure the parameters of the current routing function

const router = express.Router(); // Initialize the route

controllerList.forEach(controller= > {
  const { path: basePath, target: cTarget } = controller;

  routeList
    // Fetch the route under the current root route
    .filter(({ target }) = > target === cTarget.prototype)
    .forEach(route= > {
      const { name: funcName, type, path, func } = route;
      // Handler (res, req, next) => {}
      const handler = handlerFactory(
          func,
          // Takes the list of arguments decorated under the current routing function
          paramList.filter(param= > param.name === funcName),
          parseList.filter(parse= > parse.name === funcName),
        );
      // Configure the Express Router
      router[type](basePath + path, handler);
    });
});

// Put the loaded router into express
app.use('/', router);
Copy the code
  1. Routing handler factory
export function handlerFactory(func: (... args: any[]) => any, paramList: ParamType[], parseList: ParseType[]) { return async (req: Request, res: Response, next: NextFunction) => {try {const args = extractParameters(req, res, next, paramList, parseList); const result = await func(... args); res.send(result); } catch (err) { next(err); }}; }Copy the code
  1. According to the result of reQ processing decoration
export function extractParameters(req: Request, res: Response, next: NextFunction, paramArr: ParamType[] = [], parseArr: ParseType[] = [],) {
  if(! paramArr.length)return [req, res, next];

  const args = [];
  // Perform the third level traversal
  paramArr.forEach(param= > {
    const { key, index, type } = param;
    // Get the corresponding value, such as @query ('id') req.query.id
    switch (type) {
      case 'query':
        args[index] = key ? req.query[key] : req.query;
        break;
      case 'body':
        args[index] = key ? req.body[key] : req.body;
        break;
      case 'headers':
        args[index] = key ? req.headers[key.toLowerCase()] : req.headers;
        break;
      // ...}});// Small optimizations to handle parameter types
  parseArr.forEach(parse= > {
    const { type, index } = parse;
    switch (type) {
      case 'number':
        args[index] = +args[index];
        break;
      case 'string':
        args[index] = args[index] + ' ';
        break;
      case 'boolean':
        args[index] = Boolean(args[index]);
        break; }}); args.push(req, res, next);return args;
}
Copy the code

3) Use decorators

Next is the happy use of time 😏

@Controller('/') The controller / / decoration
export default class Index {
  @Get('/') The route / / decoration
  index(@Parse('number') @Query('id') id: number) { // Decorate parameters
    return { code: 200, id, message: 'success' };
  }

  @Post('/login')
  login(
    @Headers('authorization') auth: string,
    @Body() body: { name: string; password: string },
    @Body('name') name: string,
    @Body('password') psd: string,
  ) {
    console.log(body, auth);
    if(name ! = ='lawler'|| psd ! = ='111111') {
      return { code: 401.message: 'auth failed' };
    }
    return { code: 200.token: 't:111111'.message: 'success'}; }}Copy the code

Second, reflect the metadata

Except for the writing of the code, the idea is exactly the same

1) Define the decorator

export const CONTROLLER_METADATA = 'controller';
export const ROUTE_METADATA = 'method';
export const PARAM_METADATA = 'param';

export function Controller(path = ' ') :ClassDecorator {
  return (target: object) = > {
    Reflect.defineMetadata(CONTROLLER_METADATA, path, target);
  };
}

export function createMethodDecorator(method: HttpMethod = 'get') {
  return (path = '/') :MethodDecorator= >
    (target: object, name: string, descriptor: any) => {
      Reflect.defineMetadata(ROUTE_METADATA, { type: method, path }, descriptor.value);
    };
}

export function createParamDecorator(type: Param) {
  return(key? : string):ParameterDecorator= >
    (target: object, name: string, index: number) => {
      // Notice here that defineMetadata is attached to target.name
      // preMetadata is used to store preMetadata as the next decorator defines the arguments and overwrites them
      const preMetadata =
        Reflect.getMetadata(PARAM_METADATA, target, name) || [];
      const newMetadata = [{ key, index, type }, ...preMetadata];

      Reflect.defineMetadata(PARAM_METADATA, newMetadata, target, name);
    };
}
Copy the code

2) Decorator injection

const router = express.Router();

const controllerStore = {
  index: new IndexController(),
  user: new UserController(),
};

Object.values(controllerStore).forEach(instance= > {
  const controllerMetadata: string = Reflect.getMetadata(CONTROLLER_METADATA, instance.constructor);

  const proto = Object.getPrototypeOf(instance);
  // Get the prototype method for this instance
  const routeNameArr = Object.getOwnPropertyNames(proto).filter(
    n= >n ! = ='constructor' && typeof proto[n] === 'function',); routeNameArr.forEach(routeName= > {
    const routeMetadata: RouteType = Reflect.getMetadata(ROUTE_METADATA, proto[routeName]);
    const { type, path } = routeMetadata;
    const handler = handlerFactory(
        proto[routeName],
        Reflect.getMetadata(PARAM_METADATA, instance, routeName),
        Reflect.getMetadata(PARSE_METADATA, instance, routeName),
      );
    router[type](controllerMetadata + path, handler);
  });
});
Copy the code

test

  1. Demo Source code

  2. How to run: yarn && yarn start

  3. To test the URL, postman is recommended

  • get: http://localhost:8080? id=66666

  • post: http://localhost:8080/login

body: { name: ‘lawler’, password: ‘111111’ }

  • get: http://localhost:8080/users? id=66666

headers: { authorization: ‘t:111111’ }

body: { name: ‘lawler’, password: ‘111111’ }

  • post: http://localhost:8080/users? id=66666

headers: { authorization: ‘t:111111’ }

body: { name: ‘lawler’, password: ‘111111’, gender: 0 }

The resources

  • Understand TypeScript in depth

  • inversify-express-utils

  • The magic of Javascript decorators

Like the remember point ❤️ oh ~