有趣的裝飾器:使用 Reflect Metadata 實踐依賴注入

原文地址:observable-other-typehtml

簡介

  1. 控制反轉和依賴注入是常見一種設計模式,在先後端均有很深的應用場景,不瞭解的小夥伴能夠先看下資料:wiki/設計模式_(計算機)wiki/控制反轉node

  2. 若是以前有過 Angular 開發經歷,那麼確定用過 InjectableComponent 等常見的裝飾器,其做用就是完成控制反轉和依賴注入git

  3. 對於 node 後端,也一樣有不少以 IoCDI 這套思想爲主打的庫,好比:NestJsInversifyJses6

  4. 今天主要聊聊這些依賴注入框架下的裝飾器的使用原理與簡單實現,還不瞭解裝飾器的小夥伴看這裏:ES6 入門:裝飾器github

引入

1、express 開發

express 開發中,常常能看到這樣的代碼。爲獲取核心數據去寫一些與業務邏輯無關的代碼,數據一多的話,代碼就會很冗雜typescript

const app = express();

app.use('/users', (req, res) => {
  const id = req.query.id;
  const uid = req.cookies.get('uid');
  const auth = req.header['authorization'];
  // 業務邏輯...
  res.send(...);
});
複製代碼

2、nestjs 開發

有了 nest 強力的裝飾器,咱們能夠這樣寫。把路由抽成一個類,從而更專一和具體的維護;路由參數使用裝飾器捕獲,直接拿到核心數據,這樣代碼可讀性也變強了express

@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);
  }
}
複製代碼

3、強大的 Reflect Metadata

  1. Reflect Metadata 是 ES7 的一個提案,它主要用來在聲明的時候添加和讀取元數據json

  2. 在 Angular 2+ 的版本中,控制反轉與依賴注入即是基於此實現後端

  3. NestJs 在創做時也是吸取了 Angular 的依賴注入思想設計模式

  4. 使用很簡單,請參考:reflect metadata api

  5. 在項目中配置:

// 下載
yarn add reflect-metadata

// tsconfig.json 中添加
"compilerOptions": {
  "types": ["reflect-metadata", "node"],
  "emitDecoratorMetadata": true,
}

// index.ts 根文件中引入
import 'reflect-metadata';
複製代碼

開擼

設計了兩套實現方案,第一套是利用全局變量記錄裝飾對象完成,供學習使用;第二套是用 reflect metadata 實現

1、使用全局變量

在 reflect metadata 還沒推出以前,node 中的依賴注入是怎麼作的呢

其實就是維護一個全局的 list,經過初始化 controller 時裝飾器的調用進行依賴的收集,在客戶端請求資源時截獲並修改數據

1)定義裝飾器

  1. 簡單的類裝飾器

這裏只是簡單的收集依賴,並不作什麼處理

export const controllerList: ControllerType[] = [];

export function Controller(path = ''): ClassDecorator {
  // target: controller 類,不是實例
  return (target: object) => {
    controllerList.push({ path, target });
  };
}
複製代碼
  1. http 請求裝飾器

根據 http 方法又封裝了一層而已

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

export const routeList: RouteType[] = [];

export function createMethodDecorator(method: HttpMethod = 'get') {
  return (path = '/'): MethodDecorator =>
    // target:當前類實例,name:當前函數名,descriptor:當前屬性(函數)的描述符
    (target: object, name: string, descriptor: any) => {
      routeList.push({ type: method, target, name, path, func: descriptor.value });
    };
}

// 使用
export const Get = createMethodDecorator('get');
export const Post = createMethodDecorator('post');
複製代碼
  1. 參數裝飾器

根據參數封裝了一層

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

export const paramList: ParamType[] = [];

export function createParamDecorator(type: Param) {
  return (key?: string): ParameterDecorator =>
    // target:當前類實例,name:當前函數名,index:當前函數參數順序
    (target: object, name: string, index: number) => {
      paramList.push({ key, index, type, name });
    };
}

// 使用
export const Query = createParamDecorator('query');
export const Body = createParamDecorator('body');
export const Headers = createParamDecorator('headers');
複製代碼
  1. 類型裝飾器

這類裝飾器屬於優化,用法同 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 });
  };
}
複製代碼

2)裝飾器注入(register)

  1. 三層遍歷注入

controller 的遍歷,配置全部根路由

route 的遍歷,配置當前根路由下的子路由

param 和 parse 的遍歷,配置當前路由函數中的各個參數

const router = express.Router(); // 初始化路由

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

  routeList
    // 取出當前根路由下的 route
    .filter(({ target }) => target === cTarget.prototype)
    .forEach(route => {
      const { name: funcName, type, path, func } = route;
      // handler 即咱們常見的 (res, req, next) => {}
      const handler = handlerFactory(
          func,
          // 取當前路由函數下裝飾的參數列表
          paramList.filter(param => param.name === funcName),
          parseList.filter(parse => parse.name === funcName),
        );
      // 配置 express router
      router[type](basePath + path, handler);
    });
});

// 將裝載好的 router 放到 express 中
app.use('/', router);
複製代碼
  1. 路由處理函數工廠
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);
    }
  };
}
複製代碼
  1. 根據 req 處理裝飾的結果
export function extractParameters( req: Request, res: Response, next: NextFunction, paramArr: ParamType[] = [], parseArr: ParseType[] = [], ) {
  if (!paramArr.length) return [req, res, next];

  const args = [];
  // 進行第三層遍歷
  paramArr.forEach(param => {
    const { key, index, type } = param;
    // 獲取相應的值,如 @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;
      // ...
    }
  });

  // 小優化,處理參數類型
  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;
}
複製代碼

3)使用裝飾器

接來下就是愉快的使用時間 😏

@Controller('/') // 裝飾 controller
export default class Index {
  @Get('/') // 裝飾 route
  index(@Parse('number') @Query('id') id: number) { // 裝飾參數
    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' };
  }
}
複製代碼

2、reflect metadata

除了代碼書寫的不一樣,思想徹底同樣

1)定義裝飾器

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) => {
      // 這裏要注意這裏 defineMetadata 掛在 target.name 上
      // 但該函數的參數有順序之分,下一個裝飾器定義參數後覆蓋以前的,因此要用 preMetadata 保存起來
      const preMetadata =
        Reflect.getMetadata(PARAM_METADATA, target, name) || [];
      const newMetadata = [{ key, index, type }, ...preMetadata];

      Reflect.defineMetadata(PARAM_METADATA, newMetadata, target, name);
    };
}
複製代碼

2)裝飾器注入

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);
  // 拿到該實例的原型方法
  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);
  });
});
複製代碼

測試

  1. Demo 源碼獲取

  2. 如何運行:yarn && yarn start

  3. 測試 url,建議使用 postman

body: { name: 'lawler', password: '111111' }

headers: { authorization: 't:111111' }

body: { name: 'lawler', password: '111111' }

headers: { authorization: 't:111111' }

body: { name: 'lawler', password: '111111', gender: 0 }

參考資料

喜歡的記得點 ❤️哦~

相關文章
相關標籤/搜索