Egg学习
基础功能
内置对象
-
Applicant。它是一个全局应用对象,一个应用中只有一个实例。他有四个事件:
- server:http服务启动后会触发。
- error:发生异常的时候会触发。可以在这里进行日志记录等处理。
- request和response:在应用收到请求或者响应请求的时候会分别触发request和response。
我们可以在Context对象上获取它:
ctx.app
;或者在继承于 Controller, Service 基类的实例中,可以通过
this.app
访问到 Application 对象。 -
Context。每次收到用户的请求的时候都会创建一个实例。实例上有很多实用的方法。在继承于 Controller, Service 基类的实例中可以通过
this.ctx
获取,在Middleware中可以通过以下方式获取:// Koa v1 function* middleware(next) { // this is instance of Context console.log(this.query); yield next; } // Koa v2 async function middleware(ctx, next) { // ctx is instance of Context console.log(ctx.query); }
其他情况下获取是否常用?例如在service/model、定时任务中
-
Request和Response。这两者是继承了KOA的Request和Response。可在Context对象上获取。
-
Control。官方推荐所有Control都继承于框架提供的基类。基类含有ctx、app、config、service、logger属性。引入基类的方法如下:
// 从 egg 上获取(推荐) const Controller = require('egg').Controller; class UserController extends Controller { // implement } module.exports = UserController; // 从 app 实例上获取 module.exports = app => { return class UserController extends app.Controller { // implement }; };
-
Service。他和Control差不多,含有的属性也一致。引入基类的方法和Control一样,把上面代码中的”Control”换成”Service”即可。
-
Helper。Helper用于提供一些实用函数。它身上的属性和Control基类一样。每次请求时都会实例化。可通过
ctx.helper
获取。helper具体使用案例?只能在app/extend/helper.js中写自定义helper?
-
Config。顾名思义,就是写配置的地方。可以通过
app.config
从 Application 实例上获取,也可以在 Controller, Service, Helper 的实例上通过
this.config
获取到 config 对象。 -
Logger。该对象用于打印日志。每个logger都有四个方法:
logger.debug()
,
logger.info()
,
logger.warn()
,
logger.error()
。以下是各个logger:-
App Logger:通过
app.logger
获取。可用于记录应用级别的日志,如记录启动阶段的一些数据信息,记录一些业务上与请求无关的信息。 -
App CoreLogger:通过
app.coreLogger
获取。框架和插件则需要通过它来打印应用级别的日志,这样可以更清晰的区分应用和框架打印的日志。 -
Context Logger:通过
ctx.logger
获取。用于记录和请求相关的日志。 -
Context CoreLogger:通过
ctx.coreLogger
获取。和 Context Logger 的区别是一般只有插件和框架会通过它来记录日志。 -
Controller Logger & Service logger:通过
this.logger
获取。它们本质上就是一个 Context Logger,不过在打印日志的时候还会额外的加上文件路径,方便定位日志的打印位置。
-
App Logger:通过
-
Subscription。一个规范的订阅模式基类。定时任务使用到了它。引入方法:
const Subscription = require('egg').Subscription; class Schedule extends Subscription { // 需要实现此方法 // subscribe 可以为 async function 或 generator function async subscribe() {} }
配置
egg中默认配置为
config.default.js
。指定env时会加载默认配置和对应的配置文件,并且env对应的配置文件会覆盖掉默认配置中的同名配置。下面是三种配置写法:
//使用对象方式
module.exports = {
logger: {
dir: '/home/admin/logs/demoapp'
}
}
//使用`exports.key = value`形式
exports.logger = {
level: 'DEBUG'
}
//使用function方式,function会接受一个参数,参数内置的属性可参考官方文档
module.exports = appInfo => {
return {
logger: {
dir: '/home/admin/logs/demoapp'
}
}
}
中间件
编写中间件
/**
* 中间件需要exports一个函数,函数接受两个参数
* @param {*} options 中间件的配置项,框架会将 app.config[${middlewareName}] 传递进来。
* @param {*} app 当前应用 Application 的实例。
*/
module.exports = (options, app) => {
return async function test(ctx, next) {
await next();
console.log('It is a test middleware. ' + options.msg);
};
};
使用中间件
//1.在 config.default.js 中加入配置
module.exports = {
// 配置需要的中间件,数组顺序即为中间件的加载顺序
middleware: [ 'test' ],
// 配置 test 中间件的配置
test: {
msg: 'config msg', // 小于 1k 的响应体不压缩
},
};
//2.在app.js中加入以下代码,配置放置在 config.default.js 中
app.config.coreMiddleware.unshift('test');
//3.在指定路由上添加,如在 app/router.js 中添加
module.exports = app => {
const test = app.middleware.test({ test: 'config msg' });
app.router.get('/test', test, app.controller.handler);
};
官网说“以上两种方式配置的中间件是全局的,会处理每一次请求。”,可是测试的打印次数不一致,怎么回事?
在应用中使用指的是我们自己书写的中间件,在框架和插件中使用指的是框架和插件本身的中间件,在router中使用指的是就是在特定的路由中使用。
通用配置
通用配置只能在
config.default.js
中进行设置,在其他地方设置并不会生效。一共有三个通用配置:
enable
,
match
和
ignore
。
match
和
ignore
只能使用其中一个。
-
enable
用于设置中间件是否开启。可以使用的值为
true
/
false
。 -
match
用于匹配,匹配到的对应的规则则开启中间件。值可以是字符串、正则和函数。 -
ignore
和
match
一样,区别是ignore会在匹配成功时关闭中间件。
路由
路由用于描述url和controller的对应关系。路由会把请求中的url交给对应的controller进行处理。路由的定义方式有以下几种:
router.verb('path-match', app.controller.action);
router.verb('router-name', 'path-match', app.controller.action);
router.verb('path-match', middleware1, ..., middlewareN, app.controller.action);
router.verb('router-name', 'path-match', middleware1, ..., middlewareN, app.controller.action);
“Verb”指的是http请求的方法。”path-match”指的是匹配的路由,可以是正则和字符串。”router-name”指的是路由别名,似乎用的不多。middleware是给当前的路由添加的中间件,可以有多个。controller就是当前路由要对应的controller。以下是一些例子:
// app/router.js
module.exports = app => {
const { router, controller } = app;
router.get('/home', controller.home);
router.get('/user/:id', controller.user.page);
router.post('/admin', isAdmin, controller.admin);
router.post('/user', isLoginUser, hasAdminPermission, controller.user.create);
router.post('/api/v1/comments', controller.v1.comments.create); // app/controller/v1/comments.js
};
控制器
控制器功能是处理某个请求,并返回该请求对应的结果。比如某个请求需要根据请求中的参数返回对应的页面,那么控制器就是需要解析参数,找到和参数对应的页面并返回。
编写controller
我们一般使用class来编写。
const { Controller } = require('egg').Controller;
class DemoController extends Controller {
async test(){
//在实例中我们可以访问Applicant, Context, Service, Config, Logger对象
const { ctx, app, service, config, logger } = this;
ctx.body = `hello world!`;
}
}
module.exports = DemoController;
获取http请求参数
-
query:通过
ctx.query
可获取url上参数,返回的是一个对象。若有key有多个,则获取第一个。 -
queries:通过
ctx.queries
可获取url上参数,返回的是一个对象,对象中每个key值对应的是一个数组。若有key有多个,则获取全部。 -
router params:通过
ctx.params
获取,返回的是一个对象。 -
body:egg内置了bodyparse中间件,可通过
cox.request.body
获取body。 -
上传的文件:具体看官方文档。
-
header:通过
ctx.headers
,
ctx.header
,
ctx.request.headers
,
ctx.request.header
这些都可以获取到。header中一些常用或者特殊的字段内置在了框架中的getter上,可通过
ctx.(specific name)
来获取,详细见官网。 -
cookie:通过
ctx.cookies
获取,通过
ctx.cookies.set()
来设置。cookie的samesite和httponly属性可在config.default.js中配置。 -
session:通过
ctx.session
获取,通过
ctx.session[name]
来设置,若要删除则置为null。session的key和maxAge属性可对config.default.js中的exports.session进行配置。
参数校验
//在 config.default.js 中开启
exports.validate = {
enable: true,
package: 'egg-validate',
};
//在具体的Controller中进行校验
//校验函数:ctx.validate(rule, [body])
class PostController extends Controller {
async test() {
// 如果不传第二个参数会自动校验 `ctx.request.body`
this.ctx.validate({
title: { type: 'string' },
});
}
}
响应请求
-
设置status:
ctx.status = value
-
设置body:
ctx.body = value
-
设置header:
ctx.set(key, value)
-
重定向:
ctx.redirect(url)
和
cox.unsafeRedirect(url)
。前者需要在config.default.js中配置白名单,例如:
exports.security = {
domainWhiteList:['.domain.com'], // 安全白名单,以 . 开头
};
服务
服务是供给Controller使用的,一般是向Controller提供数据服务或者第三方调用服务。以下是使用服务的例子:
//controller/test.js
async test() {
const { ctx } = this;
const data = await ctx.service.test.getTestData();
ctx.body = JSON.parse(data);
}
//service/test.js
const { Service } = require('egg');
class MxTestService extends Service {
async getTestData() {
return JSON.stringify({
test: 'it is a test data',
});
}
}
module.exports = MxTestService;
插件
插件的定义:插件用于管理相对独立的业务逻辑。与中间件的区别是:中间件的加载是有先后顺序的,插件会先于框架和应用之前加载。中间件用于拦截请求,插件与请求的关联并不大。
在
config/plugin.js
中配置插件。每个插件的配置选项都有四个选项:
- enable:布尔值,是否开启插件,默认true
- package:字符串,npm的模块名称
- path:字符串,插件的绝对路径
- env:数组,在指定的环境才会开启
其中path和package是互斥的。内置插件可以通过
exports.innerPlugin = boolean
来进行开关。
在plugin.[env].js中配置插件,可在对应的环境下加载插件。项目中plugin.local.js中配置的插件在生产环境下不会下载下来。
插件自身的默认配置可在
config.default.js
中进行配置。如:
exports.pluginName={}
。
定时器
定时器用于定时执行一些任务。例如定时清除日志文件,临时文件,定时更新缓存等。
一个简单的定时器如下:
const { Subscription } = require('egg');
class MxTest extends Subscription {
static get schedule() {
return {
interval: '2s',
type: 'worker',
};
}
async subscribe() {
// console.log(`MxTest ${new Date()}`);
}
}
module.exports = MxTest;
定时方式有
interval
和
cron
两种。
-
interval
可以是数字,单位为毫秒,也可以是字符串,需要带上时间单位。定时任务将每隔指定的时间执行一次 - `cron是字符串,格式为”* * * * * *”,具体定义见官方文档。它将在指定的时间执行一次,如每三小时执行一次:”0 0 */3 * * *“
类型字段有待理解,”worker”是什么?
框架扩展
在extend文件夹下编写对应的文件,则可以对对应的进行扩展。可扩展的对象和对应的文件命名:
- Application: application.js
- Context: context.js
- Request: request.js
- Response: response.js
- Helper: helper.js
启动自定义
框架生命周期函数主要有以下几个:
- configWillLoad:配置文件即将加载前
- configDidLoad:配置文件加载完成后
- didLoad:文件加载完成后
- willReady:插件启动完毕后
- didReady:worker 准备就绪后
- serverDidReady:应用启动完成后
- beforeClose:应用即将关闭前
注意不要在生命周期函数中做些耗时任务。
核心功能
日志
日志分类
-
appLogger,记录在
[appName]-web.log
-
coreLogger,记录在
egg-web.log
-
errorLogger,记录在
common-error.log
-
agentLogger,记录在
egg-agent.log
日志打印
一共有这么几种logger可以使用:ctx.logger, ctx.coreLogger, app.logger, app.coreLogger, agent.logger, agent.corelogger。他们有四种打印方法:debug, info, warn, error。
日志级别
日志分为
NONE
,
DEBUG
,
INFO
,
WARN
和
ERROR
5 个级别。对于文件日志,我们可以在
config.[env].js
文件中配置logger.level为刚才描述的级别,即可控制打印特定级别的日志。默认输出
INFO
级别以上的日志。生产环境打印debug级别日志需要配置logger.allowDebugAtProd为true。对于终端日志,我们可以在
config.[env].js
文件中配置logger.consoleLevel为刚才描述的级别,即可控制打印特定级别的日志。默认输出
INFO
级别以上的日志。
日志切割
默认是按照每天进行切割的,也可以选择按照文件大小和按照每小时进行切割。后两者需要进行配置,详细见文档。
Cookie and Session
Cookie
获取cookie:
ctx.cookies
。获取cookie某个值:
ctx.cookies.get(key [,options])
。设置cookie:
ctx.cookies.set(key, value [,options])
。options可以有signed和encrypt两个键,值类型为boolean。在get中使用则分别表示是否验签和是否解密。在set中使用则分别表示是否加签和是否加密。我们可以在
config/config.default.js
中设置
exports.key
来设置一个的私钥,供我们对cookie加解密和验签。
Session
访问session:ctx.session[key]。设置session:ctx.session[key]=value。删除session:ctx.session=null。session可在config.default.js中的exports.session进行配置。它可以配置一个属性”key”,来表示存储session的cookie的键是什么。在这里配置的
maxAge
是对全局的session配置的,单个session可以通过
ctx.session.maxAge=xxx
进行配置。
HttpClient
使用HttpClient
app.curl(url, options)
,
ctx.curl(url, options)
。HttpClient在框架初始化时会挂载到
app.httpclient
上。
app.httpclient.request(url, options)
可以通过
app.curl(url, options)
来使用。
curl
方法也同样被挂载到了
ctx
对象上,我们也可以通过
ctx.curl(url, options)
进行使用。
options
参数配置详见文档。
表单提交
-
Form表单不需要上传文件时,通常都要求以
content-type: application/x-www-form-urlencoded
的格式提交请求数据。HttpClient 会默认以 application/x-www-form-urlencoded 格式发送请求。 -
Form表单需要上传文件时,请求数据格式就必须以
multipart/form-data
进行提交了。这时候我们需要使用到
formstream
来处理表单。
formstream
的使用具体见该模块的文档。最后我们要在
options
字段中设置
stream: (你创建的formstream)
。项目中使用的是这种方式上传pdf。 -
Form表单需要上传文件时,我们还可以使用steam的方式。我们要使用到
fs
模块创建一个文件流,最后我们要在
options
字段中设置
stream: (你创建的文件流)
。
全局
request
和
response
事件
request
response
在
httpclient
发出请求或接收到响应后,会分别触发
request
事件和
response
事件。我们可以在此对请求或者响应做拦截处理。使用方法如下:
app.httpclient.on('request', req => {
req.url //请求 url
req.ctx //是发起这次请求的当前上下文
// 可以在这里设置一些 trace headers,方便全链路跟踪
});
app.httpclient.on('response', result => {
result.res.status
result.ctx //是发起这次请求的当前上下文
result.req //对应的 req 对象,即 request 事件里面那个 req
});
模板渲染
模板渲染需要使用到插件,官方推荐使用
egg-view-nunjucks
。这里也以其为例。启动该插件直接在
config/plugins.js
中启动即可。
其配置可见官方文档。
在
Context
上会挂载三个渲染的方法:
-
render(name, locals)
渲染模板文件, 并赋值给 ctx.body -
renderView(name, locals)
渲染模板文件, 仅返回不赋值 -
renderString(tpl, locals)
渲染模板字符串, 仅返回不赋值
locals
就是我们需要在模板中使用到的数据。