eggjs 1

https://zhuanlan.zhihu.com/p/47178799

应用 插件 框架

  1. 应用基于框架运行,可配置多个插件
  2. 插件只完成独立功能
  3. 启动器,启动应用;封装器,可在已有基础上进行封装,也可以配置插件 egg eggcore 都是框架
  4. 框架基础上可以拓展新的
  5. 以上三个组成 加载单元loadunit, service/controller/config/middleware等目录结构

egg-core 源码

# 加载单元的目录结构如下图,其中插件和框架没有 controller 和 router.js
# 这个目录结构很重要,后面所有的 load 方法都是针对这个目录结构进行的
        loadUnit
        ├── package.json
        ├── app
        │   ├── extend
        │   |   ├── helper.js
        │   |   ├── request.js
        │   |   ├── response.js
        │   |   ├── context.js
        │   |   ├── application.js
        │   |   └── agent.js
        │   ├── service
        |   ├── controller
        │   ├── middleware
        │   └── router.js
        └── config
            ├── config.default.js
            ├── config.prod.js
            ├── config.test.js
            ├── config.local.js
            └── config.unittest.js```
## egg-core 主要功能
export 四个对象 
- eggcore 继承koaapplication 初始化工作,最重要的属性 loader,也就是 eggloader的实例
- eggloader 整个unit rcsem 的加载和初始化工作,主要提供load函数  loadplugin loadconfig loadmiddleware loadservice loadcontroller loadrouter 会根据指定目录下文件输出形式不同进行适配,最终输出内容
- basecontextclass 提供controller和service中的基类,只有继承,才能使用this.ctx
- utils,提供主要函数,包括转化成中间件函数middleware,根据不同类型文件获取文件导出内容函数loadfile等
# eggcore就是根据unit结构范式,将rcsem 加载到app context上
```javascript
// egg-core 源码 -> 导出的数据结构
const EggCore = require('./lib/egg');
const EggLoader = require('./lib/loader/egg_loader');
const BaseContextClass = require('./lib/utils/base_context_class');
const utils = require('./lib/utils');

module.exports = {
  EggCore,
  EggLoader,
  BaseContextClass,
  utils,
};

eggcore 具体实现源码

eggcore类继承于koa,做初始化工作,主要三个属性
loader 是eggloder的实例,定义多个load函数,对loadunit目录下文件进行加载
router eggrouter实例,继承koarouter 路由管理和分发
lifecycle app生命周期管理

// egg-core 源码 -> EggCore 类的部分实现

const KoaApplication = require('koa');
const EGG_LOADER = Symbol.for('egg#loader');

class EggCore extends KoaApplication {
    constructor(options = {}) {
        super();
        const Loader = this[EGG_LOADER];
        //初始化 loader 对象
        this.loader = new Loader({
            baseDir: options.baseDir,          //项目启动的根目录
            app: this,                         // EggCore 实例本身
            plugins: options.plugins,          //自定义插件配置信息,设置插件配置信息有多种方式,后面我们会讲
            logger: this.console,             
            serverScope: options.serverScope, 
        });
    }
    get [EGG_LOADER]() {
        return require('./loader/egg_loader');
    }
    // router 对象
    get router() {
        if (this[ROUTER]) {
          return this[ROUTER];
        }
        const router = this[ROUTER] = new Router({ sensitive: true }, this);
        this.beforeStart(() => {
          this.use(router.middleware());
        });
        return router;
    }
    // 生命周期对象初始化
    this.lifecycle = new Lifecycle({
        baseDir: options.baseDir,
        app: this,
        logger: this.console,
    });
}

eggloader 类源码学习

eggloader 先对app基本信息pkg/eggpaths/serverenv/appinfo/serverscope/basedir/进行整理,定义基础共用函数 geteggpaths/gettypefiles/getloadunits/loadfile 为后期load函数准备

// egg-core源码 -> EggLoader 中基本属性和基本函数的实现

