小程序的填坑小技巧之网络请求改造

前言

小程序在内测的时候就已经开始玩了,不过最开始的时候觉得,这sx东西东西怎么这么坑的样子,网络请求居然不是返回Promise而是用Callback的方式, 传值居然不能把值写在方法里只能用dataset,在这个全面组件化的大环境下居然不支持组件化…

其实最开始主要是书写时习惯的问题,秉承着我又不做小程序开发,就先忍着你的态度放任不管了。然而天有不测风云,最近因为业务的需求不得不做小程序相关的开发,我就倔脾气果断就不能忍了。果断的把不爽的地方改成能按我喜欢的方式来走,其中还遇到了一些其他的坑,一个个慢慢填,并把这些记录下来整理成了这篇文章。

网络请求

网络请求小程序提供了wx.request,这个是我最想吐槽的点, 仔细看一下 api,这货不就是n年前的$.ajax吗,好古老啊。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 官方例子
wx.request({
url: 'test.php', //仅为示例,并非真实的接口地址
data: {
x: '' ,
y: ''
},
header: {
'content-type': 'application/json' // 默认值
},
success: function(res) {
console.log(res.data)
}
})

现在还是只有一个请求就已经感觉写的很长了,如果一个页面需要多个请求呢,如果请求的顺序还有要求呢该怎么办,各种嵌套又臭又长,如果要求所以请求都完成之后再显示界面呢 瞬间懵逼。

这个时候我弱弱的看了一眼小程序的JS版本的支持,欧耶,比较良心的支持ES6。也就是说我们可以将其改成Promise是可能的。接下来看我如何改造。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/* utils/api.js  自定义网络请求 */
const baseURL = 'https://yourapi.com' // 自己后台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(`耗时:${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为关键字段
myDelete(url, params = {}) {
return http({
url,
params,
method: 'delete'
})
}
}

接了下来我们就可以正常的时候用了,写一下简单的例子吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const api = require('../../utils/api.js')

// 单个请求
api.get('list').then(res => {
console.log(res)
}).catch(e => {
console.log(e)
})

// 一个页面多个请求
Promise.all([
api.get('list'),
api.get(`detail/${id}`)
]).then(result => {
console.log(result)
}).catch(e => {
console.log(e)
})

如果习惯了 fetch 以及 axios 的朋友应该都会比较喜欢这种写法。 在做网络请求的时候还遇到一个问题,登录授权的问题。

登录问题

做一个应用,肯定避免不了登录操作。用户的个人信息啊,相关的收藏列表等功能都需要用户登录之后才能操作。一般我们使用token做标识。然后又会涉及到token不存在,用户第一次登录,token过期后,重新登录等问题。比较常规的操作是直接跳转到登录页面。

然后坑就出现了,小程序并没有登录界面,使用的是 wx.loginwx.login 会获取到一个 code,拿着该 code 去请求我们的后台会最后返回一个token到小程序这边,保存这个值为 token 每次请求的时候带上这个值。(详情可以查看小程序登录。)

然而仅仅这样就够了吗? 很显然并不是。一般还需要把用户的信息带上比如用户微信昵称,微信头像等,这时候就需要使用 wx.getUserInfo,这里涉及到一个用户授权的问题,留一个坑接下来再解决。带上用户信息就够了嘛? too young too simple!我们的项目不可能只有小程序,相应的微信公众平台可能还有相应的App,我们需要把账号系统打通,让用户在我们的项目中的账户是同一个。这就需要用到微信开放平台提供的 UnionID

ps.基于小程序在微信中的易传播性, 为了鼓励用户去传播分享一般还会提供邀请奖励机制。但是微信这边又会对诱导分享进行和谐处理。视情况慎用。(本文会在例子上加上该功能)

看到这,是不是觉得头都大了,就一个小小的登录功能坑这么多。 年轻的我瑟瑟发抖~~~。慢慢开始填吧。先上登录代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/* utils/api.js  自定义网络请求 */
...
function login() {
return new Promise((resolve, reject) => {
// 先调用 wx.login 获取到 code
wx.login({
success: res => {
// 再调用 wx.getUserInfo 获取到用户的一些信息 (一些基本信息,以及生成UnionID 所用到的信息 比如 rawData, signature, encryptedData, iv)
wx.getUserInfo({
// 若获取不到用户信息 (最大可能是用户授权不允许,也有可能是网络请求失败,但该情况很少)
fail: (e) => {
reject(e)
},
success: ({ rawData, signature, encryptedData, iv }) => {
let param = {
code: res.code,
rawData,
signature,
encryptedData,
iv
}
// 若有邀请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))
}
})
}
})
})
}
...

