egg(蛋)

egg是什么?

我们做后端应用的开发,都是基于MVC这种模式,虽然是一个统一的程序设计思想,但是在实现上肯定是千奇百怪,不同的人对框架的设计一定是不同的,那么对于一个团队的开发来讲,就带来了难度,正所谓众口难调。

egg是基于js的后端开发服务框架,奉行一个理念约定优于配置,按照统一的一套约定进行应用开发。约定优于配置,当我第一次在egg文档中看到这句话的时候还是挺亲切的,因为以前在看spring boot的时候,也有相同的理念,约定优于配置(慢慢体会,意味深长),下面就是的项目目录就是egg的约定。app中的目录结构就是应用的约定,config目录下就是各种开发环境,中间件等的配置文件。

egg(蛋)

如果一个框架有固定的技术选型会使框架的扩展性变差,无法满足各种定制需求。通过 Egg,团队的架构师和技术负责人可以非常容易地基于自身的技术架构在 Egg 基础上扩展出适合自身业务场景的框架。这就是egg的第二个特点,没有固定的插件绑定,我们可以根据自身业务的需求来扩张应用的框架,选取心仪的插件。

第三个特点就是egg继承自koa,继承了很多koa中的优秀策略和基本对象。例如,koa中的中间件模式是一种洋葱型的模式,egg页一样,那么koa中的中间件也可以直接拿过来在egg中使用。再如koa中请求的request对象,response对象,在egg中还是存在的,可以通过ctx对象获得。

如何快速初始化一个egg项目

快速的初始化,推荐直接使用脚手架,只需几条简单指令,即可快速生成项目:

$ npm i egg-init -g
$ egg-init egg-example --type=simple
$ cd egg-example
$ npm i

然后打开egg-example项目就可以看到初始化之后的工程项目就是下图所示

egg(蛋)

 其中有一个package文件标示当前工程所有版本信息和依赖等等。

egg(蛋)

我们看一下文档中关于目录的约定规范

egg-project
├── package.json
├── app.js (可选)
├── agent.js (可选)
├── app
|   ├── router.js
│   ├── controller
│   |   └── home.js
│   ├── service (可选)
│   |   └── user.js
│   ├── middleware (可选)
│   |   └── response_time.js
│   ├── schedule (可选)
│   |   └── my_task.js
│   ├── public (可选)
│   |   └── reset.css
│   ├── view (可选)
│   |   └── home.tpl
│   └── extend (可选)
│       ├── helper.js (可选)
│       ├── request.js (可选)
│       ├── response.js (可选)
│       ├── context.js (可选)
│       ├── application.js (可选)
│       └── agent.js (可选)
├── config
|   ├── plugin.js
|   ├── config.default.js
│   ├── config.prod.js
|   ├── config.test.js (可选)
|   ├── config.local.js (可选)
|   └── config.unittest.js (可选)
└── test
    ├── middleware
    |   └── response_time.test.js
    └── controller
        └── home.test.js

