本文共 16177 字,大约阅读时间需要 53 分钟。
是一个 Egg.js 的拓展框架,他提供了更多 ts 以及依赖注入方面的支持。今天我们来看一下 Midway 的启动过程。
midway 的代码所在地是 下。是一个汇总的 mono 仓库。你可以方便的在这一个 git 仓库里找到 midway 的全部拓展代码。不过一些原本 egg 的代码依旧是需要去到 下阅读。
为了帮助我们l了解整个 midway 的启动,你可以使用 midway-init 这个手脚架工具来初始化一个空的项目,具体步骤是:
# 安装手脚架工具npm install -g midway-init# 初始化项目mkdir midway-testcd midway-testmidway-init .
或者可以直接下载使用 midway-example 中的空项目, , 随后执行:
# 安装依赖npm i## 启动测试项目npm run dev
当你看到 midway started on 的字样时,就意味着空项目已经启动好。
有了 example 之后,我们就通过这个项目以及 node_modules
中完整的 midway 和 egg 依赖来研究整个启动的过程。
midway-bin 主要是面向 CLI 命令行的启动处理。当你开始通过 npm run dev
来启动 midway 的时候,就已经是通过 NODE_ENV=local midway-bin dev --ts
的方式调用了 midway-bin 来启动 midway 应用。
当你在执行 npm scripts
时,npm 会帮你在 node_modules 中查找通过 package.json 中的 bin 属性配置好的可执行命令。而 midway-bin 这个命令是在 node_modules/midway-bin/package.json 中的 bin 字段定义的:
{ "name": "midway-bin", // ... "bin": { "midway-bin": "bin/midway-bin.js", "mocha": "bin/mocha.js" },}
也就是说 midway-bin 这个命令其实调用的是 node_modules/midway-bin/bin/midway-bin.js
这个脚本来执行的。这里就是启动命令的一个入口。打开这个文件会发现如下代码:
#!/usr/bin/env node'use strict';const Command = require('../').MidwayBin;new Command().start();
根据这个代码的语音,我们可以知道,这里是有一个 Command 类会自动解析命令传入的参数和环境变量(如 dev 和 --ts 这样的命令和 flag),继续去看 '../' 的内容:
// node_modules/midway-bin/index.js'use strict';// 继承 egg-bin 的 Command 类const Command = require('egg-bin/lib/command');class MidwayBin extends Command { constructor(rawArgv) { // 调用父类的初始化 super(rawArgv); // 设置单纯执行 midway-bin 时返回的命令提示 this.usage = 'Usage: egg-bin [command] [options]'; // 加载用户在 midway-bin/lib/cmd 下定义的命令 this.load(path.join(__dirname, 'lib/cmd')); }}exports.MidwayBin = MidwayBin;// ...// dev 命令的逻辑exports.DevCommand = require('./lib/cmd/dev');// ...
发现这里导出了刚刚 new Command().start();
的 Command 类(MidwayBin),并且在这个类的 constructor 中加载用户在 midway-bin 下定义的命令。按照面向对象的逻辑,我们只需要关心 midway-bin 下的 dev 命令实现(当然如果你感兴趣也可以顺着继承链去看 egg-bin
-> common-bin
的构造函数内的初始化过程)。
我们来到 midway-bin/lib/cmd/dev.js
:
'use strict';class DevCommand extends require('egg-bin/lib/cmd/dev') { constructor(rawArgv) { // 调用父类的初始化 super(rawArgv); // 设置执行 midway-bin dev 时返回的命令提示 this.usage = 'Usage: midway-bin dev [dir] [options]'; // 设置默认参数 (端口) 为 7001 this.defaultPort = process.env.PORT || 7001; } * run(context) { // 设置默认的 midway 进程启动参数 (Arguments Vector) context.argv.framework = 'midway'; // 运行父类 egg-bin 的 dev 启动 yield super.run(context); }}module.exports = DevCommand;
通过代码注释,我们可以知道通过 midway-bin dev
启动时,与原本的 egg-bin 启动一个项目唯一的区别就是 midway-bin 设置了一下默认端口,以及启动的框架参数为 'midway'。最后还是调用的 egg-bin 内的 dev 命令的 run 方法来走的。
其中 egg-bin 的启动逻辑,简单来说就两步:
① 整理启动参数
② 根据启动参数创建 egg(这里是midway) 进程
综上,具体情况是:
到这里完成 midway-bin 的全部工作。
midway 这个在启动流程和所做的事情等同于 egg-cluster 这个包。主要是区别处理 Application 和 Agent 启动之前的逻辑,然后分别启动这两个部分。
在进入 midway 模块前,我们需要接着上方 midway-bin 的最后一步,来看一下 start-cluster 脚本:
#!/usr/bin/env node'use strict';const debug = require('debug')('egg-bin:start-cluster');const options = JSON.parse(process.argv[2]);debug('start cluster options: %j', options);require(options.framework).startCluster(options);
其中的 options.framework 就是前文提到过的在 midway-bin/lib/cmd/dev.js
中设置写死的参数,也就是 'midway'
,所以这里实际上调用的就是 node_modules/midway/dist/index.js
中的 startCluster 方法,注意在 midway 的 package.json 中配置了 main: 'dist/index'
, 所以 require ('midway')
拿到的是 midway/dist/index
。不过 midway 这个库是用 ts 写的,所以我们直接来看 ts 代码:
// ...// export * 导出各项定义:'injection', 'midway-core', 'midway-web', 'egg'const Master = require('../cluster/master');// .../** * 应用启动的方法 */export function startCluster(options, callback) { // options 就是 midway-bin 过程中整理的启动一个 midway 所需的所有参数 new Master(options).ready(callback);}
接下来我们来看这个 new Master 的逻辑:
const EggMaster = require('egg-cluster/lib/master');const path = require('path');const formatOptions = require('./utils').formatOptions;class Master extends EggMaster { constructor(options) { // TypeScript 默认支持的参数判断 options = formatOptions(options); super(options); // 输出 egg 格式的版本日志 this.log('[master] egg version %s, egg-core version %s', require('egg/package').version, require('egg-core/package').version); } // 设置 Agent 的 fork 入口 getAgentWorkerFile() { return path.join(__dirname, 'agent_worker.js'); } // 设置 Application 的 fork 入口 getAppWorkerFile() { return path.join(__dirname, 'app_worker.js'); }}module.exports = Master;
此处继承了 egg-cluster 的 master 类(管理 app 和 agent 启动),在构造函数的过程中加上了 TypeScript 的支持参数,然后重写了 getAgentWorkerFile
、getAppWorkerFile
,让 egg-cluster 在通过子进程 fork 启动 app 和 agent 的时候分别通过 midway/cluster/agent_work.js
和 midway/cluster/app_worker.js
这两个本地的脚本入口启动。
// midway/cluster/app_worker.js'use strict';const utils = require('./utils');const options = JSON.parse(process.argv[2]);utils.registerTypescriptEnvironment(options);require('egg-cluster/lib/app_worker');
以 app_worker
为例,实际上这两个 midway 下的入口只做了一件事,就是根据 TypeScript 支持检查的参数来决定是否默认帮用户注册 TypeScript 的运行环境。之后就继续走 egg-cluster 下原本的 app_worker
的入口逻辑。
而打开 egg-clsuter/lib/app_worker.js
,这个启动脚本:
'use strict';// 检查 options 中是否有 require 字段// 有的话 for 循环挨个 require// ...// ...// 初始化 logger// 此处 options.framework 就是 'midway'const Application = require(options.framework).Application;// 启动 Applicationconst app = new Application(options);// ...// 初始化好之后 callbackapp.ready(startServer);// 超时检查处理// ...// Application 初始化好之后做一些检查或者监听function startServer(err) { // 看启动是否 // 报错 // 超时 // 监听 on('error') 处理 // ...}// 如果出现异常,则优化 exit 进程gracefulExit({ logger: consoleLogger, label: 'app_worker', beforeExit: () => app.close(),});
看起来内容很多,但实际上我们需要关系的只有 2 句,一个是 require 获取 Application,另外一个是 new Application
。
其中 require 获取 Application,这里面的 options.framework 就是 'midway'
,所以约等于 require('midway').Application
,我们可以找到 node_modules/midway/src/index.ts
看到开头有这么一段:
export * from 'injection';export * from 'midway-core';export * from 'midway-web';export { Context, IContextLocals, EggEnvType, IEggPluginItem, EggPlugin, PowerPartial, EggAppConfig, FileStream, IApplicationLocals, EggApplication, EggAppInfo, EggHttpClient, EggContextHttpClient, Request, Response, ContextView, LoggerLevel, Router,} from 'egg';// ...
也就是说 require('midway').Application
其实拿到的是从 'midway-web'
中 export 出来的 Application。也就说从这里就进入了 midway-web 的逻辑。
midway-web/src/index.ts
里面 export 了很多内容,其实 Application 和 Agent 也在这里 export 出来。
export {AgentWorkerLoader, AppWorkerLoader} from './loader/loader';export {Application, Agent} from './midway';export {BaseController} from './baseController';export * from './decorators';export {MidwayWebLoader} from './loader/webLoader';export * from './constants';export {loading} from './loading';接着我们就可以到 midwa-web/src/midway.ts 中来看 Application 的代码(agent 类似所以省略),简单看一下代码,随后会有专门的讲解:import { Agent, Application } from 'egg';import { AgentWorkerLoader, AppWorkerLoader } from './loader/loader';// ...class MidwayApplication extends (Application as { new(...x)}) { // 使用 midway-web 下的 loader 来加载各项资源 get [Symbol.for('egg#loader')]() { return AppWorkerLoader; } // ... /* * 通过 midway-web 自定义的 loader 来获取当前的目录 * 这个可以解决代码编写在 src/ 目录下的执行问题 */ get baseDir(): string { return this.loader.baseDir; } get appDir(): string { return this.loader.appDir; } /* * 通过 midway-web 自定义的 loader 加载出 * midway 自定义的 plugin 上下文, application 上下文 */ getPluginContext() { return (this.loader as AppWorkerLoader).pluginContext; } getApplicationContext() { return (this.loader as AppWorkerLoader).applicationContext; } generateController(controllerMapping: string) { return (this.loader as AppWorkerLoader).generateController(controllerMapping); } get applicationContext() { return this.loader.applicationContext; } get pluginContext() { return this.loader.pluginContext; }}class MidwayAgent extends (Agent as { new(...x)}) { // 省略...}export { MidwayApplication as Application, MidwayAgent as Agent};
主要的来说,MidwayApplication 所做的事情是继承 egg 的 Application,然后替换了原本 egg 的 loader,使用 midway 自己的 loader。
被继承的 egg 的 Application 中,默认的一些初始化结束后,就会走到 midway-web 的 loader 中开始加载各种资源:
// midway-web/src/loader/loader.tsimport {MidwayWebLoader} from './webLoader';// ...const APP_NAME = 'app';export class AppWorkerLoader extends MidwayWebLoader { /** * intercept plugin when it set value to app */ loadCustomApp() { this.interceptLoadCustomApplication(APP_NAME); } /** * Load all directories in convention */ load() { // app > plugin > core this.loadApplicationExtend(); this.loadRequestExtend(); this.loadResponseExtend(); this.loadContextExtend(); this.loadHelperExtend(); this.loadApplicationContext(); // app > plugin this.loadCustomApp(); // app > plugin this.loadService(); // app > plugin > core this.loadMiddleware(); this.app.beforeStart(async () => { await this.refreshContext(); // get controller await this.loadController(); // app this.loadRouter(); // 依赖 controller }); }}export class AgentWorkerLoader extends MidwayWebLoader { // ...}
midway-web/loader/loader.ts
也是 midway 拓展 egg 的一个核心文件。在 AppWorkerLoader 重写的 load 方法中,按照顺序加载了 extend、egg 的 service、midway 的各种 IoC 容器、middleware 以及 Midway 的 controller/router 等等。
加载 Application、Request 等拓展,复用 egg-core 的逻辑。多继承 AppWorkerLoader -> MidwayWebLoader -> MidwayLoader -> EggLoader -> egg-core/lib/loader/mixin/extend()。
loadApplicationContext 方法继承关系:AppWorkerLoader -> MidwayWebLoader -> MidwayLoader。
其中被 load 的 ApplicationContext 就是 IoC 容器的一个具体实例,用于存储应用级的,单例的对象。另外ApplicationContext 是 MidwayContainer 的实例,MidwayContainer 继承自 injection (依赖注入库)
在这一步中,如果用户没有配置关闭自扫描的话,会扫描用户 src 目录下的所有代码。如果发现 @controller, @router 类型的装饰修饰的 class 等都会被预先加载定义到 IoC 容器中。
通过 this.interceptLoadCustomApplication 设置 this.app 的 setter,让用户引入的插件要往 this.app挂插件的时候直接挂在 IoC 容器上。
复用 egg 的 service 逻辑。
复用 egg 的 middleware 加载逻辑。
刷新 applicationContext 和 pluginContext 等 IoC 容器。并且从 applicationContext 上取到预先解析的 controllerIds。然后循环通过 applicationContext.getAsync(id) 挨个获取 controller 的实例。
挨个 controller 获取通过 @controller('/user') 等装饰器的方式注册的一些控制器信息来初始化各个 controller,然后对多个 controller 进行排序最后直接传递给 app.use。
src/router.ts 文件加载,直接复用 egg 的逻辑。
看完了主流程之后,我们在回过头看看看一开始使用 midway-init 脚手架生成的空项目。其中的 src 目录结构如下:
src├── app│ ├── controller│ │ ├── home.ts│ │ └── user.ts│ └── public│ └── README.md├── config│ ├── config.default.ts│ └── plugin.ts├── interface.ts└── lib └── service └── user.ts
在 midway 启动的时候,midway 自定义的 loader 会返回一个基于 src (测试环境) 或者 dist (生产环境) 的 baseDir 目录。同时在 Application 中初始化到 load 阶段中的 loadApplicationContext 时。
// midway-core/src/loader.ts// ...export class MidwayLoader extends EggLoader { protected pluginLoaded = false; applicationContext; pluginContext; baseDir; appDir; options; constructor(options: MidwayLoaderOptions) { super(options); this.pluginContext = new MidwayContainer(); } // ... protected loadApplicationContext() { // 从 src/config/config.default.ts 获取配置 const containerConfig = this.config.container || this.app.options.container || {}; // 实例化 ApplicationContext this.applicationContext = new MidwayContainer(this.baseDir); // 实例化 requestContext const requestContext = new MidwayRequestContainer(this.applicationContext); // 注册一些实例到 applicationContext 上 // ... // 如果没有关闭自扫描 (autoLoad) 则进行自扫描 if (!containerConfig.disableAutoLoad) { // 判断默认扫的目录, 默认 'src/' const defaultLoadDir = this.isTsMode ? [this.baseDir] : ['app', 'lib']; // 按照扫描 export 出来的 class 统计到上下文 this.applicationContext.load({ loadDir: (containerConfig.loadDir || defaultLoadDir).map(dir => { return this.buildLoadDir(dir); }), pattern: containerConfig.pattern, ignore: containerConfig.ignore }); } // 注册 config, plugin, logger 的 handler for container // ... } // ...}
在 this.applicationContext.load 过程中,会有 globby 获取到 controller 下的 home.ts、user.ts 以及 lib/service/user.ts 等文件,取其 export 的 class 存在 applicationContext 中。另外还有 ControllerPaser 来解析识别文件是否是 controller,是的话就会 push 到 applicationContext.controllerIds 数组中。
所以用户在 src/app/controller/home.ts 中的代码:
import { controller, get, provide } from 'midway';@provide()@controller('/')export class HomeController { @get('/') async index(ctx) { ctx.body = `Welcome to midwayjs!`; }}
就是此时被扫到 HomeController 这个定义已经存储在 applicationContext 中,controllerIds 中也存储了该 controller。
随后在 this.refreshContext()
的过程中,执行了 this.preloadControllerFromXml()
即预加载 controller:
// midway-web/src/loader/webLoader.tsexport class MidwayWebLoader extends MidwayLoader { // ... async preloadControllerFromXml() { // 获取控制器 id 数组 const ids = this.applicationContext.controllersIds; // for 循环遍历 id if (Array.isArray(ids) && ids.length > 0) { for (const id of ids) { // 异步获取 controller 实例 const controllers = await this.applicationContext.getAsync(id); const app = this.app; if (Array.isArray(controllers.list)) { controllers.list.forEach(c => { // 初始化 egg 的 router const newRouter = new router_1.EggRouter({ sensitive: true, }, app); // 将 controller 方法 expose 到具体的 router c.expose(newRouter); // 绑定对应 controller 的 router 到 app app.use(newRouter.middleware()); }); } } } } // ...}
随后回到 midway-web/src/loader.ts
中,继续执行下一步 this.loadController()
:
// midway-web/src/loader/webLoader.tsexport class MidwayWebLoader extends MidwayLoader { // ... async loadController(opt: { directory? } = {}): Promise{ // 设置 controller 所在的基础目录 const appDir = path.join(this.options.baseDir, 'app/controller'); // 加载目录下的所有文件 const results = loading(this.getFileExtension('**/*'), { loadDirs: opt.directory || appDir, call: false, }); // 遍历每个文件 for (const exports of results) { /* 如果是 export default class */ if (is.class(exports)) { await this.preInitController(exports); } else { /* 如果是 export 多个 class */ for (const m in exports) { const module = exports[m]; if (is.class(module)) { await this.preInitController(module); } } } } // must sort by priority // 按照优先级排序 // 调用 egg 的 controller 加载 super.loadController(opt); } /** * 获取使用 @controller 装饰器注解的信息 * 根据提取的信息来注册 router */ private async preInitController(module): Promise { // ... } // ...}
完成以上步骤就可以通过 curl localhost:7001/ 来调用该 controller:
import { controller, get, provide } from 'midway';@provide()@controller('/')export class HomeController { @get('/') async index(ctx) { ctx.body = `Welcome to midwayjs!`; }}
HomeController 的 index 方法。返回收到 'Welcom to midwayjs!'
midway-bin
作为继承自 egg-bin 的 midway 手脚架,在启动的过程中, 主要是设置了 midway 的默认端口和框架名。
而 midway
这个模块主要是作为启动入口并在 app 和 agent 启动的时候注册 typescript 环境,同时将 midway-web,egg,injection 等多个模块的定义在此统一导出。
最后的 midway-web
部分,在继承的 Application 和 Agent 类上并没有做太多的改动,主要是指定了替换了 egg 原本的 loadder,使用 midway 自己提供的 loader。并在 loader 的过程中添加很多 midway 特有的 feature,如 applicationContext、pluginContext、requestContext 等 IoC 容器。
而用户自己的 home.ts 这个路由,则是在自扫描阶段被解析到 controller 并且暂存 class 定义。随后在加载 controller 的环节中,通过自扫描的数据来反向(可以不需要使用曾经 Egg.js 中 router.js 这样的定义文件声明)查找到 controller 和 router,并初始化好使用 app.use 装载,从而是的 '/' 可以被请求。
本例中,主要是一个 Hello world 式的 example 的启动流程,更多 midway 的优秀用例会在后面慢慢补充希望大家支持。
转载地址:http://akabl.baihongyu.com/