egg内置的HttpClient

HttpClient


最早做项目时,还不知道 egg 基于 urllib 内置实现了一个 HttpClient, 可以非常方便的完成任何 HTTP 请求。最近刚好有时间来看下 egg 的文档,就来聊聊 egg 内置的 HttpClient。

可以通过 app 使用 HttpClient,也可以通过 Context 使用 HttpClient。

框架在初始化时,自动将HttpClient 初始化到 app.httpclient,同时增加了一个 app.curl(url, options) 方法, 同 app.httpclient.request(url, options)是等价的。

1
2
3
4
5
6
7
8
// app.js
module.exports = app => {
app.beforeStart(async () => {
const result = await app.curl('https://registry.npm.taobao.org/egg/latest', {
dataType: 'json',
});
})
}

在 Context 中同样提供了 ctx.curl(url, options) 和 ctx.httpclient,使用方法同 app 下使用相同。

1
2
3
4
5
6
7
8
9
10
11
// app/controller/npm.js
class NpmController extends Controller {
async index() {
const ctx = this.ctx;
const result = await ctx.curl('https://registry.npm.taobao.org/egg/latest', {
dataType: 'json',
timeout: 3000,
});
// ...
}
}

基本HTTP请求

GET

1
2
3
4
5
6
7
8
9
10
// app/controller/npm.js
class NpmController extends Controller {
async get() {
const ctx = this.ctx;
const result = await ctx.curl('https://httpbin.org/get?foo=bar');
ctx.status = result.status;
ctx.set(result.headers);
ctx.body = result.data;
}
}
  • GET 请求可以不用设置 options.methods, HttpClient 的默认 method 会设置为 GET。
  • 返回值 result 会包含3个属性: status, headers, data
    • status: 响应状态码,这个就是大家熟悉的 http 状态码: 200, 302, 404等;
    • headers: 响应头,类似 { ‘content-type’: ‘text/html’}
    • data: 响应body, 默认 HttpClient 不会做处理,直接返回 Buffer 类型数据。一旦设置了 options.dataType, HttpClient 会根据此参数对 data 进行处理。

POST

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// app/controller/npm.js
class NpmController extends Controller {
async post() {
const ctx = this.ctx;
const result = await ctx.curl('https://httpbin.org/post', {
method: 'POST',
// 通过 contentType 告诉 HttpClient 以 JSON 格式发送数据
contentType: 'json',
data: {
hello: 'world',
now: Date.now(),
},
// 明确告诉 HttpClient 以 JSON 格式处理响应 body
dataType: 'json',
})
}
}

PUT

用法与 POST 类似,更加适合更新数据和替换数据的语义,除了 method 参数需要设置为 PUT,其他参数与 POST 几乎一致。

DELETE

删除数据会选择 DELETE 请求,通常不需要加请求 body, 但 HttpClient 不会限制。

1
2
3
4
5
6
7
8
9
10
11
// app/controller/npm.js
class NpmController extends Controller {
async del() {
const ctx = this.ctx;
const result = await ctx.curl('https://httpbin.org/delete', {
method: 'DELETE',
dataType: 'json',
});
ctx.body = result.data;
}
}

高级 HTTP 请求

这部分主要是涉及到表单及文件上传相关的请求。

Form 表单提交

面向浏览器的 Form 表单(不包含文件)的提交接口,通常都是以 content-type: application/x-www-form-urlencoded 的格式提交请求数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// app/controller/npm.js
class NpmController extends Controller {
async submit() {
const ctx = this.ctx;
const result = await ctx.curl('https://httpbin.org/post', {
// 必须指定 method,支持 POST,PUT 和 DELETE
method: 'POST',
// 不需要设置 contentType,HttpClient 会默认以 application/x-www-form-urlencoded 格式发送请求
data: {
now: Date.now(),
foo: 'bar',
},
// 明确告诉 HttpClient 以 JSON 格式处理响应 body
dataType: 'json',
});
ctx.body = result.data.form;
// 响应最终会是类似以下的结果:
// {
// "foo": "bar",
// "now": "1483864184348"
// }
}
}

以 Multipart 方式上传文件

当 From 表单提交包含文件的时候,请求数据格式,就必须以 multipart/form-data 进行提交。此时,需引入 fromstream 帮助我们生成可被 HttpClient 消费的 form 对象。

