小程序在內(nèi)測(cè)的時(shí)候就已經(jīng)開始玩了,不過最開始的時(shí)候覺得,這sx東西東西怎么這么坑的樣子,網(wǎng)絡(luò)請(qǐng)求居然不是返回Promise而是用Callback的方式, 傳值居然不能把值寫在方法里只能用dataset,在這個(gè)全面組件化的大環(huán)境下居然不支持組件化...
其實(shí)最開始主要是書寫時(shí)習(xí)慣的問題,秉承著我又不做小程序開發(fā),就先忍著你的態(tài)度放任不管了。然而天有不測(cè)風(fēng)云,最近因?yàn)闃I(yè)務(wù)的需求不得不做小程序相關(guān)的開發(fā),我就倔脾氣果斷就不能忍了。果斷的把不爽的地方改成能按我喜歡的方式來走,其中還遇到了一些其他的坑,一個(gè)個(gè)慢慢填,并把這些記錄下來整理成了文章。
網(wǎng)絡(luò)請(qǐng)求
網(wǎng)絡(luò)請(qǐng)求小程序提供了wx.request,這個(gè)是我最想吐槽的點(diǎn), 仔細(xì)看一下 api,這貨不就是n年前的 $.ajax 嗎,好古老啊。
// 官方例子
wx.request({
url: 'test.php', //僅為示例,并非真實(shí)的接口地址
data: {
x: '' ,
y: ''
},
header: {
'content-type': 'application/json' // 默認(rèn)值
},
success: function(res) {
console.log(res.data)
}
})
現(xiàn)在還是只有一個(gè)請(qǐng)求就已經(jīng)感覺寫的很長(zhǎng)了,如果一個(gè)頁面需要多個(gè)請(qǐng)求呢,如果請(qǐng)求的順序還有要求呢該怎么辦,各種嵌套又臭又長(zhǎng),如果要求所以請(qǐng)求都完成之后再顯示界面呢 瞬間懵逼。
這個(gè)時(shí)候我弱弱的看了一眼小程序的JS版本的支持,歐耶,比較良心的支持ES6。也就是說我們可以將其改成Promise是可能的。接下來看我如何改造。
/* utils/api.js 自定義網(wǎng)絡(luò)請(qǐng)求 */
const baseURL = 'https://yourapi.com' // 自己后臺(tái)API地址
const http = ({ url = '', params = {}, ...other} = {}) => {
wx.showLoading({
title: '加載中...'
})
let time = Date.now()
console.log(`開始:${time}`)
return new Promise((resolve, reject) => {
wx.request({
url: getUrl(url),
data: params,
header: getHeader(),
...other,
complete: (res) => {
wx.hideLoading()
console.log(`耗時(shí):${Date.now() - time}`)
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.data)
} else {
reject(res)
}
}
})
})
}
const getUrl = url => {
if (url.indexOf('://') == -1) {
url = baseURL + url
}
return url
}
const getHeader = () => {
try {
var token = wx.getStorageSync('token')
if (token) {
return { 'token': token }
}
return {}
} catch (e) {
return {}
}
}
module.exports = {
baseURL,
get (url, params = {}) {
return http({
url,
params
})
},
post(url, params = {}) {
return http({
url,
params,
method: 'post'
})
},
put(url, params = {}) {
return http({
url,
params,
method: 'put'
})
},
// 這里不能使用 delete, delete為關(guān)鍵字段
myDelete(url, params = {}) {
return http({
url,
params,
method: 'delete'
})
}
}
接了下來我們就可以正常的時(shí)候用了,寫一下簡(jiǎn)單的例子吧
const api = require('../../utils/api.js')
// 單個(gè)請(qǐng)求
api.get('list').then(res => {
console.log(res)
}).catch(e => {
console.log(e)
})
// 一個(gè)頁面多個(gè)請(qǐng)求
Promise.all([
api.get('list'),
api.get(`detail/${id}`)
]).then(result => {
console.log(result)
}).catch(e => {
console.log(e)
})
如果習(xí)慣了 fetch 以及 axios 的朋友應(yīng)該都會(huì)比較喜歡這種寫法。 在做網(wǎng)絡(luò)請(qǐng)求的時(shí)候還遇到一個(gè)問題,登錄授權(quán)的問題。
登錄問題
做一個(gè)應(yīng)用,肯定避免不了登錄操作。用戶的個(gè)人信息啊,相關(guān)的收藏列表等功能都需要用戶登錄之后才能操作。一般我們使用token做標(biāo)識(shí)。然后又會(huì)涉及到token不存在,用戶第一次登錄,token過期后,重新登錄等問題。比較常規(guī)的操作是直接跳轉(zhuǎn)到登錄頁面。
然后坑就出現(xiàn)了,小程序并沒有登錄界面,使用的是 wx.login 。 wx.login 會(huì)獲取到一個(gè) code,拿著該 code 去請(qǐng)求我們的后臺(tái)會(huì)最后返回一個(gè)token到小程序這邊,保存這個(gè)值為 token 每次請(qǐng)求的時(shí)候帶上這個(gè)值。(詳情可以查看小程序登錄。)
然而僅僅這樣就夠了嗎? 很顯然并不是。一般還需要把用戶的信息帶上比如用戶微信昵稱,微信頭像等,這時(shí)候就需要使用 wx.getUserInfo ,這里涉及到一個(gè)用戶授權(quán)的問題,留一個(gè)坑接下來再解決。帶上用戶信息就夠了嘛? too young too simple!我們的項(xiàng)目不可能只有小程序,相應(yīng)的微信公眾平臺(tái)可能還有相應(yīng)的App,我們需要把賬號(hào)系統(tǒng)打通,讓用戶在我們的項(xiàng)目中的賬戶是同一個(gè)。這就需要用到微信開放平臺(tái)提供的 UnionID 。
ps.基于小程序在微信中的易傳播性, 為了鼓勵(lì)用戶去傳播分享一般還會(huì)提供邀請(qǐng)獎(jiǎng)勵(lì)機(jī)制。但是微信這邊又會(huì)對(duì)誘導(dǎo)分享進(jìn)行和諧處理。視情況慎用。(本文會(huì)在例子上加上該功能)
看到這,是不是覺得頭都大了,就一個(gè)小小的登錄功能坑這么多。 年輕的我瑟瑟發(fā)抖~~~。慢慢開始填吧。先上登錄代碼
/* utils/api.js 自定義網(wǎng)絡(luò)請(qǐng)求 */
...
function login() {
return new Promise((resolve, reject) => {
// 先調(diào)用 wx.login 獲取到 code
wx.login({
success: res => {
// 再調(diào)用 wx.getUserInfo 獲取到用戶的一些信息 (一些基本信息,以及生成UnionID 所用到的信息 比如 rawData, signature, encryptedData, iv)
wx.getUserInfo({
// 若獲取不到用戶信息 (最大可能是用戶授權(quán)不允許,也有可能是網(wǎng)絡(luò)請(qǐng)求失敗,但該情況很少)
fail: (e) => {
reject(e)
},
success: ({ rawData, signature, encryptedData, iv }) => {
let param = {
code: res.code,
rawData,
signature,
encryptedData,
iv
}
// 若有邀請(qǐng)ID
try {
let invite = wx.getStorageSync('invite')
if (invite) {
param.invite = invite
}
} catch (e) {
}
// 登錄操作
http({
url: 'login',
params: param,
method: 'post'
}).then(res => {
// 該為我們后端的邏輯 若code > 0為登錄成功,其他情況皆為異常 (視自身情況而定)
if (res.code > 0) {
// 保存用戶信息
wx.setStorage({
key: 'userinfo',
data: res.data
})
wx.setStorage({
key: "token",
data: res.message,
success: () => {
resolve(res)
}
})
} else {
reject(res)
}
}).catch(error => reject(error))
}
})
}
})
})
}
...
授權(quán)問題
根據(jù)上面的代碼,可以很清楚的看到,若用戶在登錄的時(shí)候不允許小程序獲取他的用戶信息之后才能繼續(xù)。若用戶在這個(gè)時(shí)候點(diǎn)拒絕了呢, 會(huì)怎么樣? 一片空白!~~ What’s the fuck! 怎么什么都沒有!垃圾破小程序~~ 冷靜點(diǎn)的用戶也會(huì)百臉懵逼狀。我是誰?我該怎么辦? 也許你會(huì)覺得,用戶點(diǎn)允許就好了啊,怎么會(huì)這么笨,這種用戶肯定不會(huì)多之類的話。我在我們小程序中加了統(tǒng)計(jì)大約有 20% 的用戶點(diǎn)了拒絕, 如果后續(xù)我們沒有做任何引導(dǎo)的話,這 20% 的用戶就會(huì)永遠(yuǎn)失去。這個(gè)后果我們完全不能接受。
經(jīng)過我們的小組研究與討論,給出了一下的一套方案。

