基于ts重构axios
基于ts重构axios
ustbhuangyi 老师的 基于TypeScript从零重构axios学习记录。
知识点
TypeScript 常用语法:
基础类型
、 函数
、 变量声明
、 接口
、 类
、 泛型
、 类型推新
、 高级类型
axios js库:
项目脚手架
、 基础功能实现
、 异常情况处理
、 接口扩展
、 拦截器实现
、 配置化实现
、 取消功能实现
、 其他功能实现等等
主要工具:
Jest
、 TSLint
、 Commitizen
、 Prettier
、 RollupJS
、 Semantic release
基本语法
需求分析
Features
- 在浏览器使用 XMLHttpRequest 对象通讯
- 支持 Promise API
- 支持请求和响应的拦截器
- 支持请求数据和响应数据的转换
- 支持请求的取消
- JSON数据的自动转换
- 客户端防止 XSRF
基于 XMLHttpRequest 编写基本请求代码
处理请求数据:url/body/headers
src/types/index.ts
export type Method =
| 'get'
| 'GET'
| 'delete'
| 'Delete'
| 'head'
| 'HEAD'
| 'options'
| 'OPTIONS'
| 'post'
| 'POST'
| 'put'
| 'PUT'
| 'patch'
| 'PATCH';
export interface AxiosRequestConfig {
url: string;
method?: Method;
data?: any;
params?: any;
headers?: any;
}
src/xhr.ts
import { AxiosRequestConfig } from './types';
export default function xhr(config: AxiosRequestConfig): void {
const { data = null, url, method = 'get', headers } = config;
const request = new XMLHttpRequest();
// method,url,async
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);
}
src/index.ts
import { AxiosRequestConfig } from './types';
import { buildURL } from './helpers/url';
import { transformRequest } from './helpers/data';
import xhr from './xhr';
import { processHeaders } from './helpers/header';
function axios(config: AxiosRequestConfig): void {
processConfig(config);
xhr(config);
}
function processConfig(config: AxiosRequestConfig): void {
config.url = transformURL(config);
config.data = transformRequestData(config);
config.headers = transformHeaders(config);
}
function transformHeaders(config: AxiosRequestConfig): void {
const { headers = {}, data } = config;
return processHeaders(headers, data);
}
function transformRequestData(config: AxiosRequestConfig): void {
return transformRequest(config.data);
}
function transformURL(config: AxiosRequestConfig): string {
const { url, params } = config;
return buildURL(url, params);
}
export default axios;
工具类
data.ts
import { isPlainObject } from './util';
export function transformRequest(data: any): any {
if (isPlainObject(data)) {
return JSON.stringify(data);
}
return data;
}
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;
}
url.ts
import { isDate, isPlainObject } from './util'
function encode(val: string): string {
return encodeURIComponent(val)
.replace(/%40/g, '@')
.replace(/%3A/ig, ':')
.replace(/%24/g, '**util.ts**
```tsx
const toString = Object.prototype.toString
export function isDate(val: any): val is Date {
return toString.call(val) === '[object Date]'
}
export function isPlainObject(val: any): val is Object {
return toString.call(val) === '[object Object]'
}
处理响应数据
定义响应接口
types/index
export interface AxiosResponse {
data: any;
status: number;
statusText: string;
headers: any;
config: AxiosRequestConfig;
request: any;
}
export interface AxiosPromise extends Promise<AxiosResponse> {}
处理 headers 的数据
helpers/header.ts
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;
}
export function parseHeaders(headers: string): any {
let parsed = Object.create(null);
if (!headers) {
return headers;
}
headers.split('\r\n').forEach(line => {
let [key, val] = line.split(':');
key = key.trim().toLowerCase();
if (!key) {
return;
}
if (val) {
val = val.trim();
}
parsed[key] = val;
});
return parsed;
}
处理 响应data
helpers/data.ts
export function transformResponse(data: any): any {
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch (e) {
// do nothing
}
}
return data;
}
修改 xhr, 返回一个 Promise
xhr.ts
import { AxiosRequestConfig, AxiosPromise, AxiosResponse } from './types';
import { parseHeaders } from './helpers/headers';
export default function xhr(config: AxiosRequestConfig): AxiosPromise {
return new Promise(resolve => {
const { data = null, url, method = 'get', headers, responseType } = config;
const request = new XMLHttpRequest();
if (responseType) {
request.responseType = responseType;
}
// method,url,async
request.open(method.toUpperCase(), url, true);
request.onreadystatechange = function handleLoad() {
if (request.readyState !== 4) {
return;
}
const responseHeaders = parseHeaders(request.getAllResponseHeaders());
const responseData = responseType && responseType !== 'text' ? request.response : request.responseText;
const response: AxiosResponse = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config,
request
};
resolve(response);
};
Object.keys(headers).forEach(name => {
if (data === null && name.toLowerCase() === 'content-type') {
delete headers[name];
} else {
request.setRequestHeader(name, headers[name]);
}
});
request.send(data);
});
}
)
.replace(/%2C/ig, ',')
.replace(/%20/g, '+')
.replace(/%5B/ig, '[')
.replace(/%5D/ig, ']')
}
export function buildURL(url: string, params?: any): string { if (!params) {
return url
} const parts: string[] = [] Object.keys(params).forEach(key => {
const val = params[key]
if (val === null || typeof val === 'undefined') {
return
}
let values = []
if (Array.isArray(val)) {
values = val
key += '[]'
} else {
values = [val]
}
values.forEach(val => {
if (isDate(val)) {
val = val.toISOString()
} else if (isPlainObject(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 }
**util.ts**
```tsx
const toString = Object.prototype.toString
export function isDate(val: any): val is Date {
return toString.call(val) === '[object Date]'
}
export function isPlainObject(val: any): val is Object {
return toString.call(val) === '[object Object]'
}
处理响应数据
定义响应接口
types/index
export interface AxiosResponse {
data: any;
status: number;
statusText: string;
headers: any;
config: AxiosRequestConfig;
request: any;
}
export interface AxiosPromise extends Promise<AxiosResponse> {}
处理 headers 的数据
helpers/header.ts
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;
}
export function parseHeaders(headers: string): any {
let parsed = Object.create(null);
if (!headers) {
return headers;
}
headers.split('\r\n').forEach(line => {
let [key, val] = line.split(':');
key = key.trim().toLowerCase();
if (!key) {
return;
}
if (val) {
val = val.trim();
}
parsed[key] = val;
});
return parsed;
}
处理 响应data
helpers/data.ts
export function transformResponse(data: any): any {
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch (e) {
// do nothing
}
}
return data;
}
修改 xhr, 返回一个 Promise
xhr.ts
import { AxiosRequestConfig, AxiosPromise, AxiosResponse } from './types';
import { parseHeaders } from './helpers/headers';
export default function xhr(config: AxiosRequestConfig): AxiosPromise {
return new Promise(resolve => {
const { data = null, url, method = 'get', headers, responseType } = config;
const request = new XMLHttpRequest();
if (responseType) {
request.responseType = responseType;
}
// method,url,async
request.open(method.toUpperCase(), url, true);
request.onreadystatechange = function handleLoad() {
if (request.readyState !== 4) {
return;
}
const responseHeaders = parseHeaders(request.getAllResponseHeaders());
const responseData = responseType && responseType !== 'text' ? request.response : request.responseText;
const response: AxiosResponse = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config,
request
};
resolve(response);
};
Object.keys(headers).forEach(name => {
if (data === null && name.toLowerCase() === 'content-type') {
delete headers[name];
} else {
request.setRequestHeader(name, headers[name]);
}
});
request.send(data);
});
}