从koa到egg.js到midway.js

Posted by Run-dream Blog on July 23, 2021

从KOA开始

KOA 是什么?

KOA 从设计理念上来说并不是一个完整的应用程序框架,相反更多的可以视为是 node.js 的 http 模块的抽象。

KOA 源码分析

  • 我们从使用的角度入手,

    const Koa = require('koa');
    const app = new Koa();
      
    // middleware
    app.use(async (ctx, next) => {
      const start = Date.now();
      await next();
      const ms = Date.now() - start;
      console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
    });
    // response
    app.use(ctx => {
      ctx.body = 'Hello Koa';
    });
      
    app.listen(3000);
    

    可以看到 核心方法, use ,这个方法也是KOA洋葱圈模型的核心。

  • 再看看koa的文件目录

    很简单,只有四个文件

    - application.js 应用 暴露给调用方使用
    - context 上下文 表示一次请求 包含了 request 和 response 对象
    - request 请求
    - response 响应
    

    主要的关键逻辑在于 application.js 这个文件,仔细阅读后发现

    • Application 继承于 Emitter, 所以可以直接使用EventEmitter的事件机制,即 emit, on 的方式进行消息传递

       // application.js
       class Application extends Emitter {
           super();
           ...
           this.env = options.env || process.env.NODE_ENV || 'development';
           this.middleware = []; // 中间件
           this.context = Object.create(context); // 请求上下文
           this.request = Object.create(request); // 请求
           this.response = Object.create(response); // 响应
           ...
       }
              	
       // context.js 
       this.app.emit('error', err, this);
      
    • context 是如何抽象出来的呢?

       // application.js
       // 1. listen 监听 callback 方法
         listen(...args) {
           debug('listen');
           const server = http.createServer(this.callback());
           return server.listen(...args);
         }
       // 2. callback 方法里为每次请求都会调用 createContext 方法
         callback() {
           const fn = compose(this.middleware);
           
           if (!this.listenerCount('error')) this.on('error', this.onerror);
           
           const handleRequest = (req, res) => {
             const ctx = this.createContext(req, res);
             return this.handleRequest(ctx, fn);
           };
           
           return handleRequest;
         }
        // 3. createContext 将request 和 response 绑定到 ctx 上
         createContext(req, res) {
           const context = Object.create(this.context);
           const request = context.request = Object.create(this.request);
           const response = context.response = Object.create(this.response);
           ...
           return context;
         }
           
      
    • KOA的中间件是怎么实现的呢?

      // application.js
      // 我们看到callback里有handleRequest的片段
      const fn = compose(this.middleware);
      const handleRequest = (req, res) => {
          const ctx = this.createContext(req, res);
          return this.handleRequest(ctx, fn);
      };
      // 可以看到handleRequest传入的fnMiddleware类型是Promise
      handleRequest(ctx, fnMiddleware) {
        const res = ctx.res;
        res.statusCode = 404;
        const onerror = err => ctx.onerror(err);
        const handleResponse = () => respond(ctx);
        onFinished(res, onerror);
        return fnMiddleware(ctx).then(handleResponse).catch(onerror);
      }
      // 于是我们找到 compose 方法的实现 koa-compose
      function compose (middleware) {
      if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
      for (const fn of middleware) {
        if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
      }
      // 关键的代码片段,将 middleware: (Context, Function)=> Promise 数组组装成一个Promise
      return function (context, next) {
        // last called middleware #
        let index = -1
        return dispatch(0)
        function dispatch (i) {
          if (i <= index) return Promise.reject(new Error('next() called multiple times'))
          index = i
          let fn = middleware[i]
          if (i === middleware.length) fn = next
          if (!fn) return Promise.resolve()
          try {
            return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
          } catch (err) {
            return Promise.reject(err)
          }
        }
      }
    }
    

KOA 扩展

Koa 没有任何内置路由,但是有 koa-router 和 koa-route 第三方库可用。

Egg.js

