egg 介绍
egg 是什么?
egg 是阿里出品的一款 node.js 后端 web 框架,基于 koa 封装,并做了一些约定。
为什么叫 egg ?
egg 有孕育的含义,因为 egg 的定位是企业级 web 基础框架,旨在帮助开发者孕育适合自己团队的框架。
哪些产品是用 egg 开发的?
语雀 就是用 egg 开发的,架构图如下:
哪些公司在用 egg?
盒马,转转二手车、PingWest、小米、58同城等(技术栈选型参考链接)
egg 支持 Typescript 吗?
虽然 egg 本身是用 JavaScript 写的,但是 egg 应用可以采用 Typescript 来写,使用下面的命令创建项目即可(参考链接):
$ npx egg-init --type=ts showcase
用 JavaScript 写 egg 会有智能提示吗?
会的,只要在 package.json 中添加下面的声明之后,会在项目根目录下动态生成 typings 目录,里面包含各种模型的类型声明(参考链接):
"egg": {
"declarations": true
}
egg 和 koa 是什么关系?
koa 是 egg 的基础框架,egg 是对 koa 的增强。
学习 egg 需要会 koa 吗?
不会 koa 也可以直接上手 egg,但是会 koa 的话有助于更深层次的理解 egg。
创建项目
我们采用基础模板、选择国内镜像创建一个 egg 项目:
$ npm init egg --type=simple --registry=china
# 或者 yarn create egg --type=simple --registry=china
解释一下 npm init egg
这种语法:
npm@6 版本引入了
npm-init <initializer>
语法,等价于npx create-<initializer>
命令,而npx
命令会去$PATH
路径和node_modules/.bin
路径下寻找名叫create-<initializer>
的可执行文件,如果找到了就执行,找不到就去安装。也就是说,
npm init egg
会去寻找或下载create-egg
可执行文件,而 create-egg 包就是 egg-init 包的别名,相当于调用了egg-init
。
创建完毕之后,目录结构如下(忽略 README文件 和 test 目录):
├── app
│?? ├── controller
│?? │?? └── home.js
│?? └── router.js
├── config
│?? ├── config.default.js
│?? └── plugin.js
├── package.json
这就是最小化的 egg 项目,用 npm i
或 yarn
安装依赖之后,执行启动命令:
$ npm run dev
[master] node version v14.15.1
[master] egg version 2.29.1
[master] agent_worker#1:23135 started (842ms)
[master] egg started on http://127.0.0.1:7001 (1690ms)
打开 http://127.0.0.1:7001/
会看到网页上显示 hi, egg
。
目录约定
上面创建的项目只是最小化结构,一个典型的 egg 项目有如下目录结构:
egg-project
├── package.json
├── app.js (可选)
├── agent.js (可选)
├── app/
| ├── router.js # 用于配置 URL 路由规则
│ ├── controller/ # 用于存放控制器(解析用户的输入、加工处理、返回结果)
│ ├── model/ (可选) # 用于存放数据库模型
│ ├── service/ (可选) # 用于编写业务逻辑层
│ ├── middleware/ (可选) # 用于编写中间件
│ ├── schedule/ (可选) # 用于设置定时任务
│ ├── public/ (可选) # 用于放置静态资源
│ ├── view/ (可选) # 用于放置模板文件
│ └── extend/ (可选) # 用于框架的扩展
│ ├── helper.js (可选)
│ ├── request.js (可选)
│ ├── response.js (可选)
│ ├── context.js (可选)
│ ├── application.js (可选)
│ └── agent.js (可选)
├── config/
| ├── plugin.js # 用于配置需要加载的插件
| ├── config.{env}.js # 用于编写配置文件(env 可以是 default,prod,test,local,unittest)
这是由 egg 框架或内置插件约定好的,是阿里总结出来的最佳实践,虽然框架也提供了让用户自定义目录结构的能力,但是依然建议大家采用阿里的这套方案。在接下来的篇章当中,会逐一讲解上述约定目录和文件的作用。
路由(Router)
路由定义了**请求路径(URL)和控制器(Controller)**之间的映射关系,即用户访问的网址应交由哪个控制器进行处理。我们打开 app/router.js
看一下:
module.exports = app => {
const { router, controller } = app
router.get('/', controller.home.index)
};
可以看到,路由文件导出了一个函数,接收 app 对象作为参数,通过下面的语法定义映射关系:
router.verb('path-match', controllerAction)
其中 verb
一般是 HTTP 动词的小写,例如:
- HEAD -
router.head
- OPTIONS -
router.options
- GET -
router.get
- PUT -
router.put
- POST -
router.post
- PATCH -
router.patch
- DELETE -
router.delete
或router.del
除此之外,还有一个特殊的动词 router.redirect
表示重定向。
而 controllerAction
则是通过点(·)语法指定 controller
目录下某个文件内的某个具体函数,例如:
controller.home.index // 映射到 controller/home.js 文件的 index 方法
controller.v1.user.create // controller/v1/user.js 文件的 create 方法
下面是一些示例及其解释:
module.exports = app => {
const { router, controller } = app
// 当用户访问 news 会交由 controller/news.js 的 index 方法进行处理
router.get('/news', controller.news.index)
// 通过冒号 `:x` 来捕获 URL 中的命名参数 x,放入 ctx.params.x
router.get('/user/:id/:name', controller.user.info)
// 通过自定义正则来捕获 URL 中的分组参数,放入 ctx.params 中
router.get(/^/package/([w-.]+/[w-.]+)$/, controller.package.detail)
}
除了使用动词的方式创建路由之外,egg 还提供了下面的语法快速生成 CRUD 路由:
// 对 posts 按照 RESTful 风格映射到控制器 controller/posts.js 中
router.resources('posts', '/posts', controller.posts)
会自动生成下面的路由:
HTTP方法
请求路径
路由名称
控制器函数
GET
/posts
posts
app.controller.posts.index
GET
/posts/new
new_post
app.controller.posts.new
GET
/posts/:id
post
app.controller.posts.show
GET
/posts/:id/edit
edit_post
app.controller.posts.edit
POST
/posts
posts
app.controller.posts.create
PATCH
/posts/:id
post
app.controller.posts.update
DELETE
/posts/:id
post
app.controller.posts.destroy
只需要到 controller 中实现对应的方法即可。
当项目越来越大之后,路由映射会越来越多,我们可能希望能够将路由映射按照文件进行拆分,这个时候有两种办法:
-
手动引入,即把路由文件写到
app/router
目录下,然后再app/router.js
中引入这些文件。示例代码:// app/router.js module.exports = app => { require('./router/news')(app) require('./router/admin')(app) }; // app/router/news.js module.exports = app => { app.router.get('/news/list', app.controller.news.list) app.router.get('/news/detail', app.controller.news.detail) }; // app/router/admin.js module.exports = app => { app.router.get('/admin/user', app.controller.admin.user) app.router.get('/admin/log', app.controller.admin.log) };
-
使用 egg-router-plus 插件自动引入
app/router/**/*.js
,并且提供了 namespace 功能:// app/router.js module.exports = app => { const subRouter = app.router.namespace('/sub') subRouter.get('/test', app.controller.sub.test) // 最终路径为 /sub/test }
除了 HTTP verb 之外,Router 还提供了一个 redirect 方法,用于内部重定向,例如:
module.exports = app => {
app.router.get('index', '/home/index', app.controller.home.index)
app.router.redirect('/', '/home/index', 302)
}
中间件(Middleware)
egg 约定一个中间件是一个放置在 app/middleware
目录下的单独文件,它需要导出一个普通的函数,该函数接受两个参数:
- options: 中间件的配置项,框架会将
app.config[${middlewareName}]
传递进来。 - app: 当前应用 Application 的实例。
我们新建一个 middleware/slow.js
慢查询中间件,当请求时间超过我们指定的阈值,就打印日志,代码为:
module.exports = (options, app) => {
return async function (ctx, next) {
const startTime = Date.now()
await next()
const consume = Date.now() - startTime
const { threshold = 0 } = options || {}
if (consume > threshold) {
console.log(`${ctx.url}请求耗时${consume}毫秒`)
}
}
}
然后在 config.default.js
中使用:
module.exports = {
// 配置需要的中间件,数组顺序即为中间件的加载顺序
middleware: [ 'slow' ],
// slow 中间件的 options 参数
slow: {
enable: true
},
}
这里配置的中间件是全局启用的,如果只是想在指定路由中使用中间件的话,例如只针对 /api
前缀开头的 url 请求使用某个中间件的话,有两种方式:
-
在
config.default.js
配置中设置 match 或 ignore 属性:module.exports = { middl