class EggLoader {
    constructor(options) {
        this.options = options;
        this.app = this.options.app;
        //pkg 是根目录的 package.json 输出对象
        this.pkg = utility.readJSONSync(path.join(this.options.baseDir, 'package.json'));
        // eggPaths 是所有框架目录的集合体,虽然我们上面提到一个应用只有一个框架,但是框架可以在框架的基础上实现多级继承,所以是多个 eggPath
        //在实现框架类的时候,必须指定属性 Symbol.for('egg#eggPath') ,这样才能找到框架的目录结构
        //下面有关于 getEggPaths 函数的实现分析
        this.eggPaths = this.getEggPaths();
        this.serverEnv = this.getServerEnv();
        //获取 app 的一些基本配置信息(name,baseDir,env,scope,pkg 等)
        this.appInfo = this.getAppInfo();
        this.serverScope = options.serverScope !== undefined
            ? options.serverScope
            : this.getServerScope();
    }
    //递归获取继承链上所有 eggPath
    getEggPaths() {
        const EggCore = require('../egg');
        const eggPaths = [];
        let proto = this.app;
        //循环递归的获取原型链上的框架 Symbol.for('egg#eggPath') 属性
        while (proto) {
            proto = Object.getPrototypeOf(proto);
            //直到 proto 属性等于 EggCore 本身,说明到了最上层的框架类,停止循环
            if (proto === Object.prototype || proto === EggCore.prototype) {
                break;
            }
            const eggPath = proto[Symbol.for('egg#eggPath')];
            const realpath = fs.realpathSync(eggPath);
            if (!eggPaths.includes(realpath)) {
                eggPaths.unshift(realpath);
            }
        }
        return eggPaths;
    }

    //函数输入:config 或者 plugin ,函数输出:当前环境下的所有配置文件
    //该函数会根据 serverScope,serverEnv 的配置信息,返回当前环境对应 filename 的所有配置文件
    //比如我们的 serverEnv=prod,serverScope=online,那么返回的 config 配置文件是 ['config.default', 'config.online', 'config.prod', 'config.online_prod']
    //这几个文件加载顺序非常重要,因为最终获取到的 config 信息会进行深度的覆盖,后面的文件信息会覆盖前面的文件信息
    getTypeFiles(filename) {
        const files = [ `${filename}.default` ];
        if (this.serverScope) files.push(`${filename}.${this.serverScope}`);
        if (this.serverEnv === 'default') return files;

        files.push(`${filename}.${this.serverEnv}`);
        if (this.serverScope) files.push(`${filename}.${this.serverScope}_${this.serverEnv}`);
        return files;
    }

    //获取框架、应用、插件的 loadUnits 目录集合,上文有关于 loadUnits 的说明
    //这个函数在下文中介绍的 loadSerivce,loadMiddleware,loadConfig,loadExtend 中都会用到,因为 plugin,framework,app 中都会有关系这些信息的配置
    getLoadUnits() {
        if (this.dirs) {
            return this.dirs;
        }
        const dirs = this.dirs = [];
        //插件目录,关于 orderPlugins 会在后面的loadPlugin函数中讲到
        if (this.orderPlugins) {
            for (const plugin of this.orderPlugins) {
                dirs.push({
                    path: plugin.path,
                    type: 'plugin',
                });
            }
        }
        //框架目录
        for (const eggPath of this.eggPaths) {
            dirs.push({
                path: eggPath,
                type: 'framework',
            });
        }
        //应用目录
        dirs.push({
            path: this.options.baseDir,
            type: 'app',
        });
        return dirs;
    }

    //这个函数用于读取某个 loadUnit 下的文件具体内容,包括 js 文件,json 文件及其它普通文件
    loadFile(filepath, ...inject) {
        if (!filepath || !fs.existsSync(filepath)) {
            return null;
        }
        if (inject.length === 0) inject = [ this.app ];
        let ret = this.requireFile(filepath);
        //这里要注意,如果某个 js 文件导出的是一个函数,且不是一个 Class,那么Egg认为这个函数的格式是:app => {},输入是 EggCore 实例,输出是真正需要的信息
        if (is.function(ret) && !is.class(ret)) {
            ret = ret(...inject);
        }
        return ret;
    }
}

