umi-request & useRequest 原始碼分析及業務實踐
theme: github highlight: monokai
導讀
文章通過業務點分析以及深入umi-request,useResut的原始碼,來探索 umi-request
賦能小程式請求,以及 useRequest
賦能後臺業務.
1. 業務梳理
1.1 常見問題
- page目錄、service目錄、model目錄、typescript目錄,utils 目錄,所有不同型別的業務邏輯都被拆分在不同的目錄內,且還存在相互引用,以及巢狀層級很深,在開發一個頁面的時候難以聚焦。
- 被頻繁引用的模組中
if else
使用很多,且存在巢狀,如果需要加入新的邏輯,由於邏輯不清晰,在引入新功能時容易影響老的邏輯,由於被各個頁面頻繁引用,波及範圍較大,容易出現線上問題。 - 在涉及到後臺的定義介面以及使用的過程中,每增加一個介面,都會涉及到對應介面的 loading狀態,data狀態,error狀態,請求前,請求後的處理,當一個業務點涉及的非同步邏輯很多的時候,整個業務邏輯會變得很臃腫。
1.2 解決方案
- 每一個 page 的不同型別的業務邏輯都放在一個目錄內
- 引入umi-request 來包裝 uni-request
- 引入 useRequest hook,在定義介面檔案的時候,同時預例項化對應 service 的hook,便於開發。
- 將具體頁面的狀態管理交給 hook,全域性的狀態交給 model。
2. 將 umi-request
整合進小程式,告別 if else
基於uni-app 的 uni.request
,非 promise,只有 onSuccess,onFail 回撥,小程式不支援urlSearchParams, 手動替換 coreMiddleware
2.1 小程式請求的一般邏輯流程圖
以下程式碼均為虛擬碼,描述整體的脈絡。
```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 應用
- 支援 prefix
- 支援 request/response interceptor
- 支援 middleware
- 支援 error Handling
- 支援 timeout
- 支援 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 組合機制,中介軟體呼叫
2.3.1 extend
預先例項化部分 options
2.3.2 request core 核心邏輯
- 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) } }) })
- add request/response interceptor
js useInterceptor(handler){ this.interceptors.push(handler) }
- 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 middlewares
js
useMiddleware(newMiddleware, opts) {
if (opts.global) {
globalMiddlewares.unshift(newMiddleware)
} else if (opts.core) {
coreMiddlewares.unshift(newMiddleware)
} else {
middlewares.push(newMiddleware)
}
}
5. run middleware
js
middlewaresChain(params){
const fn= koaCompose([
...middlewares,
...globalMiddlewares,
...coreMiddlewares
])
return fn(params)
}
6. response interceptor
js
// 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
- 定義 model 裡的 data 和 Effect
- connect 元件
- 處理同步非同步,非函式式,概念很多,痛苦面具
- 粒度較大,難複用
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
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
- change state
js // fetch.ts setState(s){ this.state={ ...this.state, ...s, } update() }
- run lifecycle
js runPluginHandler(lifeCycleName, ...params) { const r = this.pluginImpls .map((plugin) => plugin[lifeCycleName](params)) .filter(Boolean) return Object.assign({}, ...r) }
- runAsync core method,
options.lifecycle
為自定義 lifecycle,this.runPluginHandler(lifecycleName)
為hookPlugin
內建 lifecycle
```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
原始碼以及核心流程,以及怎樣很好的整合在日常的小程式以及後臺的業務邏輯中,