前言:
axios是一个的前端请求工具,其优秀的场景复用性使它可以运行在node环境和浏览器环境,在浏览器环境中使用的是xhr,在node中则是使用http模块,最近在封装一些工具函数,恰好接触到了这一块,于是想分享一下心得,希望对大家有帮助。
注:文章中有一些类型和函数未给出可以在这个工具包中找到
功能特性:
浏览器环境下,我使用的是fetch而摒弃了xhr的封装,这会使低版本浏览器兼容上有一定缺陷,后续有时间的话可能会加上,node环境下依旧使用的http模块
功能上实现了基础请求功能,内部采用的是promise的方式,实现了请求及响应的拦截以及超时取消请求,或手动取消请求
api设计
// request export type IRequestParams<T> = T | IObject<any> | null // 请求路径 export type IUrl = string // 环境判断 export type IEnv = 'Window' | 'Node' // fetch返回取值方式 export type IDataType = "text" | "json" | "blob" | "formData" | "arrayBuffer" // 请求方式 export type IRequestMethods = "GET" | "POST" | "DELETE" | "PUT" | "OPTION" | "HEAD" | "PATCH" // body结构 export type IRequestBody = IRequestParams<BodyInit> // heads结构 export type IRequestHeaders = IRequestParams<HeadersInit> // 请求基础函数 export type IRequestBaseFn = (url: IUrl, opts: IRequestOptions) => Promise<any> // 请求函数体 export type IRequestFn = (url?: IUrl, query?: IObject<any>, body?: IRequestBody, opts?: IRequestOptions) => Promise<any> // 请求参数 export type IRequestOptions = { method?: IRequestMethods query?: IRequestParams<IObject<any>> body?: IRequestBody headers?: IRequestHeaders // AbortController 中断控制器,用于中断请求 controller?: AbortController // 超时时间 timeout?: number // 定时器 timer?: number | unknown | null [key: string]: any } // 拦截器 export type IInterceptors = { // 添加请求,响应,错误拦截 use(type: "request" | "response" | "error", fn: Function): void get reqFn(): Function get resFn(): Function get errFn(): Function } // 公共函数 export type IRequestBase = { // 请求根路由 readonly origin: string // 简单判断传入的路由是否是完整url chackUrl: (url: IUrl) => boolean // 环境判断,node或浏览器 envDesc: () => IEnv // 全局的错误捕获 errorFn: <Err = any, R = Function>(reject: R) => (err: Err) => R // 清除当前请求的超时定时器 clearTimer: (opts: IRequestOptions) => void // 初始化超时取消 initAbort: <T = IRequestOptions>(opts: T) => T // 策略模式,根据环境切换请求方式 requestType: () => IRequestBaseFn // 拼接请求url fixOrigin: (fixStr: string) => string // 请求函数 fetch: IRequestBaseFn http: IRequestBaseFn // fetch响应转换方式 getDataByType: (type: IDataType, response: Response) => Promise<any> } // 初始化并兼容传入的参数 export type IRequestInit = { initDefaultParams: (url: IUrl, opts: IRequestOptions) => any initFetchParams: (url: IUrl, opts: IRequestOptions) => any initHttpParams: (url: IUrl, opts: IRequestOptions) => any } // 请求主体类 export type IRequest = { GET: IRequestFn POST: IRequestFn DELETE: IRequestFn PUT: IRequestFn OPTIONS: IRequestFn HEAD: IRequestFn PATCH: IRequestFn } & IRequestBase
功能实现:
首先是拦截器的钩子函数,在请求响应以及错误时运行这些函数,将回调函数返回至外部
class Interceptors implements IInterceptors { private requestSuccess: Function private responseSuccess: Function private error: Function use(type, fn) { switch (type) { case "request": this.requestSuccess = fn break; case "response": this.responseSuccess = fn break; case "error": this.error = fn break; } return this } get reqFn() { return this.requestSuccess } get resFn() { return this.responseSuccess } get errFn() { return this.error } }
接下来是基础工具函数,请求时使用的工具函数一般会封装在这,这里还对请求函数做了个抽象处理,因为工具函数requestType 会使用到这两个请求函数
abstract class RequestBase extends Interceptors implements IRequestBase { readonly origin: string constructor(origin) { super() this.origin = origin ?? '' } abstract fetch(url, opts): Promise<void> abstract http(url, opts): Promise<void> chackUrl = (url: string) => { return url.startsWith('/') } fixOrigin = (fixStr: string) => { if (this.chackUrl(fixStr)) return this.origin + fixStr return fixStr } envDesc = () => { if (typeof Window !== "undefined") { return "Window" } return "Node" } errorFn = reject => err => reject(this.errFn?.(err) ?? err) clearTimer = opts => !!opts.timer && (clearTimeout(opts.timer), opts.timer = null) initAbort = (params) => { const { controller, timer, timeout } = params !!!timer && (params.timer = setTimeout(() => controller.abort(), timeout)) return params } requestType = () => { switch (this.envDesc()) { case "Window": return this.fetch case "Node": return this.http } } getDataByType = (type, response) => { switch (type) { case "text": case "json": case "blob": case "formData": case "arrayBuffer": return response[type]() default: return response['json']() } } }
在后面的函数实现时,发现两个请求参数都会用到初始化参数,所以我把这几个函数又剥离出来了,以下是初始化参数的类
abstract class RequestInit extends RequestBase implements IRequestInit { constructor(origin) { super(origin) } abstract fetch(url, opts): Promise<void> abstract http(url, opts): Promise<void> initDefaultParams = (url, { method = "GET", query = {}, headers = {}, body = null, timeout = 30 * 1000, controller = new AbortController(), type = "json", ...others }) => ({ url: urlJoin(this.fixOrigin(url), query), method, headers, body: method === "GET" ? null : jsonToString(body), timeout, signal: controller?.signal, controller, type, timer: null, ...others }) initFetchParams = (url, opts) => { const params = this.initAbort(this.initDefaultParams(url, opts)) return this.reqFn?.(params) ?? params } initHttpParams = (url, opts) => { const params = this.initAbort(this.initDefaultParams(url, opts)) const options = parse(params.url, true) return this.reqFn?.({ ...params, ...options }) ?? params } }
最后是将请求函数完整的实现
export class Request extends RequestInit implements IRequest { private request: Function constructor(origin) { super(origin) this.request = this.requestType() } fetch = (_url, _opts) => { const { promise, resolve, reject } = defer() const { url, ...opts } = this.initFetchParams(_url, _opts) const { signal } = opts promise.finally(() => this.clearTimer(opts)) signal.addEventListener('abort', () => this.errorFn(reject)); fetch(url, opts).then((response) => { if (response?.status >= 200 && response?.status < 300) { return this.getDataByType(opts.type, response) } return this.errorFn(reject) }).then(res => resolve(this.resFn?.(res) ?? res)).catch(this.errorFn(reject)) return promise } http = (_url, _opts) => { const { promise, resolve, reject } = defer() const params = this.initHttpParams(_url, _opts) const { signal } = params promise.finally(() => this.clearTimer(params)) const req = request(params, (response) => { if (response?.statusCode >= 200 && response?.statusCode < 300) { let data = ""; response.setEncoding('utf8'); response.on('data', (chunk) => data += chunk); return response.on("end", () => resolve(this.resFn?.(data) ?? data)); } return this.errorFn(reject)(response?.statusMessage) }) signal.addEventListener('abort', () => this.errorFn(reject)(req.destroy(new Error('request timeout')))); req.on('error', this.errorFn(reject)); req.end(); return promise } GET = (url?: IUrl, query?: IObject<any>, _?: IRequestBody | void, opts?: IRequestOptions) => { return this.request(url, { query, method: "GET", ...opts }) } POST = (url?: IUrl, query?: IObject<any>, body?: IRequestBody, opts?: IRequestOptions) => { return this.request(url, { query, method: "POST", body, ...opts }) } PUT = (url?: IUrl, query?: IObject<any>, body?: IRequestBody, opts?: IRequestOptions) => { return this.request(url, { query, method: "PUT", body, ...opts }) } DELETE = (url?: IUrl, query?: IObject<any>, body?: IRequestBody, opts?: IRequestOptions) => { return this.request(url, { query, method: "DELETE", body, ...opts }) } OPTIONS = (url?: IUrl, query?: IObject<any>, body?: IRequestBody, opts?: IRequestOptions) => { return this.request(url, { query, method: "OPTIONS", body, ...opts }) } HEAD = (url?: IUrl, query?: IObject<any>, body?: IRequestBody, opts?: IRequestOptions) => { return this.request(url, { query, method: "HEAD", body, ...opts }) } PATCH = (url?: IUrl, query?: IObject<any>, body?: IRequestBody, opts?: IRequestOptions) => { return this.request(url, { query, method: "PATCH", body, ...opts }) } }
以上代码有几个注意点:
node中的http请求和浏览器的fetch请求的参数不同,需要把参数初始化并做成兼容的格式
AbortController api在node环境下对http模块的兼容性问题,所以需要自己手动去调用超时取消请求
get请求与其他请求不同,带body会被浏览器屏蔽
功能验证:
node环境下:
使用以下命令初始化dev项目:
pnpm init pnpm i utils-lib-js
在项目根目录下新建server.js,咱们先写个简单的get请求,内容如下:
const Request = require("utils-lib-js").Request; const resource = new Request("http://127.0.0.1:1024"); resource.GET("/getList").then(console.log).catch(console.log);
之后再试试post:
resource.POST("/getList").then(console.log).catch(console.log);
默认的请求超时是30秒,如果需要自定义请求时间可以添加timeout
resource .GET("/getList", {}, null, { timeout: 100, }) .then(console.log) .catch(console.log);
同时也支持取消请求(请求超时和取消请求不会等待结果,直接返回reject):
const controller = new AbortController(); setTimeout(() => controller.abort(), 1000); resource .GET("/getList", {}, null, { controller, }) .then(console.log) .catch(console.log);
拦截器的使用方式
const Request = require("utils-lib-js").Request; const resource = new Request("http://127.0.0.1:1024"); resource .use("request", (params) => { console.log(params.query); return params; }) .use("response", (params) => { console.log(params); return params.length; }) .use("error", (error) => { console.log(error); return error; }); resource.GET("/getList", { name: "abc" }).then(console.log)
vite-dev环境下:
我使用的是vite+vue,运行以下命令安装工具:
pnpm i utils-lib-js
然后在main.ts文件中试试,可以看到Request已经适配了fetch
import { createApp } from 'vue' import './style.css' import App from './App.vue' import { Request } from "utils-lib-js" const resource = new Request("http://127.0.0.1:1024"); resource .use("request", (params) => { console.log(params.url); return params; }) .use("response", (params) => { console.log(params); return params.length; }) .use("error", (error) => { console.log(error); return error; }); resource.GET("/getList", { name: "abc" }).then(console.log) createApp(App).mount('#app')
写在最后
以上就是文章的所有内容了,需要源码的同学可以在下面的链接中获取
仓库: utils-lib-js: JavaScript工具函数,封装的一些常用的js函数
源码:src/request.ts · Hunter/utils-lib-js - Gitee.com
npm:utils-lib-js - npm