授权问题

根据上面的代码,可以很清楚的看到,若用户在登录的时候不允许小程序获取他的用户信息之后才能继续。若用户在这个时候点拒绝了呢, 会怎么样?
一片空白!~~ What’s the fuck! 怎么什么都没有!垃圾破小程序~~ 冷静点的用户也会百脸懵逼状。我是谁?我该怎么办?
也许你会觉得,用户点允许就好了啊,怎么会这么笨,这种用户肯定不会多之类的话。我在我们小程序中加了统计大约有 20%的用户点了拒绝, 如果后续我们没有做任何引导的话,这 20% 的用户就会永远失去。这个后果我们完全不能接受。

经过我们的小组研究与讨论,给出了一下的一套方案。

具体代码可以如下表示,用到了 wx.openSetting 来跳转到设置授权界面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/* index.js */
// 若有用户信息存在则继续
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 就是对用户信息做一下处理 不具体展开了

/* index.wxml */
<view class="unauth" wx:if="{{!isauth}}">
<image class="unauth-img" src="../../images/auth.png"></image>
<text class="unauth-text">检查到您没打开授权</text>
<button class="color-button unauth-button" bindtap="toSetting">去设置</button>
</view>
<view class="container" wx:else>
...
</view>

token 失效问题

登录获取到的 token 是有时效的,失效过了会怎么样 ? 如果后台小伙伴严格按照 REST API 规范设计接口 API 的话,他会给我们返回一个错了 http code 为 401。(常见的Http Code以及相关代码的意义本文就不做展开了,不了解的小伙伴可以自行 google 百度一下。)401 之后我们就需要对该Code进行相应的处理。可以如下这么写

1
2
3
4
5
6
7
8
9
10
11
api.get('list').then(res => {
/* do something */
}).catch(e => {
if (res.statusCode === 401) {
api.login().then(() => {
api.get('list').then(res => {
/* do something */
})
})
}
})

看起来没什么问题,也完成需求了。但是会发现这有很大的问题。

  1. 每个请求都需要加 401 的判断,项目大起来 这块的代码量是非常恐怖的
  2. 接口返回的之后的处理 /* do something */ 也是重复的 (当然把这整块内容都提取出来,这里就调用也行。不过还是想把调用这边也省略掉 ^-^ )

屡一下我们要实现的目标。

  1. 需要在每个请求后面都加一个 401 的判断
  2. 若未授权 则进行重新登录
  3. 重新登录之后继续前一个请求
  4. 将该请求结果返回到第一个请求的结果里去(实现无感知重新登录获取信息)

这个体现出把自己封装一个网络请求的好处, 我们可以直接改写 api.js 中的 http 方法里对 error 的处理就好。上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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(`耗时:${Date.now() - time}`)
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.data)
} else if (res.statusCode === 401) {
// 401 为鉴权失败 很大可能是token过期
// 重新登录 并且重复请求
login().then(res => {
http({ url, params, ...other }).then(res => {
resolve(res)
})
})
} else {
reject(res)
}
}
})
})
}

小结

网络请求这块,算目前开发项目中必不可少的一块。但是例如 小程序,vue, react, weex 等其实都有一套自己的或者自己推荐的一套API以及相应的写法。没一个都按照他推荐的来写,其实挺蛋疼的,用着很不爽。把他们的API封装一下,暴露出来统一的API, 给自己用或者尤其是给自己团队的小伙伴用就比较方便,少了很多重复学习成本,并且因为统一的API带来的统一的格式也是很大的一个好处。

说到小程序要弄清楚的东西不少,有些坑我还在摸索怎么处理。比如小程序的组件化,全局变量的使用(什么值可以放在app.js里),html标签的转换等,后续弄透了我会再出来献丑的。

(^ _ ^) 上一篇文章获得了 484的喜欢 200多人的关注 其实我挺开心的。比起大神来说可能很少, 但是对于一个很难下定决心才开始写博客的人来说,给了我很大的鼓舞,谢谢~