egg 依旧是框架的「胚子」。可以重点关注其精粹设计的部分:
- Egg extends Koa,因此具备了 Koa 中间件的设计 & 实现。
- Egg 定义了诸多符合企业级要求的规范,比如:router、controller、service … 等等扩展了 KoaApplication 的能力范畴
插件系统的设计
一个插件其实就是一个『迷你的应用』,和应用(app)几乎一样:
以 jsonp 插件的源代码为例:
- 使用框架扩展 Application 增加了
jsonp
方法,处理 json 请求 - 使用框架扩展 Context 增加了
acceptJSONP
等方法,便于 KOA 中间件等访问 Context - 提供了 config.js 中的插件相关的配置
这样自然就比较清晰了。
框架扩展和约定
框架提供了多种扩展点扩展自身的功能:
- Application
- Context
- Request
- Response
- Helper
在开发中,我们既可以使用已有的扩展 API 来方便开发,也可以对以上对象进行自定义扩展,进一步加强框架的功能。
换言之,使用类似于 mixin 的方法,扩展 👆 相关联的对象,注入方法、getter 等等。通过 module.exports
一个 mixin 对象,对存量对象去做扩展并在 loader 加载的时候实现「注入」。
约定俗成的东西
- 按照环境分隔的配置(Config)
*.prod.config
/*.pre.config
… 便于按照不同环境加载,做配置隔离 - 中间件(Middleware):参考 Koa 的经典设计,实现对 Response / Request 的处理过程的切面(AOP)
- 路由(Router):内置的请求路由处理对象
ctx.router
- 控制器(Controller):MVC 架构里面的经典 Controller,负责处理和响应用户的操作和请求,是典型的连接服务的中间层
- 服务层(Service):原则上这一层应该是做 DDD(DomainDrivenDesign) 相关的事情,负责维护领域模型相关的服务,当然 DDD 也可以再隔离下放一层,但终归属于服务,Service 是继承
egg.Service
的,因此可以获取到ctx
app
config
等对象 - 定时任务(Schedule):服务端经典的定时任务的内部封装,由于高频使用,也被放在了 egg 框架层维护
- 启动生命周期(LifeCycle):但凡框架一般都会提供生命周期 Hook
Cluster 和多进程机制
Egg 的三种进程类型:
类型 | 进程数量 | 作用 | 稳定性 | 是否运行业务代码 |
---|---|---|---|---|
Master | 1 | 进程管理,进程间消息转发 | 非常高 | 否 |
Agent | 1 | 后台运行工作(长连接客户端) | 高 | 少量 |
Worker | 一般设置为 CPU 核数 | 执行业务代码 | 一般 | 是 |
egg 的进程启动时序:
- Master 启动后先 fork Agent 进程
- Agent 初始化成功后,通过 IPC 通道通知 Master
- Master 再 fork 多个 App Worker
- App Worker 初始化成功,通知 Master
- 所有的进程初始化成功后,Master 通知 Agent 和 Worker 应用启动成功
- 增加 AgentWorker 用于 delegate 一些期望单进程操作的工作(防止 worker 都去做,浪费资源或者导致一些资源访问的死锁等问题)
- 至于进程之间 IPC 通道的通信设计,进程与 IPC 通信机制:
- AppWorker 和 Agent 的通信是通过 Master 来实现转发的
- 内置 Messager 类实现通信
- 文档中还讨论到长连接问题是如何来优雅解决的:
- 长连接的管理,放置到 AgentWorker 中
- 使用 Leader / Follower 的模式,让 AgentWorker 扮演 Leader 的角色,通过本地 Socket 和其它 AppWorker 连接,解决 Master 代理通信的模式
- 实现一个用于通信的简单报文协议(Protocol)实现对通信信息的规范化载体
Loader 的设计机制
Loader 的出现,本质上是用来控制配置、扩展结构的复杂性的。解决应用、框架、插件之间的千丝万缕的关系,好比细胞中的马达蛋白,有条不紊地处理中各种细胞中的搬运工作。
- 确保加载顺序的争取性
- 插件 => 框架 => 应用
- 类型加载的顺序:
- 加载 plugin,找到应用和框架,加载 config/plugin.js
- 加载 config,遍历 loadUnit 加载 config/config.{env}.js
- 加载 extend,遍历 loadUnit 加载 app/extend/xx.js
- 自定义初始化,遍历 loadUnit 加载 app.js 和 agent.js
- 加载 service,遍历 loadUnit 加载 app/service 目录
- 加载 middleware,遍历 loadUnit 加载 app/middleware 目录
- 加载 controller,加载应用的 app/controller 目录
- 加载 router,加载应用的 app/router.js
https://eggjs.org/zh-cn/advanced/loader.html
源代码结构
核心类及其关系:Koa -> Egg
- egg 整体推崇 继承 的写法 👆 一大堆的继承关系就略见一斑了 🙉
- egg 整体拆包比较琐碎,非 lerna 写法,比较分散,除了 egg-core 之外,拆了大量的包,不太推崇 mono-repo 的设计,个人其实还是比较喜欢 mono-repo 的,毕竟代码组织在一起方便管理和联合 debug
- LifeCycle 作为框架层确乎需要明确设计,对一个框架生命周期的解析是比较关键的,egg 将 LifeCycle 类放置到了 Core 中留给底层继承 & 使用
- 用好日志还是很关键的,发现 egg 的日志系统的设计比较舒服,同时也大量使用了
%s %o
等字符用于更精致的 log output - 框架使用大量 mixin 的机制,将扩展的属性、方法等,通过 Loader 加载后挂载到相关的对象上,实现动态注入方法
不得不感叹 egg 的文档真的是业界典范,不仅仅给你说明用法,会给你讲明这么做的原因和背后的关键思考。能够让人学习的同时,理解框架的设计,加深对使用方法的理解,甚是 🐮🍺。
Ref
- Egg 3.0 rfc Egg 3.0 RFC 关注点