umi-request & useRequest 源码分析及业务实践

导读

文章通过业务点分析以及深入umi-request,useResut的源码,来探索怎么使用这两个工具将业务整理的更清晰。

1. 业务梳理

1.1 常见问题

  1. page目录、service目录、model目录、typescript目录,utils 目录,所有不同类型的业务逻辑都被拆分在不同的目录内,且还存在相互引用,以及嵌套层级很深,在开发一个页面的时候难以聚焦。
  2. 被频繁引用的模块中 if else 使用很多,且存在嵌套,如果需要加入新的逻辑,由于逻辑不清晰,在引入新功能时容易影响老的逻辑,由于被各个页面频繁引用,波及范围较大,容易出现线上问题。
  3. 在涉及到后台的定义接口以及使用的过程中,每增加一个接口,都会涉及到对应接口的 loading状态,data状态,error状态,请求前,请求后的处理,当一个业务点涉及的异步逻辑很多的时候,整个业务逻辑会变得很臃肿。

1.2 解决方案

  1. 每一个 page 的不同类型的业务逻辑都放在一个目录内
  2. 引入umi-request 来包装 uni-request
  3. 引入 useRequest hook,在定义接口文件的时候,同时预实例化对应 service 的hook,便于开发。
  4. 将具体页面的状态管理交给 hook,全局的状态交给 model。

2. 将 umi-request整合进小程序,告别 if else

基于uni-app 的 uni.request ,非 promise,只有 onSuccess,onFail 回调,小程序不支持urlSearchParams, 手动替换 coreMiddleware

2.1 小程序请求的一般逻辑流程图

mini-request.png

function request(url: string, options) {
  const { noErrToast = false, noLoginToken = false } = options;
  
  const params = cloneDeep(data);
  
  if (needLoginToken) {
    if (token) setToken();
    else return noLoginCallback();
  }
  
  // 删除无效入参
  removeNillParams(params);

  return new Promise(function (resolve, reject) {
    uni.request({
      url,
      ...options,
      success(res) {
        printSucessResponse(res);
        if (res.code === ErrorCodeA) return specificCodeErrorCallback(res);
        if (res.code === ErrorCodeB) return specificCodeErrorCallback(res);
        if (res.code === ErrorCodeC) return specificCodeErrorCallback(res);
        if (res.code === LOGIN_EXPIRED)
          return specificCodeErrorCallback(res);
        if (options.simpleResponse) {
          if (res.code === SuccessCode) resolve(simplify(res));
          else {
            if (options.autoErrToast) return specificCodeErrorCallback(res);
            reject(res);
          }
        } else {
          resolve(res);
        }
      },
      fail(err) {
        printFailResponse(err);
        if (err.errMsg?.startsWith(ErrorMessageA))
          return specificCodeErrorCallback(err);
        if (err.errMsg.startsWith(ErrorMessageB))
          return specificCodeErrorCallback(err);
        return specificCodeErrorCallback(err);
      },
      complete(res) {
        console.log("request complete =>", url, res);
      },
    });
  });
}

2.2 umi-request 应用

  1. 支持 prefix
  2. 支持 request/response interceptor
  3. 支持 middleware
  4. 支持 error Handling
  5. 支持 timeout
  6. 支持 cache
const request = extend({
    prefix: urlPrefix,
    timeout: 3000,
    errorHandler: handleError,
})
// 上传图片走不同接口
request.use(uploadImgMiddleware, { core: true })
// 去除 null 或 undefined 入参
request.interceptors.request.use(removeNillProperty)
// 打印 request 请求参数方便调试
request.interceptors.request.use(printRequest)
// 检查网络是否连接
request.interceptors.request.use(checkNetWork)
// 检查是否有 token
request.interceptors.request.use(noLogin)
// 打印 response 结果
request.interceptors.response.use(printResponse)
// 打印 error 信息
request.interceptors.response.use(handleErrorCode)
// 拍平 response 结果
request.interceptors.response.use(simpleData)
// 做 debounce 支持
injectDebounceSupportOptions(request)

2.3 umi-request 源码剖析

先从 extend 实例化入手,到具体的 request core method,接下来是 request interceptor,然 后 middleware, 再来 koa-compose middleware 组合机制,中间件调用

image.png

2.3.1 extend

预先实例化部分 options

2.3.2 request core 核心逻辑

  1. promise 链式调用
return new Promise((resolve, reject) => {
    requestInterceptors(obj)
      .then(() => middlewaresChain(obj))
      .then(() => resolve(obj.res))
      .catch((err) => {
        try {
          resolve(errorHandler(err))
        } catch (e) {
          reject(e)
        }
      })
  })
  1. add request/response interceptor
