前言

本次解读主要以归总性做铺垫,再阅读代码的逻辑编写。为什么变了呢,因为这里其实不完全接上一章的内容,整个流程跳过了Agent和Workers,所以就以总结性的理解作为阅读代码的铺垫,否则直接看代码,都不知道从哪看起了。
**

上一章回顾

上一章分析完egg-cluster的最后,明白了Master启动了两类子进程,并且守护着它们。一个由child_process.fork启动的Agent,另一个是cluster.fork(cfork)启动的Workers。由下图引出本章内容:

  1. +--------------+ 实例 +--------------+
  2. | Agent 子进程 | --------> | Agent |
  3. +--------------+ +---------------+
  4. / \
  5. child_process.fork / \ 继承
  6. / \
  7. +---------------+ +-------------------+ 继承 +------------+ 继承 +------------+
  8. | Master 主进程 | | EggApplication | ------> | EggCore | -----> | Koa |
  9. +-------------- + +------------------ + +------------+ +------------+
  10. \ /
  11. \ / 继承
  12. cluster.fork \ /
  13. +---------------+ 实例 +----------------+
  14. | Worker 子进程 | -------> | Application |
  15. +---------------+ +-----------------+

目录结构

  1. .
  2. ├── History.md
  3. ├── LICENSE
  4. ├── README.md
  5. ├── index.d.ts
  6. ├── index.js
  7. ├── lib
  8. ├── egg.js
  9. ├── lifecycle.js
  10. ├── loader
  11. ├── context_loader.js
  12. ├── egg_loader.js
  13. ├── file_loader.js
  14. └── mixin
  15. ├── config.js
  16. ├── controller.js
  17. ├── custom.js
  18. ├── custom_loader.js
  19. ├── extend.js
  20. ├── middleware.js
  21. ├── plugin.js
  22. ├── router.js
  23. └── service.js
  24. └── utils
  25. ├── base_context_class.js
  26. ├── index.js
  27. ├── sequencify.js
  28. └── timing.js
  29. └── package.json

egg-core的作用

  1. 'use strict';
  2. const EggCore = require('./lib/egg');
  3. const EggLoader = require('./lib/loader/egg_loader');
  4. const BaseContextClass = require('./lib/utils/base_context_class');
  5. const utils = require('./lib/utils');
  6. module.exports = {
  7. EggCore,
  8. EggLoader,
  9. BaseContextClass,
  10. utils,
  11. };
  • EggCore——继承于Koa,在Koa的基础上增加了生命周期、loader、重写了Koa的use方法等等。
  • EggLoader——集万千加载于一身,将config、controller、middleware、plugin、extend、custom、service、router、custom_loader全部内容整合都一起的类。
  • BaseContextClass——在项目中,controller和service都基于该类,只有继承该类才能使用this.ctx获取当前请求的执行上下文对象
  • utils——utils里主要是写了几个方法,中间件方法转换、加载文件等等

EggCore

  1. const KoaApplication = require('koa');
  2. const BaseContextClass = require('./utils/base_context_class');
  3. const Timing = require('./utils/timing');
  4. const Lifecycle = require('./lifecycle');
  5. // ...省略部分代码
  6. class EggCore extends KoaApplication {
  7. constructor(options = {}) {
  8. options.baseDir = options.baseDir || process.cwd(); // 项目路径
  9. options.type = options.type || 'application'; // 'application'或者'agent'
  10. super();
  11. // 挂载文件的开始时间,结束时间,使用时间
  12. this.timing = new Timing();
  13. // cache deprecate object by file
  14. this[DEPRECATE] = new Map();
  15. this._options = this.options = options;
  16. this.deprecate.property(this, '_options', 'app._options is deprecated, use app.options instead');
  17. this.console = new EggConsoleLogger();
  18. // 将BaseContextClass的类对象挂载在this下的BaseContextClass、Controller、Service
  19. this.BaseContextClass = BaseContextClass;
  20. const Controller = this.BaseContextClass;
  21. this.Controller = Controller;
  22. const Service = this.BaseContextClass;
  23. this.Service = Service;
  24. // 新建生命周期对象
  25. this.lifecycle = new Lifecycle({
  26. baseDir: options.baseDir,
  27. app: this,
  28. logger: this.console,
  29. });
  30. this.lifecycle.on('error', err => this.emit('error', err));
  31. this.lifecycle.on('ready_timeout', id => this.emit('ready_timeout', id));
  32. this.lifecycle.on('ready_stat', data => this.emit('ready_stat', data));
  33. // 初始化loader对象, Loader的来自'./loader/egg_loader'
  34. this.loader = new Loader({
  35. baseDir: options.baseDir, // 项目全路径
  36. app: this,
  37. plugins: options.plugins, // 插件
  38. logger: this.console, // 输出
  39. serverScope: options.serverScope, //
  40. env: options.env, //
  41. });
  42. }
  43. // 重写了koa的use函数
  44. use(fn) {
  45. assert(is.function(fn), 'app.use() requires a function');
  46. debug('use %s', fn._name || fn.name || '-');
  47. // 如果是老写法generator函数转成promise
  48. this.middleware.push(utils.middleware(fn));
  49. return this;
  50. }
  51. // 路由,由EggRouter生成
  52. get router() {
  53. if (this[ROUTER]) {
  54. return this[ROUTER];
  55. }
  56. const router = this[ROUTER] = new Router({ sensitive: true }, this);
  57. // register router middleware
  58. this.beforeStart(() => {
  59. this.use(router.middleware());
  60. });
  61. return router;
  62. }
  63. }

