第一份工做的时候咱们老大让我封装下请求,我立即就说:封装什么?为何要封装,自己人家的库就已经进行封装了啊,只须要几个参数就能够调用了,封装的仍是要传一些参数的。嗯~当时仍是有点义正词严的,正所谓无知者无谓😂固然最后我仍是听老大的了,那时候我只是封装了几个默认参数吧🐶然后通过几年的历练,对api请求的封装也一直在升级,如今请陪着我来一块儿回顾下
很明显,回调容易陷入回调地狱,因此不管请求仍是其余场景咱们目前的编程方式基本都是推荐使用promise的,尤为是新的async/await的引入更是让promise的编程方式更加优雅编程
请求老是被resolve?为何?若是不是,会怎样?json
async function f() { try { await Promise.reject('出错了'); } catch(e) { ... } ... }
正如上面这段代码,若是咱们不加catch的话会怎样?该f函数后面的全部的代码都不会被执行,也就是说若是咱们要保证代码的健壮性则必须给async/await函数增try/catch容错
那咱们不用async了呗,确实是个不错的主意,但我必须提醒async的几点好处:小程序
if (check) { return true } else { return apiPromise() }
你是判断返回值的类型仍是resolve true?而async则比较完美的解决了这类问题!segmentfault
我更相信你已经在大量使用async,因此,若是使用了async/await那么try/catch千万别忘记哦 后端
即使是简单场景下不须要使用async,promise被拒绝也会有一些小问题,例如微信小程序
api().then(res=>{ this.hideLoading() ... }).catch(err=>{ this.hideLoading() ... })
不管是否被成功resolve,都要执行的一些代码须要在两处书写 api
因此,你想推介什么?
封装的api请求老是被resolve,这样是否是就不必关心reject了?也就不用管刚才那一堆问题了,是否是很爽?😊不对啊,老是有异常状况的啊,难道无论了?也resolve啊!😊添加字段区分就好了啊,是否是很聪明?😁promise
什么意思?咱们先回想下本身是否曾大量写过这样的代码,若是没有请忽略微信
api().then(res=>{ this.hideLoading() if (res.code !== 0) { ... } ... })
由于如今不少后端因监控运行状态等缘由都不直接返回异常的http状态码,而是以各类code来标示是否处理成功,因此200的请求不必定是真正的请求完成,那校验code就成为必须的了网络
api有点被乱用了,api请求的api是后台提供的业务服务接口,抛去这一种,咱们脑中正常的api是什么样子的?是否是像这样array.push(1)
,是预先定义的函数,是不须要关心内部实现的,因此请把api请求也封装成像真正的api那样,简单好用,随处可用
至此,我的关于对封装api请求的思想基本都阐述了,咱们来看看代码实现(基于小程序项目,供参考,核心代码用===============标示)
先来看看最终你用的爽不爽
// 例如后台文档是这样的 // curl --request POST \ // --url 'http://user-interaction.ylf.org/controller/fun' \ // --header 'Content-Type: application/json' \ // --data '{ // "page":1 // }' // 你只须要这样 api.controller.fun({page: 10}).then(res=>{ this.hideLoaing() if (res.errType) { ... // 异常处理 } ... // 正常逻辑 }) // async方式 async function() { const res = await api.controller.fun({page: 10}) this.hideLoaing() if (res.errType) { ... // 异常处理 } ... // 正常逻辑 }
目录结构
api ├── doRequest.js // 封装的请求方法 ├── index.js // 生成api和export请求方法等 ├── inject.js // 拦截器 ├── renewToken.js // 从新获取token └── serviceJson.js // 供生成API的配置
import _ from '../lib/tools' import injectMap from './inject' import {api as constApi} from '../constVar' import renewToken from './renewToken' const apiDomain = constApi.domain let getTokenPromise = ''// 只能同时存在一个实例 let wxSessionValid = null // 微信session_key的有效性 const checkWxSession = function () { return new Promise(resolve => { wx.checkSession({ success() { resolve(true) // session_key 未过时,而且在本生命周期一直有效 }, fail() { resolve(false) // session_key 已经失效,须要从新执行登陆流程 } }) }) } // 检查业务层是否也处理成功,参数为请求的返回值 const defaultCheckDoRequestSuccess = (res) => !res.data.error_code export async function doRequestWithCheckSession(data = {}, opts) { const opt = Object.assign({needToken: true}, opts) if (typeof opt.needToken === 'function') { // 是否须要鉴权有必定逻辑性,则能够将needToken配置设置为返回布尔值的函数,无参 opt.needToken = opt.needToken() } if (typeof wxSessionValid !== 'boolean') { wxSessionValid = await checkWxSession() // 检查微信session是否有效 } let jwt = wx.getStorageSync('jwt') // 鉴权方式:业务侧的鉴权和对微信session有效性的鉴权 if (opt.needToken && (!jwt || jwt.expire_after <= +new Date() || !wxSessionValid)) { // 须要受权,已过时,去续租 let jwt = '' if (getTokenPromise) { jwt = await getTokenPromise } else { getTokenPromise = renewToken() jwt = await getTokenPromise } wxSessionValid = true getTokenPromise = '' wx.setStorageSync('jwt', jwt) } Object.assign(opt, opt.needToken ? {httpOpt: {header: {Authorization: jwt.token}}} : {}) return doRequest(opt.url, data, opt) } ============================================================================================ /** * 请求接口函数 * @param url * @param data 请求body * @param opt 具体配置见该函数的参数 * @returns {Promise<any>} * * !!! 老是被解决,永远不会被拒绝,不过你能够经过判断是否有errType值来判断是否请求OK * errType === 'http' 是请求出错 * errType === 'server' 是服务端处理出错,须要checkDoRequestSuccess函数提供判断逻辑 */ export function doRequest(url, data, { method = 'get', httpOpt = {}, needToken = true, needToast = true, checkDoRequestSuccess = defaultCheckDoRequestSuccess } = {}) { return new Promise((resolve) => { wx.request({ url, data, method, ...httpOpt, success: (res) => { // 请求成功 if (checkDoRequestSuccess(res)) { // 服务端也处理成功 injectMap.forEach((val, key) => { // 匹配拦截规则 if (key.indexOf(url.replace(apiDomain, '')) !== -1) { val() } }) resolve(res) } else { // 服务端处理失败 needToast && wx.showToast({ title: res.data.reason || '请求出错,请稍后重试', icon: 'none', duration: 2000 }) resolve(Object.assign({ errType: 'server' }, res)) } }, fail: (err) => { // 请求失败 resolve({ errType: 'http', err }) checkNetWorkAndSaveCurrentPath() } }) }) } ============================================================================================ // 检查网络问题和记录当前页面的路径 function checkNetWorkAndSaveCurrentPath() { /* eslint-disable no-undef */ const pages = getCurrentPages() // 获取当前的页面栈 const page = pages[pages.length - 1] // 当前的页面 // 避免多个请求失败形成多个弱网页面栈,影响回跳 if (['pages/normal/network', 'pages/normal/load'].indexOf(page.route) !== -1) { return } wx.getNetworkType({ success: function (res) { const pathParamsStrArr = [] // 记录当前页面的路径参数 _.forOwn(page.options, (v, k) => { pathParamsStrArr.push(`${k}=${v}`) }) const path = `${page.route}?${pathParamsStrArr.join('&')}` wx.setStorageSync('badNetPagePath', path) // 记录被弱网中断的页面完整路径 if (res.networkType === 'none') { // 若是是没有网络环境 wx.redirectTo({ url: '/pages/normal/network' }) } else { // 弱网环境和其余异常状况 wx.redirectTo({ url: '/pages/normal/load' }) } } }) }
import serviceJson from './serviceJson' import { doRequestWithCheckSession, doRequest } from './doRequest' import _ from '../lib/tools' import {api as constApi} from '../constVar' const apiDomain = constApi.domain const api = {} serviceJson.forEach(obj => { const keys = obj.url.replace(/\//g, '.') obj.url = apiDomain + obj.url _.set(api, keys, function (data) { return doRequestWithCheckSession(data, obj) }) }) /** * 调用示例 * api.controller.fun({page: 10}) * * 同时暴露出两个封装好的请求方法 */ export default { ...api, doRequest, doRequestWithCheckSession }
/** * 项目请求配置 * * 参数请前往 ./doRequest.js 查看doRequest函数说明,一下参数可能会出现变更而致使不许确 * needToken=true 是否须要token认证 * method=get 请求方法 * dataType=json dataType * check 函数,参数为请求的返回值,要求返回布尔值,true表明请求成功(后台处理成功),false反之 */ export default [ {'url': 'joke/content/list'} ]
// 请求hooks,当请求被匹配则执行预设的回调 // map的key为 ./serviceJson.js 配置里的url,value为callback // import _ from '../lib/tools' const map = new Map() export default map
import {doRequest} from './doRequest' import _ from '../lib/tools' import {api as constApi} from '../constVar' const apiDomain = constApi.domain function navToLogin(resolve) { /* eslint-disable no-undef */ const pages = getCurrentPages() const page = pages[pages.length - 1] page.openLoginModal(resolve) } export default async function renewToken() { // 确保有用户信息 // 虽然只要有code便可换取用户id,但一般咱们都须要 await new Promise(resolve => { wx.getSetting({ success: (res) => { // 若是用户没有受权或者没有必要的用户信息 if (!res.authSetting['scope.userInfo'] || !_.isRealTrue(wx.getStorageSync('userInfoRes').userInfo)) { wx.hideLoading() navToLogin(resolve) } else { resolve() } } }) }) return new Promise((resolve) => { wx.login({ success: res => { login(res.code).then((jwt) => { resolve(jwt) // resolve jwt }) // 经过code进行登陆 }, fail(err) { wx.showToast({ title: err.errMsg, icon: 'none', duration: 2000 }) } }) }) } /** * 登录,获取jwt * @param code * @returns {Promise<any>} */ function login(code) { return new Promise((resolve) => { // 模拟登陆换取业务端的用户信息和登陆信息,仅测试 doRequest(apiDomain + 'test/getToken', {code}, { needToast: false }).then(res => { if (res.errType) { // resolve('loginerr') resolve({ 'token': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9', 'expire_after': +new Date() + 1000 * 360 * 24 }) return } resolve({ 'token': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9', 'expire_after': +new Date() + 1000 * 360 * 24 }) }) }) }
虽然是对API的思考,不只限小程序,但做为同期的思考和总结,来波系列连接😄
开发微信小程序必需要知道的事
微信小程序之登陆态的探索
欢迎交流指正,谢谢