如何将不同类型文件加载 eggcore中文件加载都在一个独立文件。

加载 controller 文件可以通过 './mixin/controller' 目录下的 loadController 完成,加载 service 文件可以通过 './mixin/service' 下的 loadService 函数完成,然后将这些方法挂载 EggLoader 的原型上

// egg-core 源码 -> 混入不同目录文件的加载方法到 EggLoader 的原型上

const loaders = [
  require('./mixin/plugin'),            // loadPlugin方法
  require('./mixin/config'),            // loadConfig方法
  require('./mixin/extend'),            // loadExtend方法
  require('./mixin/custom'),            // loadCustomApp和loadCustomAgent方法
  require('./mixin/service'),           // loadService方法
  require('./mixin/middleware'),        // loadMiddleware方法
  require('./mixin/controller'),        // loadController方法
  require('./mixin/router'),            // loadRouter方法
];

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

loadplugin函数

插件不含 router和controller,应用和框架里都可以有,还可以通过环境变量和初始化参数传入
enable 是否开启插件
env 插件运行环境
path 插件所在的路径
package 和path 只能设置一个,根据package取node_modules查询plugin

// egg-core 源码 -> loadPlugin 函数部分源码

loadPlugin() {
    //加载应用目录下的 plugins
    // readPluginConfigs 这个函数会先调用我们上文提到的 getTypeFiles 获取到 app 目录下所有的 plugin 文件名,然后按照文件顺序进行加载并合并,并规范 plugin 的数据结构
    const appPlugins = this.readPluginConfigs(path.join(this.options.baseDir, 'config/plugin.default'));

    //加载框架目录下的 plugins
    const eggPluginConfigPaths = this.eggPaths.map(eggPath => path.join(eggPath, 'config/plugin.default'));
    const eggPlugins = this.readPluginConfigs(eggPluginConfigPaths);

    //可以通过环境变量 EGG_PLUGINS 配置 plugins,从环境变量加载 plugins
    let customPlugins;
    if (process.env.EGG_PLUGINS) {
      try {
        customPlugins = JSON.parse(process.env.EGG_PLUGINS);
      } catch (e) {
        debug('parse EGG_PLUGINS failed, %s', e);
      }
    }

    //从启动参数 options 里加载 plugins
    //启动参数的 plugins 和环境变量的 plugins 都是自定义的 plugins,可以对默认的应用和框架 plugin 进行覆盖
    if (this.options.plugins) {
      customPlugins = Object.assign({}, customPlugins, this.options.plugins);
    }

    this.allPlugins = {};
    this.appPlugins = appPlugins;
    this.customPlugins = customPlugins;
    this.eggPlugins = eggPlugins;

    //按照顺序对 plugin 进行合并及覆盖
    // _extendPlugins 在合并的过程中,对相同 name 的 plugin 中的属性进行覆盖,有一个特殊处理的地方,如果某个属性的值是空数组,那么不会覆盖前者
    this._extendPlugins(this.allPlugins, eggPlugins);
    this._extendPlugins(this.allPlugins, appPlugins);
    this._extendPlugins(this.allPlugins, customPlugins);

    const enabledPluginNames = [];
    const plugins = {};
    const env = this.serverEnv;
    for (const name in this.allPlugins) {
      const plugin = this.allPlugins[name];
      // plugin 的 path 可能是直接指定的,也有可能指定了一个 package 的 name,然后从 node_modules 中查找
      //从 node_modules 中查找的顺序是:{APP_PATH}/node_modules -> {EGG_PATH}/node_modules -> $CWD/node_modules
      plugin.path = this.getPluginPath(plugin, this.options.baseDir);
      //这个函数会读取每个 plugin.path 路径下的 package.json,获取 plugin 的 version,并会使用 package.json 中的 dependencies,optionalDependencies, env 变量作覆盖
      this.mergePluginConfig(plugin);
      // 有些 plugin 只有在某些环境(serverEnv)下才能使用,否则改成 enable=false
      if (env && plugin.env.length && !plugin.env.includes(env)) {
        plugin.enable = false;
        continue;
      }
      //获取 enable=true 的所有 pluginnName
      plugins[name] = plugin;
      if (plugin.enable) {
        enabledPluginNames.push(name);
      }
    }

    //这个函数会检查插件的依赖关系,插件的依赖关系在 dependencies 中定义,最后返回所有需要的插件
    //如果 enable=true 的插件依赖的插件不在已有的插件中,或者插件的依赖关系存在循环引用,则会抛出异常
    //如果 enable=true 的依赖插件为 enable=false,那么该被依赖的插件会被改为 enable=true
    this.orderPlugins = this.getOrderPlugins(plugins, enabledPluginNames, appPlugins);

    //最后我们以对象的方式将 enable=true 的插件挂载在 this 对象上
    const enablePlugins = {};
    for (const plugin of this.orderPlugins) {
      enablePlugins[plugin.name] = plugin;
    }
    this.plugins = enablePlugins;
}

