前言
本次解读主要以归总性做铺垫,再阅读代码的逻辑编写。为什么变了呢,因为这里其实不完全接上一章的内容,整个流程跳过了Agent和Workers,所以就以总结性的理解作为阅读代码的铺垫,否则直接看代码,都不知道从哪看起了。
**
上一章回顾
上一章分析完egg-cluster的最后,明白了Master启动了两类子进程,并且守护着它们。一个由child_process.fork启动的Agent,另一个是cluster.fork(cfork)启动的Workers。由下图引出本章内容:
+--------------+ 实例 +--------------+| Agent 子进程 | --------> | Agent 类 |+--------------+ +---------------+/ \child_process.fork / \ 继承/ \+---------------+ +-------------------+ 继承 +------------+ 继承 +------------+| Master 主进程 | | EggApplication 类 | ------> | EggCore 类 | -----> | Koa |+-------------- + +------------------ + +------------+ +------------+\ /\ / 继承cluster.fork \ /+---------------+ 实例 +----------------+| Worker 子进程 | -------> | Application 类 |+---------------+ +-----------------+
目录结构
.├── History.md├── LICENSE├── README.md├── index.d.ts├── index.js├── lib│ ├── egg.js│ ├── lifecycle.js│ ├── loader│ │ ├── context_loader.js│ │ ├── egg_loader.js│ │ ├── file_loader.js│ │ └── mixin│ │ ├── config.js│ │ ├── controller.js│ │ ├── custom.js│ │ ├── custom_loader.js│ │ ├── extend.js│ │ ├── middleware.js│ │ ├── plugin.js│ │ ├── router.js│ │ └── service.js│ └── utils│ ├── base_context_class.js│ ├── index.js│ ├── sequencify.js│ └── timing.js└── package.json
egg-core的作用
'use strict';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,在Koa的基础上增加了生命周期、loader、重写了Koa的use方法等等。
- EggLoader——集万千加载于一身,将config、controller、middleware、plugin、extend、custom、service、router、custom_loader全部内容整合都一起的类。
- BaseContextClass——在项目中,controller和service都基于该类,只有继承该类才能使用this.ctx获取当前请求的执行上下文对象
- utils——utils里主要是写了几个方法,中间件方法转换、加载文件等等
EggCore
const KoaApplication = require('koa');const BaseContextClass = require('./utils/base_context_class');const Timing = require('./utils/timing');const Lifecycle = require('./lifecycle');// ...省略部分代码class EggCore extends KoaApplication {constructor(options = {}) {options.baseDir = options.baseDir || process.cwd(); // 项目路径options.type = options.type || 'application'; // 'application'或者'agent'super();// 挂载文件的开始时间,结束时间,使用时间this.timing = new Timing();// cache deprecate object by filethis[DEPRECATE] = new Map();this._options = this.options = options;this.deprecate.property(this, '_options', 'app._options is deprecated, use app.options instead');this.console = new EggConsoleLogger();// 将BaseContextClass的类对象挂载在this下的BaseContextClass、Controller、Servicethis.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.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));// 初始化loader对象, Loader的来自'./loader/egg_loader'this.loader = new Loader({baseDir: options.baseDir, // 项目全路径app: this,plugins: options.plugins, // 插件logger: this.console, // 输出serverScope: options.serverScope, //env: options.env, //});}// 重写了koa的use函数use(fn) {assert(is.function(fn), 'app.use() requires a function');debug('use %s', fn._name || fn.name || '-');// 如果是老写法generator函数转成promisethis.middleware.push(utils.middleware(fn));return this;}// 路由,由EggRouter生成get router() {if (this[ROUTER]) {return this[ROUTER];}const router = this[ROUTER] = new Router({ sensitive: true }, this);// register router middlewarethis.beforeStart(() => {this.use(router.middleware());});return router;}}
BaseContextClass
BaseContextClass类代码一共就8行,在项目中使用class XXXService extends Service或者class XXXController extends Controller时,继承的都是该类,所以在写请求接口时可以使用const { ctx, service } = this等等。
class BaseContextClass {constructor(ctx) {this.ctx = ctx;this.app = ctx.app;this.config = ctx.app.config;this.service = ctx.service;}}
utils
utils里主要是写了几个方法,中间件方法转换、加载文件等等
module.exports = {extensions: Module._extensions,loadFile(filepath) {try {// 如果不是模块,直接返回bufferconst extname = path.extname(filepath);if (extname && !Module._extensions[extname]) {return fs.readFileSync(filepath);}// 作为模块引用const obj = require(filepath);if (!obj) return obj;// 如果是es6 模块if (obj.__esModule) return 'default' in obj ? obj.default : obj;return obj;} catch (err) {err.message = `[egg-core] load file: ${filepath}, error: ${err.message}`;throw err;}},// http请求的方法,值得注意的是没有trace,在官方文档说了egg已经做了安全防护,防止通知trace方法攻击,这里应该是对应上了methods: [ 'head', 'options', 'get', 'put', 'patch', 'post', 'delete' ],// 改造generator函数async callFn(fn, args, ctx) {args = args || [];if (!is.function(fn)) return;if (is.generatorFunction(fn)) fn = co.wrap(fn);return ctx ? fn.call(ctx, ...args) : fn(...args);},// 改造中间件的generator函数middleware(fn) {return is.generatorFunction(fn) ? convert(fn) : fn;},getCalleeFromStack(withLine, stackIndex) {stackIndex = stackIndex === undefined ? 2 : stackIndex;const limit = Error.stackTraceLimit;const prep = Error.prepareStackTrace;Error.prepareStackTrace = prepareObjectStackTrace;Error.stackTraceLimit = 5;// capture the stackconst obj = {};Error.captureStackTrace(obj);let callSite = obj.stack[stackIndex];let fileName;/* istanbul ignore else */if (callSite) {// egg-mock will create a proxy// https://github.com/eggjs/egg-mock/blob/master/lib/app.js#L174fileName = callSite.getFileName();/* istanbul ignore if */if (fileName && fileName.endsWith('egg-mock/lib/app.js')) {// TODO: add testcallSite = obj.stack[stackIndex + 1];fileName = callSite.getFileName();}}Error.prepareStackTrace = prep;Error.stackTraceLimit = limit;/* istanbul ignore if */if (!callSite || !fileName) return '<anonymous>';if (!withLine) return fileName;return `${fileName}:${callSite.getLineNumber()}:${callSite.getColumnNumber()}`;},getResolvedFilename(filepath, baseDir) {const reg = /[/\\]/g;return filepath.replace(baseDir + path.sep, '').replace(reg, '/');},};
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的总结往上面看,不再重复。