BaseContextClass

BaseContextClass类代码一共就8行,在项目中使用class XXXService extends Service或者class XXXController extends Controller时,继承的都是该类,所以在写请求接口时可以使用const { ctx, service } = this等等。

  1. class BaseContextClass {
  2. constructor(ctx) {
  3. this.ctx = ctx;
  4. this.app = ctx.app;
  5. this.config = ctx.app.config;
  6. this.service = ctx.service;
  7. }
  8. }

utils

utils里主要是写了几个方法,中间件方法转换、加载文件等等

  1. module.exports = {
  2. extensions: Module._extensions,
  3. loadFile(filepath) {
  4. try {
  5. // 如果不是模块,直接返回buffer
  6. const extname = path.extname(filepath);
  7. if (extname && !Module._extensions[extname]) {
  8. return fs.readFileSync(filepath);
  9. }
  10. // 作为模块引用
  11. const obj = require(filepath);
  12. if (!obj) return obj;
  13. // 如果是es6 模块
  14. if (obj.__esModule) return 'default' in obj ? obj.default : obj;
  15. return obj;
  16. } catch (err) {
  17. err.message = `[egg-core] load file: ${filepath}, error: ${err.message}`;
  18. throw err;
  19. }
  20. },
  21. // http请求的方法,值得注意的是没有trace,在官方文档说了egg已经做了安全防护,防止通知trace方法攻击,这里应该是对应上了
  22. methods: [ 'head', 'options', 'get', 'put', 'patch', 'post', 'delete' ],
  23. // 改造generator函数
  24. async callFn(fn, args, ctx) {
  25. args = args || [];
  26. if (!is.function(fn)) return;
  27. if (is.generatorFunction(fn)) fn = co.wrap(fn);
  28. return ctx ? fn.call(ctx, ...args) : fn(...args);
  29. },
  30. // 改造中间件的generator函数
  31. middleware(fn) {
  32. return is.generatorFunction(fn) ? convert(fn) : fn;
  33. },
  34. getCalleeFromStack(withLine, stackIndex) {
  35. stackIndex = stackIndex === undefined ? 2 : stackIndex;
  36. const limit = Error.stackTraceLimit;
  37. const prep = Error.prepareStackTrace;
  38. Error.prepareStackTrace = prepareObjectStackTrace;
  39. Error.stackTraceLimit = 5;
  40. // capture the stack
  41. const obj = {};
  42. Error.captureStackTrace(obj);
  43. let callSite = obj.stack[stackIndex];
  44. let fileName;
  45. /* istanbul ignore else */
  46. if (callSite) {
  47. // egg-mock will create a proxy
  48. // https://github.com/eggjs/egg-mock/blob/master/lib/app.js#L174
  49. fileName = callSite.getFileName();
  50. /* istanbul ignore if */
  51. if (fileName && fileName.endsWith('egg-mock/lib/app.js')) {
  52. // TODO: add test
  53. callSite = obj.stack[stackIndex + 1];
  54. fileName = callSite.getFileName();
  55. }
  56. }
  57. Error.prepareStackTrace = prep;
  58. Error.stackTraceLimit = limit;
  59. /* istanbul ignore if */
  60. if (!callSite || !fileName) return '<anonymous>';
  61. if (!withLine) return fileName;
  62. return `${fileName}:${callSite.getLineNumber()}:${callSite.getColumnNumber()}`;
  63. },
  64. getResolvedFilename(filepath, baseDir) {
  65. const reg = /[/\\]/g;
  66. return filepath.replace(baseDir + path.sep, '').replace(reg, '/');
  67. },
  68. };

