携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第5天,点击查看活动详情
前言
上一篇我们实现了一个简单的请求发送,并编写了相关的 demo。但是现在存在一些问题:我们传入的 params 数据并没有用,也没有拼接到 url 上;我们对 request body 的数据格式、请求头 headers 也没有做处理;另外我们虽然从网络层面收到了响应的数据,但是我们代码层面也并没有对响应的数据做处理。那么这一章,我们就来解决这些问题。
处理请求url参数
需求分析
还记得我们上节遗留了一个问题,再来看这个例子:
axios({
method: 'get',
url: '/base/get',
params: {
a: 1,
b: 2
}
})
我们希望最终请求的 url 是 /base/get?a=1&b=2,这样服务端就可以通过请求的 url 解析到我们传来的参数数据了。实际上就是把 params 对象的 key 和 value 拼接到 url 上。
再来看几个更复杂的例子。
参数值为数组
axios({
method: 'get',
url: '/base/get',
params: {
foo: ['bar', 'baz']
}
})
最终请求的 url 是 /base/get?foo[]=bar&foo[]=baz'。
参数值为对象
axios({
method: 'get',
url: '/base/get',
params: {
foo: {
bar: 'baz'
}
}
})
最终请求的 url 是 /base/get?foo=%7B%22bar%22:%22baz%22%7D,foo 后面拼接的是 {"bar":"baz"} encode 后的结果。
参数值为 Date 类型
const date = new Date()
axios({
method: 'get',
url: '/base/get',
params: {
date
}
})
最终请求的 url 是 /base/get?date=2019-04-01T05:55:39.030Z,date 后面拼接的是 date.toISOString() 的结果。
特殊字符支持
对于字符 @、:、$、,、``、[、],我们是允许出现在 url 中的,不希望被 encode。
axios({
method: 'get',
url: '/base/get',
params: {
foo: '@:$, '
}
})
最终请求的 url 是 /base/get?foo=@:$+,注意,我们会把空格 ``转换成 +。
空值忽略
对于值为 null 或者 undefined 的属性,我们是不会添加到 url 参数中的。
axios({
method: 'get',
url: '/base/get',
params: {
foo: 'bar',
baz: null
}
})
最终请求的 url 是 /base/get?foo=bar。
丢弃 url 中的哈希标记
axios({
method: 'get',
url: '/base/get#hash',
params: {
foo: 'bar'
}
})
最终请求的 url 是 /base/get?foo=bar
保留 url 中已存在的参数
axios({
method: 'get',
url: '/base/get?foo=bar',
params: {
bar: 'baz'
}
})
最终请求的 url 是 /base/get?foo=bar&bar=baz
buildURL 函数实现
根据我们之前的需求分析,我们要实现一个工具函数,把 params 拼接到 url 上。我们希望把项目中的一些工具函数、辅助方法独立管理,于是我们创建一个 helpers 目录,在这个目录下创建 url.ts 文件,未来会把处理 url 相关的工具函数都放在该文件中。
helpers/url.ts:
import { isDate, isObject } from './util'
function encode (val: string): string {
return encodeURIComponent(val)
.replace(/%40/g, '@')
.replace(/%3A/gi, ':')
.replace(/%24/g, '$')
.replace(/%2C/gi, ',')
.replace(/%20/g, '+')
.replace(/%5B/gi, '[')
.replace(/%5D/gi, ']')
}
export function bulidURL (url: string, params?: any) {
if (!params) {
return url
}
const parts: string[] = []
Object.keys(params).forEach((key) => {
let val = params[key]
if (val === null || typeof val === 'undefined') {
return
}
let values: string[]
if (Array.isArray(val)) {
values = val
key += '[]'
} else {
values = [val]
}
values.forEach((val) => {
if (isDate(val)) {
val = val.toISOString()
} else if (isObject(val)) {
val = JSON.stringify(val)
}
parts.push(`${encode(key)}=${encode(val)}`)
})
})
let serializedParams = parts.join('&')
if (serializedParams) {
const markIndex = url.indexOf('#')
if (markIndex !== -1) {
url = url.slice(0, markIndex)
}
url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams
}
return url
}
helpers/util.ts:
const toString = Object.prototype.toString
export function isDate (val: any): val is Date {
return toString.call(val) === '[object Date]'
}
export function isObject (val: any): val is Object {
return val !== null && typeof val === 'object'
}
实现 url 参数处理逻辑
我们已经实现了 buildURL 函数,接下来我们来利用它实现 url 参数的处理逻辑。
在 index.ts 文件中添加如下代码:
function axios (config: AxiosRequestConfig): void {
processConfig(config)
xhr(config)
}
function processConfig (config: AxiosRequestConfig): void {
config.url = transformUrl(config)
}
function transformUrl (config: AxiosRequestConfig): string {
const { url, params } = config
return bulidURL(url, params)
}
在执行 xhr 函数前,我们先执行 processConfig 方法,对 config 中的数据做处理,除了对 url 和 params 处理之外,未来还会处理其它属性。
在 processConfig 函数内部,我们通过执行 transformUrl 函数修改了 config.url,该函数内部调用了 buildURL。
那么至此,我们对 url 参数处理逻辑就实现完了,接下来我们就开始编写 demo 了。
demo 编写
在 examples 目录下创建 base 目录,在 base 目录下创建 index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Base example</title>
</head>
<body>
<script src="/__build__/base.js"></script>
</body>
</html>
接着创建 app.ts 作为入口文件:
import axios from '../../src/index'
axios({
method: 'get',
url: '/base/get',
params: {
foo: ['bar', 'baz']
}
})
axios({
method: 'get',
url: '/base/get',
params: {
foo: {
bar: 'baz'
}
}
})
const date = new Date()
axios({
method: 'get',
url: '/base/get',
params: {
date
}
})
axios({
method: 'get',
url: '/base/get',
params: {
foo: '@:$, '
}
})
axios({
method: 'get',
url: '/base/get',
params: {
foo: 'bar',
baz: null
}
})
axios({
method: 'get',
url: '/base/get#hash',
params: {
foo: 'bar'
}
})
axios({
method: 'get',
url: '/base/get?foo=bar',
params: {
bar: 'baz'
}
})
接着在 server.js 添加新的接口路由:
router.get('/base/get', function(req, res) {
res.json(req.query)
})
然后在命令行运行 npm run dev,接着打开 chrome 浏览器,访问 http://localhost:8080/ 即可访问我们的 demo 了,我们点到 Base 目录下,通过开发者工具的 network 部分我们可以看到成功发送的多条请求,并可以观察它们最终请求的 url,已经如期添加了请求参数。
那么至此我们的请求 url 参数处理编写完了,下一小节我们会对 request body 数据做处理。
处理请求 body 数据
需求分析
我们通过执行 XMLHttpRequest 对象实例的 send 方法来发送请求,并通过该方法的参数设置请求 body 数据,我们可以去 mdn 查阅该方法支持的参数类型。
我们发现 send 方法的参数支持 Document 和 BodyInit 类型,BodyInit 包括了 Blob, BufferSource, FormData, URLSearchParams, ReadableStream、USVString,当没有数据的时候,我们还可以传入 null。
但是我们最常用的场景还是传一个普通对象给服务端,例如:
axios({
method: 'post',
url: '/base/post',
data: {
a: 1,
b: 2
}
})
这个时候 data是不能直接传给 send 函数的,我们需要把它转换成 JSON 字符串。
transformRequest 函数实现
根据需求分析,我们要实现一个工具函数,对 request 中的 data 做一层转换。我们在 helpers 目录新建 data.ts 文件。
helpers/data.ts:
import { isPlainObject } from './util'
export function transformRequest (data: any): any {
if (isPlainObject(data)) {
return JSON.stringify(data)
}
return data
}
helpers/util.js:
export function isPlainObject (val: any): val is Object {
return toString.call(val) === '[object Object]'
}
这里为什么要使用 isPlainObject 函数判断,而不用之前的 isObject 函数呢,因为 isObject 的判断方式,对于 FormData、ArrayBuffer 这些类型,isObject 判断也为 true,但是这些类型的数据我们是不需要做处理的,而 isPlainObject 的判断方式,只有我们定义的普通 JSON 对象才能满足。
helpers/url.ts:
if (isDate(val)) {
val = val.toISOString()
} else if (isPlainObject(val)) {
val = JSON.stringify(val)
}
对于上节课我们对请求参数值的判断,我们也应该用 isPlainObject 才更加合理。
helpers/util.js
// export function isObject (val: any): val is Object {
// return val !== null && typeof val === 'object'
// }
既然现在 isObject 方法不再使用,我们先将其注释。
实现请求 body 处理逻辑
index.ts:
import { transformRequest } from './helpers/data'
```typescript
function processConfig (config: AxiosRequestConfig): void {
config.url = transformURL(config)
config.data = transformRequestData(config)
}
function transformRequestData (config: AxiosRequestConfig): any {
return transformRequest(config.data)
}
我们定义了 transformRequestData 函数,去转换请求 body 的数据,内部调用了我们刚刚实现的的 transformRequest 方法。
然后我们在 processConfig 内部添加了这段逻辑,在处理完 url 后接着对 config 中的 data 做处理。
编写 demo
axios({
method: 'post',
url: '/base/post',
data: {
a: 1,
b: 2
}
})
const arr = new Int32Array([21, 31])
axios({
method: 'post',
url: '/base/buffer',
data: arr
})
我们在 examples/base/app.ts 添加 2 段代码,第一个 post 请求的 data 是一个普通对象,第二个请求的 data 是一个 Int32Array 类型的数据,它是可以直接传给 XMLHttpRequest 对象的 send 方法的。
router.post('/base/post', function(req, res) {
res.json(req.body)
})
router.post('/base/buffer', function(req, res) {
let msg = []
req.on('data', (chunk) => {
if (chunk) {
msg.push(chunk)
}
})
req.on('end', () => {
let buf = Buffer.concat(msg)
res.json(buf.toJSON())
})
})
我们接着在 examples/server.js 中添加 2 个路由,分别针对这俩种请求,返回请求传入的数据。
然后我们打开浏览器运行 demo,看一下结果,我们发现 /base/buffer 的请求是可以拿到数据,但是 base/post 请求的 response 里却返回的是一个空对象,这是什么原因呢?
实际上是因为我们虽然执行 send 方法的时候把普通对象 data 转换成一个 JSON 字符串,但是我们请求header 的 Content-Type 是 text/plain;charset=UTF-8,导致了服务端接受到请求并不能正确解析请求 body 的数据。
知道这个问题后,下面一节课我们来实现对请求 header 的处理。
处理请求 header
需求分析
我们上节遗留了一个问题:
axios({
method: 'post',
url: '/base/post',
data: {
a: 1,
b: 2
}
})
我们做了请求数据的处理,把 data 转换成了 JSON 字符串,但是数据发送到服务端的时候,服务端并不能正常解析我们发送的数据,因为我们并没有给请求 header 设置正确的 Content-Type。
所以首先我们要支持发送请求的时候,可以支持配置 headers 属性,如下:
axios({
method: 'post',
url: '/base/post',
headers: {
'content-type': 'application/json;charset=utf-8'
},
data: {
a: 1,
b: 2
}
})
并且在当我们传入的 data 为普通对象的时候,headers 如果没有配置 Content-Type 属性,需要自动设置请求 header 的 Content-Type 字段为:application/json;charset=utf-8。
processHeaders 函数实现
根据需求分析,我们要实现一个工具函数,对 request 中的 headers 做一层加工。我们在 helpers 目录新建 headers.ts 文件。
helpers/headers.ts:
import { isPlainObject } from './util'
function normalizeHeaderName (headers: any, normalizedName: string): void {
if (!headers) {
return
}
Object.keys(headers).forEach(name => {
if (name !== normalizedName && name.toUpperCase() === normalizedName.toUpperCase()) {
headers[normalizedName] = headers[name]
delete headers[name]
}
})
}
export function processHeaders (headers: any, data: any): any {
normalizeHeaderName(headers, 'Content-Type')
if (isPlainObject(data)) {
if (headers && !headers['Content-Type']) {
headers['Content-Type'] = 'application/json;charset=utf-8'
}
}
return headers
}
这里有个需要注意的点,因为请求 header 属性是大小写不敏感的,比如我们之前的例子传入 header 的属性名 content-type 就是全小写的,所以我们先要把一些 header 属性名规范化。
实现请求 header 处理逻辑
在这之前,我们先修改一下 AxiosRequestConfig 接口类型的定义,添加 headers 这个可选属性:
types/index.ts
export interface AxiosRequestConfig {
url: string
method?: Method
data?: any
params?: any
headers?: any
}
index.ts:
function processConfig (config: AxiosRequestConfig): void {
config.url = transformURL(config)
config.headers = transformHeaders(config)
config.data = transformRequestData(config)
}
function transformHeaders (config: AxiosRequestConfig) {
const { headers = {}, data } = config
return processHeaders(headers, data)
}
因为我们处理 header 的时候依赖了 data,所以要在处理请求 body 数据之前处理请求 header。
xhr.ts:
export default function xhr (config: AxiosRequestConfig): void {
const { data = null, url, method = 'get', headers } = config
const request = new XMLHttpRequest()
request.open(method.toUpperCase(), url, true)
Object.keys(headers).forEach((name) => {
if (data === null && name.toLowerCase() === 'content-type') {
delete headers[name]
} else {
request.setRequestHeader(name, headers[name])
}
})
request.send(data)
}
这里要额外判断一个逻辑,当我们传入的 data 为空的时候,请求 header 配置 Content-Type 是没有意义的,于是我们把它删除。
demo 编写
axios({
method: 'post',
url: '/base/post',
data: {
a: 1,
b: 2
}
})
axios({
method: 'post',
url: '/base/post',
headers: {
'content-type': 'application/json;'
},
data: {
a: 1,
b: 2
}
})
const paramsString = 'q=URLUtils.searchParams&topic=api'
const searchParams = new URLSearchParams(paramsString)
axios({
method: 'post',
url: '/base/post',
data: searchParams
})
通过 demo 我们可以看到,当我们请求的数据是普通对象并且没有配置 headers 的时候,会自动为其添加 Content-Type:application/json;charset=utf-8;同时我们发现当 data 是某些类型如 URLSearchParams 的时候,浏览器会自动为请求 header加上合适的 Content-Type。
至此我们对于请求的处理逻辑暂时告一段落。目前我们的请求从网络层面是可以收到服务端的响应的,下一章我们就从代码层面来处理服务端响应,并且让调用方可以拿到从服务端返回的数据。