JS案例:实现一个简易版axios

简介: JS案例:实现一个简易版axios

前言:

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);

1.png


同时也支持取消请求(请求超时和取消请求不会等待结果,直接返回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)

1.png

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')

1.png

写在最后

以上就是文章的所有内容了,需要源码的同学可以在下面的链接中获取


仓库: utils-lib-js: JavaScript工具函数,封装的一些常用的js函数


源码:src/request.ts · Hunter/utils-lib-js - Gitee.com


npm:utils-lib-js - npm


相关文章
|
3月前
|
数据采集 Web App开发 JavaScript
利用Selenium和XPath抓取JavaScript动态加载内容的实践案例
利用Selenium和XPath抓取JavaScript动态加载内容的实践案例
|
2月前
|
JavaScript 前端开发
vue配合axios连接express搭建的node服务器接口_简单案例
文章介绍了如何使用Express框架搭建一个简单的Node服务器,并使用Vue结合Axios进行前端开发和接口调用,同时讨论了开发过程中遇到的跨域问题及其解决方案。
54 0
vue配合axios连接express搭建的node服务器接口_简单案例
|
1月前
|
前端开发 JavaScript
JavaScript动态渲染页面爬取——CSS位置偏移反爬案例分析与爬取实战
JavaScript动态渲染页面爬取——CSS位置偏移反爬案例分析与爬取实战
|
3月前
|
JavaScript 前端开发 UED
Javaweb之javascript的小案例的详细解析
通过上述步骤,我们得到了一个动态更新的实时时钟,这个简单的JavaScript案例展示了定时器的使用方法,并讲解了如何处理日期和时间。这个案例说明了JavaScript在网页中添加动态内容与交互的能力。对于涉足JavaWeb开发的学习者来说,理解和运用这些基础知识非常重要。
41 11
|
3月前
|
JavaScript API 图形学
一个案例带你从零入门Three.js,深度好文!
【8月更文挑战第1天】本教程无需任何Threejs知识!本教程以入门为主,带你快速了解Three.js开发
81 2
一个案例带你从零入门Three.js,深度好文!
|
3月前
|
前端开发 JavaScript 开发者
fuse.js前端搜索简单使用的三个案例
通过这三个例子可以看出,Fuse.js 是一个功能丰富、易于实现的前端搜索库。它使开发者能够便捷地实现从基础到高级的搜索功能,无论是简单的列表搜索还是实时的搜索建议,都能够高效、精确地提供给用户所需的信息。
169 0
|
3月前
|
JavaScript 前端开发 开发者
Vue.js 2 项目实战(四):成绩案例
Vue.js 2 项目实战(四):成绩案例
|
3月前
|
JavaScript 前端开发 开发者
Vue.js 2 项目实战(三):综合案例-小黑记事本
Vue.js 2 项目实战(三):综合案例-小黑记事本
|
3月前
|
JavaScript 前端开发 开发者
Vue.js 2 项目实战(二):图书管理案例
Vue.js 2 项目实战(二):图书管理案例
|
3月前
|
JavaScript 前端开发 开发者
Vue.js 2 项目实战(一):图片切换案例
Vue.js 2 项目实战(一):图片切换案例

热门文章

最新文章

  • 1
    若依修改,若依如何发送请求---王清江07,axios的请求在request.js文件中,若依发送GET请求,必须用param
    116
  • 2
    axios发送post请求,如何接受和返回一个axios的字段,解决bug的方法,困难的事情先从简单做起,先从发送一个axios的post请求做起,解决方法查别人的资料,查看F12看network就行
    39
  • 3
    文本,前后端数据交互,简单请求,如何去给data数据赋值,在mounted()里赋值,利用axios发送的请求,res就是数据集,就是后端的数据,this.users = res.data.data
    44
  • 4
    Request failed with status code 400,使用axios.post要发送参数,认真比对原项目,看看有没有忘记什么?
    83
  • 5
    vue3 在 watchEffect 里中断未完成的 axios 请求(只保留最后一次请求的方法---连续点击查询按钮的优化)
    112
  • 6
    前后端数据交互.js文件的axios的写法,想要往后端发送数据,页面注入API,await的意思是同步等待服务器数据,并返回,axios注入在其他页面,其他页面调用的时候,同步作用
    42
  • 7
    前后端数据交互,API风格组合式API和选项式API,setup是一个标识,组合式编写的意思,使定义count.value的值自增的写法,组合式API使用axios的写法,ref定义响应数据写法
    27
  • 8
    网页设计,若依项目修改(It must be done)02------axios封装后发get请求,axios请求的位置在呢?
    43
  • 9
    前后端数据交互之axios的路径怎样找?axios的路径是那个,是你打开Tomcat之后,出现的路径+你项目写的接口路径
    34
  • 10
    数据交互,前后端数据请求,axios请求,对象结构的使用,E6的使用,结构赋值是什么?函数形参的obj如何,函数形参的obj就改成对象结构接收传入的数据对象
    24