Egg.js 是什么?

在KOA的基础上,奉行约定优于配置,并提供了默认的多进程管理的一套比较完备的开发框架。

具体来看一下一个典型的Egg.js项目的目录

├── package.json
├── app.js (optional) // Worker 进程的启动入口
├── agent.js (optional) // 后台运行 进程的启动入口
├── app		
|   ├── router.js // 路由 一般
│   ├── controller // 控制器
│   │   └── home.js
|   ├── extend (optional) // 对内置对象的扩展
│   |   ├── helper.js (optional)
│   |   ├── filter.js (optional)
│   |   ├── request.js (optional)
│   |   ├── response.js (optional)
│   |   ├── context.js (optional)
│   |   ├── application.js (optional)
│   |   └── agent.js (optional)
│   ├── service (optional) // 服务
│   ├── middleware (optional) // 中间件
│   │   └── response_time.js
│   └── view (optional)
|       ├── layout.html
│       └── home.html
├── config	// 配置
|   ├── config.default.js
│   ├── config.prod.js
|   ├── config.test.js (optional)
|   ├── config.local.js (optional)
|   ├── config.unittest.js (optional)
│   └── plugin.js

Egg.js 文档研读

看完Egg.js文档,了解一下基本的概念。

Egg.js 源码分析

index.js

首先打开egg.js/index.js看看暴露了哪些方法,大致可以看出提供了哪些核心功能

// 多进程管理 Master
exports.startCluster = require('egg-cluster').startCluster;

// 单进程启动
exports.start = require('./lib/start');

// 类似 KOA 的 Application,表示应用
exports.Application = require('./lib/application');

// Agent 进程 暴露出来方便扩展
exports.Agent = require('./lib/agent');

// 用来加载 Worker 进程
exports.AppWorkerLoader = require('./lib/loader').AppWorkerLoader;

// 用来加载 Agent 进程
exports.AgentWorkerLoader = require('./lib/loader').AgentWorkerLoader;

// 对应MVC里的Controller,解析用户的输入,处理后返回相应的结果
exports.Controller = require('./lib/core/base_context_class');

// 对应MVC里的Service,做业务逻辑封装的一个抽象层
exports.Service = require('./lib/core/base_context_class');

// 定时器
exports.Subscription = require('./lib/core/base_context_class');

// 可以认为是一个接口,在这个类中注入了 ctx, app, config, service
exports.BaseContextClass = require('./lib/core/base_context_class');

// 启动 暴露出来方便扩展
exports.Boot = require('./lib/core/base_hook_class');

启动

查看 egg.js/lib/start.js,这里是单进程启动的入口,看到核心部分。

注意部分我们不关心的代码已经省略了,后面所有出现代码的部分都有所删减。

  let Agent;
  let Application;
 // 框架允许从package.egg.framework中覆盖 Application 和 Agent, 用来扩展
 if (options.framework) {
    Agent = require(options.framework).Agent;
    Application = require(options.framework).Application;
  } else {
    Application = require('./application');
    Agent = require('./agent');
  }

  // agent 是用来单独处理后台任务的进程
  const agent = new Agent(Object.assign({}, options));
  await agent.ready();
  const application = new Application(Object.assign({}, options));
  application.agent = agent;
  agent.application = application;
  await application.ready();

  // 可以看到 application 里有一个很重要的属性,messenger, 也就是文档里说的 IPC 通道。
  // emit egg-ready message in agent and application
  application.messenger.broadcast('egg-ready');

可以看到 启动的时候 做了两件事情:

  1. agent.ready()
  2. application.ready()

application.ready

于是我们来看看 Application 到底是什么,做了些什么。

// lib/application.js
class Application extends EggApplication {
  constructor(options = {}) {
    options.type = 'application';
    super(options);

    try {
      this.loader.load();
    } catch (e) {
      // close gracefully
      this[CLUSTER_CLIENTS].forEach(cluster.close);
      throw e;
    }
  }

可以看到构造函数这里主要做了两件事情:

