赞
踩
我们团队现在开发的node项目都是基于 Koa 框架实现的,虽然现在也形成了一套团队内的标准,但是在开发的过程中也遇到了一些问题:
而阿里团队基于 Koa 开发的 Egg 框架,基于一套统一约定进行应用开发,很好的解决了我们遇到的一些问题,看了 Egg 的官方开发文档 后,比较好奇它是怎么把 controller,service,middleware,extend,route.js 等关联在一起并加载的,后面看了源码发现这块逻辑主要在 egg-core 这个库中实现的,所以关于自己对egg-core源码的学习收获做一个总结:
在学习 egg-core 是什么之前,我们先了解一下关于 Egg 框架中应用、框架、插件这三个概念及其之间的关系:
- # 加载单元的目录结构如下图,其中插件和框架没有 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.js 的大部分核心代码实现都在 egg-core 库 中,egg-core 主要 export 四个对象:
所以 egg-core 做的主要事情就是根据 loadUnit 的目录结构规范,将目录结构中的 config,controller,service,middleware,plugin,router 等文件 load 到 app 或者 context 上,开发人员只要按照这套约定规范,就可以很方便进行开发,以下是 egg-core 的 exports 对象源码:
- // 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 类是算是上文提到的框架范畴,它从 Koa 类继承而来,并做了一些初始化工作,其中有三个主要属性是:
- // 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,
- });
- }
如果说 EggCore 是 Egg 框架的精华所在,那么 EggLoader 可以说是 EggCore 的精华所在,下面我们主要从 EggLoader 的实现细节开始学习 EggCore 这个库:
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;
- }
- }
上文中只是介绍了 EggLoader 中的一些基本属性和函数,那么如何将 loadUnits 中的不同类型的文件分别加载进来呢,eggCore 中每一种类型(service/controller 等)的文件加载都在一个独立的文件里实现。比如我们加载 controller 文件可以通过 './mixin/controller' 目录下的 loadController 完成,加载 service 文件可以通过 './mixin/service' 下的 loadService 函数完成,然后将这些方法挂载 EggLoader 的原型上,这样就可以直接在 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);
- }
我们按照上述 loaders 中定义的元素顺序,对各个 load 函数的源码实现进行一一分析:
插件是一个迷你的应用,没有包含 router.js 和 controller 文件夹,我们上文也提到,应用和框架里都可以包含插件,而且还可以通过环境变量和初始化参数传入,关于插件初始化的几个参数:
- // 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;
- }
配置信息的管理对于一个应用来说非常重要,我们需要对不同的部署环境的配置进行管理,Egg 就是针对环境加载不同的配置文件,然后将配置挂载在 app 上,
加载 config 的逻辑相对简单,就是按照顺序加载所有 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 下的配置顺序进行加载
下面看一下 loadExtend 这个函数的实现,一个通用的加载函数:
- // 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 函数,loadController函数, loadRouter函数的实现源码分析,请看下文
张佃鹏:Egg 源码分析之 egg-core(二)zhuanlan.zhihu.comCopyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。