返回首页

EggJS 的"约定优于配置":企业级框架在约束什么

Egg 基于 Koa,却走了一条完全不同的路。目录约定、插件机制、多进程模型,理解 Egg 为什么适合大团队协作。

14 约 4 分钟 · 4259 字 后端

如果说 Express / Koa 给你的是"一堆乐高零件,随便拼",那 Egg.js 给你的是"一套带图纸的施工规范"。它由阿里开源,基于 Koa 之上,核心理念只有一句:约定优于配置(Convention over Configuration)

这篇讲清 Egg 在"约定"什么,以及这种约束对大团队意味着什么。

约定优于配置:少做选择题

Express 给你最大的自由,但自由是有代价的:十个团队能写出十种目录结构、十种代码组织方式。新人接手项目,光摸清"这个项目把 service 放哪了"就要花半天。

Egg 的思路是反过来的:框架替你把这些决定都做了。文件放哪、怎么命名、按什么规则被加载,全有明确约定。你只要按约定把文件放进对应目录,框架自动识别、自动挂载。

app/
├── router.js          # 路由配置,统一入口
├── controller/        # 控制器:处理请求、参数、响应
│   └── post.js
├── service/           # 服务:业务逻辑、数据访问
│   └── post.js
├── middleware/        # 中间件
│   └── auth.js
├── extend/            # 扩展 ctx / application / helper
│   └── context.js
└── schedule/          # 定时任务
    └── cleanup.js
config/
├── config.default.js  # 默认配置
├── config.prod.js     # 生产环境配置(自动按环境合并)
└── plugin.js          # 插件开关

你不需要 require 这些文件,Egg 的 Loader 会自动加载并按约定挂载。

一个请求是怎么流转的

约定不只在目录,也在调用关系上。Egg 强约定 Controller → Service 的分层:

// app/router.js —— 路由集中声明
module.exports = (app) => {
  const { router, controller } = app
  router.get('/posts/:id', controller.post.show)
}
// app/controller/post.js —— 只管参数和响应,不写业务
const { Controller } = require('egg')

class PostController extends Controller {
  async show() {
    const { ctx } = this
    const id = Number(ctx.params.id)
    // 调用 service 拿数据
    const post = await ctx.service.post.findById(id)
    if (!post) {
      ctx.status = 404
      ctx.body = { message: '文章不存在' }
      return
    }
    ctx.body = post
  }
}

module.exports = PostController
// app/service/post.js —— 业务逻辑、数据库访问都在这
const { Service } = require('egg')

class PostService extends Service {
  async findById(id) {
    // this.app.mysql 是 egg-mysql 插件提供的
    return await this.app.mysql.get('posts', { id })
  }
}

module.exports = PostService

注意 ctx.service.postthis.app.mysql 这些 —— 你从来没手动 new 过 PostService,也没手动连过数据库。Egg 把这些都按约定注入好了,你只管按规矩调用。这是约定带来的"心智省力"。

插件机制:可插拔的能力

Egg 把"能力"做成了插件(plugin)。数据库、Redis、安全防护、定时任务…… 都是插件。需要什么,装上、在 plugin.js 里打开开关、在 config 里配好参数即可:

// config/plugin.js
exports.mysql = {
  enable: true,
  package: 'egg-mysql',
}
// config/config.default.js
exports.mysql = {
  client: {
    host: '127.0.0.1',
    port: 3306,
    user: 'root',
    password: '...',
    database: 'blog',
  },
}

启用后,app.mysql / ctx.service 里就能直接用了。这种"约定好的扩展点"让团队能沉淀自己的插件库,把通用能力复用到每个项目。

Egg 的多进程模型:Master / Agent / Worker

这是 Egg 最有特色、也最值得理解的设计。Egg 应用启动后,不是一个进程,而是一组角色明确的进程:

                ┌─────────┐
                │ Master  │  总管:不跑业务,负责守护和调度
                └────┬────┘
          ┌──────────┴──────────┐
     ┌────┴────┐          ┌─────┴─────┐
     │  Agent  │          │  Worker × N │
     └─────────┘          └───────────┘
   只有一个,跑              按 CPU 核数起多个,
   "全局唯一"的活           真正处理请求

三种角色分工明确:

  • Master:进程总管。不处理任何业务请求,只负责创建 / 守护 Agent 和 Worker,某个 Worker 挂了它负责重启。它是整个应用的稳定基石
  • Agent:全局只有一个。用来干那些"不该每个 Worker 都干一遍"的活 —— 比如监听配置中心、维护一个全局长连接、做集中式的定时任务调度
  • Worker:真正处理请求的进程,按 CPU 核数起多个,充分利用多核

为什么要有 Agent?设想你有 4 个 Worker,如果每个都去连配置中心、每个都跑同一个定时任务,就重复了 4 次。把这类"全局唯一"的活交给唯一的 Agent,Worker 只管处理请求,职责就清爽了。

这套多进程模型由 egg-cluster 实现。理解它对排查生产问题很有用:看到日志里 master / agent / worker 的区分,就知道是哪个角色出了状况。

框架定制:Egg 真正的野心

Egg 还有一层很多人没注意到的能力:它本身被设计成"可以被继承的框架"

大公司里,不同业务线往往有共性需求(统一的鉴权、统一的日志格式、内部 RPC)。Egg 允许你基于它定制出上层框架,把这些共性沉淀进去,各业务线再基于你的框架开发:

Koa  →  Egg  →  公司框架(如 chair)  →  各业务应用

这种"框架的框架"能力,正是 Egg 主打"企业级"的底气 —— 它不只服务一个应用,而是服务一整个组织的工程规范。

和 Nest 怎么选

同样是企业级,Egg 和 Nest 风格迥异:

  • Egg:约定驱动,基于 Koa,JS 友好(也支持 TS),多进程模型开箱即用,适合"按规范快速产出"、团队希望强统一的场景
  • Nest:依赖注入驱动,TS 优先,装饰器风格,架构概念更重,适合习惯 Spring/Angular 思路、追求强类型和细粒度切面的团队

没有谁更好,看团队基因。喜欢"约定省心"选 Egg,喜欢"DI + 强类型"选 Nest。

小结

  • Egg 的核心是约定优于配置:目录怎么放、文件怎么命名、按什么规则加载,框架都定好了,减少团队的"选择内耗"
  • 强约定 Controller(管请求)→ Service(管业务) 分层,靠 Loader 自动加载注入
  • 能力做成插件,装上 + 开开关 + 配参数即可用,便于团队沉淀复用
  • 独特的 Master / Agent / Worker 多进程模型:Master 守护、Agent 干全局唯一的活、Worker 处理请求
  • 可以基于 Egg 定制上层框架,沉淀整个组织的工程规范
  • 想要"约定省心"用 Egg,想要"DI + 强类型"用 Nest

约定优于配置的本质,是用"框架的约束"换取"团队的一致性"。项目越大、人越多,这种约束的价值就越明显。