  1. super(options)
  2. this.loader.load()

但是我们在这里没有看到 this.loader 的声明 也没有看到 start 启动的时候 ready 方法,所以我们继续去看 EggApplication 的代码

/**
 * Base on koa's Application
 * @see https://github.com/eggjs/egg-core
 * @see http://koajs.com/#application
 * @extends EggCore
 */
class EggApplication extends EggCore {

  /**
   * @class
   * @param {Object} options
   *  - {Object} [type] - type of instance, Agent and Application both extend koa, type can determine what it is.
   *  - {String} [baseDir] - app root dir, default is `process.cwd()`
   *  - {Object} [plugins] - custom plugin config, use it in unittest
   *  - {String} [mode] - process mode, can be cluster / single, default is `cluster`
   */
  constructor(options = {}) {
    options.mode = options.mode || 'cluster';
    super(options);
      
    // 加载配置
    this.loader.loadConfig();

    /**
     * messenger instance
     * @member {Messenger}
     * @since 1.0.0
     */
    this.messenger = Messenger.create(this);

    // trigger serverDidReady hook when all app workers
    // and agent worker is ready
    this.messenger.once('egg-ready', () => {
      this.lifecycle.triggerServerDidReady();
    });


    this[CLUSTER_CLIENTS] = [];

    /**
     * Wrap the Client with Leader/Follower Pattern
     *
     * @description almost the same as Agent.cluster API, the only different is that this method create Follower.
     *
     * @see https://github.com/node-modules/cluster-client
     * @param {Function} clientClass - client class function
     * @param {Object} [options]
     *   - {Boolean} [autoGenerate] - whether generate delegate rule automatically, default is true
     *   - {Function} [formatKey] - a method to tranform the subscription info into a string,default is JSON.stringify
     *   - {Object} [transcode|JSON.stringify/parse]
     *     - {Function} encode - custom serialize method
     *     - {Function} decode - custom deserialize method
     *   - {Boolean} [isBroadcast] - whether broadcast subscrption result to all followers or just one, default is true
     *   - {Number} [responseTimeout] - response timeout, default is 3 seconds
     *   - {Number} [maxWaitTime|30000] - leader startup max time, default is 30 seconds
     * @return {ClientWrapper} wrapper
     */
    this.cluster = (clientClass, options) => {
      options = Object.assign({}, this.config.clusterClient, options, {
        singleMode: this.options.mode === 'single',
        // cluster need a port that can't conflict on the environment
        port: this.options.clusterPort,
        // agent worker is leader, app workers are follower
        isLeader: this.type === 'agent',
        logger: this.coreLogger,
      });
      const client = cluster(clientClass, options);
      this._patchClusterClient(client);
      return client;
    };
  }
    
}

这里依然没有出现 ready 和 loader 的实现,但是出现了两行重要的东西

  1. ` this.cluster = …` 先标记一下,大概知道这里是 多进程管理的部分,之后再具体分析
  2. this.messenger = Messenger.create(this); 同样这里也只先知道这里是 IPC 通信的部分。

继续看 EggCore 的实现,

class EggCore extends KoaApplication {
  constructor(options = {}) {
    // 这里表明 EggCore 是 application 或者 agent 的抽象
    assert(options.type === 'application' || options.type === 'agent', 'options.type should be application or agent');

    super();
	
    
    this.lifecycle = new Lifecycle({
      baseDir: options.baseDir,
      app: this,
      logger: this.console,
    });
    this.lifecycle.on('error', err => this.emit('error', err));
    this.lifecycle.on('ready_timeout', id => this.emit('ready_timeout', id));
    this.lifecycle.on('ready_stat', data => this.emit('ready_stat', data));

    /**
     * The loader instance, the default class is {@link EggLoader}.
     * If you want define
     * @member {EggLoader} EggCore#loader
     * @since 1.0.0
     */
    const Loader = this[EGG_LOADER];
    assert(Loader, 'Symbol.for(\'egg#loader\') is required');
    this.loader = new Loader({
      baseDir: options.baseDir,
      app: this,
      plugins: options.plugins,
      logger: this.console,
      serverScope: options.serverScope,
      env: options.env,
    });
  }
  
