前言
- 最近比较忙,四个项目并行整,学习计划又拖了点,抓紧摸鱼学习下egg。
官网
- https://eggjs.org/zh-cn/intro/
安装
- 使用:
npx egg-init --type=ts packageName
- 也可以新建个目录使用国内镜像模板来安装:
npm init egg --type=simple --registry=china
路由
- 路由定义映射关系,语法如下:
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)
app.router.get('index', '/home/index', app.controller.home.index)
app.router.redirect('/', '/home/index', 302)
}
CRUD路由
- 除了使用动词的方式创建路由之外,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.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)
};
- 2 使用 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
}
中间件
- koa封装的都长这样:
async function gzip(ctx, next) {
// 前置代码
await next()
// 后置代码
}
-
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中加入该中间件:
config.middleware = ["slow"];
-
这的里配置的中间件是全局启用的,如果只是想在指定路由中使用中间件的话,例如只针对 /api 前缀开头的 url 请求使用某个中间件的话,有两种方式:
-
在 config.default.js 配置中设置 match 或 ignore 属性:
module.exports = {
middleware: [ 'slow' ],
slow: {
threshold: 1,
match: '/api'
},
};
- 如果config写法是这样:
config.slow = {
threshold: 1,
match: "/api",
};
- 或者在路由中加入:(注意需要在config.default中把middleware那些去了,所以 app.config.appMiddleware应该没有该中间件)
module.exports = app => {
const { router, controller } = app
// 在 controller 处理之前添加任意中间件
router.get('/api/home', app.middleware.slow({ threshold: 1 }), controller.home.index)
}
- 可以通过app看见框架的中间件
console.log(app.config.appMiddleware)
console.log(app.config.coreMiddleware)
- coreMiddleware是默认的,如果不需要,可以关闭:
module.exports = {
i18n: {
enable: false
}
}
控制器
- 简单的控制器如下:
const { Controller } = require('egg')
class HomeController extends Controller {
async index() {
const { ctx } = this
ctx.body = 'hi, egg'
}
}
module.exports = HomeController
- 在 Controller 中通过 this.ctx 可以获取上下文对象,方便获取和设置相关参数,例如:
ctx.query:URL 中的请求参数(忽略重复 key)
ctx.quries:URL 中的请求参数(重复的 key 被放入数组中)
ctx.params:Router 上的命名参数
ctx.request.body:HTTP 请求体中的内容
ctx.request.files:前端上传的文件对象
ctx.getFileStream():获取上传的文件流
ctx.multipart():获取 multipart/form-data 数据
ctx.cookies:读取和设置 cookie
ctx.session:读取和设置 session
ctx.service.xxx:获取指定 service 对象的实例(懒加载)
ctx.status:设置状态码
ctx.body:设置响应体
ctx.set:设置响应头
ctx.redirect(url):重定向
ctx.render(template):渲染模板
Service
- Controller 可以调用任何一个 Service 上的任何方法,值得注意的是:Service 是懒加载的,即只有当访问到它的时候框架才会去实例化它。
- service案例:
// app/service/user.js
const { Service } = require('egg').Service;
class UserService extends Service {
async find(uid) {
const user = await this.ctx.db.query('select * from user where uid = ?', uid);
return user;
}
}
module.exports = UserService;
- 在controller中调用:
class UserController extends Controller {
async info() {
const { ctx } = this;
const userId = ctx.params.id;
const userInfo = await ctx.service.user.find(userId);
ctx.body = userInfo;
}
}
- 注意,Service 文件必须放在 app/service 目录,支持多级目录,访问的时候可以通过目录名级联访问:
app/service/biz/user.js => ctx.service.biz.user
app/service/sync_user.js => ctx.service.syncUser
app/service/HackerNews.js => ctx.service.hackerNews
- Service 里面的函数,可以理解为某个具体业务逻辑的最小单元,Service 里面也可以调用其他 Service,值得注意的是:Service 不是单例,是 请求级别 的对象,框架在每次请求中首次访问 ctx.service.xx 时延迟实例化,都继承于 egg.Service,每个 Service 实例都会拥有下列属性:
this.ctx: 当前请求的上下文 Context 对象的实例
this.app: 当前应用 Application 对象的实例
this.service:等价于 this.ctx.service
this.config:应用运行时的配置项
this.logger:logger 对象,上面有四个方法(debug,info,warn,error),分别代表打印四个不同级别的日志
模板渲染
- egg 框架内置了 egg-view 作为模板解决方案,并支持多种模板渲染,例如 ejs、handlebars、nunjunks 等模板引擎,每个模板引擎都以插件的方式引入,默认情况下,所有插件都会去找 app/view 目录下的文件,然后根据 config\config.default.js 中定义的后缀映射来选择不同的模板引擎:
config.view = {
defaultExtension: '.nj',
defaultViewEngine: 'nunjucks',
mapping: {
'.nj': 'nunjucks',
'.hbs': 'handlebars',
'.ejs': 'ejs',
},
}
上面的配置表示,当文件:
后缀是 .nj 时使用 nunjunks 模板引擎
后缀是 .hbs 时使用 handlebars 模板引擎
后缀是 .ejs 时使用 ejs 模板引擎
当未指定后缀时默认为 .html
当未指定模板引擎时默认为 nunjunks
接下来安装模板引擎插件:
$ npm i egg-view-nunjucks egg-view-ejs egg-view-handlebars --save
- 然后在 config/plugin.js 中启用该插件:
exports.nunjucks = {
enable: true,
package: 'egg-view-nunjucks',
}
exports.handlebars = {
enable: true,
package: 'egg-view-handlebars',
}
exports.ejs = {
enable: true,
package: 'egg-view-ejs',
}
- 在app/view下新增下列文件:
app/view
├── ejs.ejs
├── handlebars.hbs
└── nunjunks.nj
<!-- ejs.ejs 文件代码 -->
<h1>ejs</h1>
<ul>
<% items.forEach(function(item){ %>
<li><%= item.title %></li>
<% }); %>
</ul>
<!-- handlebars.hbs 文件代码 -->
<h1>handlebars</h1>
{{#each items}}
<li>{{title}}</li>
{{~/each}}
<!-- nunjunks.nj 文件代码 -->
<h1>nunjunks</h1>
<ul>
{% for item in items %}
<li>{{ item.title }}</li>
{% endfor %}
</ul>
- 在router中配置:
module.exports = app => {
const { router, controller } = app
router.get('/ejs', controller.home.ejs)
router.get('/handlebars', controller.home.handlebars)
router.get('/nunjunks', controller.home.nunjunks)
}
- controller中返回:
const Controller = require('egg').Controller
class HomeController extends Controller {
async ejs() {
const { ctx } = this
const items = await ctx.service.view.getItems()
await ctx.render('ejs.ejs', {items})
}
async handlebars() {
const { ctx } = this
const items = await ctx.service.view.getItems()
await ctx.render('handlebars.hbs', {items})
}
async nunjunks() {
const { ctx } = this
const items = await ctx.service.view.getItems()
await ctx.render('nunjunks.nj', {items})
}
}
module.exports = HomeController
- 数据放到service
const { Service } = require('egg')
class ViewService extends Service {
getItems() {
return [
{ title: 'foo', id: 1 },
{ title: 'bar', id: 2 },
]
}
}
module.exports = ViewService
- egg-view 对 context 进行扩展,为 ctx 上下文对象增加了 render、renderView 和 renderString 三个方法。
集成mysql
- 这里简单写一下,有需要再查文档。
- 安装插件
$ npm i egg-mysql
- 在 config/plugin.js 中开启插件:
exports.mysql = {
enable: true,
package: 'egg-mysql',
}
- 在 config/config.default.js 中定义连接参数:
config.mysql = {
client: {
host: 'localhost',
port: '3306',
user: 'root',
password: 'root',
database: 'cms',
}
}
- 即可拿到mysql对象:
class UserService extends Service {
async find(uid) {
const user = await this.app.mysql.get('users', { id: 11 });
return { user }
}
}
- 如果mysql启动报错,可能是密码问题,这么解决:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password'
flush privileges
- 集成sequelize
npm install egg-sequelize --save
- 然后在 config/plugin.js 中开启 egg-sequelize 插件:
exports.sequelize = {
enable: true,
package: 'egg-sequelize',
}
- 同样要在 config/config.default.js 中编写 sequelize 配置
config.sequelize = {
dialect: 'mysql',
host: '127.0.0.1',
port: 3306,
database: 'example',
}
- 然后在 egg_example 库中创建 books 表:
CREATE TABLE `books` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'primary key',
`name` varchar(30) DEFAULT NULL COMMENT 'book name',
`created_at` datetime DEFAULT NULL COMMENT 'created time',
`updated_at` datetime DEFAULT NULL COMMENT 'updated time',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='book';
- 创建 model/book.js 文件,代码是:
module.exports = app => {
const { STRING, INTEGER } = app.Sequelize
const Book = app.model.define('book', {
id: { type: INTEGER, primaryKey: true, autoIncrement: true },
name: STRING(30),
})
return Book
}
添加 controller/book.js 控制器:
const Controller = require('egg').Controller
class BookController extends Controller {
async index() {
const ctx = this.ctx
ctx.body = await ctx.model.Book.findAll({})
}
async show() {
const ctx = this.ctx
ctx.body = await ctx.model.Book.findByPk(+ctx.params.id)
}
async create() {
const ctx = this.ctx
ctx.body = await ctx.model.Book.create(ctx.request.body)
}
async update() {
const ctx = this.ctx
const book = await ctx.model.Book.findByPk(+ctx.params.id)
if (!book) return (ctx.status = 404)
await book.update(ctx.request.body)
ctx.body = book
}
async destroy() {
const ctx = this.ctx
const book = await ctx.model.Book.findByPk(+ctx.params.id)
if (!book) return (ctx.status = 404)
await book.destroy()
ctx.body = book
}
}
module.exports = BookController
- 最后配置 RESTful 路由映射:
module.exports = app => {
const {router, controller} = app
router.resources('books', '/books', controller.book)
}
- 同时用mysql和mongoose貌似有坑,需要改配置:https://github.com/eggjs/egg/issues/805
定时任务
- egg 框架提供了定时任务功能,在 app/schedule 目录下,每一个文件都是一个独立的定时任务,可以配置定时任务的属性和要执行的方法,例如创建一个 update_cache.js 的更新缓存任务,每分钟执行一次:
const Subscription = require('egg').Subscription
class UpdateCache extends Subscription {
// 通过 schedule 属性来设置定时任务的执行间隔等配置
static get schedule() {
return {
interval: '1m', // 1 分钟间隔
type: 'all', // 指定所有的 worker 都需要执行
}
}
// subscribe 是真正定时任务执行时被运行的函数
async subscribe() {
const res = await this.ctx.curl('http://www.api.com/cache', {
dataType: 'json',
})
this.ctx.app.cache = res.data
}
}
module.exports = UpdateCache
* * * * * *
┬ ┬ ┬ ┬ ┬ ┬
│ │ │ │ │ |
│ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun)
│ │ │ │ └───── month (1 - 12)
│ │ │ └────────── day of month (1 - 31)
│ │ └─────────────── hour (0 - 23)
│ └──────────────────── minute (0 - 59)
└───────────────────────── second (0 - 59, optional)
-
执行任务的类型有两种:
-
worker 类型:只有一个 worker 会执行这个定时任务(随机选择)
-
all 类型:每个 worker 都会执行这个定时任务
错误处理
- 如果我们的项目是前后端分离的,所有返回都是 JSON 格式的话,可以在 config/plugin.js 中进行如下配置:
module.exports = {
one rror: {
accepts: () => 'json',
},
};
- 那么就会把错误调用栈直接以 JSON 的格式返回:
{
"message": "Cannot read property 'find' of undefined",
"stack": "TypeError: Cannot read property 'find' of undefined\n at UserController.index (/Users/keliq/code/egg-project/app/controller/user.js:7:37)",
"name": "TypeError",
"status": 500
}
- 也可返回html
module.exports = {
one rror: {
accepts: (ctx) => {
if (ctx.get('content-type') === 'application/json') return 'json';
return 'html';
}
},
};
- 自定义错误页:
- 在 config/config.default.js 中自定义错误:
module.exports = {
one rror: {
errorPageUrl: '/public/error.html',
},
};
- 404不会当作异常处理。
- 很多厂都是自己写 404 页面的,如果你也有这个需求,也可以自己写一个 HTML,然后在 config/config.default.js 中指定:
module.exports = {
notfound: {
pageUrl: '/404.html',
}
}
生命周期
class AppBootHook {
constructor(app) {
this.app = app
}
configWillLoad() {
// config 文件已经被读取并合并,但是还并未生效,这是应用层修改配置的最后时机
// 注意:此函数只支持同步调用
}
configDidLoad() {
// 所有的配置已经加载完毕,可以用来加载应用自定义的文件,启动自定义的服务
}
async didLoad() {
// 所有的配置已经加载完毕,可以用来加载应用自定义的文件,启动自定义的服务
}
async willReady() {
// 所有的插件都已启动完毕,但是应用整体还未 ready
// 可以做一些数据初始化等操作,这些操作成功才会启动应用
}
async didReady() {
// 应用已经启动完毕
}
async serverDidReady() {
// http / https server 已启动,开始接受外部请求
// 此时可以从 app.server 拿到 server 的实例
}
async beforeClose() {
// 应用即将关闭
}
}
module.exports = AppBootHook