loadconfig 函数

按照顺序加载所有 loadUnits 目录下的 config 文件内容,进行合并,最后将 config 信息挂载在 this 对象上

// egg-core 源码 -> loadConfig 函数分析

loadConfig() {
    this.configMeta = {};
    const target = {};
    //这里之所以先加载 app 相关的 config ,是因为在加载 plugin 和 framework 的 config 时会使用到 app 的 config
    const appConfig = this._preloadAppConfig();

    // config的加载顺序为:plugin config.default -> framework config.default -> app config.default -> plugin config.{env} -> framework config.{env} -> app config.{env}
    for (const filename of this.getTypeFiles('config')) {
      // getLoadUnits 函数前面有介绍,获取 loadUnit 目录集合
      for (const unit of this.getLoadUnits()) {
        const isApp = unit.type === 'app';
        //如果是加载插件和框架下面的 config,那么会将 appConfig 当作参数传入
        //这里 appConfig 已经加载了一遍了,又重复加载了,不知道处于什么原因,下面会有 _loadConfig 函数源码分析
        const config = this._loadConfig(unit.path, filename, isApp ? undefined : appConfig, unit.type);
        if (!config) {
          continue;
        }
        // config 进行覆盖
        extend(true, target, config);
      }
    }
    this.config = target;
}

_loadConfig(dirpath, filename, extraInject, type) {
    const isPlugin = type === 'plugin';
    const isApp = type === 'app';

    let filepath = this.resolveModule(path.join(dirpath, 'config', filename));
    //如果没有 config.default 文件,则用 config.js 文件替代,隐藏逻辑
    if (filename === 'config.default' && !filepath) {
      filepath = this.resolveModule(path.join(dirpath, 'config/config'));
    }
    // loadFile 函数我们在 EggLoader 中讲到过,如果 config 导出的是一个函数会先执行这个函数,将函数的返回结果导出,函数的参数也就是[this.appInfo extraInject]
    const config = this.loadFile(filepath, this.appInfo, extraInject);
    if (!config) return null;

    //框架使用哪些中间件也是在 config 里作配置的,后面关于 loadMiddleware 函数实现中有说明
    // coreMiddleware 只能在框架里使用
    if (isPlugin || isApp) {
      assert(!config.coreMiddleware, 'Can not define coreMiddleware in app or plugin');
    }
    // middleware 只能在应用里定义
    if (!isApp) {
      assert(!config.middleware, 'Can not define middleware in ' + filepath);
    }
    //这里是为了设置 configMeta,表示每个配置项是从哪里来的
    this[SET_CONFIG_META](config, filepath);
    return config;
  }

loadextend 函数

针对koa app.response app.respond app.context 以及app本身,同样根据loadunits下配置顺序进行加载,

// egg-core -> loadExtend 函数实现

