第一份工做的時候咱們老大讓我封裝下請求,我立即就說:封裝什麼?爲何要封裝,自己人家的庫就已經進行封裝了啊,只須要幾個參數就能夠調用了,封裝的仍是要傳一些參數的。嗯~當時仍是有點義正詞嚴的,正所謂無知者無謂😂固然最後我仍是聽老大的了,那時候我只是封裝了幾個默認參數吧🐶然後通過幾年的歷練,對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的思考,不只限小程序,但做爲同期的思考和總結,來波系列連接😄
開發微信小程序必需要知道的事
微信小程序之登陸態的探索
歡迎交流指正,謝謝