zoukankan      html  css  js  c++  java
  • ”控制反转Ioc,依赖注入DI“如何实现的?

    牛顿曾说:如果说我看得比别人更远些,那是因为我站在巨人的肩膀上。

    阅读优质框架库的源码,能学到不少,更有甚者基于此创造了更优秀的,大致就是如此。

    midwayjs 已经忘了是怎么认识它的了。印象中是个 nodejs 的优质框架,看了介绍,很厉害!!!

    Midway - 一个面向未来的云端一体 Node.js 框架

    midwayjs

    好不好用,用过才知道。一边阅读使用文档,一边建个本地项目感受一下。用着真的是很“高效”呢。

    midway 核心 ”依赖注入“ 代码写法

    默认使用 egg 作为上层框架(支持是 express, koa),这么创建项目

    $ npm -v
    
    # 如果是 npm v6
    $ npm init midway --type=web hello_koa
    
    # 如果是 npm v7
    $ npm init midway -- --type=web hello_koa
    

    使用:

    controller/api.ts

    import { Inject, Controller, Get, Provide, Query } from '@midwayjs/decorator';
    import { Context } from 'egg';
    import { UserService } from '../service/user';
    
    @Provide()
    @Controller('/api')
    export class APIController {
      @Inject()
      ctx: Context;
    
      @Inject()
      userService: UserService;
    
      @Get('/get_user')
      async getUser(@Query() uid) {
        const user = await this.userService.getUser({ uid });
        return { success: true, message: 'OK', data: user };
      }
    }
    

    service/user.js

    import { Provide } from '@midwayjs/decorator';
    import { IUserOptions } from '../interface';
    
    @Provide()
    export class UserService {
      async getUser(options: IUserOptions) {
        return {
          uid: options.uid,
          username: 'mockedName',
          phone: '12345678901',
          email: 'xxx.xxx@xxx.com',
        };
      }
    }
    

    书写 node 项目,controller、service、router 这些都必不可少。但是现在只需要向上面那样,即可。很快速的就定义好一个get请求了:localhost:7001/api/get_user。不再需要处理 router、controller、service 直接的绑定映射,也不需要初始化这个 Class(new),然后将实例放在需要调用的地方。

    悄悄告诉你,以下代码(上面的代码改造),只要在src目录下,不管文件是什么名字,都可以通过 localhost:7001/api/get_user 访问到呢。

    import { Inject, Controller, Get, Provide, Query } from '@midwayjs/decorator';
    
    @Provide()
    export class UserService {
      async getUser(options) {
        return {
          uid: options.uid,
          username: 'mockedName',
          phone: '123456789022',
          email: 'xxx.xxx@xxx.com',
        };
      }
    }
    
    @Provide()
    @Controller('/api')
    export class APIController {
    
      @Inject()
      userService: UserService;
    
      @Get('/get_user')
      async getUser(@Query() uid: string) {
        const user = await this.userService.getUser({ uid });
        return { success: true, message: 'OK', data: user };
      }
    }
    

    很神奇吧。表面上看和平时写的代码不一样的地方,就只有装饰器:@Controller@Get@Provide,@Inject。那么是这些装饰器背后做了什么,比如悄悄实例化并进行了实例的绑定?

    继续阅读文档,知道是通过依赖注入实现的。依赖注入装饰器和规则如下:

    依赖注入装饰器作用:

    @Provide 装饰器的作用:

    1. 这个 Class,被依赖注入容器托管,会自动被实例化(new)
    2. 这个 Class,可以被其他在容器中的 Class 注入

    而对应的 @Inject 装饰器,作用为:

    1. 在依赖注入容器中,找到对应的属性名,并赋值为对应的实例化对象

    @Provide 和 @Inject 装饰器是有参数的,并且他们是成对出现。
    @Inject 的类中,必须有 @Provide 才会生效。

    依赖注入约定:

    @Provide@Inject 装饰器是有可选参数的,并且他们是成对出现。

    默认情况下:

    1. @Provide 取 类名的驼峰字符串 作为依赖注入标识符
    2. @Inject 根据 规则 获取 key

    规则如下:

    1. 如果装饰器包含参数,则以 参数字符串 作为 key
    2. 如果没有参数,标注的 TS 类型为 Class,则将类 @Provide 的 key 作为 key
    3. 如果没有参数,标注的 TS 类型为非 Class,则将 属性名 作为 key

    依赖注入的代码写法,能减少不少代码量,日常开发非常高效。那么依赖注入是如何实现的呢?值得探索一下!

    依赖注入原理。文章中提供了一篇扩展阅读的文章:[这一次,教你从零开始写一个 IoC 容器](https://mp.weixin.qq.com/s/g07BByYS6yD3QkLsA7zLYQ

    看了上面的文档,大致是:创建个容器,在合适的时机,扫描文件,收集有@Provide的类,在@Inject的地方进行实例化绑定。

    理解依赖注入

    看之前,理解下:

    依赖注入解决的问题是:解耦。

    ioc

    (图是用excalidraw画的,简单的图用着还是挺方便的)

    如图所示,考虑下:此时若C实例化需要一个参数,则需要从A一直传递到C,造成了强耦合。而借助了 IOC 思想后,就可解耦,降低依赖。

    思想就是:直接传递对象,对象的属性和方法的更改,对象的一切操作内部自己消化。外部要改对象,必须调用对象提供的方法。函数传参的时候就是这么写的。

    传递的对象,如果是类,那就是实例化后的对象。在需要使用的地方能通过对象直接访问到。如果使用的也是类,可以再将实例化的对象绑定一次。

    这个过程可以有不同的实现方案:

    比如 midwayjs 是通过 @Provide、@Inject 先标注模块之间的依赖关系,再通过加载程序的时候扫描 @Provide 收集模块(要用的模块A,被使用的模块B)最后通过 @Inject 将模块B实例化后绑定到模块A上,模块A就可以直接使用了。

    比如 koa 的 use 就是绑定插件到app上,app就可以直接使用插件了,插件的操作自己消化。可阅读从前端中的IOC理念理解koa中的app.use()

    过程中需要管理收集到的模块,就会涉及到容器,用容器收集到一块,方便管理,也方便读写。

    midway 依赖注入部分源码解读

    巧合之下,搜文档midwayjs文档,找到了之前版本的 midwayjs,看到里面关于依赖注入的说明:默认使用 injection 这个包来做依赖注入, 这个包也是 MidwayJs 团队根据业界已有的实现而产出的自研产品,它除了常见的依赖了注入之外,还满足了 Midway 自身的一些特殊需求。强烈推荐阅读文档理解。

    一开始应该是这样设计的,后面把它融入到 midwayjs(2.x) 中了。大部分代码都是一致的,核心是一样的。大部分代码文件比对理解如下:

    1. midwayjs/packages/core/src/context/ vs injection/src/ 的ioc容器

    ioc容器是实现依赖注入的关键。

    IoC 容器就像是一个对象池,管理着每个对象实例的信息(Class Definition),所以用户无需关心什么时候创建,当用户希望拿到对象的实例 (Object Instance) 时,可以直接拿到依赖对象的实例,容器会 自动将所有依赖的对象都自动实例化。主要有以下几种,分别处理不同的逻辑。

    • AppliationContext 基础容器,提供了基础的增加定义和根据定义获取对象实例的能力
    • MidwayContainer 用的最多的容器,做了上层封装,通过 bind 函数能够方便的生成类定义,midway 从此类开始扩展
    • RequestContext 用于请求链路上的容器,会自动销毁对象并依赖另一个容器创建实例。

    其中ApplicationContext是基类,而MidwayContainerRequestContext继承于它。

    1. midwayjs/packages/core/src/definitions vs injection/src/base
      依赖注入的核心实现,加载对象的class,同步、异步创建对象实例化,对象的属性绑定等。
    2. midwayjs/packages/decorator/src/annotation vs injection/src/annotation
      包含装饰器 provide.ts、inject.ts 的实现,在midwayjs中是有一个装饰器管理类DecoratorManager, 用来管理midwayjs的所有装饰器。

    @provide() 的作用是简化绑定,能被 IoC 容器自动扫描,并绑定定义到容器上,对应的逻辑是 绑定对象定义(ObjectDefinition.ts)。

    @inject() 的作用是将容器中的定义实例化成一个对象,并且绑定到属性中,这样,在调用的时候就可以访问到该属性。

    注意,注入的时机为构造器(new)之后,所以在构造方法(constructor)中是无法获取注入的属性的,如果要获取注入的内容,可以使用 构造器注入

    父类的属性使用 @inject() 装饰器装饰,子类实例会得到装饰后的属性。

    其中查找类的原型使用 reflect-metadata 仓库的 OrdinaryGetPrototypeOf 方法,使用 recursiveGetPrototypeOf 方法以数组形式返回该类的所有原型。

    function recursiveGetPrototypeOf(target: any): any[] {
      const properties = [];
      let parent = ordinaryGetPrototypeOf(target);
      while (parent !== null) {
        properties.push(parent);
        parent = ordinaryGetPrototypeOf(parent);
      }
      return properties;
    }
    
    1. mideayjs/packages/core/src/context/managedResolverFactory.ts vs injection/src/factory/common/managedResolverFactory.ts
      主要定义了一个解析工厂类:ManagedResolverFactory,用来创建对象(同步create和异步createAsync),解析对象的参数,生命周期钩子事件,如创建单例初始化结束事件,遍历依赖树判断是否循环依赖。

    其他说明:

    1. 基准测试

    injection 的基准测试是用 inversify 这个比较著名的 ioc 容器库做测试的。而后面的 midwayjs 中已经放弃了,直接用它自己的逻辑。

    1. 作用域:

    Singleton 单例,全局唯一(进程级别)
    Request 默认,请求作用域,生命周期随着请求链路,在请求链路上唯一,请求结束立即销毁
    Prototype 原型作用域,每次调用都会重复创建一个新的对象。

    在这三种作用域中,midway 的默认作用域为 请求作用域

    基于 TypeScript 的控制反转、依赖注入理解及简单实现

    理解了原理,也看了源码,实现个简单的。

    只要能实现后能像利用 injection 解耦的案例一样,能通过c.a获取到类A的属性和方法,就表示表示基本实现了依赖注入。

    // 使用 IoC
    import {Container} from 'injection';
    import {A} from './A';
    import {B} from './B';
    const container = new Container();
    container.bind(A);
    container.bind(B);
    
    class C {
      constructor() {
        this.a = container.get('a');
        this.b = container.get('b');
      }
    }
    

    补充下前置知识,Reflect-metadata

    Reflect Metadata是ES7的一项提案,主要用于在声明阶段添加和读取元数据,TypeScript 1.5+支持该功能。

    元数据可以被视为有关类和类的某些属性的描述性信息,本质上不会影响类的行为,但是你可以设置一些预定义的数据到类,并根据元数据对类进行某些操作。

    Reflect Metadata的用法非常简单,首先需要安装该 reflect-metadata 库:

    npm i reflect-metadata --save
    

    然后在 tsconfig.jsonemitDecoratorMetadata 需要中配置 true

    然后,我们可以使用 Reflect.defineMetadata 和 定义并获取元数据 Reflect.getMetadata ,例如:

    import 'reflect-metadata';
    
    const CLASS_KEY = 'ioc:key';
    
    function ClassDecorator() {
      return function (target: any) {
        Reflect.defineMetadata(CLASS_KEY, {
          metaData: 'metaData',
        }, target);
    
        return target;
      }
    }
    
    @ClassDecorator()
    class D {
      constructor(){}
    }
    
    console.log(Reflect.getMetadata(ClASS_KEY, D)); // => {metaData: 'metaData'}
    

    使用 Reflect ,我们可以标记任何类,并将特殊操作应用于标记化的类。

    使用:

    src/ioc/demo/a.ts

    import { Provider } from "../provider"; // 需实现
    import { Inject } from "../inject"; // 需实现
    import B from './b'
    import C from './c'
    
    @Provider('a')
    export default class A {
      @Inject()
      private b: B
    
      @Inject()
      c: C
    
      print () {
        this.c.print()
      }
    }
    

    src/ioc/demo/b.ts

    import { Provider } from '../provider' // 需实现
    
    @Provider('b', [10])
    export default class B {
      n: number
      constructor (n: number) {
        this.n = n
      }
    }
    

    src/ioc/demo/c.ts

    import { Provider } from '../provider' // 需实现
    
    @Provider()
    export default class C {
      print () {
        console.log('hello')
      }
    }
    

    使用就可和 midwayjs 一致。可以看到不再有手动实例化,且可以自动处理要注册的类,且要注入的属性。而且实例都由类本身维护,更改的话,不需要改其他文件。

    src/ioc/index.ts

    import { Container } from './container' // 管理 元信息
    import { load } from './load' // 程序加载,负责扫描,@Provide、@Inject相应的实例化及绑定处理
    
    export default function () {
    
      const container = new Container()
      const path = './src/ioc/demo'
      load(container, path)
    
      const a:any = container.get('a')
      console.log(a); // A => {b: B {n: 10}}
      a.c.print() // hello
    }
    

    由于简单版的并未将容器和路由绑定,所以这么访问了

    实现:

    由于在程序启动时,需要知道哪些类需要注册到容器中,所以需要在定义的类的元数据后附加一些特殊标记,这样就可以通过扫描识别出来。用装饰器Provider来对需要注册的类进行标记,被标记的类能被其他类使用。

    src/ioc/provider.ts

    import 'reflect-metadata'
    import * as camelcase from 'camelcase'
    export const class_key = 'ioc:tagged_class'
    
    // Provider 装饰的类,表明是要注册到Ioc容器中
    export function Provider (identifier?: string, args?: Array<any>) {
      return function (target: any) {
        // 驼峰命名,这个的目的是,注解的时候加入不传,就用类名的驼峰式
        identifier = identifier ?? camelcase(target.name)
    
        Reflect.defineMetadata(class_key, {
          id: identifier, // key,用来注册Ioc容器
          args: args || [] // 实例化所需参数
        }, target)
        return target
      }
    }
    

    需要知道类的哪些属性需要被注入,因此定义Inject装饰器来标记。

    src/ioc/inject.ts

    // 将绑定的类注入到什么地方
    import 'reflect-metadata'
    
    export const props_key = 'ioc:inject_props'
    
    export function Inject () {
      return function (target: any, targetKey: string) {
        // 注入对象
        const annotationTarget = target.constructor
        let props = {}
        // 同一个类,多个属性注入类
        if (Reflect.hasOwnMetadata(props_key, annotationTarget)) {
          props = Reflect.getMetadata(props_key, annotationTarget)
        }
    
        props[targetKey] = {
          value: targetKey
        }
    
        Reflect.defineMetadata(props_key, props, annotationTarget)
      }
    }
    

    容器必须具有两个功能,即注册实例并获取它们。很自然会想到 Map ,可用于实现一个简单的容器:

    src/ioc/container.ts

    import 'reflect-metadata'
    import { props_key } from './inject'
    
    export class Container {
      bindMap = new Map()
    
      // 绑定类信息
      bind(identifier: string, registerClass: any, constructorArgs: any[]) {
        this.bindMap.set(identifier, {registerClass, constructorArgs})
      }
    
      // 获取实例,将实例绑定到需要注入的对象上
      get<T>(identifier: string): T {
        const target = this.bindMap.get(identifier)
        if (target) {
          const { registerClass, constructorArgs } = target
          // 等价于 const instance = new A([...constructorArgs]) // 假设 registerClass 为定义的类 A
          // 对象实例化的另一种方式,new 后面需要跟大写的类名,而下面的方式可以不用,可以把一个类赋值给一个变量,通过变量实例化类
          const instance = Reflect.construct(registerClass, constructorArgs)
    
          const props = Reflect.getMetadata(props_key, registerClass)
          for (let prop in props) {
            const identifier = props[prop].value
            // 递归获取 injected object
            instance[prop] = this.get(identifier)
          }
          return instance
        }
      }
    }
    

    关于 Reflect.construct(target, args, newTarget): 方法的行为有点像 new 操作符 构造函数,相当于运行 new target(...args)。

    var obj = new Foo(...args);
    var obj = Reflect.construct(Foo, args);
    

    在启动时扫描所有文件,获取文件导出的所有类,然后根据元数据进行绑定。假设没有嵌套目录,实现如下:

    src/ioc/load.ts

    import * as fs from 'fs'
    import { resolve } from 'path'
    import { class_key } from './provider'
    
    // 启动时扫描所有文件,获取定义的类,根据元数据进行绑定
    /**
     * 单层目录扫描实现
     * @param container: the global Ioc container
     */
    export function load(container, path) {
      const list = fs.readdirSync(path)
      for (const file of list) {
        if (/.ts$/.test(file)) {
          const exports = require(resolve(path, file))
    
          for (const m in exports) {
            const module = exports[m]
            if (typeof module === 'function') {
              const metadata = Reflect.getMetadata(class_key, module)
              // register
              if (metadata) {
                container.bind(metadata.id, module, metadata.args)
              }
            }
          }
        }
      }
    }
    

    在上面的简单版的基础上,实现 api-get 处理

    能像一开始介绍的 midwayjs 使用方式一致。

    主要是增加装饰器 @Controller@Get@Query,及相应的处理,具体看下面实现。

    使用

    src/reqIoc/demo/a.ts

    import { Provider } from "../provider";
    import { Inject } from "../inject";
    import { Controller } from '../Controller'
    import { Get, Query } from '../request'
    import B from './b'
    
    @Provider()
    @Controller('/api')
    export class A {
    
      @Inject()
      b: B;
    
      @Get('/b')
      printB(@Query() id, @Query() name) {
        const bProps:any = this.b.getProps(id, name);
        bProps.className = 'b'
        return { success: true, message: 'OK', data: bProps };
      }
    
      @Get('/c')
      printC(@Query() id) {
        const bProps:any = this.b.getProps(id);
        bProps.className = 'c'
        return { success: true, message: 'OK', data: bProps };
      }
    }
    

    src/reqIoc/demo/b.ts

    import { Provider } from '../provider'
    
    @Provider()
    export default class B {
      getProps (id?: string, name?: string) {
        return {
          id: id || 'mock',
          name: name || 'mock',
        };
      }
    }
    

    能通过浏览器 http://localhost:3000/api/b?id=12&name=n 看到以下数据

    {
      success: true,
      message: "OK",
      data: {
        id: "12",
        name: "n",
        className: "b"
      }
    }
    

    和上面的简单版一致,需要初始化扫描,进行数据的处理。不一样的是,没有了数据的获取响应,只有扫描。数据的获取响应通过接口方式呈现。

    reqIoc/frame.ts

    import { Container } from './container'
    import { load } from './load'
    
    export default function (ctx) {
      const container = new Container()
      const path = './src/reqIoc/demo'
      load(container, path, ctx)
    }
    

    实现

    在基础版上,主要增加了三个装饰器@Controller@Get@Query,原有装饰器@Provider@Inject代码逻辑不变。

    关于实现,想看源码的可参考:

    • @Controller: midwayjs/packages/decorator/web/controller.ts
    • @Get: midwayjs/packages/decorator/web/paramMapping.ts
    • @Query: midwayjs/packages/decorator/web/requestMapping.ts

    src/reqIoc/controller.ts

    import 'reflect-metadata'
    export const class_key = 'ioc:controller_class'
    
    export function Controller (prefix = '/') {
      return function (target: any) {
    
        const props = {
          prefix
        }
    
        Reflect.defineMetadata(class_key, props, target)
        return target
      }
    }
    

    主要就是存一下前缀,比如/api

    src/reqIoc/request.ts

    // 将绑定的类注入到什么地方
    import 'reflect-metadata'
    
    export const props_key = 'ioc:request_method'
    export const params_key = 'ioc:request_method_params'
    
    // 装饰的是类方法,target:类,targetKey: 类的方法名
    export function Get (path?: string) {
      return function (target: any, targetKey: string) {
        // 注入对象
        const annotationTarget = target.constructor
    
        let props = []
        // 同一个类,多个方法
        if (Reflect.hasOwnMetadata(props_key, annotationTarget)) {
          props = Reflect.getMetadata(props_key, annotationTarget)
        }
    
        const routerName = path ?? ''
    
        props.push({
          method: 'GET',
          routerName,
          fn: targetKey
        })
    
        Reflect.defineMetadata(props_key, props, annotationTarget)
      }
    }
    
    // 装饰的是类方法的入参,index 代表第几个参数
    export function Query () {
      return function (target: any, targetKey: string, index: number) {
        // 注入对象
        const annotationTarget = target.constructor
    
        const fn = target[targetKey]
        // 函数的参数
        const args = getParamNames(fn)
        // 拿到绑定的参数名;index
        let paramName = ''
        if (fn.length === args.length && index < fn.length) {
          paramName = args[index]
        }
    
        let props = {}
        // 同一个类,多个方法
        if (Reflect.hasOwnMetadata(params_key, annotationTarget)) {
          props = Reflect.getMetadata(params_key, annotationTarget)
        }
    
        // 同一方法,多个参数
        const paramNames = props[targetKey] || []
        paramNames.push({type: 'query', index, paramName})
    
        props[targetKey] = paramNames
    
        Reflect.defineMetadata(params_key, props, annotationTarget)
      }
    }
    
    const STRIP_COMMENTS = /((//.*$)|(/*[sS]*?*/))/gm;
    /**
     * get parameter from function
     * @param func 
     */
    export function getParamNames(func): string[] {
      const fnStr = func.toString().replace(STRIP_COMMENTS)
      let result = fnStr.slice(fnStr.indexOf('(') + 1, fnStr.indexOf(')')).split(',').map(content => content.trim().replace(/s?=.*$/, ''))
    
      if (result.length === 1 && result[0] === '') {
        result = []
      }
      return result
    }
    

    reqIoc/container.ts

    bindReq(key: string, list: any) {
      this.bindMap.set(key, list)
    }
    
    getReq(key: string) {
      return this.bindMap.get(key)
    }
    

    在原有代码中增加以上方法。

    load.ts

    import * as fs from 'fs'
    import { resolve } from 'path'
    import { class_key } from './provider'
    import { class_key as controller_class_key } from './controller'
    import { props_key, params_key } from './request'
    
    const req_mthods_key = 'req_methods'
    const joinSymbol = '_|_'
    
    // 启动时扫描所有文件,获取定义的类,根据元数据进行绑定
    /**
     * 单层目录扫描实现
     * @param container: the global Ioc container
     * @param path: 扫描路径
     * @param ctx: 上下文,没有用框架,所以 ctx = {req, res}。而 req、res 是 server.on('request', function (req, res) {}
     */
    export function load(container, path, ctx) {
      const list = fs.readdirSync(path)
      for (const file of list) {
        if (/.ts$/.test(file)) {
          const exports = require(resolve(path, file))
    
          for (const m in exports) {
            const module = exports[m]
            if (typeof module === 'function') {
              const metadata = Reflect.getMetadata(class_key, module)
              // register
              if (metadata) {
                container.bind(metadata.id, module, metadata.args)
    
                // 上面的代码逻辑是基础版,下面的是新增的
    
                // 先收集 Controller 上的 prefix 信息,请求方法的绑定函数 Get,函数对应的参数 Query
                const controllerMetadata = Reflect.getMetadata(controller_class_key, module)
                if (controllerMetadata) {
                  const reqMethodMetadata = Reflect.getMetadata(props_key, module)
    
                  if (reqMethodMetadata) {
                    // 只需要存储信息,不需要额外的操作。简单起见,把所有请求信息都放到一个对象中了,方便后续根据接口请求及入参进行判断响应
                    const methods = container.getReq(req_mthods_key) || {};
                    const reqMethodParamsMetadata = Reflect.getMetadata(params_key, module)
    
                    // 将收集到的信息整理放到容器中
                    reqMethodMetadata.forEach(item => {
                      // 完整的请求路径
                      const path = controllerMetadata.prefix + item.routerName
                      // 用请求方法和完整路径作为 key
                      methods[item.method + joinSymbol + path] = {
                        id: metadata.id, // Controll 类
                        fn: item.fn, // Get 方法
                        args: reqMethodParamsMetadata ? reqMethodParamsMetadata[item.fn] || [] : [] // Get 方法 Query 参数
                      }
                    })
    
                    container.bindReq(req_mthods_key, methods)
                  }
                }
              }
            }
          }
        }
      }
    
      // 将所有请求数据拿出来,根据请求方法及入参进行处理响应
      const reqMethods = container.getReq(req_mthods_key)
      if (reqMethods) {
        // ctx.req.url /api/c?id=12
        const [urlPath, query] = ctx.req.url.split('?')
        // key: 请求方法 + 路径
        const methodUrl = ctx.req.method + joinSymbol + urlPath
        // 根据 key 取出数据
        const reqMethodData = reqMethods[methodUrl]
        if (reqMethodData) {
          const {id, fn, args} = reqMethodData
          let fnQueryParams = []
          // 方法有参数
          if (args.length) {
            // 将查询字符串转换为对象
            const queryObj = queryParams(query)
            // 这儿先根据参数在函数中的位置进行排序,这儿只处理了 Query 的情况, 再根据参数名从查询对象中取出数据
            fnQueryParams = args.sort((a, b) => a.index - b.index).filter(item => item.type === 'query').map(item => queryObj[item.paramName])
          }
    
          // 调用方法,获取数据,进行响应
          const res = container.get(id)[fn](...fnQueryParams)
          ctx.res.end(JSON.stringify(res))
        }
      }
    }
    
    function queryParams (searchStr: string = '') {
      const reg = /([^?&=]+)=([^?&=]*)/g;
      const obj = {}
      searchStr.replace(reg, function (rs, $1, $2) {
        var name = decodeURIComponent($1);
        var val = decodeURIComponent($2);
        val = String(val);
        obj[name] = val;
        return rs;
      });
      return obj
    }
    

    可以看到,几个装饰器,有很多代码是重复的,可抽象。因此源码中是有个装饰器类。

    为了简单起见,我只是把请求相关的数据简单的收集整理存储。所以用了一个 Container 容器。而源码是有一个继承 Container 的 RequestContainer 进行处理。

    并且源码部分关于数据的扫描,考虑到各种情况,很复杂。扫描感兴趣的可看看midwayjs/packages/web/src/base.ts

    个人github对应代码实现node-ts-sample-ioc

    插曲

    看项源码的时候,第一看 readme 文档,不用说大家都知道。那么第二去看什么呢?

    我的习惯是去看 package.json。里面信息关键信息不少呢。比如依赖哪些库,根据库能猜到项目里有些什么功能(前提是你知道这个库及库的作用)。

    遇到不知道的库,去了解一下,也许日后会用到,也能更好的了解项目在做什么。下面是我新认识的一些库(列举):

    lernajs

    Lerna 是一个优化使用 git 和 npm 管理多包存储库的工作流工具,用于管理具有多个包的 JavaScript 项目。

    将大型代码库拆分为独立的版本包对于代码共享非常有用。 然而,代码库比较大了,子库比较多,子库之间有依赖,管理子库就会比较麻烦且难以追踪(一个库的版本改了,依赖的库也需要变更),测试也不易。

    lerna 能解决上面的问题,而且可以减少包的安装时间,包占用的存储空间。毕竟统一管理了,只需要一份(即使多个子库有重复的),否则每个子库都是单独的一个npm包,需要单独安装、存储空间。

    Lerna 仓库是什么样子?

    如下所示的目录结构:

    my-lerna-repo/
      package.json
      packages/
        package-1/
          package.json
        package-2/
          package.json
    

    Lerna 能做什么?

    Lerna 中的两个主要命令是 lerna bootstrap 和 lerna publish。 bootstrap 将把 repo 中的依赖关系链接在一起。 publish 将有助于发布软件包更新。

    了解更多:

    这个库,对开发大型框架库是非常有用的,平时业务代码开发用不到。简单了解下就好,等真正有机会用到的时候再深入也不迟。

    benchmark.js

    A robust benchmarking library that supports high-resolution timers & returns statistically significant results. As seen on jsPerf.

    一个强大的基准测试库,支持高分辨率计时器并返回具有统计意义的结果。

    基准测试是一种测试代码性能的方法, 同时也可以用来识别某段代码的CPU或者内存效率问题. 许多开发人员会用基准测试来测试不同的并发模式, 或者用基准测试来辅助配置工作池的数量, 以保证能最大化系统的吞吐量.

    Benchmark.js使用与JSLitmus类似的技术:我们在while循环中运行提取的代码(模式A),重复执行直到达到最小时间(模式B),然后重复整个过程以产生具有统计意义的显着性结果。

    了解更多:

    如果是开源的或面向C端的项目,对性能有高要求的,这个库将会非常有用呢。

    inversify.js

    A powerful and lightweight inversion of control(IOC) container for JavaScript & Node.js apps powered by TypeScript.

    inversify 是一个强大且轻量级的的基于 typescript 的 IOC 容器框架,支持 js 和 nodejs。

    InversifyJS的开发具有四个主要目标:

    1. 允许JavaScript开发人员编写符合SOLID原则的代码。
    2. 促进并鼓励遵守最佳OOP和IoC惯例。
    3. 尽可能减少运行时开销。
    4. 提供最新的开发经验。

    了解更多:

    如果你的代码模块较多,且彼此之间存在强依赖,不妨考虑尝试一下采用依赖注入的方式,借用这个库实现后续逻辑,当然也可以仿 midwayjs 一样自己实现。

    总结

    断断续续的看的源码,文章也是断断续续写的,写的不是很好。不过总的来说学到了很多呢:

    • 利用装饰器做一些事,可以借助Reflect-metadata.js库来更好的管理类的元数据。实例化对象Reflect.construct
    • 库拆包git及npm版本依赖等管理,可以借助lerna.js
    • 代码性能基准测试,可以借助benchmark.js库。浏览器对微秒时间的处理
    • 代码解耦,可以借助依赖注入容器及控制反转实现的原理,参考Inversify.jsmidway.jsinjection.js

    其他:

    实际往往是在简单版的基础之上考虑各种细节、边界、抽象、融合等之后,n个迭代之后的实现,而且后续会不断完善的。

    我只是看了一部分我想看的代码,很多细节都没细看(比如web-expressweb-koa),也有一些完全没看(比如packages-serverless)。目前精力有限,也许后面需要用到了或者有空的时候会回过头来再看。

    做对的事,并把事做对。

  • 相关阅读:
    【docker】更换挂载目录
    【设计】交互走查表
    MySQL常用字符串函数
    VIM_manual
    MySQL操作符
    基础SELECT实例
    MySQL字符集及校对规则的理解
    Linux命令之tar-rsync
    Linux-PATH_环境变量
    MySQL常用数据类型
  • 原文地址:https://www.cnblogs.com/EnSnail/p/14774565.html
Copyright © 2011-2022 走看看