回顾一下上篇讲到的内容,上篇讲了:css
一个 Web 应用自己应该是无状态的,并拥有根据运行环境设置自身的能力。html
config/env
文件指定,该文件的内容就是运行环境,如 prod
EGG_SERVER_ENV
环境变量指定方式 2 比较经常使用,由于经过 EGG_SERVER_ENV
环境变量指定运行环境更加方便,好比在生产环境启动应用:node
EGG_SERVER_ENV=prod npm start
复制代码
使用 app.config.env
获取ios
框架默认支持的运行环境及映射关系(若是未指定 EGG_SERVER_ENV
会根据 NODE_ENV
来匹配)git
NODE_ENV | EGG_SERVER_ENV | 说明 |
---|---|---|
local | 本地开发环境 | |
test | unittest | 单元测试 |
production | prod | 生产环境 |
当 NODE_ENV
为 production
而 EGG_SERVER_ENV
未指定时,框架会将 EGG_SERVER_ENV
设置成 prod
。github
常规开发流程可能不只仅只有以上几种环境,Egg 支持自定义环境来适应本身的开发流程。web
好比,要为开发流程增长集成测试环境 SIT。将 EGG_SERVER_ENV
设置成 sit
(并建议设置 NODE_ENV = production
),启动时会加载 config/config.sit.js
,运行环境变量 app.config.env
会被设置成 sit
。npm
在 Koa 中咱们经过 app.env
来进行环境判断,app.env
默认的值是 process.env.NODE_ENV
。json
在 Egg(和基于 Egg 的框架)中,配置统一都放置在 app.config
上,因此咱们须要经过 app.config.env
来区分环境,app.env
再也不使用。后端
框架提供了强大且可扩展的配置功能,能够自动合并应用、插件、框架的配置,按顺序覆盖,且能够根据环境维护不一样的配置。合并后的配置可直接从 app.config
获取。
Egg 选择了配置即代码,配置的变动也应该通过 review 后才能发布。应用包自己是能够部署在多个环境的,只须要指定运行环境便可。
框架支持根据环境来加载配置,定义多个环境的配置文件
config
|- config.default.js
|- config.prod.js
|- config.unittest.js
|- config.local.js
复制代码
config.default.js
为默认的配置文件,全部环境都会加载这个配置文件,通常也会做为开发环境的默认配置文件。
当指定 env 时会同时加载对应的配置文件,并覆盖默认配置文件的同名配置。如 prod 环境会加载 config.prod.js
和 config.default.js
文件,config.prod.js
会覆盖 config.default.js
的同名配置。
配置文件返回的是一个 object 对象,能够覆盖框架的一些配置,应用也能够将本身业务的配置放到这里方便管理,获取时直接经过 app.config
。
导出对象式写法
// 配置 logger 文件的目录,logger 默认配置由框架提供
module.exports = {
logger: {
dir: '/home/admin/logs/demoapp',
},
};
复制代码
配置文件也能够返回一个 function,能够接受 appInfo
参数
// 将 logger 目录放到代码目录下
const path = require('path');
module.exports = appInfo => {
return {
logger: {
dir: path.join(appInfo.baseDir, 'logs'),
},
};
};
复制代码
内置的 appInfo 有:
appInfo | 说明 |
---|---|
pkg | package.json |
name | 应用名,同 pkg.name |
baseDir | 应用代码的目录 |
HOME | 用户目录,如 admin 帐户为 /home/admin |
root | 应用根目录,只有在 local 和 unittest 环境下为 baseDir,其余都为 HOME。 |
appInfo.root
是一个优雅的适配,好比在服务器环境咱们会使用 /home/admin/logs
做为日志目录,而本地开发时又不想污染用户目录,这样的适配就很好解决这个问题。
配置的合并使用 extend2 模块进行深度拷贝
const a = {
arr: [ 1, 2 ],
};
const b = {
arr: [ 3 ],
};
extend(true, a, b);
// => { arr: [ 3 ] }
// 框架直接覆盖数组而不是进行合并。
复制代码
框架在启动时会把合并后的最终配置 dump 到 run/application_config.json
(worker 进程)和 run/agent_config.json
(agent 进程)中,能够用来分析问题。
Egg 是基于 Koa 实现的,因此 Egg 的中间件形式和 Koa 的中间件形式是同样的,都是基于洋葱圈模型。每次咱们编写一个中间件,就至关于在洋葱外面包了一层。
中间件内容的写法
// app/middleware/gzip.js
const isJSON = require('koa-is-json');
const zlib = require('zlib');
async function gzip(ctx, next) {
await next();
// 后续中间件执行完成后将响应体转换成 gzip
let body = ctx.body;
if (!body) return;
if (isJSON(body)) body = JSON.stringify(body);
// 设置 gzip body,修正响应头
const stream = zlib.createGzip();
stream.end(body);
ctx.body = stream;
ctx.set('Content-Encoding', 'gzip');
}
复制代码
框架的中间件和 Koa 的中间件写法是如出一辙的,因此任何 Koa 的中间件均可以直接被框架使用。
通常来讲中间件也会有本身的配置。
咱们约定一个中间件是一个放置在 app/middleware 目录下的单独文件,它须要 exports 一个普通的 function,接受两个参数:
app.config[${middlewareName}]
传递进来,因此能够直接获取到配置文件中的中间件配置将上面的 gzip 中间件作一个简单的优化,让它支持指定只有当 body 大于配置的 threshold 时才进行 gzip 压缩,咱们要在 app/middleware
目录下新建一个文件 gzip.js
// app/middleware/gzip.js
const isJSON = require('koa-is-json');
const zlib = require('zlib');
module.exports = options => {
return async function gzip(ctx, next) {
await next();
// 后续中间件执行完成后将响应体转换成 gzip
let body = ctx.body;
if (!body) return;
// 支持 options.threshold
if (options.threshold && ctx.length < options.threshold) return;
if (isJSON(body)) body = JSON.stringify(body);
// 设置 gzip body,修正响应头
const stream = zlib.createGzip();
stream.end(body);
ctx.body = stream;
ctx.set('Content-Encoding', 'gzip');
};
};
复制代码
中间件编写完成后,咱们还须要手动挂载
在 config.default.js
中加入下面的配置就完成了中间件的开启和配置
module.exports = {
// 配置须要的中间件,数组顺序即为中间件的加载顺序
middleware: [ 'gzip' ],
// 配置 gzip 中间件的配置
gzip: {
threshold: 1024, // 小于 1k 的响应体不压缩
},
// options.gzip.threshold
};
复制代码
该配置最终将在启动时合并到 app.config.appMiddleware
框架和插件不支持在 config.default.js
中匹配 middleware
,须要经过如下方式:
// app.js
module.exports = app => {
// 在中间件最前面统计请求时间
app.config.coreMiddleware.unshift('report');
};
// app/middleware/report.js
module.exports = () => {
return async function (ctx, next) {
const startTime = Date.now();
await next();
// 上报请求时间
reportTime(Date.now() - startTime);
}
};
复制代码
应用层定义的中间件(app.config.appMiddleware
)和框架默认中间件(app.config.coreMiddleware
)都会被加载器加载,并挂载到 app.middleware
上。
以上两种方式配置的中间件是全局的,会处理每一次请求
若是你只想针对单个路由生效,能够直接在 app/router.js
中实例化和挂载,以下:
module.exports = app => {
const gzip = app.middleware.gzip({ threshold: 1024 });
app.router.get('/needgzip', gzip, app.controller.handler);
};
复制代码
除了应用层加载中间件以外,框架自身和其余的插件也会加载许多中间件。
全部的这些自带中间件的配置项都经过在配置中修改中间件同名配置项进行修改。
框架自带的中间件中有一个 bodyParser 中间件(框架的加载器会将文件名中的各类分隔符都修改为驼峰形式的变量名),咱们想要修改 bodyParser 的配置,只须要在 config/config.default.js
中编写
module.exports = {
bodyParser: {
jsonLimit: '10mb',
},
};
复制代码
在框架里面能够很是容易的引入 Koa 中间件生态。
以 koa-compress 为例,在 Koa 中使用时:
const koa = require('koa');
const compress = require('koa-compress');
const app = koa();
const options = { threshold: 2048 };
app.use(compress(options));
复制代码
Egg
// app/middleware/compress.js
// koa-compress 暴露的接口(`(options) => middleware`)和框架对中间件要求一致
module.exports = require('koa-compress');
复制代码
配置中间件
// config/config.default.js
module.exports = {
middleware: [ 'compress' ],
compress: {
threshold: 2048,
},
}
复制代码
match 和 ignore 支持的参数都同样,只是做用彻底相反,match 和 ignore 不容许同时配置。
module.exports = {
gzip: {
match: '/static',
},
};
module.exports = {
gzip: {
match(ctx) {
// 只有 ios 设备才开启
const reg = /iphone|ipad|ipod/i;
return reg.test(ctx.get('user-agent'));
},
},
};
复制代码
匹配规则,摘自 egg-path-matching
const pathMatching = require('egg-path-matching');
const options = {
ignore: '/api', // string will use parsed by path-to-regexp
// support regexp
ignore: /^\/api/,
// support function
ignore: ctx => ctx.path.startsWith('/api'),
// support Array
ignore: [ ctx => ctx.path.startsWith('/api'), /^\/foo$/, '/bar'],
// support match or ignore
match: '/api',
};
复制代码
Router 主要用来描述请求 URL 和具体承担执行动做的 Controller 的对应关系, 框架约定了 app/router.js
文件用于统一全部路由规则。
app/router.js
里面定义 URL 路由规则// app/router.js
module.exports = app => {
const { router, controller } = app;
router.get('/user/:id', controller.user.info);
};
复制代码
app/controller
目录下面实现 Controller// app/controller/user.js
class UserController extends Controller {
async info() {
const { ctx } = this;
ctx.body = {
name: `hello ${ctx.params.id}`,
};
}
}
复制代码
下面是路由的完整定义,参数能够根据场景的不一样,自由选择:
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);
复制代码
路由完整定义主要包括5个主要部分:
router.head
- HEADrouter.options
- OPTIONSrouter.get
- GETrouter.put
- PUTrouter.post
- POSTrouter.patch
- PATCHrouter.delete
- DELETErouter.del
- 因为 delete 是一个保留字,因此提供了一个 delete 方法的别名。router.redirect
- 能够对 URL 进行重定向处理,好比咱们最常用的能够把用户访问的根目录路由到某个主页。app.controller.user.fetch
- 直接指定一个具体的 controller'user.fetch'
- 能够简写为字符串形式${fileName}.${functionName}
的方式指定对应的 Controller。${directoryName}.${fileName}.${functionName}
的方式制定对应的 Controller。demo
// 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
};
复制代码
提供了 app.resources('routerName', 'pathMatch', controller)
快速在一个路径上生成 CRUD 路由结构。
// app/router.js
module.exports = app => {
const { router, controller } = app;
router.resources('posts', '/api/posts', controller.posts);
router.resources('users', '/api/v1/users', controller.v1.users); // app/controller/v1/users.js
};
复制代码
上面代码就在 /posts
路径上部署了一组 CRUD 路径结构,对应的 Controller 为 app/controller/posts.js
接下来, 你只须要在 posts.js
里面实现对应的函数就能够了。
Method | Path | Route Name | Controller.Action |
---|---|---|---|
GET | /posts | posts | app.controllers.posts.index |
GET | /posts/new | new_post | app.controllers.posts.new |
GET | /posts/:id | post | app.controllers.posts.show |
GET | /posts/:id/edit | edit_post | app.controllers.posts.edit |
POST | /posts | posts | app.controllers.posts.create |
PUT | /posts/:id | post | app.controllers.posts.update |
DELETE | /posts/:id | post | app.controllers.posts.destroy |
// app/controller/posts.js
exports.index = async () => {};
exports.new = async () => {};
exports.create = async () => {};
exports.show = async () => {};
exports.edit = async () => {};
exports.update = async () => {};
exports.destroy = async () => {};
复制代码
若是咱们不须要其中的某几个方法,能够不用在 posts.js 里面实现,这样对应 URL 路径也不会注册到 Router。
Query String 方式
ctx.query.xxx
// app/router.js
module.exports = app => {
app.router.get('/search', app.controller.search.index);
};
// app/controller/search.js
exports.index = async ctx => {
ctx.body = `search: ${ctx.query.name}`;
};
// curl http://127.0.0.1:7001/search?name=egg
复制代码
参数命名方式
/user/:id/:name
=> ctx.params.xxx
// app/router.js
module.exports = app => {
app.router.get('/user/:id/:name', app.controller.user.info);
};
// app/controller/user.js
exports.info = async ctx => {
ctx.body = `user: ${ctx.params.id}, ${ctx.params.name}`;
};
// curl http://127.0.0.1:7001/user/123/xiaoming
复制代码
复杂参数的获取
路由里面也支持定义正则,能够更加灵活的获取参数:
// app/router.js
module.exports = app => {
app.router.get(/^\/package\/([\w-.]+\/[\w-.]+)$/, app.controller.package.detail);
};
// app/controller/package.js
exports.detail = async ctx => {
// 若是请求 URL 被正则匹配, 能够按照捕获分组的顺序,从 ctx.params 中获取。
// 按照下面的用户请求,`ctx.params[0]` 的 内容就是 `egg/1.0.0`
ctx.body = `package:${ctx.params[0]}`;
};
// curl http://127.0.0.1:7001/package/egg/1.0.0
复制代码
// app/router.js
module.exports = app => {
app.router.post('/form', app.controller.form.post);
};
// app/controller/form.js
exports.post = async ctx => {
ctx.body = `body: ${JSON.stringify(ctx.request.body)}`;
};
复制代码
这里直接发起 POST 请求会报错。
上面的校验是由于框架中内置了安全插件 egg-security,提供了一些默认的安全实践,而且框架的安全插件是默认开启的,若是须要关闭其中一些安全防范,直接设置该项的 enable 属性为 false 便可。
暂时关闭 csrf
exports.security = {
csrf: false
};
复制代码
npm i -S egg-validate
复制代码
// config/plugin.js
module.exports = {
// had enabled by egg
// static: {
// enable: true,
// }
validate: {
enable: true,
package: 'egg-validate',
},
};
复制代码
// app/router.js
module.exports = app => {
app.router.resources('/user', app.controller.user);
};
// app/controller/user.js
const createRule = {
username: {
type: 'email',
},
password: {
type: 'password',
compare: 're-password',
},
};
exports.create = async ctx => {
// 若是校验报错,会抛出异常
ctx.validate(createRule);
ctx.body = ctx.request.body;
};
复制代码
内部重定向
// app/router.js
module.exports = app => {
app.router.get('index', '/home/index', app.controller.home.index);
app.router.redirect('/', '/home/index', 303); // 访问 / 自动重定向到 /home/index
};
// app/controller/home.js
exports.index = async ctx => {
ctx.body = 'hello controller';
};
复制代码
外部重定向
exports.index = async ctx => {
const type = ctx.query.type;
const q = ctx.query.q || 'nodejs';
if (type === 'bing') {
ctx.redirect(`http://cn.bing.com/search?q=${q}`);
} else {
ctx.redirect(`https://www.google.co.kr/search?q=${q}`);
}
};
复制代码
Controller 负责解析用户的输入,处理后返回相应的结果。
框架推荐 Controller 层主要对用户的请求参数进行处理(校验、转换),而后调用对应的 service 方法处理业务,获得业务结果后封装并返回:
全部的 Controller 文件都必须放在 app/controller 目录下,能够支持多级目录,访问的时候能够经过目录名级联访问
// app/controller/post.js
const Controller = require('egg').Controller;
class PostController extends Controller {
async create() {
const { ctx } = this;
ctx.body = 'PostController';
ctx.status = 201;
}
}
module.exports = PostController;
复制代码
上面定义的方法,能够在路由中经过 app.controller 根据文件名和方法名定位到它
// app/router.js
module.exports = app => {
const { router, controller } = app;
router.post('createPost', '/api/posts', controller.post.create);
}
复制代码
Controller 支持多级目录,例如若是咱们将上面的 Controller 代码放到 app/controller/sub/post.js
中,则
app.router.post('createPost', '/api/posts', app.controller.sub.post.create);
复制代码
属性介绍 定义的 Controller 类,会在每个请求访问到 server 时实例化一个全新的对象,而项目中的 Controller 类继承于 egg.Controller
,会有下面几个属性挂在 this
上。
this.ctx
: 当前请求的上下文 Context 对象的实例,经过它咱们能够拿到框架封装好的处理当前请求的各类便捷属性和方法。this.app
: 当前应用 Application 对象的实例,经过它咱们能够拿到框架提供的全局对象和方法。this.service
:应用定义的 Service,经过它咱们能够访问到抽象出的业务层,等价于 this.ctx.service 。this.config
:应用运行时的配置项。this.logger
:logger 对象,上面有四个方法(debug
,info
,warn
,error
),分别表明打印四个不一样级别的日志,使用方法和效果与 context logger 中介绍的同样,可是经过这个 logger 对象记录的日志,在日志前面会加上打印该日志的文件路径,以便快速定位日志打印位置。自定义 Controller 基类
// app/core/base_controller.js
const { Controller } = require('egg');
class BaseController extends Controller {
get user() {
return this.ctx.session.user;
}
success(data) {
this.ctx.body = {
success: true,
data,
};
}
notFound(msg) {
msg = msg || 'not found';
this.ctx.throw(404, msg);
}
}
module.exports = BaseController;
复制代码
此时在编写应用的 Controller 时,能够继承 BaseController,直接使用基类上的方法:
//app/controller/post.js
const Controller = require('../core/base_controller');
class PostController extends Controller {
async list() {
const posts = await this.service.listByUser(this.user); // 使用基类的方法
this.success(posts); // 使用基类的方法
}
}
复制代码
每个 Controller 都是一个 async function,它的入参为请求的上下文 Context 对象的实例,经过它咱们能够拿到框架封装好的各类便捷属性和方法。
// app/controller/post.js
exports.create = async ctx => {
const createRule = {
title: { type: 'string' },
content: { type: 'string' },
};
// 校验参数
ctx.validate(createRule);
// 组装参数
const author = ctx.session.userId;
const req = Object.assign(ctx.request.body, { author });
// 调用 service 进行业务处理
const res = await ctx.service.post.create(req);
// 设置响应内容和响应状态码
ctx.body = { id: res.id };
ctx.status = 201;
};
复制代码
class PostController extends Controller {
async listPosts() {
const query = this.ctx.query;
// {
// category: 'egg',
// language: 'node',
// }
}
}
复制代码
当 Query String 中的 key 重复时,ctx.query
只取 key 第一次出现时的值,后面再出现的都会被忽略。
有时候咱们的系统会设计成让用户传递相同的 key,例如 GET /posts?category=egg&id=1&id=2&id=3
。
架提供了 ctx.queries
对象,这个对象也解析了 Query String,可是它不会丢弃任何一个重复的数据,而是将他们都放到一个数组中。
// GET /posts?category=egg&id=1&id=2&id=3
class PostController extends Controller {
async listPosts() {
console.log(this.ctx.queries);
// {
// category: [ 'egg' ],
// id: [ '1', '2', '3' ],
// }
}
}
复制代码
Router 上也能够申明参数,这些参数均可以经过 ctx.params
获取到。
// app.get('/projects/:projectId/app/:appId', 'app.listApp');
// GET /projects/1/app/2
class AppController extends Controller {
async listApp() {
assert.equal(this.ctx.params.projectId, '1');
assert.equal(this.ctx.params.appId, '2');
}
}
复制代码
框架内置了 bodyParser 中间件来对这两类格式的请求 body 解析成 object 挂载到 ctx.request.body
上。
通常来讲咱们最常常调整的配置项就是变动解析时容许的最大长度,能够在 config/config.default.js
中覆盖框架的默认值。
module.exports = {
bodyParser: {
jsonLimit: '1mb',
formLimit: '1mb',
},
};
复制代码
若是用户的请求 body 超过了咱们配置的解析最大长度,会抛出一个状态码为 413
(Request Entity Too Large) 的异常,若是用户请求的 body 解析失败(错误的 JSON),会抛出一个状态码为 400
(Bad Request) 的异常。
一个常见的错误是把 ctx.request.body
和 ctx.body
混淆,后者实际上是 ctx.response.body
的简写。
通常来讲,浏览器上都是经过 Multipart/form-data
格式发送文件的,框架经过内置 Multipart 插件来支持获取用户上传的文件。
在 config 文件中启用 file 模式:
// config/config.default.js
exports.multipart = {
mode: 'file',
};
复制代码
上传 / 接收单个文件:
<form method="POST" action="/upload?_csrf={{ ctx.csrf | safe }}" enctype="multipart/form-data">
title: <input name="title" />
file: <input name="file" type="file" />
<button type="submit">Upload</button>
</form>
复制代码
对应的后端代码以下:
// app/controller/upload.js
const Controller = require('egg').Controller;
const fs = require('mz/fs');
module.exports = class extends Controller {
async upload() {
const { ctx } = this;
const file = ctx.request.files[0];
const name = 'egg-multipart-test/' + path.basename(file.filename);
let result;
try {
// 处理文件,好比上传到云端
result = await ctx.oss.put(name, file.filepath);
} finally {
// 须要删除临时文件
await fs.unlink(file.filepath);
}
ctx.body = {
url: result.url,
// 获取全部的字段值
requestBody: ctx.request.body,
};
}
};
复制代码
上传 / 接收多个文件:
对于多个文件,咱们借助 ctx.request.files
属性进行遍历,而后分别进行处理:
<form method="POST" action="/upload?_csrf={{ ctx.csrf | safe }}" enctype="multipart/form-data">
title: <input name="title" />
file1: <input name="file1" type="file" />
file2: <input name="file2" type="file" />
<button type="submit">Upload</button>
</form>
复制代码
对应的后端代码:
// app/controller/upload.js
const Controller = require('egg').Controller;
const fs = require('mz/fs');
module.exports = class extends Controller {
async upload() {
const { ctx } = this;
console.log(ctx.request.body);
console.log('got %d files', ctx.request.files.length);
for (const file of ctx.request.files) {
console.log('field: ' + file.fieldname);
console.log('filename: ' + file.filename);
console.log('encoding: ' + file.encoding);
console.log('mime: ' + file.mime);
console.log('tmp filepath: ' + file.filepath);
let result;
try {
// 处理文件,好比上传到云端
result = await ctx.oss.put('egg-multipart-test/' + file.filename, file.filepath);
} finally {
// 须要删除临时文件
await fs.unlink(file.filepath);
}
console.log(result);
}
}
};
复制代码
为了保证文件上传的安全,框架限制了支持的的文件格式,框架默认支持白名单以下:
// images
'.jpg', '.jpeg', // image/jpeg
'.png', // image/png, image/x-png
'.gif', // image/gif
'.bmp', // image/bmp
'.wbmp', // image/vnd.wap.wbmp
'.webp',
'.tif',
'.psd',
// text
'.svg',
'.js', '.jsx',
'.json',
'.css', '.less',
'.html', '.htm',
'.xml',
// tar
'.zip',
'.gz', '.tgz', '.gzip',
// video
'.mp3',
'.mp4',
'.avi',
复制代码
用户能够经过在 config/config.default.js
中配置来新增支持的文件扩展名,或者重写整个白名单。
module.exports = {
multipart: {
fileExtensions: [ '.apk' ] // 增长对 apk 扩展名的文件支持
},
};
复制代码
module.exports = {
multipart: {
whitelist: [ '.png' ], // 覆盖整个白名单,只容许上传 '.png' 格式
},
};
复制代码
除了从 URL 和请求 body 上获取参数以外,还有许多参数是经过请求 header 传递的。
ctx.headers
,ctx.header
,ctx.request.headers
,ctx.request.header
:这几个方法是等价的,都是获取整个 header 对象。ctx.get(name)
,ctx.request.get(name)
:获取请求 header 中的一个字段的值,若是这个字段不存在,会返回空字符串。ctx.get(name)
而不是 ctx.headers['name']
,由于前者会自动处理大小写。经过 ctx.cookies
,咱们能够在 Controller 中便捷、安全的设置和读取 Cookie。
class CookieController extends Controller {
async add() {
const ctx = this.ctx;
const count = ctx.cookies.get('count');
count = count ? Number(count) : 0;
ctx.cookies.set('count', ++count);
ctx.body = count;
}
async remove() {
const ctx = this.ctx;
const count = ctx.cookies.set('count', null);
ctx.status = 204;
}
}
复制代码
经过 Cookie,咱们能够给每个用户设置一个 Session,用来存储用户身份相关的信息,这份信息会加密后存储在 Cookie 中,实现跨请求的用户身份保持。
框架内置了 Session 插件,给咱们提供了 ctx.session 来访问或者修改当前用户 Session 。
class PostController extends Controller {
async fetchPosts() {
const ctx = this.ctx;
// 获取 Session 上的内容
const userId = ctx.session.userId;
const posts = await ctx.service.post.fetch(userId);
// 修改 Session 的值
ctx.session.visited = ctx.session.visited ? ++ctx.session.visited : 1;
ctx.body = {
success: true,
posts,
};
}
}
复制代码
Session 的使用方法很是直观,直接读取它或者修改它就能够了,若是要删除它,直接将它赋值为 null
:
class SessionController extends Controller {
async deleteSession() {
this.ctx.session = null;
}
};
复制代码
配置
module.exports = {
key: 'EGG_SESS', // 承载 Session 的 Cookie 键值对名字
maxAge: 86400000, // Session 的最大有效时间
};
复制代码
借助 Validate 插件提供便捷的参数校验机制,帮助咱们完成各类复杂的参数校验。
由于是插件,因此要完成插件的注册:
// config/plugin.js
exports.validate = {
enable: true,
package: 'egg-validate',
};
复制代码
经过 ctx.validate(rule, [body])
直接对参数进行校验:
this.ctx.validate({
title: { type: 'string' },
content: { type: 'string' },
});
复制代码
当校验异常时,会直接抛出一个异常,异常的状态码为422
(Unprocessable Entity),errors 字段包含了详细的验证不经过信息。若是想要本身处理检查的异常,能够经过 try catch
来自行捕获。
class PostController extends Controller {
async create() {
const ctx = this.ctx;
try {
ctx.validate(createRule);
} catch (err) {
ctx.logger.warn(err.errors);
ctx.body = { success: false };
return;
}
}
};
复制代码
参数校验经过 Parameter 完成,支持的校验规则能够在该模块的文档中查阅到。
能够经过 app.validator.addRule(type, check)
的方式新增自定义规则。
// app.js
app.validator.addRule('json', (rule, value) => {
try {
JSON.parse(value);
} catch (err) {
return 'must be json string';
}
});
复制代码
在 Controller 中能够调用任何一个 Service 上的任何方法,同时 Service 是懒加载的,只有当访问到它的时候框架才会去实例化它。
ctx.status = 201;
复制代码
绝大多数的数据都是经过 body 发送给请求方的,和请求中的 body 同样,在响应中发送的 body,也须要有配套的 Content-Type 告知客户端如何对数据进行解析。
ctx.body
是 ctx.response.body
的简写,不要和 ctx.request.body
混淆了。
class ViewController extends Controller {
async show() {
this.ctx.body = {
name: 'egg',
category: 'framework',
language: 'Node.js',
};
}
async page() {
this.ctx.body = '<html><h1>Hello</h1></html>';
}
}
复制代码
因为 Node.js 的流式特性,咱们还有不少场景须要经过 Stream 返回响应,例如返回一个大文件,代理服务器直接返回上游的内容,框架也支持直接将 body 设置成一个 Stream,并会同时处理好这个 Stream 上的错误事件。
class ProxyController extends Controller {
async proxy() {
const ctx = this.ctx;
const result = await ctx.curl(url, {
streaming: true,
});
ctx.set(result.header);
// result.res 是一个 stream
ctx.body = result.res;
}
};
复制代码
经过 ctx.set(key, value)
方法能够设置一个响应头,ctx.set(headers)
设置多个 Header。
框架经过 security 插件覆盖了 koa 原生的 ctx.redirect
实现,以提供更加安全的重定向。
ctx.redirect(url)
若是不在配置的白名单域名内,则禁止跳转。ctx.unsafeRedirect(url)
不判断域名,直接跳转,通常不建议使用,明确了解可能带来的风险后使用。用户若是使用 ctx.redirect
方法,须要在应用的配置文件中作以下配置:
// config/config.default.js
exports.security = {
domainWhiteList:['.domain.com'], // 安全白名单,以 . 开头
};
复制代码
若用户没有配置 domainWhiteList
或者 domainWhiteList
数组内为空,则默认会对全部跳转请求放行,即等同于 ctx.unsafeRedirect(url)