likes
comments
collection
share

重新搞个http请求库吧,果然,每隔一段时间看之前的代码,就想重构....最近翻了一下以前封装的请求库,发现果然每过一段

作者站长头像
站长
· 阅读数 19

最近翻了一下以前封装的请求库,发现果然每过一段就会嫌弃自己以前的代码,就想着重新个搞个请求库,然后支持一下hooks,先定义一下功能列表:

  1. 支持get和post
  2. 支持post form表单
  3. 支持jsonp
  4. 响应数据处理
  5. 响应错误拦截
  6. 全局loading
  7. 请求缓存
  8. 登陆拦截器
  9. 必须用ts写
  10. 实现react版useRequest
  11. 实现vue版的useRequest

技术选型

核心思路是有要实现http.js,又要实现2个框架的hooks版本。这里可以选择用monorepo架构,来实现核心code复用。那我们可能需要建4个子包,lib(核心code),demo(请求测试),react(hooks+测试),vue(hooks+测试),lib包可以直接用tsup快速打包,其他测试包可以用vite创建不同框架的测试包,也可以利用vite的库能力分别打包对用的hooks。

重新搞个http请求库吧,果然,每隔一段时间看之前的代码,就想重构....最近翻了一下以前封装的请求库,发现果然每过一段

核心lib模块(npc-http)

先根据我们的功能列表,先定义一些我们后面可能会涉及到ts体操。

// 消息提醒方法
export type MessageCallEventMap = Partial<
    {
        success: (data: any) => void,
        error: (error: any) => void,
        warn: (info: any) => void
    }>
// init的时候,一些全局配置
export type GlobalConfigType = Partial<
    {
        cacheType: EnumCache,
        message: MessageCallEventMap,
        watchRequestStatus: (isRequesting?: boolean) => void
    }
>
// 单独每次请求的配置
export type RequestConfigType = Partial<
    {
        cache: {
            cacheKey: string,  // 缓存key
            cacheTime: number // 缓存时间
        },
        redirect: {
            code: number,  // 重定向code
            path: string  // 重定向路径
        } | boolean, // 当为true,默认code为500,path为/login
        ignoreLoading: boolean, // 是否忽略全局loading
        ignoreTransferResult: boolean  // 是否忽略结果错误
        errorHandler: (err: any) => void, // 自定义错误处理函数
        transferResult: (data: any) => any  // 自定义处理响应结果
    }>
    
// 配置参数,全局配置+请求配置+axios配置
export type ConfigType = GlobalConfigType & CustomConfigType & AxiosRequestConfig   

// post,get,postForm的出入参
export type RequestMethodType = (
    url: string,
    params?: any,
    config?: RequestConfigType
) => Promise<any>

// jsonp的出入参
export type JsonpMethodType = (url: string, options?: any) => Promise<any>    

根据ts类型定义,我们先写一个函数空壳,后面再往里补内容。

class request {
    config: ConfigType
    axios: typeof axios
    constructor() {
        this.config = {
        }
        this.axios = axios
        this.init()
    }
    // 初始化,把默认配置和传进来的配置进行合并,并存起来
    init = (config?: ConfigType) => {
        this.config = {
            ...this.config,
            ...config || {}
        }
    }
    
    // 读取配置
    get = (key?: keyof ConfigType) => {
        return key ? this.config[key] : this.config
    }
    //设置配置
    set = (key, value) => {
        this.config[key] = value
    }

    httpGet: RequestMethodType = (url, params, config) => {
        return new Promise((resolve, reject) => {

        })
    }
    httpPost: RequestMethodType = (url, params, config) => {
        return new Promise((resolve, reject) => {

        })

    }
    postForm: RequestMethodType = (url, params, config) => {
        return new Promise((resolve, reject) => {
       
        })
    }

    jsonp: JsonpMethodType = async (url, options) => {
        return new Promise((resolve, reject) => {
  
        });
    }
}

const instance = new request()
export const httpInit = instance.init
export const httpConfigSet = instance.set
export const httpConfigGet = instance.get
export const httpPost = instance.httpPost
export const httpGet = instance.httpGet
export const postForm = instance.postForm
export const jsonp = instance.jsonp

