前言
本次解读主要以归总性做铺垫,再阅读代码的逻辑编写。为什么变了呢,因为这里其实不完全接上一章的内容,整个流程跳过了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 file
this[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、Service
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.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函数转成promise
this.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 middleware
this.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 {
// 如果不是模块,直接返回buffer
const 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 stack
const 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#L174
fileName = callSite.getFileName();
/* istanbul ignore if */
if (fileName && fileName.endsWith('egg-mock/lib/app.js')) {
// TODO: add test
callSite = 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的总结往上面看,不再重复。