」控制反轉Ioc,依賴注入DI「如何實現的?

牛頓曾說:若是說我看得比別人更遠些,那是由於我站在巨人的肩膀上。

閱讀優質框架庫的源碼,能學到很多,更有甚者基於此創造了更優秀的,大體就是如此。javascript

midwayjs 已經忘了是怎麼認識它的了。印象中是個 nodejs 的優質框架,看了介紹,很厲害!!!html

Midway - 一個面向將來的雲端一體 Node.js 框架

<img src="/img/bVcR2g3" alt="midwayjs">前端

好很差用,用過才知道。一邊閱讀使用文檔,一邊建個本地項目感覺一下。用着真的是很「高效」呢。java

midway 核心 」依賴注入「 代碼寫法

默認使用 egg 做爲上層框架(支持是 express, koa),這麼建立項目node

$ npm -v

# 若是是 npm v6
$ npm init midway --type=web hello_koa

# 若是是 npm v7
$ npm init midway -- --type=web hello_koa

使用:git

controller/api.tsgithub

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.jsweb

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),而後將實例放在須要調用的地方。typescript

悄悄告訴你,如下代碼(上面的代碼改造),只要在src目錄下,無論文件是什麼名字,均可以經過 localhost:7001/api/get_user 訪問到呢。express

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/g0...

看了上面的文檔,大體是:建立個容器,在合適的時機,掃描文件,收集有@Provide的類,在@Inject的地方進行實例化綁定。

理解依賴注入

看以前,理解下:

依賴注入解決的問題是:解耦。

<img src="/img/bVcR2g4" alt="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 = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/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)。目前精力有限,也許後面須要用到了或者有空的時候會回過頭來再看。

作對的事,並把事作對

相關文章
相關標籤/搜索