export default instance

然后根据暴露出去的请求方法进行分类:

post,get,postForm这些都走axios,应该算是一类

// 请求方法的枚举
export enum EnumAxiosType {
    get = 'get',
    post = 'post',
    postForm = 'postForm'
}
// 以get为例,对axios进行二次封装
 sendRequest = async (method: EnumAxiosType, url: string, params?: any, config?: RequestConfigType) => {
        if (method === EnumAxiosType.postForm) {
            return this.axios[method](url, params, config as AxiosRequestConfig)
        }
        return this.axios({
            method,
            url: processUrl(url),
            [method === EnumAxiosType.get ? "params" : "data"]: params,
            ...this.config,
            ...config
        })

    }
// 封装get请求,统一调用sendRequest,通过method进行区分请求类型    
httpGet: RequestMethodType = (url, params, config) => {
        return new Promise((resolve, reject) => {
            this.sendRequest(
                EnumAxiosType.get,
                url,
                params,
                config
            ).then((res) => {
                resolve(res)
            }).catch(reject)
        })
    }

jsonp,自己算一类,直接调用第三方jsonp包就行

 jsonp: JsonpMethodType = async (url, options) => {
        // https://github.com/webmodules/jsonp
        const jsonp = (await import('jsonp')).default;
        return new Promise((resolve, reject) => {
            jsonp(
                url,
                {
                    ...(options || {}),
                },
                (err: Error | null, data: any) => {
                    if (err) {
                        errorLog(err)
                        reject(err);
                    } else {
                        resolve(data);
                    }
                },
            );
        });

    }

目前我们正常的请求基本就完成,剩下就是完成,我们的拦截器 创建一个interceptors拦截器目录,在初始化的时候,直接调一下拦截器初始化

重新搞个http请求库吧,果然,每隔一段时间看之前的代码,就想重构....最近翻了一下以前封装的请求库,发现果然每过一段

import * as resultInterceptor from './resultInterceptor'
import * as loadingInterceptor from './loadingInterceptor'
import * as cacheInterceptor from './cacheInterceptor'
import * as loginInterceptor from './loginInterceptor'

// 这里预设了hasInit参数,防止多次初始化,因为init函数可能被多次调用
let hasInit = false
export const initInterceptors = (axios:any) => {
  // 缓存拦截器
  if(!hasInit){
    axios.interceptors.response.use(cacheInterceptor.onResponseFulfilled, cacheInterceptor.onResponseRejected)
    // loading拦截器
    axios.interceptors.request.use(loadingInterceptor.onRequestFulfilled, loadingInterceptor.onRequestRejected)
    axios.interceptors.response.use(loadingInterceptor.onResponseFulfilled, loadingInterceptor.onResponseRejected)
    // 登陆拦截器
    axios.interceptors.response.use(loginInterceptor.onFulfilled, loginInterceptor.onRejected)
    // 响应拦截器
    axios.interceptors.response.use(resultInterceptor.onFulfilled, resultInterceptor.onRejected)
  }
  hasInit=true

}    
 init = (config?: ConfigType) => {
        initInterceptors(this.axios)
        this.config = {
            ...this.config,
            ...config || {}
        }
   }

然后我们来依次完成我们需要的拦截器:

loginInterceptor

let requestCount = 0;

// 统计requestCount时的延迟时间,避免一个很快的请求也显示loading效果
const START_DELAY_TIME = 1500;
const END_DELAY_TIME = 250;

const changeRequestLoading = (watchRequestStatus:GlobalConfigType['watchRequestStatus']) => {
    if (watchRequestStatus && typeof watchRequestStatus === "function") {
        watchRequestStatus(requestCount > 0);
    }
};

export const startRequest = (config: any) => {
    // 此请求不使用loading
    if (config && config.ignoreLoading) {
        return;
    }
    requestCount += 1;
    // 请求开始一段时间后,如果还有未结束的请求
    setTimeout(() => {
        if (requestCount > 0) {
            changeRequestLoading(config.watchRequestStatus);
        }
    }, START_DELAY_TIME);
};

