taro 如何使用dom_基于Taro封装适用于复杂业务场景的Request工具
背景
网络请求是前端频繁操作的工作,为了方便开发人员快速开发业务,不用关注网络请求中的业务,同时兼容在Taro在各个不同端的差异,我们对请求类进行了封装。
需求
-
请求数据加密及返回数据的解密
-
网络性能埋点
-
请求异常处理
-
版本号上报
-
token令牌获取
等等....
你要想看这些需求的实现,不好意思,没有,这里只教你怎么灵活地给你的请求灵活快速地增加统一的业务处理功能。我只想说需求瞬息万变,虽不能以不变应万变, 但我们可以以最小的改动来实现需求的变化
解决方案
看到请求,我脑海中第一个想到的是koa的洋葱圈模型. 利用该模型的设计思路,我们可以对请求之前对请求及请求结果"层层处理".
这里简单说明下洋葱圈模型, 请看下图,这是一个洋葱横截面图, 众所周知,洋葱内部是一层层的结构, 关注箭头部分, 要想穿过洋葱核心,从左边最外层进入洋葱,层层穿透,进入核心, 最后层层穿透走出洋葱。每个洋葱圈都是一个中间件,由中间件进行单一功能处理。所以每次发起一个请求时,每个中间件都会被执行两次。如果业务发生变化,我们只需修改对应的中间件的功能,或者在中间件队列中插入一个新的中间件即可。该模型已经被广泛地运用,想深入了解请自行查阅相关资料。
那么有人会问, Taro已经支持并提供了Taro.addInterceptor(callback)是不是可以直接用? 没错, Taro提供了API addInterceptor,并遵循洋葱圈模型设计。但是我们的项目设计了可拔插式架构 , 每一个业务模块都可能独立打包作为一个端(小程序/H5/RN/快应用)来运行,复杂的业务就可能需要增加自己业务特有的中间件,如果使用Taro.addInterceptor, 那么该中间件(在Taro中称为拦截器)会污染全局的请求流程。
假设业务A要使用 1,2 的中间件, 业务B要使用1,3的中间件, 使用Taro.addInterceptor将导致生成1,2,1,3(也可能是1,3,1,2, 根据不同分包加载次序,插入的中间件次序也不同) 顺序的中间件. 这就完全出乎我们的预期.
所以我们独立设计了请求类以及中间件,让各个业务模块独立生成一个请求实例,自行加入自己业务所需的中间件.如果你的项目中的业务简单,请求前后的流程统一,那么还是推荐你使用Taro.addInterceptor.
代码实现
我们一步步来,先创建一个Request类,增加一个基本的方法,可以发起一个最简单的请求
class Request {
basicRequest(arg) {
return Taro.request(arg)
}
}
const request = new Request()
request.basicRequest({...})
然后给Request拓展HTTP 请求方法, 在构造函数中给实例动态添加POST,GET等方法. 这样使用调用方法来省去了在参数中传入HTTP method.
class Request {
static httpAllowedMethods = ['POST', 'GET', 'PUT', 'DELETE', 'OPTIONS', 'HEAD']
constructor() {
Request.httpAllowedMethods.forEach((method) => {
this[method.toLowerCase()] = (arg) => this.basicRequest({ ...arg, method })
})
}
basicRequest(arg) {
return Taro.request(arg)
}
}
const request = new Request()
request.post({...})
request.get({...})
接下来我们增加中间件
function compose(...funcs) {
if (funcs.length === 0) {
return (arg) => arg;
}
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce((a, b) => (...args) => a(b(...args)));
}
class Request {
applyMiddleware = (...middlewares) => {
//将中间件对'核心'方法进行包裹,虽然`this.basicRequest`被赋值了,但是初始的`basicRequest`原方法还是保存在内存中。
this.basicRequest = compose(...middlewares)(this.basicRequest)
}
}
// 中间件, 输出log, next函数为下一个中间件, params是上一个中间件的传参,或者是request.basicRequest({...})的入参
const logMiddleware = (next) => async (params) => {
console.log(`发起请求 ${params.url}`);
console.log('请求入参', params.data);
let r = await next(params);
console.log(`请求成功 ${params.url}`);
console.log('请求回参', r);
return r;
};
// 实例化
const request = new Request()
// 应用中间件
request.applyMiddleware(logMiddleware)
// 那么使用该request实例发起任何请求可以在控制台输出请求参数与返回值,
// 这里参数还是跟Taro.request文档中的一致
request.basicRequest({...})
这段代码的核心在compose函数,他的功能是返回函数集 functions 组合后的复合函数, 也就是一个函数执行完之后把返回的结果再作为参数给下一个函数来执行.
function compose(...funcs) {
if (funcs.length === 0) {
// 中间件长度为0时,因为compose需要返回一个函数并执行
// 所以直接返回一个函数,执行后直接返回入参,
// 结合之前的代码 相当于 this.basicRequest = this.basicRequest
return (arg) => arg;
}
if (funcs.length === 1) {
// 当中间件长度为1时, 直接返回中间件, 结合之前的代码
// logMiddleware中的next参数就是this.basicRequest
return funcs[0];
}
// 有多个中间情况逻辑比较复杂,我们看下面一端代码
return funcs.reduce((a, b) => (...args) => a(b(...args)));
}
const func1 = () => { /**/ }
const func2 = () => { /**/ }
const func3 = () => { /**/ }
const chain = [func1, func2, func3]
const composedFun = compose(...chain)
// 最后composedFun是这个样子
// composedFun = (args) => func1(func2(func3(args)))
const centerFun = () => {/**/}
composedFun(centerFun)()
// 执行顺序是 func1(next函数执行之前部分) => func2(同上) => func3(同上) => centerFun => func3(next函数执行之后部分) => func2(同上) => func1(同上)
根据这段代码可以很直观地观察到,compose实现了一个函数执行完之后把返回的结果再作为参数给下一个函数来执行的功能
至此我们已经完成了请求中间件,可以灵活地给发起请求这流程增加统一的处理。这时候有人就会问,如果执行请求不需要执行某个指定的中间件怎么办?
虽然我们不能不执行某个中间件函数,但我们可以控制不去执行中间件内部的逻辑, 之前我们一直没有改动request.basicRequest的参数格式,他的签名与Taro.request是一致的,每个中间都可以读取到这个参数, 所以我们只需要对这个参数稍加修改即可实现
request.basicRequest({
middleOpts: {
noLog: true
}
...
})
我们给参数增加了 middleOpts 属性,由他来控制中间件的使用规则,所以我们在log中间件稍加修改即可实现通过参数控制是否需要执行中间件内部的逻辑
const logMiddleware = (next) => async (params) => {
// 增加了这行代码, 跳过后面的业务流程,直接进入下一个中间件
if(params.middleOpts.noLog){
return next(params)
}
console.log(`发起请求 ${params.url}`);
console.log('请求入参', params.data);
let r = await next(params);
console.log(`请求成功 ${params.url}`);
console.log('请求回参', r);
return r;
};
你以为这样就结束了? 图样图森破, request.basicRequest这么多参数每次都写一个url不嫌麻烦么.
所以我们将基本不会变的参数提取出来单独做了配置文件,给Request再增加一个方法extend
给request对象中动态增加了一些方法,方便业务开发的同学直接使用.先看看最终是如何使用的
const request = new Request()
request.extend(requestConfig)
// 业务中使用
request.user.login({
userName: '法外狂徒张三'
})
我们先看看配置文件 requestConfig.js
import config from '@src/constants/config'
// 将请求地址HOST提取出来放在了config,方便在不同的(测试,预发布,生产)环境切换.
// 根据业务模块划分,例子中有user和order模块
const user = {
// 示例 1
login: {
url: `${config.HOST}/login`,
method: 'put', // method 默认 post,
middleWareOpts: { // 中间件参数
defaultParams: {}, // 默认请求参数,如果在使用时传入params,最后请求的 params 为 Object.assign({},defaultParams,params)
defaultHeader: {}, // 默认添加的头部,
noEncrypt: false, // 跳过加密,默认false
noAuth: false, // 跳过token验证,默认false
},
},
// 示例 2
logout: `${config.HOST}/logout`,
}
const order = {
// ...
}
export default { user, order }
我们继续看看extend方法,为了阅读方便,已经删除了数据格式的校验. 逻辑比较简单, 只增加了一些注释
const defaultRequestOpts = {
// ...
}
class Request {
extend = (requestConfig) => {
Object.entries(requestConfig).forEach(([moduleKey, urlConfig]: [string, Object]) => {
const moduleRequest = {}
Object.keys(urlConfig).forEach(key => {
if (typeof urlConfig[key] === 'string') {
// 属性是字符串, 默认为url地址, 创建一个可以发起post请求的匿名函数
// 如上面的例子user.logout
moduleRequest[key] = (reqData, otherRequestOpts) => this.post({
url: urlConfig[key],
data: reqData,
middleWareOpts: defaultMiddleWareOpts,
...defaultRequestOpts,
...otherRequestOpts,
})
} else if (typeof urlConfig[key] === 'object') {
// 属性是对象的话, 根据配置的method(可不声明,默认是post)创建可以发起该method请求的匿名函数
// 与单纯的字符串配置不同的是,可以配置middleWareOpts中间件参数
// 如上面例子中user.login
const { url: httpUrl, method: httpMethod = 'post', middleWareOpts = {} } = urlConfig[key]
moduleRequest[key] = (reqData, otherRequestOpts) => this[httpMethod]({
url: httpUrl,
data: reqData,
middleWareOpts: Object.assign({},defaultMiddleWareOpts,middleWareOpts),
...defaultRequestOpts,
...otherRequestOpts,
})
}
})
// 给request实例动态添加业务请求方法模块
this[moduleKey] = moduleRequest
})
}
}
经过extend函数处理之后,我们得到了以下request对象
request = {
get(){...}
post(){...}
put(){...}
// 其他http method 方法
// ...
basicRequest(){...}
extend(){...}
applyMiddleware(){...}
user: {
login(){...}
logOut(){...}
}
order: {...}
}
那么业务开发的同学在配置好requestConfig.js文件后,就可以在业务中愉快地开发了。
好了,至此,我们为业务封装好的Request已经完成了,再啰嗦一下, 看看完整使用例子,我们列出了我们项目中常用的中间件
// src/utils/request.js
import requestConfig from '@src/constants/requestConfig' // 还是上面例子中的requestConfig
import Request from '@crgt/request' // 已经封装好的Requset,发布到了公司内部的npm上了
import { /*各类中间件*/} from '@crgt/request/middlewares'
const request = new Request()
request.applyMiddleware(
errorHandleMiddleware, // 异常处理中间件
headerMiddleware, // header处理中间件,添加版本号,加入requestConfig.js配置文件中的header等
bodyMiddleware, // 请求参数data处理中间件,使用requestConfig.js配置文件中的默认请求参数等处理.
authMiddleware, // 身份认证中间件,未登录状态不发起请求,或者是跳转登录页面
encryptionMiddleware, // 加密中间件,请求加密,返回值解密
responseMiddleware, // 请求返回值处理中间件
)
request.extend(requestConfig)
export default request
// 业务代码文件
import request from '@src/utils/requset'
const response = await request.user.login({
userName:'法外狂徒张三'
},{
header: {
test: 'test'
},
timeout: 5000,
retryTimes: 2,
// 其他Taro.request参数,注意不需要再增加url与method,因为已经在requestConfig.js配置了
// ...
})
好了,你学fei了吗
Taro 跨端开发快应用项目-全局埋点解决方案
Taro 跨端开发快应用项目-深入解析生命周期
小程序跨端业务模块可插拔式架构探索
基于Taro开发的快应用体积优化方案
解除NPM封印,实现弯道超车 - React Native 复杂业务模块加载方案
Taro跨端开发之让Taro UI支持React Native
Taro跨端开发之多业务模块管理 React Native篇(终篇)
Taro跨端开发之多业务模块管理 React Native篇(上)
多端,多业务,多团队的我们如何管理依赖
Taro跨端开发之跨端开发新时代的思考与举措
更多推荐



所有评论(0)