// name输入是 "response"/"respond"/"context"/"app" 中的一个,proto 是被扩展的对象
loadExtend(name, proto) {
    //获取指定 name 所有 loadUnits 下的配置文件路径
    const filepaths = this.getExtendFilePaths(name);
    const isAddUnittest = 'EGG_MOCK_SERVER_ENV' in process.env && this.serverEnv !== 'unittest';
    for (let i = 0, l = filepaths.length; i < l; i++) {
      const filepath = filepaths[i];
      filepaths.push(filepath + `.${this.serverEnv}`);
      if (isAddUnittest) filepaths.push(filepath + '.unittest');
    }

    //这里并没有对属性的直接覆盖,而是对原先的 PropertyDescriptor 的 get 和 set 进行合并
    const mergeRecord = new Map();
    for (let filepath of filepaths) {
      filepath = this.resolveModule(filepath);
      const ext = this.requireFile(filepath);

      const properties = Object.getOwnPropertyNames(ext)
        .concat(Object.getOwnPropertySymbols(ext));
      for (const property of properties) {
        let descriptor = Object.getOwnPropertyDescriptor(ext, property);
        let originalDescriptor = Object.getOwnPropertyDescriptor(proto, property);
        if (!originalDescriptor) {
          const originalProto = originalPrototypes[name];
          if (originalProto) {
            originalDescriptor = Object.getOwnPropertyDescriptor(originalProto, property);
          }
        }
        //如果原始对象上已经存在相关属性的 Descriptor,那么对其 set 和 get 方法进行合并
        if (originalDescriptor) {
          descriptor = Object.assign({}, descriptor);
          if (!descriptor.set && originalDescriptor.set) {
            descriptor.set = originalDescriptor.set;
          }
          if (!descriptor.get && originalDescriptor.get) {
            descriptor.get = originalDescriptor.get;
          }
        }
        //否则直接覆盖
        Object.defineProperty(proto, property, descriptor);
        mergeRecord.set(property, filepath);
      }
    }
  }

loadservice 函数

最复杂的一个函数,先看如何使用service

// egg-core 源码 -> 如何在 egg 框架中使用 service

//方式 1 :app/service/user1.js
//这个是最标准的做法,导出一个 class ,这个 class 继承了 require('egg').Service ,其实也就是我们上文提到的 eggCore 导出的 BaseContextClass
//最终我们在业务逻辑中获取到的是这个class的一个实例,在 load 的时候是将 app.context 当作新建实例的参数
//在 controller 中调用方式:this.ctx.service.user1.find(1)
const Service = require('egg').Service;
class UserService extends Service {
  async find(uid) {
    //此时我们可以通过 this.ctx,this.app,this.config,this.service 获取到有用的信息,尤其是 this.ctx 非常重要,每个请求对应一个 ctx,我们可以查询到当前请求的所有信息
    const user = await this.ctx.db.query('select * from user where uid = ?', uid);
    return user;
  }
}
module.exports = UserService;

//方式 2 :app/service/user2.js
//这个做法是我模拟了一个 BaseContextClass,当然也就可以实现方法 1 的目的,但是不推荐
class UserService {
  constructor(ctx) {
    this.ctx = ctx;
    this.app = ctx.app;
    this.config = ctx.app.config;
    this.service = ctx.service;
  }
  async find(uid) {
    const user = await this.ctx.db.query('select * from user where uid = ?', uid);
    return user;
  }
}
module.exports = UserService;

//方式 3 :app/service/user3.js
// service 中也可以 export 函数,在 load 的时候会主动调用这个函数,把 appInfo 参数传入,最终获取到的是函数返回结果
//在 controller 中调用方式:this.ctx.service.user3.getAppName(1) ,这个时候在 service 中获取不到当前请求的上下文 ctx
module.exports = (appInfo) => {
    return {
        async getAppName(uid){
            return appInfo.name;
        }
    }
};

//方式 4 :app/service/user4.js
// service 也可以直接 export 普通的原生对象,load 的时候会将该普通对象返回,同样获取不到当前请求的上下文 ctx
//在 controller 中调用方式:this.ctx.service.user4.getAppName(1)
module.exports = {
    async getAppName(uid){
        return appInfo.name;
    }
};