EggLoader

EggLoader放在最后,是整个egg-core重中之重的地方,同时内容也很多。那EggLoad的作用是什么?先举一个通俗易懂的例子:项目文件中controller文件夹下写一个带index方法的home.js文件,再在router.js下配置完路由router.get('/', controller.home.index);,接口就写好了,写完之后为什么就能直接用了,靠的就是EggLoad。详细流程:controller写的home.js写好index方法之后会被EggLoader原型上的方法loadController执行,loadController会调用EggLoader下的loadToApp,loadToApp再执行new FileLoader(opt).load();最后load返回内容挂载在app的controller下,此时在router中就可以取到controller.home.index方法,这样对EggLoader的认识就很清晰了。所以EggLoader是egg框架中一个将config、controller、middleware、plugin、extend、custom、service、router、custom_loader全部内容整合都一起的类,也是egg的精华所在。
简化版例子:controller文件夹下的home.js => EggLoader => app.controller.home
service文件夹下的home.js => EggLoader => app.service.home
当然EggLoader中执行的过程是不一样,具体的内容等看源码再细说。

{
  home: {
    index: [Function: classControllerMiddleware] {
      [Symbol(EGG_LOADER_ITEM_FULLPATH)]: '/Volumes/sec/projects/egg-example/app/controller/home.js#HomeController.index()'
    },
    index2: [Function: classControllerMiddleware] {
      [Symbol(EGG_LOADER_ITEM_FULLPATH)]: '/Volumes/sec/projects/egg-example/app/controller/home.js#HomeController.index2()'
    },
    index3: [Function: classControllerMiddleware] {
      [Symbol(EGG_LOADER_ITEM_FULLPATH)]: '/Volumes/sec/projects/egg-example/app/controller/home.js#HomeController.index3()'
    },
    [Symbol(EGG_LOADER_ITEM_FULLPATH)]: '/Volumes/sec/projects/egg-example/app/controller/home.js',
    [Symbol(EGG_LOADER_ITEM_EXPORTS)]: true
  }
}

上面的内容只需要了解个大体的流程就可以,再看具体的内容,看完后回头就会清晰明了。

class EggLoader {
  constructor(options) {
    this.options = options;

    this.app = this.options.app; // 上一层的this
    this.lifecycle = this.app.lifecycle; // 上一层的声明周期
    this.timing = this.app.timing || new Timing(); 上一层的timing
    this[REQUIRE_COUNT] = 0;
    // 项目的package.json
    this.pkg = utility.readJSONSync(path.join(this.options.baseDir, 'package.json'));
        // egg的path
    this.eggPaths = this.getEggPaths();
    debug('Loaded eggPaths %j', this.eggPaths);

    this.serverEnv = this.getServerEnv();
    debug('Loaded serverEnv %j', this.serverEnv);
        // app信息(name、version、description、devDependencies等等)信息大杂烩
    this.appInfo = this.getAppInfo();

    this.serverScope = options.serverScope !== undefined
      ? options.serverScope
      : this.getServerScope();
  }
  // 加载文件,例如jsonp支持、i18n多语言、watcher 文件和文件夹监控、router等等
  loadFile(filepath, ...inject) {
    filepath = filepath && this.resolveModule(filepath);
    if (!filepath) {
      return null;
    }

    // function(arg1, args, ...) {}
    if (inject.length === 0) inject = [ this.app ];

    let ret = this.requireFile(filepath);
    if (is.function(ret) && !is.class(ret)) {
      ret = ret(...inject);
    }
    console.log(ret, 'ret')
    return ret;
  }
  // 加载到app,controller用的就是这里的方法
  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);
  }
    // 加载到ctx,service用的就是这里的方法
  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);
  }
}

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'),
];
// 把loaders数组里的文件引用后的方法全都挂载到EggLoader的原型上,所以EggLoader集万千方法于一身了,可以直接使用挂载的方法了
for (const loader of loaders) {
  Object.assign(EggLoader.prototype, loader);
}

