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 的进程启动时序:

  1. Master 启动后先 fork Agent 进程
  2. Agent 初始化成功后,通过 IPC 通道通知 Master
  3. Master 再 fork 多个 App Worker
  4. App Worker 初始化成功,通知 Master
  5. 所有的进程初始化成功后,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 设计分析和思考 - 图1

  • egg 整体推崇 继承 的写法 👆 一大堆的继承关系就略见一斑了 🙉
  • egg 整体拆包比较琐碎,非 lerna 写法,比较分散,除了 egg-core 之外,拆了大量的包,不太推崇 mono-repo 的设计,个人其实还是比较喜欢 mono-repo 的,毕竟代码组织在一起方便管理和联合 debug
  • LifeCycle 作为框架层确乎需要明确设计,对一个框架生命周期的解析是比较关键的,egg 将 LifeCycle 类放置到了 Core 中留给底层继承 & 使用
  • 用好日志还是很关键的,发现 egg 的日志系统的设计比较舒服,同时也大量使用了 %s %o 等字符用于更精致的 log output
  • 框架使用大量 mixin 的机制,将扩展的属性、方法等,通过 Loader 加载后挂载到相关的对象上,实现动态注入方法

不得不感叹 egg 的文档真的是业界典范,不仅仅给你说明用法,会给你讲明这么做的原因和背后的关键思考。能够让人学习的同时,理解框架的设计,加深对使用方法的理解,甚是 🐮🍺。

Ref