export const endRequest = (config: any) => {
    if (config) {
        // 此请求不使用loading
        if (config.ignoreLoading) {
            return;
        }
    }
    requestCount -= 1;

    // 请求结束一段时间后,如果还有未结束的请求
    setTimeout(() => {
        if (requestCount === 0) {
            changeRequestLoading(config.watchRequestStatus);
        }
    }, END_DELAY_TIME);
};

export const onRequestFulfilled = (config: any) => {
    startRequest(config);
    return config;
};

export const onRequestRejected = (error: any) => {
    return Promise.reject(error);
};

export const onResponseFulfilled = (response: any) => {
    endRequest(response && response.config);
    return response;
};

export const onResponseRejected = (error: any) => {
    endRequest(error && error.config);
    return Promise.reject(error);
};

resultInterceptor,响应拦截器虽然代码是最先写的,但是我们需要把执行顺序放到最后一个 因为拦截器是按序执行的,上一个拦截器的结果会传给下一个拦截器,因为resultInterceptor涉及到数据加工,为了防止我们自定义的一些参数配置后面拦截器取不到,所以一定要放到后面执行!


// 内置的响应结果处理函数
export const transferResult = (res: any, config: ConfigType) => {
  if (config?.ignoreTransferResult) {
    return res
  }

  if (res?.code === HTTP_SUCCESS_CODE) {
    return res.data || res
  }
  if (config?.errorHandler) {
    config.errorHandler(res)
  } else {
    const message = instance.get('message')
    message && message.error(res?.msg || ERROR_MESSAGE)
    errorLog(res)
  }
  return Promise.reject(res)
}


// 这里可以统一加工数据,可以通过自定义传递,或者内置的处理函数
export const onFulfilled = (response: any) => {
  const transfer = response?.config?.transferResult || transferResult
  return transfer(response?.data || response?.response?.data || response, response.config);
};

// 对错误信息统一处理,建议和后端定好协议,错误直接返回错误信息,在这里统一message.error
export const onRejected = (res: any) => {
  if (res.response) {
    const message = instance.get('message')
    message && message.error(res?.response?.data?.message || ERROR_MESSAGE)
    errorLog(res.response)
  }
  return Promise.reject(res);
};

loginInterceptor,因为这里要对响应结果进行判断,resultInterceptor.onFulfilled来处理数据


const ERROR_CODE = 500
const ERROR_PATH = "/login"


import * as resultInterceptor from './resultInterceptor';
export const onFulfilled = async (response: any) => {
    if (response?.config?.redirect) {
        try {
            await resultInterceptor.onFulfilled({
                ...response,
                config: {
                    ...response.config,
                    errorHandler: () => { }
                }
            })

        } catch (error) {
            const { code = ERROR_CODE, path = ERROR_PATH } = response.config.redirect
            if ((error as any)?.code === ERROR_CODE && path && code) {
                window.location.href = path
            }
        }
    }

    return response
};

export const onRejected = (res: any) => {
    return res
};


cacheInterceptor,这个也一样,涉及到缓存数据响应结果,也直接调用resultInterceptor.onFulfilled来处理数据

存数据:

export const isPromise=(obj:any)=>{
      return !!obj && (typeof obj === "object" || typeof obj === "function") && typeof obj.then === "function";
  }

import * as resultInterceptor from './resultInterceptor'
import dayjs from 'dayjs'

const DEFAULT_CACHE_TIME = 300;


export const getRequestCacheKey = (key: string) => {
    return `request_key_${key}`
}


export const getCacheData = (config:ConfigType) => {
    const { cache, url } = config
    const cacheKey = getRequestCacheKey(cache?.cacheKey || url as string)
    let data = instance.cacheMethod().getItem(cacheKey) as any
    if (data) {
        data = JSON.parse(data)
    }
    if (data && data?.expireTime > dayjs().toString()) {
        return data.data
    } else {
        data = null
    }
    return data
}