  ready(flagOrFunction) {
    return this.lifecycle.ready(flagOrFunction);
  }
    
  /**
   * Create egg context
   * @function EggApplication#createContext
   * @param  {Req} req - node native Request object
   * @param  {Res} res - node native Response object
   * @return {Context} context object
   */
  createContext(req, res) {
    const app = this;
    const context = Object.create(app.context);
    const request = context.request = Object.create(app.request);
    const response = context.response = Object.create(app.response);
    context.app = request.app = response.app = app;
    context.req = request.req = response.req = req;
    context.res = request.res = response.res = res;
    request.ctx = response.ctx = context;
    request.response = response;
    response.request = request;
    return context;
  }
}

终于我们看到了 this.loaderready 的声明,于此同时又有一段很重要的声明:

this.lifecycle = new Lifecycle 我们也先mark 一下。

先把 loader 搞明白

class EggLoader {

  /**
   * @class
   * @param {Object} options - options
   * @param {String} options.baseDir - the directory of application
   * @param {EggCore} options.app - Application instance
   * @param {Logger} options.logger - logger
   * @param {Object} [options.plugins] - custom plugins
   * @since 1.0.0
   */
  constructor(options) {
    this.options = options;
      
    this.app = this.options.app;
    this.lifecycle = this.app.lifecycle;
    
    // package.json
    this.pkg = utility.readJSONSync(path.join(this.options.baseDir, 'package.json'));
      
    /**
     * All framework directories.
     *
     * You can extend Application of egg, the entry point is options.app,
     *
     * loader will find all directories from the prototype of Application,
     * you should define `Symbol.for('egg#eggPath')` property.
     *
     * ```
     * // lib/example.js
     * const egg = require('egg');
     * class ExampleApplication extends egg.Application {
     *   constructor(options) {
     *     super(options);
     *   }
     *
     *   get [Symbol.for('egg#eggPath')]() {
     *     return path.join(__dirname, '..');
     *   }
     * }
     * ```
     * @member {Array} EggLoader#eggPaths
     * @see EggLoader#getEggPaths
     * @since 1.0.0
     */
    this.eggPaths = this.getEggPaths();
      
      
    
    /**
     * @member {String} EggLoader#serverEnv
     * @see AppInfo#env
     * @since 1.0.0
     */
    this.serverEnv = this.getServerEnv();
      
      
    /**
     * @member {AppInfo} EggLoader#appInfo
     * @since 1.0.0
     */
    this.appInfo = this.getAppInfo();

    /**
     * @member {String} EggLoader#serverScope
     * @see AppInfo#serverScope
     */
    this.serverScope = options.serverScope !== undefined
      ? options.serverScope
      : this.getServerScope();
  }
   /**
   * Load files using {@link FileLoader}, inject to {@link Application}
   * @param {String|Array} directory - see {@link FileLoader}
   * @param {String} property - see {@link FileLoader}
   * @param {Object} opt - see {@link FileLoader}
   * @since 1.0.0
   */
  loadToApp(directory, property, opt) {
    const target = this.app[property] = {};
    opt = Object.assign({}, {
      directory,
      target,
      inject: this.app,
    }, opt);

    const timingKey = `Load "${String(property)}" to Application`;
    this.timing.start(timingKey);
    new FileLoader(opt).load();
    this.timing.end(timingKey);
  }