具體代碼可以如下表示,用到了 wx.openSetting 來跳轉(zhuǎn)到設(shè)置授權(quán)界面。
/* index.js */
// 若有用戶信息存在則繼續(xù)
Page({
onLoad () {
wx.getStorage({
key: 'userinfo',
success: (res) => {
this.setUserinfo(res)
},
fail: (res) => {
api.login().then((res) => {
this.setUserinfo(res)
}).catch(e => {
if (e.errMsg && e.errMsg === 'getUserInfo:fail auth deny') {
this.setData({
isauth: false
})
}
})
}
})
},
toSetting() {
wx.openSetting({
success: (res) => {
this.setData({
isauth: res.authSetting['scope.userInfo']
})
if (res.authSetting['scope.userInfo']) {
api.login().then((res) => {
this.setUserinfo(res)
})
}
}
})
}
})
// setUserinfo 就是對(duì)用戶信息做一下處理 不具體展開了
/* index.wxml */
<view class="unauth" wx:if="{{!isauth}}">
<image class="unauth-img" src="../../images/auth.png"></image>
<text class="unauth-text">檢查到您沒打開授權(quán)</text>
<button class="color-button unauth-button" bindtap="toSetting">去設(shè)置</button>
</view>
<view class="container" wx:else>
...
</view>
token 失效問題
登錄獲取到的 token 是有時(shí)效的,失效過了會(huì)怎么樣 ? 如果后臺(tái)小伙伴嚴(yán)格按照 REST API 規(guī)范設(shè)計(jì)接口 API 的話,他會(huì)給我們返回一個(gè)錯(cuò)了 http code 為 401。(常見的Http Code以及相關(guān)代碼的意義本文就不做展開了,不了解的小伙伴可以自行 google 百度一下。)401 之后我們就需要對(duì)該Code進(jìn)行相應(yīng)的處理。可以如下這么寫
api.get('list').then(res => {
/* do something */
}).catch(e => {
if (res.statusCode === 401) {
api.login().then(() => {
api.get('list').then(res => {
/* do something */
})
})
}
})
看起來沒什么問題,也完成需求了。但是會(huì)發(fā)現(xiàn)這有很大的問題。
-
每個(gè)請(qǐng)求都需要加 401 的判斷,項(xiàng)目大起來 這塊的代碼量是非常恐怖的
-
接口返回的之后的處理 /* do something */ 也是重復(fù)的 (當(dāng)然把這整塊內(nèi)容都提取出來,這里就調(diào)用也行。不過還是想把調(diào)用這邊也省略掉 ^-^ )
屢一下我們要實(shí)現(xiàn)的目標(biāo)。
-
需要在每個(gè)請(qǐng)求后面都加一個(gè) 401 的判斷
-
若未授權(quán) 則進(jìn)行重新登錄
-
重新登錄之后繼續(xù)前一個(gè)請(qǐng)求
-
將該請(qǐng)求結(jié)果返回到第一個(gè)請(qǐng)求的結(jié)果里去(實(shí)現(xiàn)無感知重新登錄獲取信息)
這個(gè)體現(xiàn)出把自己封裝一個(gè)網(wǎng)絡(luò)請(qǐng)求的好處, 我們可以直接改寫 api.js 中的 http 方法里對(duì) error 的處理就好。上代碼:
const http = ({ url = '', params = {}, ...other} = {}) => {
wx.showLoading({
title: '加載中...'
})
let time = Date.now()
console.log(`開始:${time}`)
return new Promise((resolve, reject) => {
wx.request({
url: getUrl(url),
data: params,
header: getHeader(),
...other,
complete: (res) => {
wx.hideLoading()
console.log(`耗時(shí):${Date.now() - time}`)
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.data)
} else if (res.statusCode === 401) {
// 401 為鑒權(quán)失敗 很大可能是token過期
// 重新登錄 并且重復(fù)請(qǐng)求
login().then(res => {
http({ url, params, ...other }).then(res => {
resolve(res)
})
})
} else {
reject(res)
}
}
})
})
}
小結(jié)
網(wǎng)絡(luò)請(qǐng)求這塊,算目前開發(fā)項(xiàng)目中必不可少的一塊。但是例如 小程序,vue, react, weex 等其實(shí)都有一套自己的或者自己推薦的一套API以及相應(yīng)的寫法。沒一個(gè)都按照他推薦的來寫,其實(shí)挺蛋疼的,用著很不爽。把他們的API封裝一下,暴露出來統(tǒng)一的API, 給自己用或者尤其是給自己團(tuán)隊(duì)的小伙伴用就比較方便,少了很多重復(fù)學(xué)習(xí)成本,并且因?yàn)榻y(tǒng)一的API帶來的統(tǒng)一的格式也是很大的一個(gè)好處。
說到小程序要弄清楚的東西不少,有些坑我還在摸索怎么處理。比如小程序的組件化,全局變量的使用(什么值可以放在app.js里),html標(biāo)簽的轉(zhuǎn)換等,后續(xù)弄透了我會(huì)再出來獻(xiàn)丑的。
|