概要
泰罗的request API函数主要用于请求网络服务,可以发起异步请求服务,使用很方便,许多开发工程师都使用过。在实际项目中,会因为一些需求而去封装或者扩展它,最常见的需求是为请求添加token头。在网络上有好几篇介绍如何封装这个函数的文章,从这些文章中收获了不少。前面几个是javascript版本的封装,缺乏强类型支持,这篇文章主要介绍用typescript封装request函数。
扩展的功能
下面这段代码实现主要添加了如下的几个功能点:
- 如果请求没有携带content-type头,则添加"application/json;charset=UTF-8"类型
- 根据程序运行环境,选择相应的后端服务地址
- 在请求头中加入用户的token令牌
- 返回值统一为ResultDto类型
- 对后台返回的业务数据进行校验,提前发现数据不匹配的错误
- 把后台错误,HTTP错误和未知的系统错误,统一归类到ResultDto的错误中
- 当遇到后台返回的token无效错误时跳到登录页,这里token无效的错误码是10002
实现细节
import Taro from "@tarojs/taro";
import TokenService from "../token/TokenService";
const jsonHader = "application/json;charset=UTF-8";
function getBaseUrl(): string {
if (process.env.NODE_ENV === "development") {
return "http://127.0.0.1:9080";
} else return "";
}
function exist<T>(a: any, ...attrs: T[]): boolean {
if (process.env.NODE_ENV === "development") {
for (let i = 0; i < attrs.length; ++i) {
let item = attrs[i];
if (a[item] != "" && !a[item]) {
return false;
}
}
}
return true;
}
export const HTTP_STATUS = {
SUCCESS: 200,
CREATED: 201,
ACCEPTED: 202,
CLIENT_ERROR: 400,
AUTHENTICATE: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
SERVER_ERROR: 500,
NOT_IMPLEMENTED: 501,
BAD_GATEWAY: 502,
SERVICE_UNAVAILABLE: 503,
GATEWAY_TIMEOUT: 504,
};
export const REFRESH_STATUS = {
NORMAL: 0,
REFRESHING: 1,
NO_MORE_DATA: 2,
};
export const getCurrentPageUrl = (): string => {
let pages = Taro.getCurrentPages();
let currentPage = pages[pages.length - 1];
let url = currentPage.route;
return url || "";
};
export const pageToLogin = () => {
TokenService.clear();
let path = getCurrentPageUrl();
if (!path.includes("login")) {
Taro.reLaunch({
url: "/pages/login/index",
});
}
};
export type ResultDto<T> = {
success: boolean;
// 错误编号
errorCode?: string;
// 错误描述
errorMessage?: string;
// 具体对象
data?: T;
// error display type: 0 silent; 1 message.warn; 2 message.error; 4 notification; 9 page
showType?: number;
// Convenient for back-end Troubleshooting: unique request ID
traceId?: string;
// onvenient for backend Troubleshooting: host of current access server
host?: string;
};
const tokenInterceptor = (chain: Taro.Chain) => {
const requestParams = chain.requestParams;
const { header } = requestParams;
let token = TokenService.load();
const tokenHeader = {
Authorization: `Bearer ${token}`,
// "content-type": jsonHader,
};
requestParams.header = { ...tokenHeader, ...header };
return chain.proceed(requestParams);
};
// Taro 提供了两个内置拦截器
// logInterceptor - 用于打印请求的相关信息
// timeoutInterceptor - 在请求超时时抛出错误。
// const interceptors = [customInterceptor, Taro.interceptors.logInterceptor]
const interceptors = [tokenInterceptor];
interceptors.forEach((interceptorItem) => Taro.addInterceptor(interceptorItem));
type FilterOptional<T extends object> = Pick<T, Exclude<{ [K in keyof T]: T extends Record<K, T[K]> ? K : never }[keyof T], undefined>>
type kType<T extends object> = keyof FilterOptional<T>;
const request = <T extends object>(params: Taro.request.Option, ...attrs: kType<T>[]): Promise<ResultDto<T>> => {
let { url, header } = params;
const baseUrl = getBaseUrl();
const url2 = baseUrl + url;
let contentType = jsonHader;
contentType = header?.contentType || jsonHader;
const option = {
...params,
header: { "content-type": contentType },
timeout: 50000,
url: url2,
};
Taro.showLoading({
title: "加载中",
});
return Taro.request(option)
.then((res: Taro.request.SuccessCallbackResult<any>) => {
const pos = contentType.indexOf("application/json");
const { statusCode, data } = res;
if (pos == -1) {
return { success: true, data };
}
// 只要请求成功,不管返回什么状态码,都走这个回调
if (statusCode == HTTP_STATUS.SUCCESS) {
if (data?.success) {
// 成功且取到了数据
if(!exist(data.data, ...attrs)){
console.error("返回值不包含必需的字段", data.data, attrs);
return Promise.resolve({...data, success: false, errorCode: "BizError", errorMessage: "返回值不能匹配"});
}
return Promise.resolve(data);
}
// 成功,但处理过程报错了
let dto: ResultDto<T> = data;
console.warn(
`url =${url2}, traceid=${dto.traceId}, error code=${dto.errorCode}, error msg=${dto.errorMessage}`
);
if(dto.errorCode === "10002"){
pageToLogin();
}
Promise.resolve(dto);
}
let dto: ResultDto<T> = {
success: false,
errorCode: statusCode + "",
errorMessage: `http status: ${statusCode}`,
};
if (statusCode === HTTP_STATUS.NOT_FOUND) {
dto.errorMessage = "请求资源不存在";
} else if (statusCode === HTTP_STATUS.FORBIDDEN) {
dto.errorMessage = "没有权限访问";
} else if (statusCode === HTTP_STATUS.AUTHENTICATE) {
dto.errorMessage = "需要鉴权";
} else if (statusCode === HTTP_STATUS.SERVER_ERROR) {
dto.errorMessage = "服务器错误";
} else if (statusCode === HTTP_STATUS.NOT_IMPLEMENTED) {
dto.errorMessage = "服务没有实现";
} else if (statusCode === HTTP_STATUS.BAD_GATEWAY) {
dto.errorMessage = "服务网关出现了问题";
} else if (statusCode === HTTP_STATUS.SERVICE_UNAVAILABLE) {
dto.errorMessage = "服务器无法处理请求";
}
Taro.showToast({ title: dto.errorMessage || "", icon: "error" });
return Promise.resolve(dto);
})
.catch((error) => {
console.error("http return error,", error);
return Promise.resolve({
success: false,
errorCode: "system",
errorMessage: error.toString(),
});
})
.finally(() => Taro.hideLoading());
};
export default request;
上面代码中的TokenService类,提供了token的本地和reducer存取功能,这个类也引用了其它的文件,这里不继续展开。
import Taro from '@tarojs/taro'
import store from '../../store'
import {setValue} from '../../store/model/token'
import StringUtil from '@/utils/stringUtil';
export default class TokenService{
public static save(data: string): void{
Taro.setStorage({
key: "token",
data
})
store.dispatch(setValue({token: data}));
}
public static load(): string {
const {TokenStateReducer} =store.getState();
if(!StringUtil.isEmpty(TokenStateReducer.token)){
return TokenStateReducer.token;
}
try {
const token = Taro.getStorageSync<string>('token');
store.dispatch(setValue({token}));
return token
} catch (e) {
console.log("can not read token in storage");
return "";
}
}
public static clear(): void {
TokenService.save("");
}
}
如何使用
这里举一个获取图形验证码的例子
import request, { ResultDto } from '../request'
export type CaptchaResponseDto = {
key: string;
code?: string;
image: string;
}
// 获取图片验证码
export async function getCaptchaImage(): Promise<ResultDto<CaptchaResponseDto>>{
return request<CaptchaResponseDto>({url: "/public/image-captcha", method: 'GET'}, "key", "image");
}
总结
通过上述的封装,getCaptchaImage使用request函数更加简单,同时也具有了完备的类型支持,提高了系统的可读性和可维护性。