  /**
   * Load files using {@link ContextLoader}
   * @param {String|Array} directory - see {@link ContextLoader}
   * @param {String} property - see {@link ContextLoader}
   * @param {Object} opt - see {@link ContextLoader}
   * @since 1.0.0
   */
  loadToContext(directory, property, opt) {
    opt = Object.assign({}, {
      directory,
      property,
      inject: this.app,
    }, opt);

    const timingKey = `Load "${String(property)}" to Context`;
    this.timing.start(timingKey);
    new ContextLoader(opt).load();
    this.timing.end(timingKey);
  }  
}

/**
 * Mixin methods to EggLoader
 * // ES6 Multiple Inheritance
 * https://medium.com/@leocavalcante/es6-multiple-inheritance-73a3c66d2b6b
 */
const loaders = [
  require('./mixin/plugin'),
  require('./mixin/config'),
  require('./mixin/extend'),
  require('./mixin/custom'),
  require('./mixin/service'),
  require('./mixin/middleware'),
  require('./mixin/controller'),
  require('./mixin/router'),
  require('./mixin/custom_loader'),
];

for (const loader of loaders) {
  Object.assign(EggLoader.prototype, loader);
}

module.exports = EggLoader;

可以看到这里封装了 依赖信息 pkg 和 文件目录 eggPath 以及 运行环境serverEnv, 并且 通过 loadToApp 和 loadToContext方法 , mixin 了 各个类型的 文件的 加载方法。

这里拿 controller 和 service 为案例来分析一个具体的加载过程。

  • controller 通过 this.loadToApp 以 FileLoader 的方式来加载文件
class FileLoader {
  constructor(options) {
    assert(options.directory, 'options.directory is required');
    assert(options.target, 'options.target is required');
    this.options = Object.assign({}, defaults, options);
  }

  // 将目录下的文件解析并挂载到 target 上 如 this.app.controller
  load() {
    const items = this.parse();
    const target = this.options.target;
    for (const item of items) {
      // item { properties: [ 'a', 'b', 'c'], exports }
      // => target.a.b.c = exports
      item.properties.reduce((target, property, index) => {
        let obj;
        const properties = item.properties.slice(0, index + 1).join('.');
        if (index === item.properties.length - 1) {
          if (property in target) {
            if (!this.options.override) throw new Error(`can't overwrite property '${properties}' from ${target[property][FULLPATH]} by ${item.fullpath}`);
          }
          obj = item.exports;
          if (obj && !is.primitive(obj)) {
            obj[FULLPATH] = item.fullpath;
            obj[EXPORTS] = true;
          }
        } else {
          obj = target[property] || {};
        }
        target[property] = obj;
        debug('loaded %s', properties);
        return obj;
      }, target);
    }
    return target;
  }
  // 将目录下的文件包装成 { fullpath: string, properties: string[], exports: Function }[]
  parse() {
    let files = this.options.match;
    if (!files) {
      files = (process.env.EGG_TYPESCRIPT === 'true' && utils.extensions['.ts'])
        ? [ '**/*.(js|ts)', '!**/*.d.ts' ]
        : [ '**/*.js' ];
    } else {
      files = Array.isArray(files) ? files : [ files ];
    }
	// 支持多级目录
    let directories = this.options.directory;
    if (!Array.isArray(directories)) {
      directories = [ directories ];
    }

    const filter = is.function(this.options.filter) ? this.options.filter : null;
    const items = [];
    for (const directory of directories) {
      const filepaths = globby.sync(files, { cwd: directory });
      for (const filepath of filepaths) {
        const fullpath = path.join(directory, filepath);
        if (!fs.statSync(fullpath).isFile()) continue;
        const properties = getProperties(filepath, this.options);
        const pathName = directory.split(/[/\\]/).slice(-1) + '.' + properties.join('.');
        // get exports from the file
        const exports = getExports(fullpath, this.options, pathName);
        if (is.class(exports)) {
          exports.prototype.pathName = pathName;
          exports.prototype.fullPath = fullpath;
        }

        items.push({ fullpath, properties, exports });
      }
    }

    return items;
  }
}
  • service 通过 this.loadToContext 以 ContextLoader 的方式来加载文件