useInterceptor(handler){
    this.interceptors.push(handler)
}
  1. run interceptor
requestInterceptors(ctx) {
  const reducer = (prevInterceptor, interceptor) =>
    prevInterceptor.then(() => p2(ctx.url, ctx.options))
    
  return this.requestInterceptors.reduce(reducer, Promise.resolve())
}

  1. add middlewares
useMiddleware(newMiddleware, opts) {
  if (opts.global) {
    globalMiddlewares.unshift(newMiddleware)
  } else if (opts.core) {
    coreMiddlewares.unshift(newMiddleware)
  } else {
    middlewares.push(newMiddleware)
  }
}
  1. run middleware
middlewaresChain(params){
    const fn= koaCompose([
        ...middlewares,
        ...globalMiddlewares,
        ...coreMiddlewares
    ])
    return fn(params)
}   
  1. response interceptor
// in request core middleware
requestCoreMiddleware(ctx,next){
    let response=fetch(url,options)
    responseInterceptors.forEach((interceptor)=>{
        response=response.then((res)=>interceptor(res,ctx.options))
    })
    return response.then((res)=>{
          ctx.res=res
          return next()
    })
}

2.3.3 与 uni-request 整合

将 request-core 中的 fetch 替换为 uni-request

3. 将 useRequest 整合进后台,告别自己管理 response,error,loading

当遇到一个页面有很多异步的业务逻辑时,将面对一堆各个异步接口的 useState Hooks,如果接口调用有依赖将更为复杂

3.1 dva case

  1. 定义 model 里的 data 和 Effect
  2. connect 组件
  3. 处理同步异步,非函数式,概念很多,痛苦面具
  4. 粒度较大,难复用

3.2 hoook case

以下是一个接口

// page.ts
useState(defaultData)
useState(requestLoading)
function request() {
  setLoading()
  makeRequest()
  setData()
  handlerError()
}
// service.ts
const service = {
    serviceA: request.post('url'),
  }

10个接口的时候? 一堆 useState 和 useEffect 业务 hooks

3.3 useRequest case

支持 debouce, 支持生命周期 onBefore,onError, onSuccess, onFinally 集成了 data,error,loading 支持 manual?或手动执行:初始化自动执行

扫描二维码关注公众号,回复: 14343702 查看本文章
// page.tsx
import service from '@/service'
import { useRequest } from 'ahooks'

const { data, run: request, loading, error } = useRequest(service.serviceA, options)
// service.ts
const service = {
    serviceA: request.post('url'),
  }

将减少至少一半的自定义业务 hooks

3.4 imporved useRequest case

流程以及存在的问题

  1. 定义 service 文件
  2. 引入到具体的 page
  3. 引入 useRequest
  4. 如果不同的 page 引用了相同的接口,需要重复引入 且定义useRequest
  5. useRequest 的 options 和 service 的 options只定义一次,一般不会改变,是否需要在两个地方定义。

解决方案: 合并在一起,在定义 service 的同时完成 useRequest的实例化,将合并的 options中的 serviceOptions 和 useRequest Options 区分开做对应的初始化。

// service.ts
const service = {
  requestA: request.get('/urlA', { manual: false }),
  requestB: request.get('/urlB', { manual: true }),
  requestC: request.get('/urlc', { debounceWait: 6000 }),
  requestD: request.get('/urld', { cacheTime: 500 }),
  requestE: request.post('/urle'),
}
// page.ts
// service.requestA 仍然是一个异步函数,可以直接调用,useRequest 是 service.requestA 的一个方法
const { data: dataA, loading: loadingA } = service.requestA.useRequest({ debounceWait: 300 })
const { data: dataB, runAsync: getAreaNames, loading: loadingB, mutate } = service.requestB.useRequest()
const { runAsync: getDataC } = service.requestC.useRequest()
const { runAsync: getDataD, loading: loadingD } = service.requestD.useRequest()
const { runAsync: getDataE, loading: loadingE } = service.requestE.useRequest()

package request implement

// request.ts
  const methodNames = ['get', 'post', 'delete'];
  
  methodNames.forEach((name) => {
    request[name] = (url, options = {}) => {
    // 从 options 中取 request options
      const requestCore = (data) =>
        request[name](
          url,
          pickRequestOptions(options),
        );
    // 从 options 中取 useRequest options,将 useRequest 挂在请求函数上
      requestCore.useRequest = (_options: object) =>
        useRequest(
          requestCore,
          [defaultUseRequestOptions, pickUseRequestOptions(options), _options].reduce(mergeRight),
        );

      return requestCore;
    };
  });