以上

  1. why this.ctx.service
    app.context=>this.ctx 只要service绑定到app.context上,请求上下文ctx即可拿到
  2. 导出实例?
    如果导出的是一个类,EggLoader 会主动以 ctx 对象去初始化这个实例并导出,所以我们就可以直接在该类中使用 this.ctx 获取当前请求的上下文了
    如果导出的是一个函数,那么 EggLoader 会以 app 作为参数运行这个函数并将结果导出
    如果是一个普通的对象,直接导出

fileloader类的分析

loadservice 函数时,有基础类fileloader,同时也是loadmiddleware loadcontroller的基础,提供个load函数,根据目录结构和文件内容进行解析,返回target对象,根据文件名和子文件名以及函数名称获取service导出的数据,target 结构
{
"file1": {
"file11": {
"function1": a => a
}
},
"file2": {
"function2": a => a
}
}

{
    "file1": {
        "file11": {
            "function1": a => a
        }
    },
    "file2": {
        "function2": a => a
    }
}
下面我们先看一下 FileLoader 这个类的实现:

// egg-core 源码 -> FileLoader 实现

class FileLoader {
  constructor(options) {
    /* options 里几个重要参数的含义:
    1. directory: 需要加载文件的所有目录
    2. target: 最终加载成功后的目标对象
    3. initializer:一个初始化函数,对文件导出内容进行初始化,这个在 loadController 实现时会用到
    4. inject:如果某个文件的导出对象是一个函数,那么将该值传入函数并执行导出,一般都是 this.app
    */
    this.options = Object.assign({}, defaults, options);
  }
  load() {
    //解析 directory 下的文件,下面有 parse 函数的部分实现
    const items = this.parse();
    const target = this.options.target;
    // item1 = { properties: [ 'a', 'b', 'c'], exports1 },item2 = { properties: [ 'a', 'b', 'd'], exports2 }
    // => target = {a: {b: {c: exports1, d: exports2}}}
    //根据文件路径名称递归生成一个大的对象 target ,我们通过 target.file1.file2 就可以获取到对应的导出内容
    for (const item of items) {
      item.properties.reduce((target, property, index) => {
        let obj;
        const properties = item.properties.slice(0, index + 1).join('.');
        if (index === item.properties.length - 1) {
          obj = item.exports;
          if (obj && !is.primitive(obj)) {
            //这步骤很重要,确定这个 target 是不是一个 exports ,有可能只是一个路径而已
            obj[FULLPATH] = item.fullpath;
            obj[EXPORTS] = true;
          }
        } else {
          obj = target[property] || {};
        }
        target[property] = obj;
        return obj;
      }, target);
    }
    return target;
  }

  //最终生成 [{ properties: [ 'a', 'b', 'c'], exports,fullpath}] 形式, properties 文件路径名称的数组, exports 是导出对象, fullpath 是文件的绝对路径
  parse() {
    //文件目录转换为数组
    let directories = this.options.directory;
    if (!Array.isArray(directories)) {
      directories = [ directories ];
    }
    //遍历所有文件路径
    const items = [];
    for (const directory of directories) {
      //每个文件目录下面可能还会有子文件夹,所以 globby.sync 函数是获取所有文件包括子文件下的文件的路径
      const filepaths = globby.sync(files, { cwd: directory });
      for (const filepath of filepaths) {
        const fullpath = path.join(directory, filepath);
        if (!fs.statSync(fullpath).isFile()) continue;
        //获取文件路径上的以 "/" 分割的所有文件名,foo/bar.js => [ 'foo', 'bar' ],这个函数会对 propertie 同一格式,默认为驼峰
        const properties = getProperties(filepath, this.options);
        // app/service/foo/bar.js => service.foo.bar
        const pathName = directory.split(/[/\\]/).slice(-1) + '.' + properties.join('.');
        // getExports 函数获取文件内容,并将结果做一些处理,看下面实现
        const exports = getExports(fullpath, this.options, pathName);
        //如果导出的是 class ,会设置一些属性,这个属性下文中对于 class 的特殊处理地方会用到
        if (is.class(exports)) {
          exports.prototype.pathName = pathName;
          exports.prototype.fullPath = fullpath;
        }
        items.push({ fullpath, properties, exports });
      }
    }
    return items;
  }
}

