前言
egg框架的应用过程中会发现有一些十分不便和优雅的中央,比方对各个环境下配置的合并和加载,对controller,service,middleware的集成和建设关联,对插件扩大等,从源码中能够发现egg是继承于egg-core的,而这些外围逻辑的实现都是在egg-core里实现的,因而能够说egg框架的外围在于egg-core。上面就对egg-core的源码进行一些解读,来领会框架设计的精妙之处。
模块关系
egg-core源码的入口导出了EggCore、EggLoader、BaseContextClass、utils 四个模块,其中EggCore类是基类,做一些初始化工作;EggLoader类是最外围的一个局部,对整个框架的controller,service,middleware等进行初始化和加载集成,并建设互相关联。BaseContextClass是另一个基类, 用来接管ctx对象,挂载在上下文上,egg框架中的 controller 和 service 都继承该类,所以都能通过this.ctx拿到上下文对象。utils则是定义了一些框架中用到的办法。看一张图片会比拟清晰:
外围模块
egg-core
egg-core继承于 Koa ,在该类中次要实现一些初始化工作,大略能够分为
- 对初始化参数的解决,包含对传入的利用的目录,运行的类型的判断等。
constructor(options = {}) {
options.baseDir = options.baseDir || process.cwd();
options.type = options.type || 'application';
assert(typeof options.baseDir === 'string', 'options.baseDir required, and must be a string');
assert(fs.existsSync(options.baseDir), `Directory ${options.baseDir} not exists`);
assert(fs.statSync(options.baseDir).isDirectory(), `Directory ${options.baseDir} is not a directory`);
assert(options.type === 'application' || options.type === 'agent', 'options.type should be application or agent');
- 要害属性的初始化和挂载,包含Controller、Service、lifecycle、loader、router等。
this.console = new EggConsoleLogger();
this.BaseContextClass = BaseContextClass;
const Controller = this.BaseContextClass;
this.Controller = Controller;
const Service = this.BaseContextClass;
this.Service = Service;
this.lifecycle = new Lifecycle({
baseDir: options.baseDir,
app: this,
logger: this.console,
});
this.loader = new Loader({
baseDir: options.baseDir,
app: this,
plugins: options.plugins,
logger: this.console,
serverScope: options.serverScope,
env: options.env,
});
- 生命周期函数的初始化和监听,中间件use办法的定义。
beforeStart(scope) {
this.lifecycle.registerBeforeStart(scope);
}
ready(flagOrFunction) {
return this.lifecycle.ready(flagOrFunction);
}
beforeClose(fn) {
this.lifecycle.registerBeforeClose(fn);
}
use(fn) {
assert(is.function(fn), 'app.use() requires a function');
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(utils.middleware(fn));
return this;
}
egg-loader
整个框架目录构造(controller,service,middleware,extend,router)的加载和初始化工作都在该类中实现的。egg-loader中定义了一系列初始化的全局办法和加载loader的根底办法。将所有离开写在各个文件中的loader办法对立在该类中引入进行加载,会依据目录构造标准将目录构造中的 config,controller,service,middleware,plugin,router 等文件 load 到 app 或者 context 上,最终挂载输入内容。开发人员只有依照这套约定标准,就能够很不便进行开发。
// 加载文件
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);
}
return ret;
}
requireFile(filepath) {
const timingKey = `Require(${this[REQUIRE_COUNT]++}) ${utils.getResolvedFilename(filepath, this.options.baseDir)}`;
this.timing.start(timingKey);
const ret = utils.loadFile(filepath);
this.timing.end(timingKey);
return ret;
}
// 挂载到App上
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);
}
// 挂载到上下文上
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);
}
在加载的过程中会用到 file_loader 和 context_loader 两个类,这两个是Load加载流程中的根底类,提供了根底的加载上下文和办法,这个在前面会有细说。
具体loader模块
err-loader中会将具体的一些loader模块require进来并挂载在原型链上,这样在err-loader中就能够拜访到具体的loader模块中的办法。这些loader模块包含plugin、config、service、middleware、controller等,别离负责各自的一些逻辑,并且模块间也存在着相干的分割。上面这张图中大抵标注出了各个loader的依赖关系,比方加载 middleware 时会用到 config 对于利用中间件的配置,对外部中间件进行 use 的被动加载。具体的关系图如下:
上面会挑其中的重点Loader进行具体分析:
plugin_loader
首先来看插件加载模块,该模块是err-core中一个十分重要的模块,egg-core中的插件大抵能够分为3类:框架级插件、利用级插件、用户自定义插件。这3种插件如何进行共存和笼罩,如何依据环境变量和开启开关进行加载?这就是plugin-loader中做的管制。该模块整体做了4件事件:
- 从以上说的3类插件的目录中读取插件,并依照框架级插件、利用级插件、用户自定义插件的程序进行插件的加载和笼罩,前面的插件会笼罩后面的插件,失去最初合并后的插件。
- 依据以后环境变量和插件的配置对插件是否开启进行解决,因为有一些插件只有在特定的环境下才会开启。
- 对所有的框架进行依赖关系的检查和相应的解决,如果有依赖插件的缺失或者循环援用,会抛出谬误。如果有依赖关系的插件没有开启,那么也会将改插件开启。
- 通过以上3步解决后,将最终合乎开启条件的插件对象挂载在 this 对象上,实现插件的解决流程。
file_loader
这是一个根底的loader模块,通过提供一个 load 函数对工程的目录构造和文件内容进行解析,这个函数如下,其外围在于调用了parse办法对文件门路进行解析,对解析后的数组中的对象的属性进行从新宰割解决。
load() {
const items = this.parse();
//items的模式: [{ properties: [ 'a', 'b', 'c'], exports1,fullpath1}, { properties: [ 'a', 'b', 'c'], exports2,fullpath2}]
const target = this.options.target;
for (const item of items) {
debug('loading item %j', item);
// item { properties: [ 'a', 'b', 'c'], exports }
// => target = {a: {b: {c: exports1, d: exports2}}}
// => 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;
}
对应的parse办法如下,代码中曾经进行了要害语句的正文
parse() {
//最终生成 [{ properties: [ 'a', 'b', 'c'], exports,fullpath}] 模式,
//properties 文件门路名称的数组, exports 是导出对象, fullpath 是文件的绝对路径
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 ignore = this.options.ignore;
if (ignore) {
ignore = Array.isArray(ignore) ? ignore : [ ignore ];
ignore = ignore.filter(f => !!f).map(f => '!' + f);
files = files.concat(ignore);
}
//文件目录转换为数组
let directories = this.options.directory;
if (!Array.isArray(directories)) {
directories = [ directories ];
}
const filter = is.function(this.options.filter) ? this.options.filter : null;
const items = [];
debug('parsing %j', directories);
for (const directory of directories) {
//每个文件目录上面可能还会有子文件夹,所以 globby.sync 函数是获取所有文件包含子文件下的文件的门路
const filepaths = globby.sync(files, { cwd: directory });
for (const filepath of filepaths) {
// 拼接成残缺文件门路
// app/service/foo/bar.js
const fullpath = path.join(directory, filepath);
// 如果不是文件跳过,进行下一次循环
if (!fs.statSync(fullpath).isFile()) continue;
// get properties
// foo/bar.js => ['foo', 'bar' ]
const properties = getProperties(filepath, this.options);
// app/service ['foo', 'bar' ] => service.foo.bar
const pathName = directory.split(/[/\\]/).slice(-1) + '.' + properties.join('.');
// get exports from the file
const exports = getExports(fullpath, this.options, pathName);
// ignore exports when it's null or false returned by filter function
if (exports == null || (filter && filter(exports) === false)) continue;
// set properties of class
if (is.class(exports)) {
exports.prototype.pathName = pathName;
exports.prototype.fullPath = fullpath;
}
items.push({ fullpath, properties, exports });
debug('parse %s, properties %j, export %j', fullpath, properties, exports);
}
}
return items;
}
}
最初会返回一个解析后的 target 对象,对象的层级构造跟目录构造绝对应,最内层是文件的导出对象或者办法。大略的格局如下:
target = {
a: {
b: {
c: exports1,
d: exports2,
}
}
}
导出的这个 target 对象会在 context_loader 里用到。service、controller的loader实现 均借助了该基类。
context_loader
context_loader类是用于解决上下文挂载的基类,继承于file_loader。上文说了 file_loader 的作用是对文件目录的解析生成 target 对象。而 context_loader 类就是在这根底上进一步实现了 target 对象的挂载。用于将 FileLoader 解析进去的 target 挂载在 app.context 上 对应传入的 property 属性上。
service_loader
service_loader解决service文件夹下文件的加载,该模块中间接就导出了一个 loadService 的办法。该办法把service的文件目录(‘app/service’) 和 解析后须要挂载的属性(’service’)作为参数传入 egg_loader的 loadToContext 办法中,loadToContext办法会创立一个 ContextLoader 类的实例,并调用其load()办法,通过上文说的 file_loader 和 context_loader 中的外围逻辑,实现将 app/service 文件夹下的文件门路和导出解析成target对象,最终挂载在app.context.service下。
middleware_loader
中间件的加载,次要做了3件事:
- 将通过 FileLoader 实例加载 app/middleware 目录下的所有文件并导出,而后将 middleware 挂载在 app 上,能够通过app.middleware进行拜访
- 对中间件函数进行包装,对立解决成async function(ctx, next) 模式
- 对在 config 中配置了 appMiddleware 和 coreMiddleware 的中间件间接调用 app.use 应用,其它中间件只是挂载在 app 上。
controller_loader
constroller的加载跟service还是有区别的:
- constroller挂载在app上,service挂载在app.ctx上,constroller的调用只须要拜访到对应的service名称,而service的调用须要具体到导出的函数,因而两者应用egg_loader中办法不同,一个是loadToApp(调用FileLoader实例),一个是LoadToContext(调用ContextLoader实例)
- controller 中生成的函数最终还是在 router.js 中当作一个中间件应用,所以咱们须要将 controller 中内容转换为中间件模式 async function(ctx, next) ,这跟service相比在调用FileLoad类实例的load函数时就要多传一个 initializer 函数,对exports的内容进行解决。
具体controller_loader类中做的事件也是围绕以上两点,解析 app/controller 文件目录生成targe对象,实现在app上对 controller 属性的挂载。同时对 initializer 函数进行了各种状况下的解决。
config_loader
config_loader对整个利用的配置加载做了治理,会依据以后环境的不同,加载不同的配置环境,并和默认的配置合并后失去最终的配置。config_loader对配置维度的加载有2个维度,大的维度来说,先会加载plugin的配置文件,再加载framework的配置文件,最初才是app的配置文件。小的维度来说会先从根本门路下的config/config 目录下加载默认的配置文件,而后依据以后的serverEnv的不同,加载不同环境的配置文件,最初将以后环境下的配置文件和默认的配置文件进行合并失去最终的配置文件。总结来说:先别离依照plugin、framework、app的程序合并失去默认的配置和以后环境下的配置。而后用合并默认的配置和以后环境下的配置,失去的才是最终的配置。
router_loader
router_loader中其实就做了加载一下 app/router 目录下的文件而已。这是因为具体的router的逻辑,都交给了eggcore中的router属性,而而 router 又是 Router 类的实例,Router 类是基于 koa-router 实现的。所以egg中对于router的原理跟koa大抵是雷同的,这里就不开展说了。
总结
看完 egg-core 的源码之后,还是有很多播种的,在我看来有以下几点值得借鉴和思考:
- 标准和代码格调的重要性,在多人单干中这一点尤其重要。而egg-core则通过定义和实现了对于目录解析和属性挂载的这一套标准,解决了标准一致性的问题,同时通过controller、service的分层设计,让代码的可读性和易维护性也失去了大大加强。
- 框架的扩大和继承的设计,err-core自身是根底koa的,而自身也被egg继承。通过这种框架之间的继承能够依据理论需要不便地构建出须要的框架。而具体到外面的各个load类,也是通过先定义了 fileLoader 和 contextLoader 两个基类,被其余loader类频繁地进行依赖和调用。而 egg_loader类 和其余 loader 类也是解耦的,将其余 loader类 加载到egg_loader类的原型链上进行拜访,入口对立,内容代码独立,体现了很好的设计思维。
- Symbol和Symbol.for的应用,对已有或缓存的内容的判断和加载通过了Symbol来实现,跟用全局Map对象进行保护更优雅和不便。
发表回复