umi-request & useRequest 原始碼分析及業務實踐

語言: CN / TW / HK

theme: github highlight: monokai


導讀

文章通過業務點分析以及深入umi-request,useResut的原始碼,來探索 umi-request 賦能小程式請求,以及 useRequest 賦能後臺業務.

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

以下程式碼均為虛擬碼,描述整體的脈絡。

```ts 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

typescript 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 鏈式呼叫 js return new Promise((resolve, reject) => { requestInterceptors(obj) .then(() => middlewaresChain(obj)) .then(() => resolve(obj.res)) .catch((err) => { try { resolve(errorHandler(err)) } catch (e) { reject(e) } }) })
  2. add request/response interceptor js useInterceptor(handler){ this.interceptors.push(handler) }
  3. run interceptor ```js requestInterceptors(ctx) { const reducer = (prevInterceptor, interceptor) => prevInterceptor.then(() => p2(ctx.url, ctx.options))

return this.requestInterceptors.reduce(reducer, Promise.resolve()) }

4. add middlewaresjs useMiddleware(newMiddleware, opts) { if (opts.global) { globalMiddlewares.unshift(newMiddleware) } else if (opts.core) { coreMiddlewares.unshift(newMiddleware) } else { middlewares.push(newMiddleware) } } 5. run middlewarejs middlewaresChain(params){ const fn= koaCompose([ ...middlewares, ...globalMiddlewares, ...coreMiddlewares ]) return fn(params) }
6. response interceptorjs // 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

以下是一個介面 js // page.ts useState(defaultData) useState(requestLoading) function request() { setLoading() makeRequest() setData() handlerError() }

js // 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?或手動執行:初始化自動執行 ```js // page.tsx import service from '@/service' import { useRequest } from 'ahooks'

const { data, run: request, loading, error } = useRequest(service.serviceA, options) js // 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 區分開做對應的初始化。 js // 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'), } js // 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 ```js // request.ts const methodNames = ['get', 'post', 'delete'];

methodNames.forEach((name) => { request[name] = (url, options = {}) => { // 從 options 中取 request options const requestCore = (data) => requestname; // 從 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 外掛聚合在一起

```js // 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 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({}),[]) 這個方法

js 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 js // fetch.ts setState(s){ this.state={ ...this.state, ...s, } update() }
  2. run lifecycle js runPluginHandler(lifeCycleName, ...params) { const r = this.pluginImpls .map((plugin) => plugin[lifeCycleName](params)) .filter(Boolean) return Object.assign({}, ...r) }
  3. runAsync core method, options.lifecycle 為自定義 lifecycle,this.runPluginHandler(lifecycleName)hookPlugin 內建 lifecycle

image.png

```js 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. 目錄梳理

梳理前 dir - /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 梳理後 dir - /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 原始碼以及核心流程,以及怎樣很好的整合在日常的小程式以及後臺的業務邏輯中,