//根据指定路径获取导出对象并作预处理
function getExports(fullpath, { initializer, call, inject }, pathName) {
  let exports = utils.loadFile(fullpath);
  //用 initializer 函数对exports结果做预处理
  if (initializer) {
    exports = initializer(exports, { path: fullpath, pathName });
  }
  //如果 exports 是 class,generatorFunction,asyncFunction 则直接返回    
  if (is.class(exports) || is.generatorFunction(exports) || is.asyncFunction(exports)) {
    return exports;
  }
  //如果导出的是一个普通函数,并且设置了 call=true,默认是 true,会将 inject 传入并调用该函数,上文中提到过好几次,就是在这里实现的
  if (call && is.function(exports)) {
    exports = exports(inject);
    if (exports != null) {
      return exports;
    }
  }
  //其它情况直接返回
  return exports;
}

contextloader 类的实现

contextloader继承fileloader,将解析出的target挂载在 app.context

// egg-core -> ContextLoader 类的源码实现

class ContextLoader extends FileLoader {
    constructor(options) {
        const target = options.target = {};
        super(options);
        // FileLoader 已经讲过 inject 就是 app
        const app = this.options.inject;
        // property 就是要挂载的属性,比如 "service"
        const property = options.property;
        //将 service 属性挂载在 app.context 上
        Object.defineProperty(app.context, property, {
            get() {
                //做缓存,由于不同的请求 ctx 不一样,这里是针对同一个请求的内容进行缓存
                if (!this[CLASSLOADER]) {
                    this[CLASSLOADER] = new Map();
                }
                const classLoader = this[CLASSLOADER];
                //获取导出实例,这里就是上文用例中获取 this.ctx.service.file1.fun1 的实现,这里的实例就是 this.ctx.service,实现逻辑请看下面的 getInstance 的实现
                let instance = classLoader.get(property);
                if (!instance) {
                    //这里传入的 this 就是为了初始化 require('egg').Service 实例时当作参数传入
                    // this 会根据调用者的不同而改变,比如是 app.context 的实例调用那么就是 app.context ,如果是 app.context 子类的实例调用,那么就是其子类的实例
                    //就是因为这个 this ,如果 service 里继承require('egg').Service ,才可以通过 this.ctx 获取到当前请求的上下文
                    instance = getInstance(target, this);
                    classLoader.set(property, instance);
                }
                return instance;
            },
        });
    }
}

// values 是 FileLoader/load 函数生成 target 对象
function getInstance(values, ctx) {
  //上文 FileLoader 里实现中我们讲过,target 对象是一个由路径和 exports 组装成的一个大对象,这里 Class 是为了确定其是不是一个 exports ,有可能是一个路径名
  const Class = values[EXPORTS] ? values : null;
  let instance;
  if (Class) {
    if (is.class(Class)) {
        //这一步很重要,如果是类,就用 ctx 进行初始化获取实例
        instance = new Class(ctx);
    } else {
        //普通对象直接导出,这里要注意的是如果是 exports 函数,在 FileLoader 实现中已经将其执行并转换为了对象
        // function 和 class 分别在子类和父类的处理的原因是, function 的处理逻辑 loadMiddleware,loadService,loadController 公用,而 class 的处理逻辑 loadService 使用
        instance = Class;
    }
  } else if (is.primitive(values)) {
       //原生类型直接导出
       instance = values;
  } else {
       //如果目前的 target 部分是一个路径,那么会新建一个 ClassLoader 实例,这个 ClassLoader 中又会递归的调用 getInstance
       //这里之所以新建一个类,一是为了做缓存,二是为了在每个节点获取到的都是一个类的实例
       instance = new ClassLoader({ ctx, properties: values });
  }
  return instance;
}

