导读
文章通过业务点分析以及深入umi-request
,useResut
的源码,来探索怎么使用这两个工具将业务整理的更清晰。
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 小程序请求的一般逻辑流程图
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
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 链式调用
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
useInterceptor(handler){
this.interceptors.push(handler)
}
- run interceptor
requestInterceptors(ctx) {
const reducer = (prevInterceptor, interceptor) =>
prevInterceptor.then(() => p2(ctx.url, ctx.options))
return this.requestInterceptors.reduce(reducer, Promise.resolve())
}
- add middlewares
useMiddleware(newMiddleware, opts) {
if (opts.global) {
globalMiddlewares.unshift(newMiddleware)
} else if (opts.core) {
coreMiddlewares.unshift(newMiddleware)
} else {
middlewares.push(newMiddleware)
}
}
- run middleware
middlewaresChain(params){
const fn= koaCompose([
...middlewares,
...globalMiddlewares,
...coreMiddlewares
])
return fn(params)
}
- 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
- 定义 model 里的 data 和 Effect
- connect 组件
- 处理同步异步,非函数式,概念很多,痛苦面具
- 粒度较大,难复用
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?或手动执行:初始化自动执行
// 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
流程以及存在的问题
- 定义 service 文件
- 引入到具体的 page
- 引入 useRequest
- 如果不同的 page 引用了相同的接口,需要重复引入 且定义useRequest
- 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
- change state
// fetch.ts
setState(s){
this.state={
...this.state,
...s,
}
update()
}
- run lifecycle
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
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
源码以及核心流程,以及怎样很好的整合在日常的小程序以及后台的业务逻辑中,