牛顿曾说:若是说我看得比别人更远些,那是由于我站在巨人的肩膀上。
阅读优质框架库的源码,能学到很多,更有甚者基于此创造了更优秀的,大体就是如此。javascript
midwayjs 已经忘了是怎么认识它的了。印象中是个 nodejs 的优质框架,看了介绍,很厉害!!!html
Midway - 一个面向将来的云端一体 Node.js 框架
<img src="/img/bVcR2g3" alt="midwayjs">前端
好很差用,用过才知道。一边阅读使用文档,一边建个本地项目感觉一下。用着真的是很“高效”呢。java
默认使用 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
装饰器的做用:
而对应的 @Inject
装饰器,做用为:
@Provide 和 @Inject 装饰器是有参数的,而且他们是成对出现。@Inject
的类中,必须有 @Provide
才会生效。
依赖注入约定:
@Provide
和 @Inject
装饰器是有可选参数的,而且他们是成对出现。
默认状况下:
规则以下:
依赖注入的代码写法,能减小很多代码量,平常开发很是高效。那么依赖注入是如何实现的呢?值得探索一下!
依赖注入原理。文章中提供了一篇扩展阅读的文章:[这一次,教你从零开始写一个 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()
过程当中须要管理收集到的模块,就会涉及到容器,用容器收集到一块,方便管理,也方便读写。
巧合之下,搜文档midwayjs文档,找到了以前版本的 midwayjs,看到里面关于依赖注入的说明:默认使用 injection 这个包来作依赖注入, 这个包也是 MidwayJs 团队根据业界已有的实现而产出的自研产品,它除了常见的依赖了注入以外,还知足了 Midway 自身的一些特殊需求。强烈推荐阅读文档理解。
一开始应该是这样设计的,后面把它融入到 midwayjs(2.x)
中了。大部分代码都是一致的,核心是同样的。大部分代码文件比对理解以下:
midwayjs/packages/core/src/context/
vs injection/src/
的ioc容器ioc容器是实现依赖注入的关键。
IoC 容器就像是一个对象池,管理着每一个对象实例的信息(Class Definition),因此用户无需关心何时建立,当用户但愿拿到对象的实例 (Object Instance) 时,能够直接拿到依赖对象的实例,容器会 自动将全部依赖的对象都自动实例化。主要有如下几种,分别处理不一样的逻辑。
其中ApplicationContext
是基类,而MidwayContainer
、RequestContext
继承于它。
midwayjs/packages/core/src/definitions
vs injection/src/base
midwayjs/packages/decorator/src/annotation
vs injection/src/annotation
@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; }
mideayjs/packages/core/src/context/managedResolverFactory.ts
vs injection/src/factory/common/managedResolverFactory.ts
其余说明:
injection 的基准测试是用 inversify
这个比较著名的 ioc 容器库作测试的。然后面的 midwayjs 中已经放弃了,直接用它本身的逻辑。
Singleton 单例,全局惟一(进程级别)
Request 默认,请求做用域,生命周期随着请求链路,在请求链路上惟一,请求结束当即销毁
Prototype 原型做用域,每次调用都会重复建立一个新的对象。
在这三种做用域中,midway 的默认做用域为 请求做用域
理解了原理,也看了源码,实现个简单的。
只要能实现后能像利用 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是ES7的一项提案,主要用于在声明阶段添加和读取元数据,TypeScript 1.5+支持该功能。
元数据能够被视为有关类和类的某些属性的描述性信息,本质上不会影响类的行为,可是你能够设置一些预约义的数据到类,并根据元数据对类进行某些操做。
Reflect Metadata的用法很是简单,首先须要安装该 reflect-metadata 库:
npm i reflect-metadata --save
而后在 tsconfig.json
的 emitDecoratorMetadata
须要中配置 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) } } } } } }
能像一开始介绍的 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.tssrc/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。里面信息关键信息很多呢。好比依赖哪些库,根据库能猜到项目里有些什么功能(前提是你知道这个库及库的做用)。
遇到不知道的库,去了解一下,也许往后会用到,也能更好的了解项目在作什么。下面是我新认识的一些库(列举):
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 将有助于发布软件包更新。
了解更多:
这个库,对开发大型框架库是很是有用的,平时业务代码开发用不到。简单了解下就好,等真正有机会用到的时候再深刻也不迟。
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端的项目,对性能有高要求的,这个库将会很是有用呢。
A powerful and lightweight inversion of control(IOC) container for JavaScript & Node.js apps powered by TypeScript.
inversify 是一个强大且轻量级的的基于 typescript 的 IOC 容器框架,支持 js 和 nodejs。
InversifyJS的开发具备四个主要目标:
了解更多:
若是你的代码模块较多,且彼此之间存在强依赖,不妨考虑尝试一下采用依赖注入的方式,借用这个库实现后续逻辑,固然也能够仿 midwayjs 同样本身实现。
断断续续的看的源码,文章也是断断续续写的,写的不是很好。不过总的来讲学到了不少呢:
Reflect-metadata.js
库来更好的管理类的元数据。实例化对象Reflect.construct
lerna.js
库benchmark.js
库。浏览器对微秒时间的处理Inversify.js
、midway.js
、injection.js
。其余:
实际每每是在简单版的基础之上考虑各类细节、边界、抽象、融合等以后,n个迭代以后的实现,并且后续会不断完善的。
我只是看了一部分我想看的代码,不少细节都没细看(好比web-express
。web-koa
),也有一些彻底没看(好比packages-serverless
)。目前精力有限,也许后面须要用到了或者有空的时候会回过头来再看。
作对的事,并把事作对。