loadservice 实现

// egg-core -> loadService 函数实现源码
// loadService 函数调用 loadToContext 函数
loadService(opt) {
    opt = Object.assign({
      call: true,
      caseStyle: 'lower',
      fieldClass: 'serviceClasses',
      directory: this.getLoadUnits().map(unit => path.join(unit.path, 'app/service')), //所有加载单元目录下的 service
    }, opt);
    const servicePaths = opt.directory;
    this.loadToContext(servicePaths, 'service', opt);
}
// loadToContext 函数直接新建 ContextLoader 实例,调用 load 函数实现加载
loadToContext(directory, property, opt) {
    opt = Object.assign({}, {
      directory,
      property,
      inject: this.app,
    }, opt);
    new ContextLoader(opt).load();
}

loadmiddleware

中间件是 Koa 框架中很重要的一个环节,通过 app.use 引入中间件,使用洋葱圈模型,所以中间件加载的顺序很重要。 - 如果在上文中的 config 中配置的中间件,系统会自动用 app.use 函数使用该中间件 - 所有的中间件我们都可以在 app.middleware 中通过中间件 name 获取到,便于在业务中动态使用

// egg-core 源码 -> loadMiddleware 函数实现源码

loadMiddleware(opt) {
    const app = this.app;
    opt = Object.assign({
      call: false,   // call=false 表示如果中间件导出是函数,不会主动调用函数做转换
      override: true,
      caseStyle: 'lower',
      directory: this.getLoadUnits().map(unit => join(unit.path, 'app/middleware')) //所有加载单元目录下的 middleware
    }, opt);
    const middlewarePaths = opt.directory;
    //将所有中间件 middlewares 挂载在 app 上,这个函数在 loadController 实现中也用到了,看下文的实现
    this.loadToApp(middlewarePaths, 'middlewares', opt);
    //将 app.middlewares 中的每个中间件重新绑定在 app.middleware 上,每个中间件的属性不可配置,不可枚举
    for (const name in app.middlewares) {
      Object.defineProperty(app.middleware, name, {
        get() {
          return app.middlewares[name];
        },
        enumerable: false,
        configurable: false,
      });
    }
    //只有在 config 中配置了 appMiddleware 和 coreMiddleware 才会直接在 app.use 中使用,其它中间件只是挂载在 app 上,开发人员可以动态使用
    const middlewareNames = this.config.coreMiddleware.concat(this.config.appMiddleware);
    const middlewaresMap = new Map();
    for (const name of middlewareNames) {
      //如果 config 中定义 middleware 在 app.middlewares 中找不到或者重复定义,都会报错
      if (!app.middlewares[name]) {
        throw new TypeError(`Middleware ${name} not found`);
      }
      if (middlewaresMap.has(name)) {
        throw new TypeError(`Middleware ${name} redefined`);
      }
      middlewaresMap.set(name, true);
      const options = this.config[name] || {};
      let mw = app.middlewares[name];
      //中间件的文件定义必须 exports 一个普通 function ,并且接受两个参数:
      // options: 中间件的配置项,框架会将 app.config[${middlewareName}] 传递进来, app: 当前应用 Application 的实例
      //执行 exports 的函数,生成最终要的中间件
      mw = mw(options, app);
      mw._name = name;
      //包装中间件,最终转换成 async function(ctx, next) 形式
      mw = wrapMiddleware(mw, options);
      if (mw) {
        app.use(mw);
        this.options.logger.info('[egg:loader] Use middleware: %s', name);
      } else {
        this.options.logger.info('[egg:loader] Disable middleware: %s', name);
      }
    }
}

//通过 FileLoader 实例加载指定属性的所有文件并导出,然后将该属性挂载在 app 上
loadToApp(directory, property, opt) {
    const target = this.app[property] = {};
    opt = Object.assign({}, {
      directory,
      target,
      inject: this.app,
    }, opt);
    new FileLoader(opt).load();
}