eggjs中curl请求、HTTP基础及RouteCtrl实战
# 前期准备
可以通过vscode的REST Client模拟
https://github.com/Huachao/vscode-restclient
curl -v http://baidu.com
jwt模式示例:egg-jwt
curl 127.0.0.1:7001
// response 401
curl 127.0.0.1:7001/login
// response hello admin
curl -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0OTAwMTU0MTN9.ehQ38YsRlM8hDpUMKYq1rHt-YjBPSU11dFm0NOroPEg" 127.0.0.1:7001/success
// response {foo: bar}
#config
// {app_root}/config/config.default.js
config.jwt = {
secret: `${config.app.name}_secret`,
// expiresIn: 60 * 60 * 24 * 30
expiresIn: '30 days'
}
#How To Create A Token
const token = app.jwt.sign({ foo: 'bar' }, app.config.jwt.secret);
return ctx.app.jwt.sign({ id }, ctx.app.config.jwt.secret, { expiresIn: config.jwt.expiresIn })
#解析后的数据:
const data = app.jwt.verify(token)
console.log('----middle---data---->', data)
----middle---data----> { id: 1, iat: 1565579945, exp: 1568171945 }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Http基础
https://eggjs.org/zh-cn/basics/controller.html
如果我们发起一个 HTTP 请求来访问前面例子中提到的 Controller:
curl -X POST http://localhost:3000/api/posts --data '{"title":"controller", "content": "what is controller"}' --header 'Content-Type:application/json; charset=UTF-8'
#通过 curl 发出的 HTTP 请求的内容就会是下面这样的:
#request
POST /api/posts HTTP/1.1
Host: localhost:3000
Content-Type: application/json; charset=UTF-8
{"title": "controller", "content": "what is controller"}
#response
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Content-Length: 8
Date: Mon, 09 Jan 2017 08:40:28 GMT
Connection: keep-alive
{"id": 1}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
和请求一样,从第二行开始到下一个空行之间都是响应头,这里的 Content-Type, Content-Length 表示这个响应的格式是 JSON,长度为 8 个字节。
最后剩下的部分就是这次响应真正的内容。
# 获取 HTTP 请求参数
# query
例如 GET /posts?category=egg&language=node
中 category=egg&language=node
就是用户传递过来的参数。
class PostController extends Controller {
async listPosts() {
const query = this.ctx.query;
// {
// category: 'egg',
// language: 'node',
// }
}
}
2
3
4
5
6
7
8
9
当 Query String 中的 key 重复时,ctx.query
只取 key 第一次出现时的值,后面再出现的都会被忽略。GET /posts?category=egg&category=koa
通过 ctx.query
拿到的值是 { category: 'egg' }
。
# queries
有时候我们的系统会设计成让用户传递相同的 key,例如 GET /posts?category=egg&id=1&id=2&id=3
。针对此类情况,框架提供了 ctx.queries
对象;
ctx.queries
上所有的 key 如果有值,也一定会是数组类型。
// 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' ],
// }
}
}
2
3
4
5
6
7
8
9
10
# Router params
在 Router (opens new window) 中,我们介绍了 Router 上也可以申明参数,这些参数都可以通过 ctx.params
获取到。
const query = ctx.helper.toInt(ctx.params.id)
/ 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');
}
}
2
3
4
5
6
7
8
9
10
# body
虽然我们可以通过 URL 传递参数,但是还是有诸多限制:
- 浏览器中会对 URL 的长度有所限制 (opens new window),如果需要传递的参数过多就会无法传递。
- 服务端经常会将访问的完整 URL 记录到日志文件中,有一些敏感数据通过 URL 传递会不安全。
https://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers
# Short answer - de facto limit of 2000 characters
我们通常会在这个部分传递 POST、PUT 和 DELETE 等方法的参数。一般请求中有 body 的时候,客户端(浏览器)会同时发送 Content-Type
告诉服务端这次请求的 body 是什么格式的。Web 开发中数据传递最常用的两类格式分别是 JSON 和 Form。
框架内置了 bodyParser (opens new window) 中间件来对这两类格式的请求 body 解析成 object 挂载到 ctx.request.body
上。HTTP 协议中并不建议在通过 GET、HEAD 方法访问时传递 body,所以我们无法在 GET、HEAD 方法中按照此方法获取到内容。
// POST /api/posts HTTP/1.1
// Host: localhost:3000
// Content-Type: application/json; charset=UTF-8
//
// {"title": "controller", "content": "what is controller"}
class PostController extends Controller {
async listPosts() {
assert.equal(this.ctx.request.body.title, 'controller');
assert.equal(this.ctx.request.body.content, 'what is controller');
}
}
2
3
4
5
6
7
8
9
10
11
框架对 bodyParser 设置了一些默认参数,配置好之后拥有以下特性:
- 当请求的 Content-Type 为
application/``json
,application/``json-patch+json
,application/vnd.api+json
和application/csp-report
时,会按照 json 格式对请求 body 进行解析,并限制 body 最大长度为100kb
。 - 当请求的 Content-Type 为
application/``x-www-form-urlencoded
时,会按照 form 格式对请求 body 进行解析,并限制 body 最大长度为100kb
。 - 如果解析成功,body 一定会是一个 Object(可能是一个数组)。
一般来说我们最经常调整的配置项就是变更解析时允许的最大长度,可以在 config/config.default.js
中覆盖框架的默认值。json/form默认都是100kb;
https://github.com/eggjs/egg/blob/master/config/config.default.js
module.exports = {
bodyParser: {
jsonLimit: '1mb',
formLimit: '1mb',
},
};
2
3
4
5
6
如果用户的请求 body 超过了我们配置的解析最大长度,会抛出一个状态码为 413
的异常,如果用户请求的 body 解析失败(错误的 JSON),会抛出一个状态码为 400
的异常。
注意:在调整 bodyParser 支持的 body 长度时,如果我们应用前面还有一层反向代理(Nginx),可能也需要调整它的配置,确保反向代理也支持同样长度的请求 body。
一个常见的错误是把 ctx.request.body
和 ctx.body
混淆,后者其实是 ctx.response.body
的简写。
# 获取上传的文件
请求 body 除了可以带参数之外,还可以发送文件,一般来说,浏览器上都是通过 Multipart/form-data
格式发送文件的,框架通过内置 Multipart (opens new window) 插件来支持获取用户上传的文件,我们为你提供了两种方式:
# File 模式:
如果你完全不知道 Nodejs 中的 Stream 用法,那么 File 模式非常合适你:
1)在 config 文件中启用
file
模式:
// config/config.default.js
exports.multipart = {
mode: 'file',
};
2
3
4
2)上传 / 接收文件:
- 上传 / 接收单个文件:
<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,
};
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- 上传 / 接收多个文件:
对于多个文件,我们借助 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);
}
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Stream 模式:
如果你对于 Node 中的 Stream 模式非常熟悉,那么你可以选择此模式。在 Controller 中,我们可以通过
ctx.getFileStream()
接口能获取到上传的文件流。
1:上传 / 接受单个文件:
要通过 ctx.getFileStream
便捷的获取到用户上传的文件,需要满足两个条件:
- 只支持上传一个文件。
- 上传文件必须在所有其他的 fields 后面,否则在拿到文件流时可能还获取不到 fields。
2:上传 / 接受多个文件:
如果要获取同时上传的多个文件,不能通过 ctx.getFileStream()
来获取,只能通过下面这种方式:
const parts = ctx.multipart();
const path = require('path');
const sendToWormhole = require('stream-wormhole');
const Controller = require('egg').Controller;
class UploaderController extends Controller {
async upload1() {
const ctx = this.ctx;
const stream = await ctx.getFileStream();
const name = 'egg-multipart-test/' + path.basename(stream.filename);
let result;
try {
result = await ctx.oss.put(name, stream);// 文件处理,上传到云存储等等
} catch (err) {
await sendToWormhole(stream);// 必须将上传的文件流消费掉,要不然浏览器响应会卡死
throw err;
}
ctx.body = {
url: result.url,
fields: stream.fields,// 所有表单字段都能通过 `stream.fields` 获取到
};
}
async upload2() {
const ctx = this.ctx;
const parts = ctx.multipart();
let part;
while ((part = await parts()) != null) { // parts() 返回 promise 对象
if (part.length) {
// 这是 busboy 的字段
console.log('field: ' + part[0]);
console.log('value: ' + part[1]);
console.log('valueTruncated: ' + part[2]);
console.log('fieldnameTruncated: ' + part[3]);
} else {
if (!part.filename) {
// 这时是用户没有选择文件就点击了上传(part 是 file stream,但是 part.filename 为空)
// 需要做出处理,例如给出错误提示消息
return;
}
// part 是上传的文件流
console.log('field: ' + part.fieldname);
console.log('filename: ' + part.filename);
console.log('encoding: ' + part.encoding);
console.log('mime: ' + part.mime);
let result;
try {// 文件处理,上传到云存储等等
result = await ctx.oss.put('egg-multipart-test/' + part.filename, part);
} catch (err) {
// 必须将上传的文件流消费掉,要不然浏览器响应会卡死
await sendToWormhole(part);
throw err;
}
console.log(result);
}
}
console.log('and we are done parsing the form!');
}
}
module.exports = UploaderController;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
为了保证文件上传的安全,框架限制了支持的的文件格式,框架默认支持白名单;
用户可以通过在 config/config.default.js
中配置来新增支持的文件扩展名,或者重写整个白名单
module.exports = {
multipart: {
whitelist: [ '.png' ], // 覆盖整个白名单,只允许上传 '.png' 格式
fileExtensions: [ '.apk' ] // 增加对 apk 扩展名的文件支持
},
};
2
3
4
5
6
注意:当重写了 whitelist 时,fileExtensions 不生效。
欲了解更多相关此技术细节和详情,请参阅 Egg-Multipart (opens new window)。
# header
除了从 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']
,因为前者会自动处理大小写。
由于 header 比较特殊,有一些是 HTTP
协议规定了具体含义的(例如 Content-Type
,Accept
),有些是反向代理设置的,已经约定俗成(X-Forwarded-For)
特别是如果我们通过 config.proxy = true
设置了应用部署在反向代理(Nginx)之后,有一些 Getter 的内部处理会发生改变。
# ctx.host
优先读通过 config.hostHeaders
中配置的 header 的值,读不到时再尝试获取 host 这个 header 的值,如果都获取不到,返回空字符串。
config.hostHeaders
默认配置为 x-forwarded-host
。
# ctx.protocol
通过这个 Getter 获取 protocol 时,首先会判断当前连接是否是加密连接,如果是加密连接,返回 https。
# ctx.ips
通过 ctx.ips
获取请求经过所有的中间设备 IP 地址列表,只有在 config.proxy = true
时,才会通过读取 config.ipHeaders
中配置的 header 的值来获取,获取不到时为空数组。
config.ipHeaders
默认配置为 x-forwarded-for
。
# ctx.ip
通过 ctx.ip
获取请求发起方的 IP 地址,优先从 ctx.ips
中获取,ctx.ips
为空时使用连接上发起方的 IP 地址。
注意:ip
和 ips
不同,ip
当 config.proxy = false
时会返回当前连接发起者的 ip
地址,ips
此时会为空数组。
# Cookie
服务端可以通过响应头(set-cookie)将少量数据响应给客户端,浏览器会遵循协议将数据保存,并在下次请求同一个服务的时候带上(浏览器也会遵循协议,只在访问符合 Cookie 指定规则的网站时带上对应的 Cookie 来保证安全性)。
通过 ctx.cookies
,我们可以在 Controller 中便捷、安全的设置和读取 Cookie。
Cookie 虽然在 HTTP 中只是一个头,但是通过 foo=bar;foo1=bar1;
的格式可以设置多个键值对。
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;
}
2
3
4
5
6
7
8
9
10
11
12
# Session
通过 Cookie,我们可以给每一个用户设置一个 Session,用来存储用户身份相关的信息,这份信息会加密后存储在 Cookie 中,实现跨请求的用户身份保持。
框架内置了 Session (opens new window) 插件,给我们提供了 ctx.session
来访问或者修改当前用户 Session 。
Session 的使用方法非常直观,直接读取它或者修改它就可以了,如果要删除它,直接将它赋值为 null
:
module.exports = {
key: 'EGG_SESS', // 承载 Session 的 Cookie 键值对名字
maxAge: 86400000, // Session 的最大有效时间
};
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,
};
}
async deleteSession() {
this.ctx.session = null;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
和 Cookie 一样,Session 也有许多安全等选项和功能,在使用之前也最好阅读 Session (opens new window) 文档深入了解。
# 配置
对于 Session 来说,主要有下面几个属性可以在 config.default.js
中进行配置:
# 参数校验
在获取到用户请求的参数后,不可避免的要对参数进行一些校验。
借助 Validate (opens new window) 插件提供便捷的参数校验机制,帮助我们完成各种复杂的参数校验。
通过 ctx.validate(rule, [body])
直接对参数进行校验:
body,默认是cxt.request.body;可以收到设置;
ctx.validate(rule, ctx.query);//参照下面自定义;
// config/plugin.js
exports.validate = {
enable: true,
package: 'egg-validate',
};
class PostController extends Controller {
async create() {
// 校验参数
// 如果不传第二个参数会自动校验 `ctx.request.body`
this.ctx.validate({
title: { type: 'string' },
content: { type: 'string' },
});
}
async create2() {
const ctx = this.ctx;
try {
ctx.validate(createRule);
} catch (err) {
ctx.logger.warn(err.errors);
ctx.body = { success: false };
return;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
当校验异常时,会直接抛出一个异常,异常的状态码为 422,errors 字段包含了详细的验证不通过信息。如果想要自己处理检查的异常,可以通过 try catch
来自行捕获。
# 校验规则
参数校验通过 Parameter (opens new window) 完成,支持的校验规则可以在该模块的文档中查阅到。
# 自定义校验规则
除了上一节介绍的内置检验类型外,有时候我们希望自定义一些校验规则,让开发时更便捷,此时可以通过 app.validator.addRule(type, check)
的方式新增自定义规则。
// app.js
app.validator.addRule('json', (rule, value) => {
try {
JSON.parse(value);
} catch (err) {
return 'must be json string';
}
});
class PostController extends Controller {
async handler() {
const ctx = this.ctx;
// query.test 字段必须是 json 字符串
const rule = { test: 'json' };
ctx.validate(rule, ctx.query);
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 调用 Service
我们并不想在 Controller 中实现太多业务逻辑,所以提供了一个 Service (opens new window) 层进行业务逻辑的封装,这不仅能提高代码的复用性,同时可以让我们的业务逻辑更好测试。
在 Controller 中可以调用任何一个 Service 上的任何方法,同时 Service 是懒加载的,只有当访问到它的时候框架才会去实例化它。
const req = Object.assign(ctx.request.body, { author });
class PostController extends Controller {
async create() {
const ctx = this.ctx;
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;
}
}
2
3
4
5
6
7
8
9
10
11
# 发送 HTTP 响应
当业务逻辑完成之后,Controller 的最后一个职责就是将业务逻辑的处理结果通过 HTTP 响应发送给用户。
# 设置 status
HTTP 设计了非常多的状态码 (opens new window),每一个状态码都代表了一个特定的含义,通过设置正确的状态码,可以让响应更符合语义。
# 设置 body
绝大多数的数据都是通过 body 发送给请求方的,和请求中的 body 一样,在响应中发送的 body,也需要有配套的 Content-Type 告知客户端如何对数据进行解析。下面默认规则;
- 作为一个 RESTful 的 API 接口 controller,通常会返回 Content-Type 为
application/json
格式的 body,内容是一个 JSON 字符串。 - 作为一个 html 页面的 controller,我们通常会返回 Content-Type 为
text/html
格式的 body,内容是 html 代码段。
注意:ctx.body
是 ctx.response.body
的简写,不要和 ctx.request.body
混淆了。
特殊:由于 Node.js 的流式特性,我们还有很多场景需要通过 Stream 返回响应,例如返回一个大文件,代理服务器直接返回上游的内容,框架也支持直接将 body 设置成一个 Stream,并会同时处理好这个 Stream 上的错误事件。streaming: true,
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;
}
2
3
4
5
6
7
8
9
# 渲染模板
通常来说,我们不会手写 HTML 页面,而是会通过模板引擎进行生成。 框架自身没有集成任何一个模板引擎,但是约定了 View 插件的规范 (opens new window),通过接入的模板引擎,可以直接使用 ctx.render(template)
来渲染模板生成 html。
# JSONP
有时我们需要给非本域的页面提供接口服务,又由于一些历史原因无法通过 CORS (opens new window) 实现,可以通过 JSONP (opens new window) 来进行响应。
由于 JSONP 如果使用不当会导致非常多的安全问题,所以框架中提供了便捷的响应 JSONP 格式数据的方法,封装了 JSONP XSS 相关的安全防范 (opens new window),并支持进行 CSRF 校验和 referrer 校验。
- 通过
app.jsonp()
提供的中间件来让一个 controller 支持响应 JSONP 格式的数据。在路由中,我们给需要支持 jsonp 的路由加上这个中间件: - 用户请求对应的 URL 访问到这个 controller 的时候,如果 query 中有
_callback=fn
参数,将会返回 JSONP 格式的数据,否则返回 JSON 格式的数据。
<span id="wizkm_highlight_tmp_span">// app/router.js
module.exports = app => {
const jsonp = app.jsonp();
app.router.get('/api/posts/:id', jsonp, app.controller.posts.show);
app.router.get('/api/posts', jsonp, app.controller.posts.list);
};
// app/controller/posts.js
class PostController extends Controller {
async show() {
this.ctx.body = {
name: 'egg',
category: 'framework',
language: 'Node.js',
};
}
}</span>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 通过上面的方式配置之后,如果用户请求
/api/posts/1?callback=fn
,响应为 JSONP 格式,如果用户请求 /api/posts/1
,响应格式为 JSON。
# JSONP 配置
框架默认通过 query 中的 _callback
参数作为识别是否返回 JSONP 格式数据的依据,并且 _callback
中设置的方法名长度最多只允许 50 个字符。应用可以在 config/config.default.js
全局覆盖默认的配置:
我们同样可以在 app.jsonp()
创建中间件时覆盖默认的配置,以达到不同路由使用不同配置的目的:
// config/config.default.js
exports.jsonp = {
callback: 'callback', // 识别 query 中的 `callback` 参数
limit: 100, // 函数名最长为 100 个字符
};
// app/router.js
module.exports = app => {
const { router, controller, jsonp } = app;
router.get('/api/posts/:id', jsonp({ callback: 'callback' }), controller.posts.show);
router.get('/api/posts', jsonp({ callback: 'cb' }), controller.posts.list);
};
2
3
4
5
6
7
8
9
10
11
12
# 跨站防御配置
默认配置下,响应 JSONP 时不会进行任何跨站攻击的防范,在某些情况下,这是很危险的。我们初略将 JSONP 接口分为三种类型:
- 查询非敏感数据,例如获取一个论坛的公开文章列表。
- 查询敏感数据,例如获取一个用户的交易记录。
- 提交数据并修改数据库,例如给某一个用户创建一笔订单。
如果我们的 JSONP 接口提供下面两类服务,在不做任何跨站防御的情况下,可能泄露用户敏感数据甚至导致用户被钓鱼。因此框架给 JSONP 默认提供了 CSRF 校验支持和 referrer 校验支持。
# CSRF
在 JSONP 配置中,我们只需要打开 csrf: true
,即可对 JSONP 接口开启 CSRF 校验。
xxx
注意,CSRF 校验依赖于 security (opens new window) 插件提供的基于 Cookie 的 CSRF 校验。
当 CSRF 和 referrer 校验同时开启时,请求发起方只需要满足任意一个条件即可通过 JSONP 的安全校验。
# 设置 Header
我们通过状态码标识请求成功与否、状态如何,在 body 中设置响应的内容。而通过响应的 Header,还可以设置一些扩展信息。
通过 ctx.set(key, value)
方法可以设置一个响应头,ctx.set(headers)
设置多个 Header。
ctx.set('show-response-time', used.toString());
async show() {
const ctx = this.ctx;
const start = Date.now();
ctx.body = await ctx.service.post.get();
const used = Date.now() - start;
// 设置一个响应头
ctx.set('show-response-time', used.toString());
}
2
3
4
5
6
7
8
# 重定向
框架通过 security 插件覆盖了 koa 原生的 ctx.redirect
实现,以提供更加安全的重定向。
ctx.redirect(url)
如果不在配置的白名单域名内,则禁止跳转。ctx.unsafeRedirect(url)
不判断域名,直接跳转,一般不建议使用,明确了解可能带来的风险后使用。
用户如果使用ctx.redirect
方法,需要在应用的配置文件中做如下配置:
若用户没有配置 domainWhiteList
或者 domainWhiteList
数组内为空,则默认会对所有跳转请求放行,即等同于ctx.unsafeRedirect(url)
// config/config.default.js
exports.security = {
domainWhiteList:['.domain.com'], // 安全白名单,以 . 开头
};
2
3
4
# RouteCtrl处理对应示例
https://eggjs.org/zh-cn/basics/router.html
# 参数获取
# Query String 方式
// 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
2
3
4
5
6
7
8
9
10
11
# 参数命名方式
// 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
2
3
4
5
6
7
8
9
10
11
# 复杂参数的获取
路由里面也支持定义正则,可以更加灵活的获取参数:
// 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
2
3
4
5
6
7
8
9
10
11
12
13
# 表单内容的获取
// 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 请求。
// curl -X POST http://127.0.0.1:7001/form --data '{"name":"controller"}' --header 'Content-Type:application/json'
2
3
4
5
6
7
8
9
10
11
12
附:
这里直接发起 POST 请求会报错:'secret is missing'。错误信息来自 koa-csrf/index.js#L69 (opens new window) 。
原因:框架内部针对表单 POST 请求均会验证 CSRF 的值,因此我们在表单提交时,请带上 CSRF key 进行提交,可参考安全威胁csrf的防范 (opens new window)
注意:上面的校验是因为框架中内置了安全插件 egg-security (opens new window),提供了一些默认的安全实践,并且框架的安全插件是默认开启的,如果需要关闭其中一些安全防范,直接设置该项的 enable 属性为 false 即可。
「除非清楚的确认后果,否则不建议擅自关闭安全插件提供的功能。」
这里在写例子的话可临时在
config/config.default.js
中设置
exports.security = {
csrf: false
};
2
3