export const onResponseFulfilled = (response: any,) => {
    const { cache, url } = response?.config||{}
    if (cache) {
        const cacheKey = getRequestCacheKey(cache.cacheKey || url);
        const cacheTime = cache.cacheTime || DEFAULT_CACHE_TIME;
        const data = resultInterceptor.onFulfilled({
            ...response,
            config:{
                ...response.config,
                errorHandler:()=>{}
            }
        })
        // resultInterceptor.onFulfilled的结果可能是reject()
        // reject的肯定是请求失败的,就不进行缓存了
        if (!isPromise(data)) {
            instance.cacheMethod().setItem(cacheKey, JSON.stringify({
                data,
                saveTime: dayjs().toString(),
                expireTime: dayjs().add(Number(cacheTime), 'm').toString()
            }));
        }

    }
    return response;
};

export const onResponseRejected = (error: any) => {
    return Promise.reject(error);
};

取数据:

sendRequest = async (method: EnumAxiosType, url: string, params?: any, config?: RequestConfigType) => {
        const cacheData = getCacheData({
            ...config,
            url
        })
        if (cacheData) {
            return cacheData
        }
        if (method === EnumAxiosType.postForm) {
            return this.axios[method](url, params, config as AxiosRequestConfig)
        }
        return this.axios({
            method,
            url: processUrl(url),
            [method === EnumAxiosType.get ? "params" : "data"]: params,
            ...this.config,
            ...config
        })

    }

这样大部分功能就完成了,我们去demo下测试调用一下吧,4种请求,一切正常!

重新搞个http请求库吧,果然,每隔一段时间看之前的代码,就想重构....最近翻了一下以前封装的请求库,发现果然每过一段

hooks

lib的功能完成了的话,其实hooks就非常简单了,只需要调用一下lib的api就可以。

react hook

同样,先定义下ts类型,看下我们要实现哪些功能:

import { RequestConfigType } from "npc-http/type";

// hooks初始化配置项
export type HookOptions = {
    immediate?: boolean; // 是否立刻执行
    onSuccess?: (res: any, params: any) => void; // 成功回调
    onError?: (res: any, params: any) => void; // 失败回调
}

// hooks返回值
export type RequestReturnType={
    data: any, // 响应数据
    error: any, // 错误数据
    params:any, // 请求参数
    loading: boolean, // loading状态
    run: (params?: any) => void, // 执行函数
    refresh:()=>void  //书信
}

export type UseRequestType = (url: string, params?: any, method?: "get" | "post", hook?: HookOptions, config?: RequestConfigType) => RequestReturnType

import { useState, useEffect } from 'react'
import { httpGet, httpPost } from 'npc-http'
import { UseRequestType } from './type'
import { EnumAxiosType } from 'npc-http/type'

const useRequest: UseRequestType = (url, defaultParams, method, hook, config) => {
    const [loading, setLoading] = useState(false)
    const [params, setParams] = useState(defaultParams)
    const { immediate = true, onSuccess, onError } = hook || {}
    const [data, setData] = useState(null)
    const [error, setError] = useState(null)
    const run = async (newParams?: any) => {
        setLoading(true)
        if (newParams ?? false) {
            setParams(newParams)
        }
        try {
            const result = await (method === EnumAxiosType.post ? httpPost : httpGet)(url, newParams || params, config)
            onSuccess && onSuccess(result, params)
            setData(result)
            setError(null)
        } catch (error) {
            setError(error as any)
            setData(null)
            onError && onError(error, params)
        } finally {
            setLoading(false)
        }
    }

    const refresh = () => {
        run()
    }

    useEffect(() => {
        if (immediate) {
            run()
        }
    }, [])

    return {
        data,
        error,
        params,
        loading,
        run,
        refresh
    }
}

export default useRequest

测下结果,一切正常:

重新搞个http请求库吧,果然,每隔一段时间看之前的代码,就想重构....最近翻了一下以前封装的请求库,发现果然每过一段

vue hooks

react的hooks完成后,那vue的hooks就简单多了,把useState更换成ref就行了

import { ref, onMounted } from 'vue'
import { httpGet, httpPost } from 'npc-http'
import { UseRequestType } from './type'
import { EnumAxiosType } from 'npc-http/type'

