请求封装
目前有一年多没有使用 uni-app
了,现在因为业务需求,重新拾起。
后面我会出一系列关于 uni-app
文章
下面讲的请求封装是借鉴 axios
最基本的
本人喜欢代码加注释讲解,下面就开始今天的主题
// 默认配置
const instanceConfig = {
baseURL: '',
header: {
'content-type': 'application/json'
},
method: 'GET',
dataType: 'json',
responseType: 'text'
}
// 拦截器
class InterceptorManager {
handlers = []
use (fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
})
return this.handlers.length - 1
}
forEach (fn) {
this.handlers.forEach(fn)
}
}
class Request {
/**
* 默认配置
*/
defaults = instanceConfig
/**
* 请求拦截器
*/
interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
}
constructor (config) {
this.defaults = merge(this.defaults, config)
}
}
基本构建就是上面的样子
下面就抽取一些工具作为辅助(为了合并参数)
// 对应 axios 项目路径 lib\utils
function isPlainObject(val) {
if (toString.call(val) !== '[object Object]') {
return false
}
var prototype = Object.getPrototypeOf(val);
return prototype === null || prototype === Object.prototype;
}
function forEach(obj, fn) {
if (obj === null || typeof obj === 'undefined') {
return
}
if (typeof obj !== 'object') {
obj = [obj]
}
if (Array.isArray(obj)) {
for (var i = 0, l = obj.length; i < l; i++) {
fn.call(null, obj[i], i, obj)
}
} else {
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
fn.call(null, obj[key], key, obj)
}
}
}
}
function merge(...args) {
var result = {}
function assignValue(val, key) {
if (isPlainObject(result[key]) && isPlainObject(val)) {
result[key] = merge(result[key], val)
} else if (isPlainObject(val)) {
result[key] = merge({}, val)
} else if (Array.isArray(val)) {
result[key] = val.slice()
} else {
result[key] = val
}
}
for (var i = 0, l = args.length; i < l; i++) {
forEach(args[i], assignValue);
}
return result
}
我们都知道有个 baseURL
需要和 url
组合起来
// 组合 url
// 在 axios 是分了多个方法的,我这里为了简便,直接封成一个方法
// 对应 axios 项目路径 lib\core\buildFullPath
function buildFullPath (baseURL, relativeURL) {
// 判断是否绝对地址
if (/^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(relativeURL)) return relativeURL
// 不是绝对地址进行拼接
return relativeURL
? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '')
: baseURL;
}
下面还有一个概念 promise chain
如下面图所示,(百度找的,如有侵权,请联系删除)
代码如下所示
request (config) {
// 合并参数
config = merge(this.defaults, config)
// 至于这里为什么需要一个 undefined,是为了这条链
const chain = [dispatchRequest, undefined]
// 创建一个 promise 而且是成功状态的
let promise = Promise.resolve(config)
// 遍历请求拦截器
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected)
})
// 遍历响应拦截器
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected)
})
// 创建 promise 链
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift())
}
return promise
}
实现 dispatchRequest
方法
function dispatchRequest (config) {
// 组合完整的 url
const url = buildFullPath(config.baseURL, config.url)
return new Promise((resolve, reject) => {
config.success = function (res) {
res.config = config
resolve(res)
}
config.fail= function (err) {
reject(err)
}
uni.request({
...config,
url
})
})
}
到此就结束了,以下是完整代码
function isPlainObject(val) {
if (toString.call(val) !== '[object Object]') {
return false
}
var prototype = Object.getPrototypeOf(val);
return prototype === null || prototype === Object.prototype;
}
function forEach(obj, fn) {
if (obj === null || typeof obj === 'undefined') {
return
}
if (typeof obj !== 'object') {
obj = [obj]
}
if (Array.isArray(obj)) {
for (var i = 0, l = obj.length; i < l; i++) {
fn.call(null, obj[i], i, obj)
}
} else {
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
fn.call(null, obj[key], key, obj)
}
}
}
}
function merge(...args) {
var result = {}
function assignValue(val, key) {
if (isPlainObject(result[key]) && isPlainObject(val)) {
result[key] = merge(result[key], val)
} else if (isPlainObject(val)) {
result[key] = merge({}, val)
} else if (Array.isArray(val)) {
result[key] = val.slice()
} else {
result[key] = val
}
}
for (var i = 0, l = args.length; i < l; i++) {
forEach(args[i], assignValue);
}
return result
}
function buildFullPath (baseURL, relativeURL) {
if (/^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(relativeURL)) return relativeURL
return relativeURL
? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '')
: baseURL;
}
const instanceConfig = {
baseURL: '',
header: {
'content-type': 'application/json'
},
method: 'GET',
dataType: 'json',
responseType: 'text'
}
function dispatchRequest (config) {
const url = buildFullPath(config.baseURL, config.url)
return new Promise((resolve, reject) => {
config.success = function (res) {
res.config = config
resolve(res)
}
config.fail= function (err) {
reject(err)
}
uni.request({
...config,
url
})
})
}
class InterceptorManager {
handlers = []
use (fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
})
return this.handlers.length - 1
}
forEach (fn) {
this.handlers.forEach(fn)
}
}
export default class Request {
defaults = instanceConfig
interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
}
constructor (config) {
this.defaults = merge(this.defaults, config)
}
request (config) {
config = merge(this.defaults, config)
const chain = [dispatchRequest, undefined]
let promise = Promise.resolve(config)
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected)
})
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected)
})
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift())
}
return promise
}
}
使用方式
// 创建请求对象
const request = new Request({
baseURL: 'https://****'
})
// 请求拦截器
request.interceptors.request.use(config => {
config.header.Authorization = '可以添加凭证 token'
return config
})
// 响应拦截器
request.interceptors.response.use(response => {
return response
}, err => {
console.log(err.errMsg)
return Promise.reject(err.errMsg)
})
// 发送请求
request.request({
url: '/****',
data: { wd: 'uni-app' }
}).then(res => {
console.log(res)
}).cacth(err => {
console.log(err)
})
后面会持续更新(业务怎么处理,请求失败了怎么重试,无感刷新 token,请求失败了是否弹框或者提示)
上面的代码仅供参考,目前项目在起步状态,没有使用到其他处理方式,后面项目用到了,会加上对应的处理方式
业务处理
注意下面的处理,每个公司的状态码 code 码是不一样的
为什么要这样处理了,这样处理后有什么好处,先看下面代码
const codeMessage = {
400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
401: '用户没有权限(令牌、用户名、密码错误)。',
403: '用户得到授权,但是访问是被禁止的。',
404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
406: '请求的格式不可得。',
410: '请求的资源被永久删除,且不会再得到的。',
422: '当创建一个对象时,发生一个验证错误。',
500: '服务器发生错误,请检查服务器。',
502: '网关错误。',
503: '服务不可用,服务器暂时过载或维护。',
504: '网关超时。'
}
request.interceptors.response.use(response => {
// 业务处理
const { data, statusCode } = response
const { response: res, code, msg } = data
// 状态码不是 200 说明出错了
if (statusCode !== 200) {
// 及时抛出错误
// uni.showToast 自行处理 可以配置要不要弹框
return Promise.reject(codeMessage[statusCode] || '请求失败')
}
// 成功了只要数据其他都不要
if (code === 1) {
return res
}
// 处理 code 不等于 1 的情况
// uni.showToast 自行处理 可以配置要不要弹框
return Promise.reject(msg)
}, err => {
// uni.showToast 自行处理 可以配置要不要弹框
return Promise.reject(err.errMsg)
})
// 经过上面处理后
// 调用请求我们就可以只关注成功的逻辑,屏蔽掉一些不必要的判断和错误处理
request.request({
url: 'https://****',
}).then(res => {
// 只有走到这里就是成功的
// 不会在这里判断是否成功了
console.log(res)
}).catch(err => {
// 失败逻辑
// 可以统一在响应拦截器处理
console.log(err)
})
说明:
很多人问我为什么不添加以下方法
request#get
request#delete
request#head
request#options
request#post
request#put
request#patch
因为没有什么必要,上面的方法都是基于 request#request
方法的,而且提供的调用方式太多了会产生一定的负担
敬请期待(装饰器实现后端 CRUD 接口)
功能已经完善了,在后台项目已经稳定运行了一段时间
后续会讲源码实现
// 创建基本 Service
const BaseService = createBaseService('axios 请求实体')
// 和后端商议: 比如用户 user
// https://**/user/list
// https://**/user/detail
// https://**/user/create
// https://**/user/update
// https://**/user/delete
// 赋能(可以根据自行商议的结构定义)
class Service extends BaseService {
list = (params?: any) => {
return this.request({
url: 'list',
params
})
}
detail = (params: any) => {
return this.request({
url: 'detail',
params
})
}
create = (data: any) => {
return this.request({
method: 'post',
url: 'create',
data
})
}
update = (data: any) => {
return this.request({
method: 'post',
url: 'update',
data
})
}
delete = (data: any) => {
return this.request({
method: 'post',
url: 'delete',
data
})
}
}
// 使用方式
@Controller('/user')
class User extends Service {}
const user = new User()
// 经过上面的定义就可以一下子生成五个接口
// url: /user/list
user.list
// url: /user/create
user.create
// url: /user/detail
user.detail
// url: /user/update
user.update
// url: /user/delete
user.delete
// 要是要扩展怎么办,比如需要一个 user 选项
@Controller('/user')
class User extends Service {
@Get('options')
getOptions () {}
// 这个写法等同于上面,只是名称不一样
// @Get()
// options () {}
}
const user = new User()
// 这样就可以扩展了
// url: /user/options
user.getOptions
// 可能有人会问有一些公共的请求,并不需要 (CRUD),这时就需要一个纯粹的 Service
// 还记得一开始创建的 BaseService 吗,这个就是纯粹的
@Controller('/api')
class Common extends BaseService {
@Get('user/info')
getUserInfo () {}
@Post()
setUserInfo
}
const common = new Common()
// url: /api/user/info
common.getUserInfo
// url: /api/setUserInfo
common.setUserInfo