    class ContextLoader extends FileLoader {
      
      constructor(options) {
        const target = options.target = {};
        if (options.fieldClass) {
          options.inject[options.fieldClass] = target;
        }
        super(options);
      
        const app = this.options.inject;
        const property = options.property;
      
        // define ctx.service
        Object.defineProperty(app.context, property, {
          get() {
            if (!this[CLASSLOADER]) {
              this[CLASSLOADER] = new Map();
            }
            // 区分属性缓存
            // cache's lifecycle is the same with this context instance
            // e.x. ctx.service1 and ctx.service2 have different cache
            const classLoader = this[CLASSLOADER];
            let instance = classLoader.get(property);
            if (!instance) {
              instance = getInstance(target, this);
              classLoader.set(property, instance);
            }
            return instance;
          },
        });
      }
    }
    

    注意这里是继承FileLoader的基本逻辑,只是同时挂载到了 app.context 上。

agent.ready

看看 agent.js

**
 * Singleton instance in Agent Worker, extend {@link EggApplication}
 * @extends EggApplication
 */
class Agent extends EggApplication {
  /**
   * @class
   * @param {Object} options - see {@link EggApplication}
   */
  constructor(options = {}) {
    options.type = 'agent';
    super(options);

    this.loader.load();

    // keep agent alive even it doesn't have any io tasks
    this.agentAliveHandler = setInterval(() => {}, 24 * 60 * 60 * 1000);
  }
    
  get [EGG_LOADER]() {
    return AgentWorkerLoader;
  }
}

Agent 继承自 EggApplication , 也就是说加载的基本逻辑是相同的。 不同的是 this.loader指向的类 是 AgentWorkerLoader,并且需要保活。

再看看 AgentWorkerLoader

 */
class AgentWorkerLoader extends EggLoader {

  /**
   * loadPlugin first, then loadConfig
   */
  loadConfig() {
    this.loadPlugin();
    super.loadConfig();
  }

  load() {
    this.loadAgentExtend();
    this.loadContextExtend();
    this.loadCustomAgent();
  }
}

也就是 agent 不需要处理 service 或者 controller。

lifecycle

好了,现在来看下 lifecycle

class Lifecycle extends EventEmitter {

  /**
   * @param {object} options - options
   * @param {String} options.baseDir - the directory of application
   * @param {EggCore} options.app - Application instance
   * @param {Logger} options.logger - logger
   */
  constructor(options) {
    super();
    this.options = options;
    this[BOOT_HOOKS] = [];
    this[BOOTS] = [];
    this[CLOSE_SET] = new Set();
    this[IS_CLOSED] = false;
    this[INIT] = false;
    getReady.mixin(this);

    this.timing.start('Application Start');
    // get app timeout from env or use default timeout 10 second
    const eggReadyTimeoutEnv = Number.parseInt(process.env.EGG_READY_TIMEOUT_ENV || 10000);
    this.readyTimeout = eggReadyTimeoutEnv;

    this[INIT_READY]();

    this.ready(err => {
      this.triggerDidReady(err);
      this.timing.end('Application Start');
    });
  } 
   /**
   * init boots and trigger config did config
   */
  init() {
    assert(this[INIT] === false, 'lifecycle have been init');
    this[INIT] = true;
    this[BOOTS] = this[BOOT_HOOKS].map(t => new t(this.app));
  }

  registerBeforeStart(scope) {
    this[REGISTER_READY_CALLBACK]({
      scope,
      ready: this.loadReady,
      timingKeyPrefix: 'Before Start',
    });
  }

