从KOA开始
KOA 是什么?
KOA 从设计理念上来说并不是一个完整的应用程序框架,相反更多的可以视为是 node.js 的 http
模块的抽象。
KOA 源码分析
-
我们从使用的角度入手,
const Koa = require('koa'); const app = new Koa(); // middleware app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); }); // response app.use(ctx => { ctx.body = 'Hello Koa'; }); app.listen(3000);
可以看到 核心方法,
use
,这个方法也是KOA洋葱圈模型的核心。 -
再看看koa的文件目录
很简单,只有四个文件
- application.js 应用 暴露给调用方使用 - context 上下文 表示一次请求 包含了 request 和 response 对象 - request 请求 - response 响应
主要的关键逻辑在于 application.js 这个文件,仔细阅读后发现
-
Application 继承于 Emitter, 所以可以直接使用EventEmitter的事件机制,即 emit, on 的方式进行消息传递
// application.js class Application extends Emitter { super(); ... this.env = options.env || process.env.NODE_ENV || 'development'; this.middleware = []; // 中间件 this.context = Object.create(context); // 请求上下文 this.request = Object.create(request); // 请求 this.response = Object.create(response); // 响应 ... } // context.js this.app.emit('error', err, this);
-
context 是如何抽象出来的呢?
// application.js // 1. listen 监听 callback 方法 listen(...args) { debug('listen'); const server = http.createServer(this.callback()); return server.listen(...args); } // 2. callback 方法里为每次请求都会调用 createContext 方法 callback() { const fn = compose(this.middleware); if (!this.listenerCount('error')) this.on('error', this.onerror); const handleRequest = (req, res) => { const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn); }; return handleRequest; } // 3. createContext 将request 和 response 绑定到 ctx 上 createContext(req, res) { const context = Object.create(this.context); const request = context.request = Object.create(this.request); const response = context.response = Object.create(this.response); ... return context; }
-
KOA的中间件是怎么实现的呢?
// application.js // 我们看到callback里有handleRequest的片段 const fn = compose(this.middleware); const handleRequest = (req, res) => { const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn); }; // 可以看到handleRequest传入的fnMiddleware类型是Promise handleRequest(ctx, fnMiddleware) { const res = ctx.res; res.statusCode = 404; const onerror = err => ctx.onerror(err); const handleResponse = () => respond(ctx); onFinished(res, onerror); return fnMiddleware(ctx).then(handleResponse).catch(onerror); } // 于是我们找到 compose 方法的实现 koa-compose function compose (middleware) { if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!') for (const fn of middleware) { if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!') } // 关键的代码片段,将 middleware: (Context, Function)=> Promise 数组组装成一个Promise return function (context, next) { // last called middleware # let index = -1 return dispatch(0) function dispatch (i) { if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i let fn = middleware[i] if (i === middleware.length) fn = next if (!fn) return Promise.resolve() try { return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } } } }
-
KOA 扩展
Koa 没有任何内置路由,但是有 koa-router 和 koa-route 第三方库可用。
Egg.js
Egg.js 是什么?
在KOA的基础上,奉行约定优于配置,并提供了默认的多进程管理的一套比较完备的开发框架。
具体来看一下一个典型的Egg.js项目的目录
├── package.json
├── app.js (optional) // Worker 进程的启动入口
├── agent.js (optional) // 后台运行 进程的启动入口
├── app
| ├── router.js // 路由 一般
│ ├── controller // 控制器
│ │ └── home.js
| ├── extend (optional) // 对内置对象的扩展
│ | ├── helper.js (optional)
│ | ├── filter.js (optional)
│ | ├── request.js (optional)
│ | ├── response.js (optional)
│ | ├── context.js (optional)
│ | ├── application.js (optional)
│ | └── agent.js (optional)
│ ├── service (optional) // 服务
│ ├── middleware (optional) // 中间件
│ │ └── response_time.js
│ └── view (optional)
| ├── layout.html
│ └── home.html
├── config // 配置
| ├── config.default.js
│ ├── config.prod.js
| ├── config.test.js (optional)
| ├── config.local.js (optional)
| ├── config.unittest.js (optional)
│ └── plugin.js
Egg.js 文档研读
看完Egg.js文档,了解一下基本的概念。
Egg.js 源码分析
index.js
首先打开egg.js/index.js看看暴露了哪些方法,大致可以看出提供了哪些核心功能
// 多进程管理 Master
exports.startCluster = require('egg-cluster').startCluster;
// 单进程启动
exports.start = require('./lib/start');
// 类似 KOA 的 Application,表示应用
exports.Application = require('./lib/application');
// Agent 进程 暴露出来方便扩展
exports.Agent = require('./lib/agent');
// 用来加载 Worker 进程
exports.AppWorkerLoader = require('./lib/loader').AppWorkerLoader;
// 用来加载 Agent 进程
exports.AgentWorkerLoader = require('./lib/loader').AgentWorkerLoader;
// 对应MVC里的Controller,解析用户的输入,处理后返回相应的结果
exports.Controller = require('./lib/core/base_context_class');
// 对应MVC里的Service,做业务逻辑封装的一个抽象层
exports.Service = require('./lib/core/base_context_class');
// 定时器
exports.Subscription = require('./lib/core/base_context_class');
// 可以认为是一个接口,在这个类中注入了 ctx, app, config, service
exports.BaseContextClass = require('./lib/core/base_context_class');
// 启动 暴露出来方便扩展
exports.Boot = require('./lib/core/base_hook_class');
启动
查看 egg.js/lib/start.js,这里是单进程启动的入口,看到核心部分。
注意部分我们不关心的代码已经省略了,后面所有出现代码的部分都有所删减。
let Agent;
let Application;
// 框架允许从package.egg.framework中覆盖 Application 和 Agent, 用来扩展
if (options.framework) {
Agent = require(options.framework).Agent;
Application = require(options.framework).Application;
} else {
Application = require('./application');
Agent = require('./agent');
}
// agent 是用来单独处理后台任务的进程
const agent = new Agent(Object.assign({}, options));
await agent.ready();
const application = new Application(Object.assign({}, options));
application.agent = agent;
agent.application = application;
await application.ready();
// 可以看到 application 里有一个很重要的属性,messenger, 也就是文档里说的 IPC 通道。
// emit egg-ready message in agent and application
application.messenger.broadcast('egg-ready');
可以看到 启动的时候 做了两件事情:
agent.ready()
application.ready()
application.ready
于是我们来看看 Application 到底是什么,做了些什么。
// lib/application.js
class Application extends EggApplication {
constructor(options = {}) {
options.type = 'application';
super(options);
try {
this.loader.load();
} catch (e) {
// close gracefully
this[CLUSTER_CLIENTS].forEach(cluster.close);
throw e;
}
}
}
可以看到构造函数这里主要做了两件事情:
super(options)
this.loader.load()
但是我们在这里没有看到 this.loader
的声明 也没有看到 start 启动的时候 ready
方法,所以我们继续去看 EggApplication 的代码
/**
* Base on koa's Application
* @see https://github.com/eggjs/egg-core
* @see http://koajs.com/#application
* @extends EggCore
*/
class EggApplication extends EggCore {
/**
* @class
* @param {Object} options
* - {Object} [type] - type of instance, Agent and Application both extend koa, type can determine what it is.
* - {String} [baseDir] - app root dir, default is `process.cwd()`
* - {Object} [plugins] - custom plugin config, use it in unittest
* - {String} [mode] - process mode, can be cluster / single, default is `cluster`
*/
constructor(options = {}) {
options.mode = options.mode || 'cluster';
super(options);
// 加载配置
this.loader.loadConfig();
/**
* messenger instance
* @member {Messenger}
* @since 1.0.0
*/
this.messenger = Messenger.create(this);
// trigger serverDidReady hook when all app workers
// and agent worker is ready
this.messenger.once('egg-ready', () => {
this.lifecycle.triggerServerDidReady();
});
this[CLUSTER_CLIENTS] = [];
/**
* Wrap the Client with Leader/Follower Pattern
*
* @description almost the same as Agent.cluster API, the only different is that this method create Follower.
*
* @see https://github.com/node-modules/cluster-client
* @param {Function} clientClass - client class function
* @param {Object} [options]
* - {Boolean} [autoGenerate] - whether generate delegate rule automatically, default is true
* - {Function} [formatKey] - a method to tranform the subscription info into a string,default is JSON.stringify
* - {Object} [transcode|JSON.stringify/parse]
* - {Function} encode - custom serialize method
* - {Function} decode - custom deserialize method
* - {Boolean} [isBroadcast] - whether broadcast subscrption result to all followers or just one, default is true
* - {Number} [responseTimeout] - response timeout, default is 3 seconds
* - {Number} [maxWaitTime|30000] - leader startup max time, default is 30 seconds
* @return {ClientWrapper} wrapper
*/
this.cluster = (clientClass, options) => {
options = Object.assign({}, this.config.clusterClient, options, {
singleMode: this.options.mode === 'single',
// cluster need a port that can't conflict on the environment
port: this.options.clusterPort,
// agent worker is leader, app workers are follower
isLeader: this.type === 'agent',
logger: this.coreLogger,
});
const client = cluster(clientClass, options);
this._patchClusterClient(client);
return client;
};
}
}
这里依然没有出现 ready 和 loader 的实现,但是出现了两行重要的东西
- ` this.cluster = …` 先标记一下,大概知道这里是 多进程管理的部分,之后再具体分析
this.messenger = Messenger.create(this);
同样这里也只先知道这里是 IPC 通信的部分。
继续看 EggCore 的实现,
class EggCore extends KoaApplication {
constructor(options = {}) {
// 这里表明 EggCore 是 application 或者 agent 的抽象
assert(options.type === 'application' || options.type === 'agent', 'options.type should be application or agent');
super();
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));
/**
* The loader instance, the default class is {@link EggLoader}.
* If you want define
* @member {EggLoader} EggCore#loader
* @since 1.0.0
*/
const Loader = this[EGG_LOADER];
assert(Loader, 'Symbol.for(\'egg#loader\') is required');
this.loader = new Loader({
baseDir: options.baseDir,
app: this,
plugins: options.plugins,
logger: this.console,
serverScope: options.serverScope,
env: options.env,
});
}
ready(flagOrFunction) {
return this.lifecycle.ready(flagOrFunction);
}
/**
* Create egg context
* @function EggApplication#createContext
* @param {Req} req - node native Request object
* @param {Res} res - node native Response object
* @return {Context} context object
*/
createContext(req, res) {
const app = this;
const context = Object.create(app.context);
const request = context.request = Object.create(app.request);
const response = context.response = Object.create(app.response);
context.app = request.app = response.app = app;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
return context;
}
}
终于我们看到了 this.loader
和 ready
的声明,于此同时又有一段很重要的声明:
this.lifecycle = new Lifecycle
我们也先mark 一下。
先把 loader 搞明白
class EggLoader {
/**
* @class
* @param {Object} options - options
* @param {String} options.baseDir - the directory of application
* @param {EggCore} options.app - Application instance
* @param {Logger} options.logger - logger
* @param {Object} [options.plugins] - custom plugins
* @since 1.0.0
*/
constructor(options) {
this.options = options;
this.app = this.options.app;
this.lifecycle = this.app.lifecycle;
// package.json
this.pkg = utility.readJSONSync(path.join(this.options.baseDir, 'package.json'));
/**
* All framework directories.
*
* You can extend Application of egg, the entry point is options.app,
*
* loader will find all directories from the prototype of Application,
* you should define `Symbol.for('egg#eggPath')` property.
*
* ```
* // lib/example.js
* const egg = require('egg');
* class ExampleApplication extends egg.Application {
* constructor(options) {
* super(options);
* }
*
* get [Symbol.for('egg#eggPath')]() {
* return path.join(__dirname, '..');
* }
* }
* ```
* @member {Array} EggLoader#eggPaths
* @see EggLoader#getEggPaths
* @since 1.0.0
*/
this.eggPaths = this.getEggPaths();
/**
* @member {String} EggLoader#serverEnv
* @see AppInfo#env
* @since 1.0.0
*/
this.serverEnv = this.getServerEnv();
/**
* @member {AppInfo} EggLoader#appInfo
* @since 1.0.0
*/
this.appInfo = this.getAppInfo();
/**
* @member {String} EggLoader#serverScope
* @see AppInfo#serverScope
*/
this.serverScope = options.serverScope !== undefined
? options.serverScope
: this.getServerScope();
}
/**
* Load files using {@link FileLoader}, inject to {@link Application}
* @param {String|Array} directory - see {@link FileLoader}
* @param {String} property - see {@link FileLoader}
* @param {Object} opt - see {@link FileLoader}
* @since 1.0.0
*/
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);
}
/**
* Load files using {@link ContextLoader}
* @param {String|Array} directory - see {@link ContextLoader}
* @param {String} property - see {@link ContextLoader}
* @param {Object} opt - see {@link ContextLoader}
* @since 1.0.0
*/
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);
}
}
/**
* Mixin methods to EggLoader
* // ES6 Multiple Inheritance
* https://medium.com/@leocavalcante/es6-multiple-inheritance-73a3c66d2b6b
*/
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'),
];
for (const loader of loaders) {
Object.assign(EggLoader.prototype, loader);
}
module.exports = EggLoader;
可以看到这里封装了 依赖信息 pkg 和 文件目录 eggPath 以及 运行环境serverEnv, 并且 通过 loadToApp 和 loadToContext方法 , mixin 了 各个类型的 文件的 加载方法。
这里拿 controller 和 service 为案例来分析一个具体的加载过程。
- controller 通过
this.loadToApp
以 FileLoader 的方式来加载文件
class FileLoader {
constructor(options) {
assert(options.directory, 'options.directory is required');
assert(options.target, 'options.target is required');
this.options = Object.assign({}, defaults, options);
}
// 将目录下的文件解析并挂载到 target 上 如 this.app.controller
load() {
const items = this.parse();
const target = this.options.target;
for (const item of items) {
// item { properties: [ 'a', 'b', 'c'], exports }
// => 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;
}
// 将目录下的文件包装成 { fullpath: string, properties: string[], exports: Function }[]
parse() {
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 directories = this.options.directory;
if (!Array.isArray(directories)) {
directories = [ directories ];
}
const filter = is.function(this.options.filter) ? this.options.filter : null;
const items = [];
for (const directory of directories) {
const filepaths = globby.sync(files, { cwd: directory });
for (const filepath of filepaths) {
const fullpath = path.join(directory, filepath);
if (!fs.statSync(fullpath).isFile()) continue;
const properties = getProperties(filepath, this.options);
const pathName = directory.split(/[/\\]/).slice(-1) + '.' + properties.join('.');
// get exports from the file
const exports = getExports(fullpath, this.options, pathName);
if (is.class(exports)) {
exports.prototype.pathName = pathName;
exports.prototype.fullPath = fullpath;
}
items.push({ fullpath, properties, exports });
}
}
return items;
}
}
-
service 通过
this.loadToContext
以 ContextLoader 的方式来加载文件class ContextLoader extends FileLoader { constructor(options) { const target = options.target = {}; if (options.fieldClass) { options.inject[options.fieldClass] = target; } super(options); const app = this.options.inject; const property = options.property; // define ctx.service Object.defineProperty(app.context, property, { get() { if (!this[CLASSLOADER]) { this[CLASSLOADER] = new Map(); } // 区分属性缓存 // cache's lifecycle is the same with this context instance // e.x. ctx.service1 and ctx.service2 have different cache const classLoader = this[CLASSLOADER]; let instance = classLoader.get(property); if (!instance) { instance = getInstance(target, this); classLoader.set(property, instance); } return instance; }, }); } }
注意这里是继承FileLoader的基本逻辑,只是同时挂载到了 app.context 上。
agent.ready
看看 agent.js
**
* Singleton instance in Agent Worker, extend {@link EggApplication}
* @extends EggApplication
*/
class Agent extends EggApplication {
/**
* @class
* @param {Object} options - see {@link EggApplication}
*/
constructor(options = {}) {
options.type = 'agent';
super(options);
this.loader.load();
// keep agent alive even it doesn't have any io tasks
this.agentAliveHandler = setInterval(() => {}, 24 * 60 * 60 * 1000);
}
get [EGG_LOADER]() {
return AgentWorkerLoader;
}
}
Agent 继承自 EggApplication , 也就是说加载的基本逻辑是相同的。 不同的是 this.loader
指向的类 是 AgentWorkerLoader,并且需要保活。
再看看 AgentWorkerLoader
*/
class AgentWorkerLoader extends EggLoader {
/**
* loadPlugin first, then loadConfig
*/
loadConfig() {
this.loadPlugin();
super.loadConfig();
}
load() {
this.loadAgentExtend();
this.loadContextExtend();
this.loadCustomAgent();
}
}
也就是 agent 不需要处理 service 或者 controller。
lifecycle
好了,现在来看下 lifecycle
class Lifecycle extends EventEmitter {
/**
* @param {object} options - options
* @param {String} options.baseDir - the directory of application
* @param {EggCore} options.app - Application instance
* @param {Logger} options.logger - logger
*/
constructor(options) {
super();
this.options = options;
this[BOOT_HOOKS] = [];
this[BOOTS] = [];
this[CLOSE_SET] = new Set();
this[IS_CLOSED] = false;
this[INIT] = false;
getReady.mixin(this);
this.timing.start('Application Start');
// get app timeout from env or use default timeout 10 second
const eggReadyTimeoutEnv = Number.parseInt(process.env.EGG_READY_TIMEOUT_ENV || 10000);
this.readyTimeout = eggReadyTimeoutEnv;
this[INIT_READY]();
this.ready(err => {
this.triggerDidReady(err);
this.timing.end('Application Start');
});
}
/**
* init boots and trigger config did config
*/
init() {
assert(this[INIT] === false, 'lifecycle have been init');
this[INIT] = true;
this[BOOTS] = this[BOOT_HOOKS].map(t => new t(this.app));
}
registerBeforeStart(scope) {
this[REGISTER_READY_CALLBACK]({
scope,
ready: this.loadReady,
timingKeyPrefix: 'Before Start',
});
}
registerBeforeClose(fn) {
assert(is.function(fn), 'argument should be function');
assert(this[IS_CLOSED] === false, 'app has been closed');
this[CLOSE_SET].add(fn);
}
}
有点难看懂,但是我们从暴露出来的方法可以看出来, 这是为了处理 EggApplication 的生命周期事件,提供了到某个阶段触发 hook 的功能。
注意这里用到了 getReady
包,用来添加只使用一次的事件。
多进程管理
前面提到了 Application 和 Agent 都继承于 EggApplication, 每个 EggApplication 可以理解成一个进程。
我们知道 Node.js 主线程是单线程,无法有效的利用多核CPU的性能,所以 node 提供了 node cluster的解决方案。注意 node cluster 的 IPC 通道底层是 libuv
提供的,具体可以参考NodeJs 子进程与 ipc。
我们来扒代码,注意到 之前的 startCluster
方法
/**
* cluster start flow:
*
* [startCluster] -> master -> agent_worker -> new [Agent] -> agentWorkerLoader
* `-> app_worker -> new [Application] -> appWorkerLoader
*
*/
/**
* start egg app
* @function Egg#startCluster
* @param {Object} options {@link Master}
* @param {Function} callback start success callback
*/
exports.startCluster = function(options, callback) {
new Master(options).ready(callback);
};
可以看到是先启动了 Master
class Master extends EventEmitter {
/**
* @class
* @param {Object} options
* - {String} [framework] - specify framework that can be absolute path or npm package
* - {String} [baseDir] directory of application, default to `process.cwd()`
* - {Object} [plugins] - customized plugins, for unittest
* - {Number} [workers] numbers of app workers, default to `os.cpus().length`
* - {Number} [port] listening port, default to 7001(http) or 8443(https)
* - {Object} [https] https options, { key, cert, ca }, full path
* - {Array|String} [require] will inject into worker/agent process
* - {String} [pidFile] will save master pid to this file
*/
constructor(options) {
super();
this.options = parseOptions(options);
this.workerManager = new Manager();
this.messenger = new Messenger(this);
ready.mixin(this);
this.ready(() => {
const action = 'egg-ready';
this.messenger.send({
action,
to: 'parent',
data: {
port: this[REAL_PORT],
address: this[APP_ADDRESS],
protocol: this[PROTOCOL],
},
});
this.messenger.send({
action,
to: 'app',
data: this.options,
});
this.messenger.send({
action,
to: 'agent',
data: this.options,
});
});
}
我们又看到了熟悉的 messager 对象了
class Messenger {
constructor(master) {
this.master = master;
this.hasParent = !!process.send;
process.on('message', msg => {
msg.from = 'parent';
this.send(msg);
});
process.once('disconnect', () => {
this.hasParent = false;
});
}
/**
* send message
* @param {Object} data message body
* - {String} from from who
* - {String} to to who
*/
send(data) {
if (!data.from) {
data.from = 'master';
}
// recognise receiverPid is to who
if (data.receiverPid) {
if (data.receiverPid === String(process.pid)) {
data.to = 'master';
} else if (data.receiverPid === String(this.master.agentWorker.pid)) {
data.to = 'agent';
} else {
data.to = 'app';
}
}
// default from -> to rules
if (!data.to) {
if (data.from === 'agent') data.to = 'app';
if (data.from === 'app') data.to = 'agent';
if (data.from === 'parent') data.to = 'master';
}
// app -> master
// agent -> master
if (data.to === 'master') {
// app/agent to master
this.sendToMaster(data);
return;
}
// master -> parent
// app -> parent
// agent -> parent
if (data.to === 'parent') {
this.sendToParent(data);
return;
}
// parent -> master -> app
// agent -> master -> app
if (data.to === 'app') {
this.sendToAppWorker(data);
return;
}
// parent -> master -> agent
// app -> master -> agent,可能不指定 to
if (data.to === 'agent') {
this.sendToAgentWorker(data);
return;
}
}
/**
* send message to master self
* @param {Object} data message body
*/
sendToMaster(data) {
this.master.emit(data.action, data.data);
}
/**
* send message to parent process
* @param {Object} data message body
*/
sendToParent(data) {
if (!this.hasParent) {
return;
}
process.send(data);
}
/**
* send message to app worker
* @param {Object} data message body
*/
sendToAppWorker(data) {
for (const id in cluster.workers) {
const worker = cluster.workers[id];
if (worker.state === 'disconnected') {
continue;
}
// check receiverPid
if (data.receiverPid && data.receiverPid !== String(worker.process.pid)) {
continue;
}
sendmessage(worker, data);
}
}
/**
* send message to agent worker
* @param {Object} data message body
*/
sendToAgentWorker(data) {
if (this.master.agentWorker) {
sendmessage(this.master.agentWorker, data);
}
}
}
这里使用了 sendmessage
包来发送跨进程的消息。可以看到 egg-cluster 将进程分成了四个部分
┌────────┐
│ parent │
/└────────┘\
/ | \
/ ┌────────┐ \
/ │ master │ \
/ └────────┘ \
/ / \ \
┌───────┐ ┌───────┐
│ agent │ ------- │ app │
└───────┘ └───────┘
-
Master 启动后先 fork Agent 进程
-
Agent 初始化成功后,通过 IPC 通道通知 Master
-
Master 再 fork 多个 App Worker
-
App Worker 初始化成功,通知 Master
-
所有的进程初始化成功后,Master 通知 Agent 和 Worker 应用启动成功
总结
至此,我们把 egg.js 的源代码大致过了一遍,现在我们来回答下面几个问题:
1. egg.js 是如何加载文件的?
以 start 为入口, 按照Application 和 Agent 的 分类, FileLoader 提供基础的加载文件的功能, EggLoader mixin了针对 controller/service/config等类型的方法, 根据加载的对象的级别, 调用loadToApp 或者 loadToContext 来加载并组织文件。
2. egg.js 是如何将 ctx 注入到 controller/service 里的?
- egg 加载 文件夹下文件,把controller的各个方法包装成接受context的中间classControllerMiddleware挂载到app.controller 上,把Service 的 Class 挂载到 app.serviceClasses 上。
- egg 继承 koa 框架,对每个请求创建一个context对象,在中间件执行的时候,为controller注入context,并且实例化当前 controller 调用到的 service
3. egg.js 是如何进程多进程管理的?
-
分类
当一个应用启动时,会同时启动三类进程。Master 进程承担了进程管理的工作(类似 pm2), Agent 负责运行只想运行在一个进程的代码,Worker 进程负责处理真正的用户请求的处理。
-
IPC
通过 messenger 对象实现了
- Master 和 Worker/Agent 之间的直接IPC通信
- Worker 与 Agent 进程通过Master转发通信
编码技巧
-
Symbol
egg的代码里用 Symbol 来实现类的私有方法,注意 Symbol(key) 和 Symbol.for(key) 的区别
-
模块拆分 安装核心功能,满足最小知识原则
- egg-core 加载
- egg-cluster 多进程
- egg-init 脚手架
- egg-bin 开发工具
- egg 提供给用户的入口
-
基于事件编程 隐藏具体实现的细节。使用get-ready包,来处理只触发一次的消息。
-
mixin
按照不同的文件类型来分离具体的load方法,减少单个文件的大小。
-
可扩展性
Loader的很多地方预留了支持使用方扩展的空间。
Midway.js
我使用midway比较少,主要是用来学习typescript。
基于ts的基于装饰器和依赖注入使得代码更加简洁。
除此之外,它的可扩展性更加强,支持使用 Koa/Express/Egg.js 生态插件。