如上,由框架约定的目录:

  • app/router.js 用于配置 URL 路由规则,具体参见 Router
  • app/controller/** 用于解析用户的输入,处理后返回相应的结果,具体参见 Controller
  • app/service/** 用于编写业务逻辑层,可选,建议使用,具体参见 Service
  • app/middleware/** 用于编写中间件,可选,具体参见 Middleware
  • app/public/** 用于放置静态资源,可选,具体参见内置插件 egg-static
  • app/extend/** 用于框架的扩展,可选,具体参见框架扩展
  • config/config.{env}.js 用于编写配置文件,具体参见配置
  • config/plugin.js 用于配置需要加载的插件,具体参见插件
  • test/** 用于单元测试,具体参见单元测试
  • app.js 和 agent.js 用于自定义启动时的初始化工作,可选,具体参见启动自定义。关于agent.js的作用参见Agent机制

Egg 在 Koa 的基础上进行增强最重要的就是基于一定的约定,根据功能差异将代码放到不同的目录下管理,对整体团队的开发成本提升有着明显的效果。Loader 实现了这套约定,并抽象了很多底层 API 可以进一步扩展。

Egg 将应用、框架和插件都称为加载单元(loadUnit),因为在代码结构上几乎没有什么差异,下面是目录结构

loadUnit
├── package.json
├── app.js
├── agent.js
├── app
│   ├── extend
│   |   ├── helper.js
│   |   ├── request.js
│   |   ├── response.js
│   |   ├── context.js
│   |   ├── application.js
│   |   └── agent.js
│   ├── service
│   ├── middleware
│   └── router.js
└── config
    ├── config.default.js
    ├── config.prod.js
    ├── config.test.js
    ├── config.local.js
    └── config.unittest.js

不过还存在着一些差异

文件 应用 框架 插件
package.json ✔︎ ✔︎ ✔︎
config/plugin.{env}.js ✔︎ ✔︎  
config/config.{env}.js ✔︎ ✔︎ ✔︎
app/extend/application.js ✔︎ ✔︎ ✔︎
app/extend/request.js ✔︎ ✔︎ ✔︎
app/extend/response.js ✔︎ ✔︎ ✔︎
app/extend/context.js ✔︎ ✔︎ ✔︎
app/extend/helper.js ✔︎ ✔︎ ✔︎
agent.js ✔︎ ✔︎ ✔︎
app.js ✔︎ ✔︎ ✔︎
app/service ✔︎ ✔︎ ✔︎
app/middleware ✔︎ ✔︎ ✔︎
app/controller ✔︎    
app/router.js ✔︎    

文件按表格内的顺序自上而下加载

在加载过程中,Egg 会遍历所有的 loadUnit 加载上述的文件(应用、框架、插件各有不同),加载时有一定的优先级

  • 按插件 => 框架 => 应用依次加载
  • 插件之间的顺序由依赖关系决定,被依赖方先加载,无依赖按 object key 配置顺序加载,具体可以查看插件章节
  • 框架按继承顺序加载,越底层越先加载。

比如有这样一个应用配置了如下依赖

app
| ├── plugin2 (依赖 plugin3)
| └── plugin3
└── framework1
    | └── plugin1
    └── egg

最终的加载顺序为

=> plugin1
=> plugin3
=> plugin2
=> egg
=> framework1
=> app

plugin1 为 framework1 依赖的插件,配置合并后 object key 的顺序会优先于 plugin2/plugin3。因为 plugin2 和 plugin3 的依赖关系,所以交换了位置。framework1 继承了 egg,顺序会晚于 egg。应用最后加载。

上面已经列出了默认会加载的文件,Egg 会按如下文件顺序加载,每个文件或目录再根据 loadUnit 的顺序去加载(应用、框架、插件各有不同)。

  • 加载 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

Egg提供了应用启动(beforeStart), 启动完成(ready), 关闭(beforeClose)这三个生命周期方法

beforeStart 方法在 loading 过程中调用, 所有的方法并行执行。 一般用来执行一些异步方法, 例如检查连接状态等, 比如 egg-mysql 就用 beforeStart 来检查与 mysql 的连接状态。所有的 beforeStart 任务结束后, 状态将会进入 ready 。不建议执行一些耗时较长的方法, 可能会导致应用启动超时。

ready 方法注册的任务在 load 结束并且所有的 beforeStart 方法执行结束后顺序执行, HTTP server 监听也是在这个时候开始, 此时代表所有的插件已经加载完毕并且准备工作已经完成, 一般用来执行一些启动的后置任务。

beforeClose 注册方法在 app/agent 实例的 close 方法被调用后, 按注册的逆序执行。一般用于资源的释放操作, 例如 egg 用来关闭 logger , 删除监听方法等。

这个方法不建议在生产环境使用, 可能遇到未执行完就结束进程的问题。