  registerBeforeClose(fn) {
    assert(is.function(fn), 'argument should be function');
    assert(this[IS_CLOSED] === false, 'app has been closed');
    this[CLOSE_SET].add(fn);
  }
}

有点难看懂,但是我们从暴露出来的方法可以看出来, 这是为了处理 EggApplication 的生命周期事件,提供了到某个阶段触发 hook 的功能。

注意这里用到了 getReady 包,用来添加只使用一次的事件。

多进程管理

前面提到了 Application 和 Agent 都继承于 EggApplication, 每个 EggApplication 可以理解成一个进程。

我们知道 Node.js 主线程是单线程,无法有效的利用多核CPU的性能,所以 node 提供了 node cluster的解决方案。注意 node cluster 的 IPC 通道底层是 libuv 提供的,具体可以参考NodeJs 子进程与 ipc

我们来扒代码,注意到 之前的 startCluster 方法

/**
 * cluster start flow:
 *
 * [startCluster] -> master -> agent_worker -> new [Agent]       -> agentWorkerLoader
 *                         `-> app_worker   -> new [Application] -> appWorkerLoader
 *
 */

/**
 * start egg app
 * @function Egg#startCluster
 * @param {Object} options {@link Master}
 * @param {Function} callback start success callback
 */
exports.startCluster = function(options, callback) {
  new Master(options).ready(callback);
};

可以看到是先启动了 Master

class Master extends EventEmitter {

  /**
   * @class
   * @param {Object} options
   *  - {String} [framework] - specify framework that can be absolute path or npm package
   *  - {String} [baseDir] directory of application, default to `process.cwd()`
   *  - {Object} [plugins] - customized plugins, for unittest
   *  - {Number} [workers] numbers of app workers, default to `os.cpus().length`
   *  - {Number} [port] listening port, default to 7001(http) or 8443(https)
   *  - {Object} [https] https options, { key, cert, ca }, full path
   *  - {Array|String} [require] will inject into worker/agent process
   *  - {String} [pidFile] will save master pid to this file
   */
  constructor(options) {
    super();
    this.options = parseOptions(options);
    this.workerManager = new Manager();
    this.messenger = new Messenger(this);
      
    ready.mixin(this);
	
    this.ready(() => {
      const action = 'egg-ready';
      this.messenger.send({
        action,
        to: 'parent',
        data: {
          port: this[REAL_PORT],
          address: this[APP_ADDRESS],
          protocol: this[PROTOCOL],
        },
      });
      this.messenger.send({
        action,
        to: 'app',
        data: this.options,
      });
      this.messenger.send({
        action,
        to: 'agent',
        data: this.options,
      });
    });
  }

我们又看到了熟悉的 messager 对象了

class Messenger {

  constructor(master) {
    this.master = master;
    this.hasParent = !!process.send;
    process.on('message', msg => {
      msg.from = 'parent';
      this.send(msg);
    });
    process.once('disconnect', () => {
      this.hasParent = false;
    });
  }

  /**
   * send message
   * @param {Object} data message body
   *  - {String} from from who
   *  - {String} to to who
   */
  send(data) {
    if (!data.from) {
      data.from = 'master';
    }

    // recognise receiverPid is to who
    if (data.receiverPid) {
      if (data.receiverPid === String(process.pid)) {
        data.to = 'master';
      } else if (data.receiverPid === String(this.master.agentWorker.pid)) {
        data.to = 'agent';
      } else {
        data.to = 'app';
      }
    }

    // default from -> to rules
    if (!data.to) {
      if (data.from === 'agent') data.to = 'app';
      if (data.from === 'app') data.to = 'agent';
      if (data.from === 'parent') data.to = 'master';
    }

    // app -> master
    // agent -> master
    if (data.to === 'master') {
      // app/agent to master
      this.sendToMaster(data);
      return;
    }

    // master -> parent
    // app -> parent
    // agent -> parent
    if (data.to === 'parent') {
      this.sendToParent(data);
      return;
    }

    // parent -> master -> app
    // agent -> master -> app
    if (data.to === 'app') {
      this.sendToAppWorker(data);
      return;
    }

    // parent -> master -> agent
    // app -> master -> agent,可能不指定 to
    if (data.to === 'agent') {
      this.sendToAgentWorker(data);
      return;
    }
  }