3.5 useRequest 源码剖析

3.5.1 bundle hookPlugins,将 hookPlugin 插件聚合在一起

// useRequest core.ts
function useRequestImplement(plugins, service, options) {
  const { manual, ...rest } = options
  // 用于状态更新触发 render
  const [, setState] = useState({})
  const update = useCallback(() => setState({}), [])
  // 将核心 fetch  实例挂在 ref 对象上
  const fetchInstance = useRef(() => {
    const initState = plugins
      .map((plugin) => plugin?.onInit?.(fetchInstance, fetchOptions))
      .filter(Boolean)
    return new Fetch(serviceRef, fetchOptions, update, Object.assign({}, ...initState))
  })

  fetchInstance.options = fetchOptions
  // 挂载所有的生命周期 Array<lifecycleObject>
  fetchInstance.pluginImpls = plugins.map((plugin) =>
    plugin(fetchInstance, fetchOptions),
  )

  return {
    loading: fetchInstance.state.loading,
    data: fetchInstance.state.data,
    error: fetchInstance.state.error,
    params: fetchInstance.state.params || [],
    cancel: fetchInstance.cancel.bind(fetchInstance),
    refresh: fetchInstance.refresh.bind(fetchInstance),
    refreshAsync: fetchInstance.refreshAsync.bind(fetchInstance),
    run: fetchInstance.run.bind(fetchInstance),
    runAsync: fetchInstance.runAsync.bind(fetchInstance),
    mutate: fetchInstance.mutate.bind(fetchInstance),
  }
}

3.5.2 一个 hookPlugin 插件,值得一提的是,plugins 内部是没有 state 状态的管理的,所有的 update 都是委托给 useRequestImplement 中的 const update = useCallback(()=>setState({}),[]) 这个方法

function hookPlugin(fetchInstance,options){
    useRef(...)
    useEffect(...)
    return {
        onBefore?:function
        onSuccess?:function
        onError?:function
        onFinally?:function
        onMutate?:function onCancel?:function } } hookPlugin.onInit=function 

3.5.3 useRequest 里的核心类 Fetch

  1. change state
// fetch.ts
setState(s){
  this.state={
    ...this.state,
    ...s,
  }
  update()
}
  1. run lifecycle
runPluginHandler(lifeCycleName, ...params) {
  const r = this.pluginImpls
    .map((plugin) => plugin[lifeCycleName](params))
    .filter(Boolean)
  return Object.assign({}, ...r)
}
  1. runAsync core method, options.lifecycle 为自定义 lifecycle,this.runPluginHandler(lifecycleName)hookPlugin 内置 lifecycle

image.png

async runAsync(...params){
    this.runPluginHandler('onBefore', params)
    this.options.onBefore?.(params);
    try{
        const res = await this.runPluginHandler('onRequest', this.serviceRef.current, params);
        this.setState(...)
        this.options.onSuccess?.(res, params);
        this.runPluginHandler('onSuccess', res, params);
        this.options.onFinally?.(params, res, undefined);
        this.runPluginHandler('onFinally', params, res, undefined);
    }catch(error){
        this.options.onError?.(error, params);
        this.runPluginHandler('onError', error, params);
        this.options.onFinally?.(params, undefined, error);
        this.runPluginHandler('onFinally', params, undefined, error);

        throw error;
    }
}

4. 目录梳理

梳理前

- /pageA
    - index.ts 
    - styles.less 
- /pageB
    - index.ts 
    - styles.less 
- /pageC
    - index.ts 
    - styles.less 
- /service
    - pageA.service.ts
    - pageB.service.ts
    - pageC.service.ts
- /model
    - pageA.model.ts
    - pageB.model.ts
    - pageC.model.ts
- /types
    - /Api
    - xxx.d.ts
    - xxx.d.ts
    - xxx.d.ts
    - /service
       - pageA.service.d.ts
       - pageB.service.d.ts
       - pageC.service.d.ts

梳理后

- /pageA
    - index.ts 
    - styles.less 
    - types.d.ts?
    - service.ts
- /pageB
    - index.ts 
    - styles.less 
    - service.ts 
- /pageC
    - index.ts 
    - styles.less 
    - service.ts 
- /service
    - global.service.ts
- /model
    - global.model.ts
- /types
    - global.d.ts

5. 总结

文章详细介绍了useRequest hook 以及 umi-request 源码以及核心流程,以及怎样很好的整合在日常的小程序以及后台的业务逻辑中,

猜你喜欢

转载自juejin.im/post/7114952512485982239