目录
axios是一个的前端请求工具,其优秀的场景复用性使它可以运行在node环境和浏览器环境,在浏览器环境中使用的是xhr,在node中则是使用http模块,最近在封装一些工具函数,恰好接触到了这一块,于是想分享一下心得,希望对大家有帮助。
注:文章中有一些类型和函数未给出可以在这个工具包中找到
浏览器环境下,我使用的是fetch而摒弃了xhr的封装,这会使低版本浏览器兼容上有一定缺陷,后续有时间的话可能会加上,node环境下依旧使用的http模块
功能上实现了基础请求功能,内部采用的是promise的方式,实现了请求及响应的拦截以及超时取消请求,或手动取消请求
- // request
-
- export type IRequestParams
= 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:
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 })
- }
- }
以上代码有几个注意点:
AbortController api在node环境下对http模块的兼容性问题,所以需要自己手动去调用超时取消请求
get请求与其他请求不同,带body会被浏览器屏蔽
使用以下命令初始化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+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
感谢你看到了这里,如果文章对你有帮助,还请点个赞支持一下