  /**
   * send message to master self
   * @param {Object} data message body
   */
  sendToMaster(data) {
    this.master.emit(data.action, data.data);
  }

  /**
   * send message to parent process
   * @param {Object} data message body
   */
  sendToParent(data) {
    if (!this.hasParent) {
      return;
    }
    process.send(data);
  }

  /**
   * send message to app worker
   * @param {Object} data message body
   */
  sendToAppWorker(data) {
    for (const id in cluster.workers) {
      const worker = cluster.workers[id];
      if (worker.state === 'disconnected') {
        continue;
      }
      // check receiverPid
      if (data.receiverPid && data.receiverPid !== String(worker.process.pid)) {
        continue;
      }
      sendmessage(worker, data);
    }
  }

  /**
   * send message to agent worker
   * @param {Object} data message body
   */
  sendToAgentWorker(data) {
    if (this.master.agentWorker) {
      sendmessage(this.master.agentWorker, data);
    }
  }
}

这里使用了 sendmessage包来发送跨进程的消息。可以看到 egg-cluster 将进程分成了四个部分

              ┌────────┐
              │ parent │
             /└────────┘\
            /     |      \
           /  ┌────────┐  \
          /   │ master │   \
         /    └────────┘    \
        /     /         \    \
      ┌───────┐         ┌───────┐
      │ agent │ ------- │  app  │
      └───────┘         └───────┘
  • Master 启动后先 fork Agent 进程

  • Agent 初始化成功后,通过 IPC 通道通知 Master

  • Master 再 fork 多个 App Worker

  • App Worker 初始化成功,通知 Master

  • 所有的进程初始化成功后,Master 通知 Agent 和 Worker 应用启动成功

总结

至此,我们把 egg.js 的源代码大致过了一遍,现在我们来回答下面几个问题:

1. egg.js 是如何加载文件的?

以 start 为入口, 按照Application 和 Agent 的 分类, FileLoader 提供基础的加载文件的功能, EggLoader mixin了针对 controller/service/config等类型的方法, 根据加载的对象的级别, 调用loadToApp 或者 loadToContext 来加载并组织文件。

2. egg.js 是如何将 ctx 注入到 controller/service 里的?
  • egg 加载 文件夹下文件,把controller的各个方法包装成接受context的中间classControllerMiddleware挂载到app.controller 上,把Service 的 Class 挂载到 app.serviceClasses 上。
  • egg 继承 koa 框架,对每个请求创建一个context对象,在中间件执行的时候,为controller注入context,并且实例化当前 controller 调用到的 service
3. egg.js 是如何进程多进程管理的?
  • 分类

    当一个应用启动时,会同时启动三类进程。Master 进程承担了进程管理的工作(类似 pm2), Agent 负责运行只想运行在一个进程的代码,Worker 进程负责处理真正的用户请求的处理。

  • IPC

    通过 messenger 对象实现了

    • Master 和 Worker/Agent 之间的直接IPC通信
    • Worker 与 Agent 进程通过Master转发通信

编码技巧

  • Symbol

    egg的代码里用 Symbol 来实现类的私有方法,注意 Symbol(key) 和 Symbol.for(key) 的区别

  • 模块拆分 安装核心功能,满足最小知识原则

    • egg-core 加载
    • egg-cluster 多进程
    • egg-init 脚手架
    • egg-bin 开发工具
    • egg 提供给用户的入口
  • 基于事件编程 隐藏具体实现的细节。使用get-ready包,来处理只触发一次的消息。

  • mixin

    按照不同的文件类型来分离具体的load方法,减少单个文件的大小。

  • 可扩展性

    Loader的很多地方预留了支持使用方扩展的空间。

Midway.js

我使用midway比较少,主要是用来学习typescript。

基于ts的基于装饰器和依赖注入使得代码更加简洁。

除此之外,它的可扩展性更加强,支持使用 Koa/Express/Egg.js 生态插件。