博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Midway 外部版启动过程分析
阅读量:6886 次
发布时间:2019-06-27

本文共 16177 字,大约阅读时间需要 53 分钟。

是一个 Egg.js 的拓展框架,他提供了更多 ts 以及依赖注入方面的支持。今天我们来看一下 Midway 的启动过程。

Index

  • before start
  • midway-bin: CLI startup
  • midway: cluster startup
  • midway-web: Application/Agent startup
  • example for the flow
  • conclusion

Before Start

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 startup

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 的启动逻辑,简单来说就两步:

  • ① 整理启动参数

    • 解析 CLI flag:如 --port=2333 --cluster=8 等 flag 解析
    • 解析环境变量:如 NODE_ENV=local
    • 获取当前目录(process.cwd())用作默认的项目 baseDir
    • 通过当前目录读取 package.json 信息(如 egg 字段)
    • 判断 typescript 环境等设置需要 require 的模块参数
  • ② 根据启动参数创建 egg(这里是midway) 进程

    • 此处直接调用 common-bin/lib/helper 下的 #forkNode 方法。这个方法是一个通用的传递参数启动子进程的方法。通过 egg-bin 下 dev 的构造函数中拼装的 start-cluster 脚本来启动子进程。

综上,具体情况是:

  1. npm run dev
  2. NODE_ENV=local midway-bin dev --ts
  3. midway-bin/bin/midway-bin.js
  4. midway-bin/index.js
  5. 父类初始化 egg-bin -> common-bin
  6. 调用 midway-bin 重写的 dev 命令
  7. midway-bin/lib/cmd/dev.js 设置 port 和 framework 参数
  8. egg-bin/lib/cmd/dev.js 整理启动参数
  9. egg-bin -> common-bin #forkNode
  10. egg-bin/lib/start-cluster.js (在子进程中 require application 并 start)

到这里完成 midway-bin 的全部工作。

midway: cluster startup

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 的支持参数,然后重写了 getAgentWorkerFilegetAppWorkerFile,让 egg-cluster 在通过子进程 fork 启动 app 和 agent 的时候分别通过 midway/cluster/agent_work.jsmidway/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: Application/Agent startup

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 等等。

loadXXXExtend

加载 Application、Request 等拓展,复用 egg-core 的逻辑。多继承 AppWorkerLoader -> MidwayWebLoader -> MidwayLoader -> EggLoader -> egg-core/lib/loader/mixin/extend()。

loadApplicationContext

loadApplicationContext 方法继承关系:AppWorkerLoader -> MidwayWebLoader -> MidwayLoader。

其中被 load 的 ApplicationContext 就是 IoC 容器的一个具体实例,用于存储应用级的,单例的对象。另外ApplicationContext 是 MidwayContainer 的实例,MidwayContainer 继承自 injection (依赖注入库)

在这一步中,如果用户没有配置关闭自扫描的话,会扫描用户 src 目录下的所有代码。如果发现 @controller, @router 类型的装饰修饰的 class 等都会被预先加载定义到 IoC 容器中。

loadCustomApp

通过 this.interceptLoadCustomApplication 设置 this.app 的 setter,让用户引入的插件要往 this.app挂插件的时候直接挂在 IoC 容器上。

loadService

复用 egg 的 service 逻辑。

loadMiddleware

复用 egg 的 middleware 加载逻辑。

refreshContext

刷新 applicationContext 和 pluginContext 等 IoC 容器。并且从 applicationContext 上取到预先解析的 controllerIds。然后循环通过 applicationContext.getAsync(id) 挨个获取 controller 的实例。

loadController

挨个 controller 获取通过 @controller('/user') 等装饰器的方式注册的一些控制器信息来初始化各个 controller,然后对多个 controller 进行排序最后直接传递给 app.use。

loadRouter

src/router.ts 文件加载,直接复用 egg 的逻辑。

example 代码对照流程

看完了主流程之后,我们在回过头看看看一开始使用 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/

你可能感兴趣的文章
HTML5 API摘要
查看>>
去除滚动条的可滚动效果
查看>>
注入攻击 初见解
查看>>
JProfiler_SN_8_x.txt
查看>>
IntelliJ IDEA 社区版没有 Spring Initializr
查看>>
(C++)从本机获取WMI数据.
查看>>
【Practical API Design学习笔记】修复奥德修斯
查看>>
CentOS镜像使用帮助
查看>>
Spring AOP 实现原理
查看>>
4.5.2 libxml/tree.h file not found解决办法
查看>>
Java反射机制class类
查看>>
android使用proguard混淆生成jar包
查看>>
疯狂Activiti6.0连载(12)DMN规范概述
查看>>
3-Elasticsearch查询API
查看>>
RemotelyAnywhere安装使用指南
查看>>
PHP中利用ICONV转化字符串编码出错【DETECTED AN ILLEGAL CHARAC...
查看>>
display table 标签
查看>>
mysql 日志维护
查看>>
用 LFS 做极简高效的流媒体服务
查看>>
使用七牛云存储解决ios7.1的app部署问题 https
查看>>