例:controller加载

项目文件夹下controller文件夹home.js

const Controller = require('egg').Controller;
// 写法一 推荐
class HomeController extends Controller {
  async index() {
    const { ctx } = this;
    ctx.body = 'hi, egg';
  }
}

module.exports = HomeController;

// 写法二
exports.index = async ctx => {
  ctx.body = 'hi, egg';
}

egg-core 包下的./lib/loader/mixin/controller.js的loadController

loadController(opt) {
  this.timing.start('Load Controller');
  opt = Object.assign({
    caseStyle: 'lower',
    // 取到app/controller的文件夹
    directory: path.join(this.options.baseDir, 'app/controller'),
    // 初始化函数,可以看到为什么controller可以有多钟写法了。
    initializer: (obj, opt) => {
      // 如果是普通函数,直接生成新的对象
      if (is.function(obj) && !is.generatorFunction(obj) && !is.class(obj) && !is.asyncFunction(obj)) {
        obj = obj(this.app);
      }
      // 如果是上述的写法一,class的形式,class中的函数转换成async function(ctx, next)中间件形式,并用ctx去初始化该class
      if (is.class(obj)) {
        obj.prototype.pathName = opt.pathName;
        obj.prototype.fullPath = opt.path;
        return wrapClass(obj);
      }
      if (is.object(obj)) {
        return wrapObject(obj, opt.path);
      }
      // support generatorFunction for forward compatbility
      if (is.generatorFunction(obj) || is.asyncFunction(obj)) {
        return wrapObject({ 'module.exports': obj }, opt.path)['module.exports'];
      }
      return obj;
    },
  }, opt);
  const controllerBase = opt.directory;
    // 处理完之后将路径已经模块名称和处理完的opt传给loadToApp
  this.loadToApp(controllerBase, 'controller', opt);
  this.options.logger.info('[egg:loader] Controller loaded: %s', controllerBase);
  this.timing.end('Load Controller');
}

egg-core 包下的 ./lib/loader/egg_loader.js的loadToApp
可以看到将opt再一次组装了后实例化了FileLoader后通过load函数处理

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);
  }

egg-core 包下的 ./lib/loader/file_loader.js的load

load() {
  const items = this.parse();
  /**
   * items:将controller文件夹下的文件处理成以下数组的形式
   *[
   *  {
   *    fullpath: '/Volumes/sec/projects/egg-example/app/controller/home.js',
   *    properties: [ 'home' ],
   *    exports: { index: [Function] }
   *  },
   *  {
   *    fullpath: '/Volumes/sec/projects/egg-example/app/controller/test.js',
   *    properties: [ 'test' ],
   *    exports: { index: [Function] }
   *  }
   *]
  */
  const target = this.options.target;
  for (const item of items) {
    debug('loading item %j', item);
    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);
  }
  /**
   * 最终处理成以下格式,挂载到app下,即有了app.controller.home.index,controller是必须先与router
   * 加载的,因为router要加载的前提是取到controller.home.index方法
   *{
   *  home: {
   *    index: [Function: classControllerMiddleware] {
   *      [Symbol(EGG_LOADER_ITEM_FULLPATH)]: '/Volumes/sec/projects/egg-example/app/controller/home.js#HomeController.index()'
   *    },
   *    [Symbol(EGG_LOADER_ITEM_FULLPATH)]: '/Volumes/sec/projects/egg-example/app/controller/home.js',
   *    [Symbol(EGG_LOADER_ITEM_EXPORTS)]: true
   *  },
   *  test: {
   *    index: [Function: classControllerMiddleware] {
   *      [Symbol(EGG_LOADER_ITEM_FULLPATH)]: '/Volumes/sec/projects/egg-example/app/controller/test.js#TestController.index()'
   *    },
   *    [Symbol(EGG_LOADER_ITEM_FULLPATH)]: '/Volumes/sec/projects/egg-example/app/controller/test.js',
   *    [Symbol(EGG_LOADER_ITEM_EXPORTS)]: true
   *  }
   *}
   */
  return target;
}

总结

以上就是egg-core的所有功能,有些模块和细节没有一一去展开,不过在全局观上应该可以有一个不错的认识。
以后有时间再把每个模块以及函数再细化分析一下,确实很有价值,无论在整体的功能设计上还是代码的风格上都可以吹爆,另外本篇总结和以往不同,对于egg-core的总结往上面看,不再重复。