Egg基于
urllib
内置实现了HttpClient
,因此在应用里可以很方便地发起HTTP请求
1、使用HttpClient
- 框架初始化时,会将HttpClient实例化到
app.httpClient
,故可以通过app.httpClient
或者app.curl(url, options)
来发起请求 - 框架也在Context中提供了
ctx.curl(url, options)
和ctx.httpClient
,故在可以使用Context
实例的地方,也可以很方便地使用HttpClient
例子:
// app/controller/npm.js
class NpmController extends Controller {
async index() {
const { ctx } = this
const result = await ctx.curl('https://registry.npm.taobao.org/egg/latest', {
dataType: 'json',
timeout: 3000
})
const { status, headers, data: package } = result
ctx.body = {
status,
headers,
package
}
}
}
2、参数介绍
对于curl(url, options)
,它的参数介绍如下:
url
请求的URLoptions
请求配置method: String
,请求方法,不配置的情况下,默认使用GET方法data: Object
,请求发送的数据,在GET/HEAD方法中,会通过querystring.stringify(data)
处理后拼接到URL的查询参数中;而在POST/PUT/DELETE这些方法中,则会根据contentType
进行处理(为json
时,通过JSON.stringify(data)
处理,其他情况下用querystring.stringify(data)
),然后设置为用body发送dataAsQueryString: Boolean
,为true
时,即使是POST情况下,options.data
也会被当做查询字符串拼接到URL中(如此可以解决以stream
发送数据,额外的请求参数用URL查询字符串的情况)content: String|Buffer
,发送请求正文,若设置了该参数,options.data
会被忽略stream: ReadStream
,设置发送请求正文的可读数据流,默认为null
,一旦设置该参数,data
/content
两个配置项会被忽略writeStream: WriteStream
,设置接受响应数据的可写数据流,默认为null
。一旦设置该参数,那么result.data
会为null
,数据都会被写入writeStream
,如:ctx.curl(url, { writeStream: fs.createWriteStream('/path/to/store') })
consumeWriteStream: Boolean
,是否等待writeStream
完全写完才算响应接收完毕(默认为true
)contentType: String
,设置请求数据格式,默认为undefined
。一般情况下会根据data
和content
参数自动设置,不过当data
是object时默认设置的是form
dataType: String
,设置响应数据格式,默认不对响应数据做任何处理,直接返回原始的Buffer数据。支持text
和json
两种格式,当设为json
时,若解析失败,则会抛出JSONResponseFormatError
fixJSONCtlChars: Boolean
,是否自动过滤响应数据中的特殊控制字符(U+0000
~U+001F
,通常会在一些CGI系统返回的JSON数据中包含),默认为false
headers: Object
,自定义请求头timeout: Number|Array
,设置请求超时时间,默认是[5000, 5000]
(创建连接超时5s,接收响应超时5秒,两个参数都相等时,可以简写为timeout: 5000
)agent: HttpAgent
,允许通过此参数覆盖默认的HttpAgent
,若不想开启KeepAlive
,可设置该参数为false
httpsAgent: HttpsAgent
,同上,不过是针对https的auth: String
,用于Basic Authentication
场景,如:ctx.url(url, { auth: 'user:pwd' })
digestAuth: String
,摘要登录授权参数,设置此参数会自动对401响应尝试生成Authorization
请求头,尝试以授权方式请求一次followRedirect: Boolean
,是否跟进重定向,默认为false
maxRedirects: Number
,设置最大自动跳转次数,避免循环跳转无法终止,默认为10次formatRedirectUrl: Function(from, to)
,允许自定义实现302/301等跳转URL拼接beforeRequest: Function(options)
,发送请求前的钩子,这里可以对请求参数做最后一次的修改streaming: Boolean
,是否直接返回响应流,默认为false
。开启后,HttpClient会在拿到响应对象res
后立即返回,此时result.headers
/result.status
已经可以读取到,但是还没有读取data
数据(注意,若result.res
没有传给body,那么必须要消费掉)gzip: Boolean
,开启后,请求时将设置头部中的Accept-Encoding: gzip
,并且自动解压带Content-Encoding: gzip
响应头的数据ca
/rejectUnauthorized
/pfx
/key
/cert
/passphrase
/ciphers
/secureProtocol
,透传给https
模块的参数timing: Boolean
,是否开启请求各阶段的时间测量,默认为false
。开启后,可以通过result.res.timing
拿到HTTP请求各阶段的时间测量值(单位是ms
),其返回的对象如下:
{
queue, // 分配Socket耗时
dnslookup, // DNS查询耗时
connected, // Socket三次握手连接成功耗时
requestSent, // 请求数据完整发送完毕耗时
waiting, // 收到第一个字节的响应数据耗时
contentDownload // 全部响应数据接收完毕耗时
}
此外,HttpClient有一些默认的全局配置,如下:
exports.httpclient = {
// 是否开启本地DNS缓存,开启后:
// 1. 所有的DNS查询都会默认优先使用缓存,即使DNS查询错误也不影响应用
// 2. 对同一个域名,在dnsCacheLookupInterval的间隔内(默认10s)只会查询一次
enableDNSCache: false,
// 对同一个域名进行DNS查询的最小间隔时间
dnsCacheLookupInterval: 10000,
// DNS同时缓存的最大域名数量
dnsCacheMaxLength: 1000,
request: {
timeout: 3000
}
httpAgent: {
// 默认开启http keepAlive
keepAlive: true,
// 空闲的KeepAlive socket最长可以存活的时间
freeSocketKeepAliveTimeout: 4000,
// 当socket超时没活动,会被处理掉
timeout: 30000,
// 允许创建的最大socket数
maxSockets: Number.MAX_SAFE_INTEGER,
// 最大空闲socket数
maxFreeSockets: 256
}
httpsAgent: { /* 配置同httpAgent,不过是针对Https */ }
}
若需覆盖这些默认配置,则可以通过修改config/config.default.js
进行
3、返回值
一般情况下,请求的返回的result
会包含status
/headers
/和data
:
status
,状态码headers
,响应头,如:{ 'content-type': 'text/html', ... }
data
,响应body,默认情况下直接返回Buffer数据,设置了options.dataType
后则对返回的数据进行相应的处理
1)Form表单情况
// app/controller/npm.js
class NpmController extends Controller {
async submit() {
const { ctx } = this
const result = await ctx.curl('https://httpbin.org/post', {
method: 'POST',
// 由于默认情况下会使用`application/x-www-form-urlencoded`,所以不需要指定contentType
data: {
now: Date.now(),
foo: 'bar'
}
dataType: 'json'
})
ctx.body = result.body.form
}
}
2)Multipart方式上传文件
若一个表单包含了文件,那么请求数据则需要以multipart/form-data
方式进行提交,需要引入formstream
模块,如下:
// app/controller/npm.js
const FormStream = require('formstream')
class NpmController extends Controller {
async upload() {
const { ctx } = this
const form = new FormStream()
// 设置普通的表单域
form.field('foo', 'bar')
// 上传文件
form.file('file1', path.resolve('path/to/file1'))
form.file('file2', path.resolve('path/to/file2'))
const result = await ctx.curl('https://httpbin.org/post', {
method: 'POST',
headers: form.headers(),
stream: form,
dataType: 'json'
})
ctx.body = result.data.files
}
}
3)Stream方式上传文件
若服务端支持流式上传,那么还可以直接发送Stream。Stream实际会以Transfer-Encoding: chunked
传输编码格式发送。实例如下:
// app/controller/npm.js
const fs = require('fs')
const FormStream = require('formstream')
class NpmController extends Controller {
async uploadByStream() {
const { ctx } = this
const fileStream = fs.createReadStream(__filename)
const url = `${ctx.protocol}://${ctx.host}/stream`
const result = await ctx.curl(url, {
method: 'POST',
stream: fileStream
})
ctx.status = result.status
ctx.set(result.headers)
ctx.body = result.data
}
}
4、事件
框架提供了request
和response
两个事件,可以用于统一tracer日志,对于HttpClient而言,它的生命周期中会进行如下过程:
初始化options -> 发送`request`事件 -> 发送请求、接收响应 -> 发送`response`事件 -> 结束
因此,request
发生在网络操作发生之前,response
则发生在网络操作结束之后,如:
app.httpclient.on('request', req => {
req.url // 获取请求url
req.ctx // 获取请求上下文
// ...
})
app.httpclient.on('response', result => {
result.res.status
result.ctx
result.req
})