1
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
// app/controller/npm.js
const FormStream = require('formstream');
class NpmController extends Controller {
async upload() {
const ctx = this.ctx;
const form = new FormStream();
// 设置普通的 key value
form.field('foo', 'bar');
// 上传当前文件本身用于测试
form.file('file', __filename);

const result = await ctx.curl('https://httpbin.org/post', {
// 必须指定 method,支持 POST,PUT
method: 'POST',
// 生成符合 multipart/form-data 要求的请求 headers
headers: form.headers(),
// 以 stream 模式提交
stream: form,
// 明确告诉 HttpClient 以 JSON 格式处理响应 body
dataType: 'json',
});
ctx.body = result.data.files;
// 响应最终会是类似以下的结果:
// {
// "file": "'use strict';\n\nconst For...."
// }
}
}

可以继续通过 form.file() 添加更多文件以实现一次性上传多个文件的需求。

1
2
form.file('file1', file1);
form.file('file2', file2);

以 Stream 方式上传文件

Stream 实际会以 Transfer-Encoding: Chunked 传输编码格式发送,这个转换是 HTTP 模块自动实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// app/controller/npm.js
const fs = require('fs');
const FormStream = require('formstream');
class NpmController extends Controller {
async uploadByStream() {
const ctx = this.ctx;
// 上传当前文件本身用于测试
const fileStream = fs.createReadStream(__filename);
// httpbin.org 不支持 stream 模式,使用本地 stream 接口代替
const url = `${ctx.protocol}://${ctx.host}/stream`;
const result = await ctx.curl(url, {
// 必须指定 method,支持 POST,PUT
method: 'POST',
// 以 stream 模式提交
stream: fileStream,
});
ctx.status = result.status;
ctx.set(result.headers);
ctx.body = result.data;
// 响应最终会是类似以下的结果:
// {"streamSize":574}
}
}

Options 参数详解

HttpClient 默认全局配置

1
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
// config/config.default.js
exports.httpclient = {
// 是否开启本地 DNS 缓存,默认关闭,开启后有两个特性
// 1. 所有的 DNS 查询都会默认优先使用缓存的,即使 DNS 查询错误也不影响应用
// 2. 对同一个域名,在 dnsCacheLookupInterval 的间隔内(默认 10s)只会查询一次
enableDNSCache: false,
// 对同一个域名进行 DNS 查询的最小间隔时间
dnsCacheLookupInterval: 10000,
// DNS 同时缓存的最大域名数量,默认 1000
dnsCacheMaxLength: 1000,
request: {
// 默认 request 超时时间
timeout: 3000,
},

httpAgent: {
// 默认开启 http KeepAlive 功能
keepAlive: true,
// 空闲的 KeepAlive socket 最长可以存活 4 秒
freeSocketTimeout: 4000,
// 当 socket 超过 30 秒都没有任何活动,就会被当作超时处理掉
timeout: 30000,
// 允许创建的最大 socket 数
maxSockets: Number.MAX_SAFE_INTEGER,
// 最大空闲 socket 数
maxFreeSockets: 256,
},

httpsAgent: {
// 默认开启 https KeepAlive 功能
keepAlive: true,
// 空闲的 KeepAlive socket 最长可以存活 4 秒
freeSocketTimeout: 4000,
// 当 socket 超过 30 秒都没有任何活动,就会被当作超时处理掉
timeout: 30000,
// 允许创建的最大 socket 数
maxSockets: Number.MAX_SAFE_INTEGER,
// 最大空闲 socket 数
maxFreeSockets: 256,
},
};

data: Object

  • GET, HEAD: 通过querystring.stringify(data)处理后拼接到url的 query 参数上。
  • POST, PUT, DELETE等:需要根据 contentType 做进一步处理判断:
    • contentTyp = json: 通过 JSON.stringify(data) 处理,并设置body 发送。
    • 其他: 通过 querystring.stringify(data) 处理,并设置 body 发送。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// GET 
ctx.curl(url, {
data: { foo: 'bar' },
});

// POST
ctx.curl(url, {
method: 'POST',
data: { foo: 'bar' }
});

// post + json + data
ctx.curl(url, {
method: 'POST',
contentType: 'json',
data: { foo: 'bar' },
});

dataAsQueryString: Boolean

如果 dataAsQueryString=true ,即使在 POST 情况下,也会强制 options.data 以 querystring.stringify 处理之后拼接到 url 的 query 参数上。

可以解决以 stream 发送数据,且额外的请求参数以 url query 形式传递的场景:

1
2
3
4
5
6
7
8
ctx.curl(url, {
method: 'POST',
dataAsQueryString: true,
data: {,
accessToken: 'some data',
},
stream: fileStream,
})

content: String|Buffer

发送请求正文,如若设置了此参数,会直接忽略 data 参数。

1
2
3
4
5
6
7
ctx.curl(url, {
method: 'POST',
content: '<xml>hello world</xml>',
headers: {
'content-type': 'text/html',
},
});