本来是想发 next.js 开发笔记的,结果发现里面涉及了太多东西,还是拆分出来发吧~
本文记录一下在 TypeScript 项目里封装 axios 的过程,之前在开发 StarBlog-Admin 的时候已经做了一次封装,不过那时是 JavaScript ,跟 TypeScript 还是有些区别的。
另外我在跟着 next.js 文档开发的时候,注意到官方文档推荐使用 @tanstack/react-query
来封装请求类的操作,浅看了一下文档之后感觉很不错,接下来我会在项目里实践。
先创建一个 global 配置,src/utilities/global.ts
export default class Global { static baseUrl = process.env.NEXT_PUBLIC_BASE_URL }
这是在 next.js 项目,可以用 next 规定的环境变量,其他项目可以自行修改。
认证这部分跟 axios 有点关系,但关系也不是很大,不过因为 axios 封装里面需要用到,所以我也一并贴出来吧。
创建 src/utilities/auth.ts
文件
/** * 登录信息 */ export interface LoginProps { token: string username: string expiration: string } /** * 认证授权工具类 */ export default abstract class Auth { static get storage(): Storage | null { if (typeof window !== 'undefined') { return window.localStorage } return null } /** * 检查是否已登录 * @return boolean */ public static isLogin() { let token = this.storage?.getItem('token') let userName = this.storage?.getItem('user') if (!token || token.length === 0) return false if (!userName || userName.length === 0) return false return !this.isExpired(); } /** * 检查登录是否过期 * @return boolean */ public static isExpired = () => { let expiration = this.storage?.getItem('expiration') if (expiration) { let now = new Date() let expirationTime = new Date(expiration) if (now > expirationTime) return true } return false } /** * 读取保存的token * @return string */ public static getToken = () => { return this.storage?.getItem('token') } /** * 保存登录信息 * @param props */ public static login = (props: LoginProps) => { this.storage?.setItem('token', props.token) this.storage?.setItem('user', props.username) this.storage?.setItem('expiration', props.expiration) } /** * 注销 */ public static logout = () => { this.storage?.removeItem('token') this.storage?.removeItem('user') this.storage?.removeItem('expiration') } }
跟认证有关的逻辑我都放在 Auth
类中了
为了在 next.js 中可以愉快使用,还得做一些特别的处理,比如我增加了 storage
属性,读取的时候先判断 window
是否存在。
关于 API 的代码我都放在 src/services
目录下。
创建 src/services/api.ts
文件,代码比较长,分块介绍,可以看到所有配置相比之前 JavaScript 版本的都多了配置,对 IDE 自动补全非常友好。
先 import
import axios, {AxiosInstance, AxiosRequestConfig, AxiosResponse, CreateAxiosDefaults} from "axios"; import Global from '@/utilities/global' import Auth from "@/utilities/auth";
定义一下 axios 的配置
const config: CreateAxiosDefaults<any> = { method: 'get', // 基础url前缀 baseURL: `${Global.baseUrl}/`, // 请求头信息 headers: { 'Content-Type': 'application/json;charset=UTF-8' }, // 参数 data: {}, // 设置超时时间 timeout: 10000, // 携带凭证 withCredentials: true, // 返回数据类型 responseType: 'json' }
设置统一的接口返回值,这个和我在 StarBlog 后端里封装的那套是一样的,现在基本是我写后端的标准返回值了,同时也发布了 CodeLab.Share
nuget包,可以快捷的引入这个统一的返回值组件。
// 统一接口返回值 export interface ApiResponse { data: any errorData: any message: string statusCode: number successful: boolean }
ApiClient
最后就是定义了 ApiClient
类,有点模仿 C# 的 HttpClient
内味了
这里面用到了 axios 的拦截器,发起请求的时候给 header 加上认证信息,返回的时候看看有没有错误,如果是 401 unauthorized 的话就跳转到登录页面。
export class ApiClient { private readonly api: AxiosInstance constructor() { this.api = axios.create({ ...config, }) this.api.interceptors.request.use( config => { config.headers.Authorization = `Bearer ${Auth.getToken()}` return config }, error => { return error }) this.api.interceptors.response.use( response => { return response }, error => { let reason = error if (error && error.response) { if (error.response.data) { reason = error.response.data if (!reason.message) reason.message = error.message } if (error.response.status === 401) { location.href = '/login' } } return Promise.reject(reason) } ) } public request(options: AxiosRequestConfig): Promise<ApiResponse> { return new Promise((resolve, reject) => { this.api(options).then((res: AxiosResponse<ApiResponse>) => { resolve(res.data) return false }).catch(error => { reject(error) }) }) } } export const api = new ApiClient() export default api
代码比之前我在 StarBlog-Admin 里的简单一些,我要尽可能用较少的代码实现需要的功能。
所有的接口调用我都写成 service (后端思维是这样的)
这里以发短信接口为例
创建 src/services/common.ts
文件,从刚才定义的 api.ts
里面引入 ApiClient
的对象,直接调用 request
方法就完事了。
参数类型是 AxiosRequestConfig
,不对 axios 本身做什么修改,我感觉比之前用 Antd Pro 魔改的接口舒服一些。
import {api} from './api' export class SmsChannel { static local = 0 static aliyun = 1 static tencent = 2 } export default abstract class CommonService { public static getSmsCode(phone: string, channel: number = SmsChannel.local) { return api.request({ url: `api/common/getSmsCode`, params: {phone, channel} }) } }
这样封装完比之前 StarBlog-Admin 的舒服很多,可惜之前那个项目用的是 vue2.x 似乎没法用 TypeScript。
就这样吧,大部分内容还是在 next.js 开发笔记中。