const useRequest: UseRequestType = (url, defaultParams, method, hook, config) => {
    const loading = ref<boolean>(false)
    const params = ref(defaultParams)
    const { immediate = true, onSuccess, onError } = hook || {}
    const data = ref(null)
    const error = ref<any>(null)
    const run = async (newParams?: any) => {
        loading.value = false
        if (newParams ?? false) {
            params.value = newParams
        }
        try {
            const result = await (method === EnumAxiosType.post ? httpPost : httpGet)(url, newParams || params, config)
            onSuccess && onSuccess(result, params)
            data.value = result
            error.value = null
        } catch (err) {
            error.value = (err as any)
            data.value = (null)
            onError && onError(err, params)
        } finally {
            loading.value = false
        }
    }

    const refresh = () => {
        run()
    }

    onMounted(() => {
        if (immediate) {
            run()
        }
    })

    return {
        data,
        error,
        params,
        loading,
        run,
        refresh
    }
}

export default useRequest

测试一下,一起正常! 重新搞个http请求库吧,果然,每隔一段时间看之前的代码,就想重构....最近翻了一下以前封装的请求库,发现果然每过一段

发版

因为几个包需要同时发版,这里推荐使用changeset, 同时进行多个包管理

  "scripts": {
    "test": "pnpm -F demo run start",
    "build-lib": "pnpm -F npc-http run build",
    "build-v": "pnpm -F use-request-v run build",
    "build-r": "pnpm -F use-request-r run build",
    "changeset": "changeset",
    "build": "npm run build-lib && npm run build-v && npm run build-r",
    "ci:publish": "pnpm publish -r --no-git-checks",
    "pre-publish": "npm run changeset && pnpm changeset version && git push",
    "publish": "npm run build && npm run ci:publish",
    "prepare": "husky"
  },

一种是完全本地打包发包:

那我直接npm run publish就可以了,然后自动打包,交互更新版本号,发包

另外一种,本地更新版本号,线上打包,发包(有大佬知道changeset如何命令行配置式生成版本号么?) 那我直接执行npm run pre-publish就可以了,然后走github actions自动打包发包

name: Changesets
on:
  push:
    branches:
      - master
env:
  CI: true
  PNPM_CACHE_FOLDER: .pnpm-store
jobs:
  version:
    timeout-minutes: 15
    runs-on: ubuntu-latest
    steps:
      - name: checkout code repository
        uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - name: setup node.js
        uses: actions/setup-node@v3
        with:
          node-version: 18.12.0
      - name: install pnpm
        run: npm i pnpm@latest -g
      - name: Setup npmrc
        run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc
      - name: setup pnpm config
        run: pnpm config set store-dir $PNPM_CACHE_FOLDER
      - name: install dependencies
        run: pnpm install
      - name: build
        run: pnpm run build
      - name: git check
        run: git status
      - name: echo token
        run: |
          echo ${{ secrets.GIT_TOKEN }}
      - name: create and publish versions
        uses: changesets/action@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GIT_TOKEN }}
        with:
          version: pnpm ci:version
          commit: "chore: update versions"
          title: "chore: update versions"
          publish: pnpm ci:publish
    

在github actions配置的时候,遇到了个问题真坑啊,报了几个次这个错误。

remote: Write access to repository not granted.
fatal: unable to access 'https://github.com/waltiu/npc-http.git/': The requested URL returned error: 403

大家遇到这个问题的时候,需要除了github token的配置权限外,还要关注下git actions的权限:

重新搞个http请求库吧,果然,每隔一段时间看之前的代码,就想重构....最近翻了一下以前封装的请求库,发现果然每过一段

重新搞个http请求库吧,果然,每隔一段时间看之前的代码,就想重构....最近翻了一下以前封装的请求库,发现果然每过一段

重新搞个http请求库吧,果然,每隔一段时间看之前的代码,就想重构....最近翻了一下以前封装的请求库,发现果然每过一段

这三个地方,大家都看下,权限配置是否正确!

总结

这样大部分的功能基本都完成了,大家也可以根据自己的需求添加拦截器,欢迎大家提建议。 代码库:github.com/waltiu/npc-… npc-http: www.npmjs.com/package/npc… use-request-v: www.npmjs.com/package/use… use-request-r: www.npmjs.com/package/use…

转载自:https://juejin.cn/post/7384646888697954304
评论
请登录