「这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战」
egg 是一个 node 的 web 服务框架,它的底层基于 koa,按官网描述它是一个为企业级框架和应用而生的框架。一方面在 koa 的基础上提供了一些规范,遵循约定优于配置原则,开发起来风格更加统一。另一方面它提供了一套较高定制化的插件能力,遵守单一职责原则,把扩展的功能放在一个个插件中,自由度较好。
但是很多时候这个所谓的企业框架似乎并不是很适合复杂的企业应用,它的约定优于配置并不能覆盖多变的应用场景,上层提供的能力比较有限,对象全局挂载的插件机制也是很不优雅,随着如今的容器化技术成熟,最初设计的复杂的多进程模型也变成了沉重的包袱。
egg 是我正式使用的第一个 node 框架,那时候的我还不会前端(我的前端是从 node 开始入坑的),还记得最早接触 node 时 express 的自由风格让我很迷惑,直到发现了 egg 才有了一种熟悉的感觉,后来也用 egg.js 写过很多项目。时至今日 egg 也并不会成为我开发 server 的选择,但是我还是想写一个 egg 源码的系列文章,一方面学习一些东西,另一方面就算是给自己留下的纪念吧。
首先就从启动一个 egg 工程开始,初始化一个 egg 工程:
npm init egg --type=ts
在开发时我们会通过 npm run dev 启动工程,查看 package.json 可以看到 dev
"dev": "egg-bin dev"
egg-bin 是一个独立的 npm 包,这里查看 egg-bin 的源码:
它是基于一个 common-bin 的包来搭建的,这里我们只需要知道相关的逻辑在 run 中就可以,我们查看 dev.js 下面的 run 方法,关键的一行:
const task = this.helper.forkNode(this.serverBin, devArgs, options);
这里 forkNode 是 common-bin 封装的能力,会创建新的线程执行脚本,执行的内容是 this.serverBin:
this.serverBin = path.join(__dirname, '../start-cluster');
查看 start-cluster,可以看到最终执行的是 framework 上面的 startCluster:
'use strict';
const debug = require('debug')('egg-bin:start-cluster');
const options = JSON.parse(process.argv[2]);
debug('start cluster options: %j', options);
require(options.framework).startCluster(options);
那么 framework 是什么,这里有一条 debug 日志打印,我们直接来查看一下,添加 DEBUG 环境变量启动我们刚刚创建的 egg 工程:
DEBUG=egg-bin:start-cluster npm run dev
我们可以在启动日志中看到 framework 就是我们工程中的 egg.js:
egg-bin:start-cluster start cluster options: {"typescript":true,"declarations":true,"tscompiler":"ts-node/register","workers":1,"baseDir":"/Users/fx/test","framework":"/Users/fx/test/node_modules/egg"}
绕了一圈终于又回到了 egg.js 的源码,在 index.js 中我们可以看到到处的 startCluster:
/**
* Start egg application with cluster mode
* @since 1.0.0
*/
exports.startCluster = require('egg-cluster').startCluster;
使用 egg-bin 是以 cluster 模式启动的,但是下面还有一行这样的代码:
/**
* Start egg application with single process mode
* @since 1.0.0
*/
exports.start = require('./lib/start');
也就是说 egg 其实也可以不使用 cluster,直接以单进程模式启动,但是这个方法貌似没有公开 API,已经处于试验阶段很久了,看样子官方没有支持的计划:github.com/eggjs/egg/i… 。
在 egg 最初的设计中,cluster 机制是很重要的一部分,egg 最初的设计理念是由框架来接管应用的生命周期,但是随着容器技术的发展,现在的应用都可以交给 k8s 来管理,应用本身可以很轻量。而 egg 中很大一部分为了 cluster 设计的代码反而成为了难以甩掉的包袱。egg 官方的内部行动计划是什么我不清楚,至少目前想实现单线程启动我们只有 start 一种解决方案。
cluster 启动模式位于 egg-cluster 工程中,源码的目录结构:
├── index.js
├── lib
│ ├── agent_worker.js
│ ├── app_worker.js
│ ├── master.js
│ └── utils
├── package.json
index.js
/**
* cluster start flow:
*
* [startCluster] -> master -> agent_worker -> new [Agent] -> agentWorkerLoader
* `-> app_worker -> new [Application] -> appWorkerLoader
*
*/
exports.startCluster = function(options, callback) {
new Master(options).ready(callback);
};
这里的注释描述了 egg 的进程模型,egg 官网上详细介绍了 agent 和 app 的设计理念 eggjs.org/zh-cn/core/…。
本文不会阅读 cluster 相关实现,我们来看 single process mode 启动的流程,查看 start.js,忽略 agent 等无关代码:
module.exports = async (options = {}) => {
const application = new Application(Object.assign({}, options));
await application.ready();
application.messenger.broadcast('egg-ready');
return application;
};
可以看到这里执行 start 之后只会初始化 application,并没有创建 server,对于 startCluster 模式,egg-cluster 会创建 server,对于 single 模式,我们需要手动启动 server:
require(egg).start().then(app => app.listen(3000));
这样就以单进程模式启动了 egg 工程。
再来看 egg 源码中的继承关系:
classDiagram
KoaApplication <|-- EggCore
EggCore <|-- EggApplication
EggApplication <|-- Application
EggApplication <|-- Agent
-
koa 中: 初始化 app、context
-
EggCore 中:初始化 timing、console、lifecycle、loader
-
EggApplication 中:loadConfig、初始化 messenger
-
Application/Agent 中:执行相应的 load 方法
核心逻辑位于 egg-core 中,我们来看一下 egg-core 里面都有什么:
-
timing 即创建了一个 Timing 的实例,用来记录某个动作的执行时间,它的实现方式很简单,调用 start 和 end 的时间做差。
-
console 的逻辑在 egg-logger 中,它提供了打印日志的能力
-
lifecycle 是 Lifecycle 的实例,它继承自 EventEmitter,管理 egg 应用的生命周期
-
loader 是 EggLoader 的实例,管理文件加载
至此 egg 的启动流程就清晰了,启动后的核心模块就是 loader,它提供了 egg 框架最基础的能力,接下来会深入 loader 源码继续学习 egg 原理。