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


    目录

    前言:

    功能特性:

    api设计

    功能实现:

    功能验证:

    node环境下:

    vite-dev环境下:

    写在最后


    前言:

    axios是一个的前端请求工具,其优秀的场景复用性使它可以运行在node环境和浏览器环境,在浏览器环境中使用的是xhr,在node中则是使用http模块,最近在封装一些工具函数,恰好接触到了这一块,于是想分享一下心得,希望对大家有帮助。

    注:文章中有一些类型和函数未给出可以在这个工具包中找到

    功能特性:

    浏览器环境下,我使用的是fetch而摒弃了xhr的封装,这会使低版本浏览器兼容上有一定缺陷,后续有时间的话可能会加上,node环境下依旧使用的http模块

    功能上实现了基础请求功能,内部采用的是promise的方式,实现了请求及响应的拦截以及超时取消请求,或手动取消请求

    api设计

    1. // request
    2. export type IRequestParams = T | IObject<any> | null
    3. // 请求路径
    4. export type IUrl = string
    5. // 环境判断
    6. export type IEnv = 'Window' | 'Node'
    7. // fetch返回取值方式
    8. export type IDataType = "text" | "json" | "blob" | "formData" | "arrayBuffer"
    9. // 请求方式
    10. export type IRequestMethods = "GET" | "POST" | "DELETE" | "PUT" | "OPTION" | "HEAD" | "PATCH"
    11. // body结构
    12. export type IRequestBody = IRequestParams<BodyInit>
    13. // heads结构
    14. export type IRequestHeaders = IRequestParams<HeadersInit>
    15. // 请求基础函数
    16. export type IRequestBaseFn = (url: IUrl, opts: IRequestOptions) => Promise<any>
    17. // 请求函数体
    18. export type IRequestFn = (url?: IUrl, query?: IObject<any>, body?: IRequestBody, opts?: IRequestOptions) => Promise<any>
    19. // 请求参数
    20. export type IRequestOptions = {
    21. method?: IRequestMethods
    22. query?: IRequestParams<IObject<any>>
    23. body?: IRequestBody
    24. headers?: IRequestHeaders
    25. // AbortController 中断控制器,用于中断请求
    26. controller?: AbortController
    27. // 超时时间
    28. timeout?: number
    29. // 定时器
    30. timer?: number | unknown | null
    31. [key: string]: any
    32. }
    33. // 拦截器
    34. export type IInterceptors = {
    35. // 添加请求,响应,错误拦截
    36. use(type: "request" | "response" | "error", fn: Function): void
    37. get reqFn(): Function
    38. get resFn(): Function
    39. get errFn(): Function
    40. }
    41. // 公共函数
    42. export type IRequestBase = {
    43. // 请求根路由
    44. readonly origin: string
    45. // 简单判断传入的路由是否是完整url
    46. chackUrl: (url: IUrl) => boolean
    47. // 环境判断,node或浏览器
    48. envDesc: () => IEnv
    49. // 全局的错误捕获
    50. errorFn: <Err = any, R = Function>(reject: R) => (err: Err) => R
    51. // 清除当前请求的超时定时器
    52. clearTimer: (opts: IRequestOptions) => void
    53. // 初始化超时取消
    54. initAbort: IRequestOptions>(opts: T) => T
    55. // 策略模式,根据环境切换请求方式
    56. requestType: () => IRequestBaseFn
    57. // 拼接请求url
    58. fixOrigin: (fixStr: string) => string
    59. // 请求函数
    60. fetch: IRequestBaseFn
    61. http: IRequestBaseFn
    62. // fetch响应转换方式
    63. getDataByType: (type: IDataType, response: Response) => Promise<any>
    64. }
    65. // 初始化并兼容传入的参数
    66. export type IRequestInit = {
    67. initDefaultParams: (url: IUrl, opts: IRequestOptions) => any
    68. initFetchParams: (url: IUrl, opts: IRequestOptions) => any
    69. initHttpParams: (url: IUrl, opts: IRequestOptions) => any
    70. }
    71. // 请求主体类
    72. export type IRequest = {
    73. GET: IRequestFn
    74. POST: IRequestFn
    75. DELETE: IRequestFn
    76. PUT: IRequestFn
    77. OPTIONS: IRequestFn
    78. HEAD: IRequestFn
    79. PATCH: IRequestFn
    80. } & IRequestBase

    功能实现:

    首先是拦截器的钩子函数,在请求响应以及错误时运行这些函数,将回调函数返回至外部

    1. class Interceptors implements IInterceptors {
    2. private requestSuccess: Function
    3. private responseSuccess: Function
    4. private error: Function
    5. use(type, fn) {
    6. switch (type) {
    7. case "request":
    8. this.requestSuccess = fn
    9. break;
    10. case "response":
    11. this.responseSuccess = fn
    12. break;
    13. case "error":
    14. this.error = fn
    15. break;
    16. }
    17. return this
    18. }
    19. get reqFn() {
    20. return this.requestSuccess
    21. }
    22. get resFn() {
    23. return this.responseSuccess
    24. }
    25. get errFn() {
    26. return this.error
    27. }
    28. }

    接下来是基础工具函数,请求时使用的工具函数一般会封装在这,这里还对请求函数做了个抽象处理,因为工具函数requestType 会使用到这两个请求函数

    1. abstract class RequestBase extends Interceptors implements IRequestBase {
    2. readonly origin: string
    3. constructor(origin) {
    4. super()
    5. this.origin = origin ?? ''
    6. }
    7. abstract fetch(url, opts): Promise<void>
    8. abstract http(url, opts): Promise<void>
    9. chackUrl = (url: string) => {
    10. return url.startsWith('/')
    11. }
    12. fixOrigin = (fixStr: string) => {
    13. if (this.chackUrl(fixStr)) return this.origin + fixStr
    14. return fixStr
    15. }
    16. envDesc = () => {
    17. if (typeof Window !== "undefined") {
    18. return "Window"
    19. }
    20. return "Node"
    21. }
    22. errorFn = reject => err => reject(this.errFn?.(err) ?? err)
    23. clearTimer = opts => !!opts.timer && (clearTimeout(opts.timer), opts.timer = null)
    24. initAbort = (params) => {
    25. const { controller, timer, timeout } = params
    26. !!!timer && (params.timer = setTimeout(() => controller.abort(), timeout))
    27. return params
    28. }
    29. requestType = () => {
    30. switch (this.envDesc()) {
    31. case "Window":
    32. return this.fetch
    33. case "Node":
    34. return this.http
    35. }
    36. }
    37. getDataByType = (type, response) => {
    38. switch (type) {
    39. case "text":
    40. case "json":
    41. case "blob":
    42. case "formData":
    43. case "arrayBuffer":
    44. return response[type]()
    45. default:
    46. return response['json']()
    47. }
    48. }
    49. }

    在后面的函数实现时,发现两个请求参数都会用到初始化参数,所以我把这几个函数又剥离出来了,以下是初始化参数的类

    1. abstract class RequestInit extends RequestBase implements IRequestInit {
    2. constructor(origin) {
    3. super(origin)
    4. }
    5. abstract fetch(url, opts): Promise<void>
    6. abstract http(url, opts): Promise<void>
    7. initDefaultParams = (url, { method = "GET", query = {}, headers = {}, body = null, timeout = 30 * 1000, controller = new AbortController(), type = "json", ...others }) => ({
    8. url: urlJoin(this.fixOrigin(url), query), method, headers, body: method === "GET" ? null : jsonToString(body), timeout, signal: controller?.signal, controller, type, timer: null, ...others
    9. })
    10. initFetchParams = (url, opts) => {
    11. const params = this.initAbort(this.initDefaultParams(url, opts))
    12. return this.reqFn?.(params) ?? params
    13. }
    14. initHttpParams = (url, opts) => {
    15. const params = this.initAbort(this.initDefaultParams(url, opts))
    16. const options = parse(params.url, true)
    17. return this.reqFn?.({ ...params, ...options }) ?? params
    18. }
    19. }

    最后是将请求函数完整的实现

    1. export class Request extends RequestInit implements IRequest {
    2. private request: Function
    3. constructor(origin) {
    4. super(origin)
    5. this.request = this.requestType()
    6. }
    7. fetch = (_url, _opts) => {
    8. const { promise, resolve, reject } = defer()
    9. const { url, ...opts } = this.initFetchParams(_url, _opts)
    10. const { signal } = opts
    11. promise.finally(() => this.clearTimer(opts))
    12. signal.addEventListener('abort', () => this.errorFn(reject));
    13. fetch(url, opts).then((response) => {
    14. if (response?.status >= 200 && response?.status < 300) {
    15. return this.getDataByType(opts.type, response)
    16. }
    17. return this.errorFn(reject)
    18. }).then(res => resolve(this.resFn?.(res) ?? res)).catch(this.errorFn(reject))
    19. return promise
    20. }
    21. http = (_url, _opts) => {
    22. const { promise, resolve, reject } = defer()
    23. const params = this.initHttpParams(_url, _opts)
    24. const { signal } = params
    25. promise.finally(() => this.clearTimer(params))
    26. const req = request(params, (response) => {
    27. if (response?.statusCode >= 200 && response?.statusCode < 300) {
    28. let data = "";
    29. response.setEncoding('utf8');
    30. response.on('data', (chunk) => data += chunk);
    31. return response.on("end", () => resolve(this.resFn?.(data) ?? data));
    32. }
    33. return this.errorFn(reject)(response?.statusMessage)
    34. })
    35. signal.addEventListener('abort', () => this.errorFn(reject)(req.destroy(new Error('request timeout'))));
    36. req.on('error', this.errorFn(reject));
    37. req.end();
    38. return promise
    39. }
    40. GET = (url?: IUrl, query?: IObject<any>, _?: IRequestBody | void, opts?: IRequestOptions) => {
    41. return this.request(url, { query, method: "GET", ...opts })
    42. }
    43. POST = (url?: IUrl, query?: IObject<any>, body?: IRequestBody, opts?: IRequestOptions) => {
    44. return this.request(url, { query, method: "POST", body, ...opts })
    45. }
    46. PUT = (url?: IUrl, query?: IObject<any>, body?: IRequestBody, opts?: IRequestOptions) => {
    47. return this.request(url, { query, method: "PUT", body, ...opts })
    48. }
    49. DELETE = (url?: IUrl, query?: IObject<any>, body?: IRequestBody, opts?: IRequestOptions) => {
    50. return this.request(url, { query, method: "DELETE", body, ...opts })
    51. }
    52. OPTIONS = (url?: IUrl, query?: IObject<any>, body?: IRequestBody, opts?: IRequestOptions) => {
    53. return this.request(url, { query, method: "OPTIONS", body, ...opts })
    54. }
    55. HEAD = (url?: IUrl, query?: IObject<any>, body?: IRequestBody, opts?: IRequestOptions) => {
    56. return this.request(url, { query, method: "HEAD", body, ...opts })
    57. }
    58. PATCH = (url?: IUrl, query?: IObject<any>, body?: IRequestBody, opts?: IRequestOptions) => {
    59. return this.request(url, { query, method: "PATCH", body, ...opts })
    60. }
    61. }

    以上代码有几个注意点:

    • node中的http请求和浏览器的fetch请求的参数不同,需要把参数初始化并做成兼容的格式
    • AbortController api在node环境下对http模块的兼容性问题,所以需要自己手动去调用超时取消请求

    • get请求与其他请求不同,带body会被浏览器屏蔽

    功能验证:

    node环境下:

    使用以下命令初始化dev项目:

    1. pnpm init
    2. pnpm i utils-lib-js

    在项目根目录下新建server.js,咱们先写个简单的get请求,内容如下:

    1. const Request = require("utils-lib-js").Request;
    2. const resource = new Request("http://127.0.0.1:1024");
    3. resource.GET("/getList").then(console.log).catch(console.log);

    之后再试试post:

    resource.POST("/getList").then(console.log).catch(console.log);

    默认的请求超时是30秒,如果需要自定义请求时间可以添加timeout

    1. resource
    2. .GET("/getList", {}, null, {
    3. timeout: 100,
    4. })
    5. .then(console.log)
    6. .catch(console.log);

    同时也支持取消请求(请求超时和取消请求不会等待结果,直接返回reject):

    1. const controller = new AbortController();
    2. setTimeout(() => controller.abort(), 1000);
    3. resource
    4. .GET("/getList", {}, null, {
    5. controller,
    6. })
    7. .then(console.log)
    8. .catch(console.log);

    拦截器的使用方式

    1. const Request = require("utils-lib-js").Request;
    2. const resource = new Request("http://127.0.0.1:1024");
    3. resource
    4. .use("request", (params) => {
    5. console.log(params.query);
    6. return params;
    7. })
    8. .use("response", (params) => {
    9. console.log(params);
    10. return params.length;
    11. })
    12. .use("error", (error) => {
    13. console.log(error);
    14. return error;
    15. });
    16. resource.GET("/getList", { name: "abc" }).then(console.log)

    vite-dev环境下:

    我使用的是vite+vue,运行以下命令安装工具:

    pnpm i utils-lib-js

     然后在main.ts文件中试试,可以看到Request已经适配了fetch

    1. import { createApp } from 'vue'
    2. import './style.css'
    3. import App from './App.vue'
    4. import { Request } from "utils-lib-js"
    5. const resource = new Request("http://127.0.0.1:1024");
    6. resource
    7. .use("request", (params) => {
    8. console.log(params.url);
    9. return params;
    10. })
    11. .use("response", (params) => {
    12. console.log(params);
    13. return params.length;
    14. })
    15. .use("error", (error) => {
    16. console.log(error);
    17. return error;
    18. });
    19. resource.GET("/getList", { name: "abc" }).then(console.log)
    20. createApp(App).mount('#app')

     

    写在最后

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

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

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

    npm:utils-lib-js - npm

    感谢你看到了这里,如果文章对你有帮助,还请点个赞支持一下

  • 相关阅读:
    面试官:ElasticSearch是什么,它有什么特性与使用场景?
    条件随机场CRF
    Maven版本管理
    51单片机基础篇系列-定时/计数器的控制&工作方式
    io,nio,aio总结
    Mathematica求解方程——Solve、Reduce、NSolve等函数
    【Java】中Maven依赖详解
    【数据库学习】图数据库:neo4j
    Java异常、断言和日志①——Java基础
    windows下使用php-ffmpeg获取视频第一帧的图片
  • 原文地址:https://blog.csdn.net/time_____/article/details/126719561