imwty

专注大前端Web应用开发实践

0%

uni-app开发小程序系列--微信登录

何为微信登录

官方文档如是说:小程序可以通过微信官方提供的登录能力方便地获取微信提供的用户身份标识,快速建立小程序内的用户体系。换句话说就是让用户在咱们小程序的用户 id 和用户的微信 open_id 相关联。这样用户访问时,我们可以通过获取用户的 open_id,从而知道用户在我们平台的 id。这样用户就可以不用输入账号密码去登录自己的账号了。

登录流程

整个登录流程官方文档中也给了一个时序图,如下:

需要解释的是,开发者服务器就是咱们的后端服务。所以要在微信小程序中实现用户微信快捷登录,需要前后端的配合完成(全栈开发除外)。前端侧主要完成的过程是图中的 1,2,6,7 四个步骤。

具体实现

有了上边的时序图,我们完成登录逻辑其实就比较简单了,照着流程做就是了。第一步当然是要拥有一个微信小程序项目,笔者使用了 uni-app 初始化的项目,还没有生成项目的小伙伴可以移步我的另一篇博客:
uni-app 开发小程序系列–项目搭建

何时触发用户登录?

何时触发用户登录?这个地方不同的应用有不同的要求和设计。在笔者看来,因为登录过程其实是静默的,用户无感知的,也未涉及获取用户的隐私数据,所以在用户直接访问时就立马登录生成用户会话态是最好的。这样我们也好分析访客的数据。在 uni-app 项目中,有个根组件 App.vue,我们可以选择在这里 onLaunch 生命周期函数中去做登录逻辑处理。笔者代码如下:

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
<script>
export default {
onLaunch: function() {
// 从storage获取登录信息,没有则需要登录
let tokenInfo = uni.getStorageSync("tokenInfo");
let hasValidToken = false;
if (tokenInfo) {
let time = new Date().valueOf();
// 存储时间小于token失效时间,才是有效token, 否则重新授权
hasValidToken = time - tokenInfo.timestamp < 3600 * 24 * 1000;
}
if (!hasValidToken) {
// 调用小程序登录api
uni.login({
provider: "weixin",
success: (wxInfo) => {
// 获取到code后,提交给服务端
this.$api.post('/wxa/login', {
code: wxInfo.code,
}).then((res) => {
// 存储获取到的token
uni.setStorage({
key: 'tokenInfo',
data: {
token: res.token,
timestamp: new Date().valueOf()
}
})
})
},
});
}
},
onShow: function() {
console.log('App Show')
},
onHide: function() {
console.log('App Hide')
}
}
</script>

上边代码已经包含了时序图中的 1,2,6 步骤。有几个地方需要解释一下:

  1. 为何使用 storage?

storage 是官方提供的 api, 用于将数据存储在本地缓存中指定的 key 中。除非用户主动删除或因存储空间原因被系统清理,否则数据都一直可用。单个 key 允许存储的最大数据长度为 1MB,所有数据存储上限为 10MB。所以我们如果把 token 存储到 storage 里后,可以减少对 wx.login 接口以及后端接口的频繁调用。

  1. 为何要判断 token 是否失效?

笔者的思路是存储 token 时顺便记录下时间戳。获取 token 时,会拿当前时间戳减去存储时的时间戳得到存储时间,然后判断是否超过 30 天(当然这个时间可以和后端协商)。如果超过,则为失效,那么则需要重新去登录。有的人觉得没必要做这一步,如果 token 失效,会在调用其他接口时得到一个类似 401 之类的错误码,然后再去重新登录就行。当然这也是可以的,只是笔者觉得在这里处理的话,逻辑更加集中,且体验相对好一些。

如何携带登录态?

当我们本地获取了 token,则表示登录成功,接下来是在其他请求中携带上 token,这样后端才知道是哪个用户的请求。我们当然不能在每个请求中去加这个逻辑,这样工程量很大。所以,最好先对 wx.request 进行一次封装,然后所有的请求走咱们封装后的方法。笔者代码如下:

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// 该处配置为后端接口地址
const defaultHost = "http://api-server.com";

const errorMsg = (response) => {
let error = {};
if (response.statusCode) {
error.code = response.statusCode;
switch (response.statusCode) {
case 400:
error.msg = "错误请求";
break;
case 401:
error.msg = "未授权,请重新登录";
break;
case 403:
error.msg = "拒绝访问";
break;
case 404:
error.msg = "请求错误,未找到该资源";
break;
case 405:
error.msg = "请求方法未允许";
break;
case 408:
error.msg = "请求超时";
break;
case 500:
error.msg = "服务器端出错";
break;
case 501:
error.msg = "网络未实现";
break;
case 502:
error.msg = "网络错误";
break;
case 503:
error.msg = "服务不可用";
break;
case 504:
error.msg = "网络超时";
break;
case 505:
error.msg = "http版本不支持该请求";
break;
default:
error.msg = `连接错误${response.statusCode}`;
}
} else {
error.code = 10010;
error.msg = "连接到服务器失败";
}
return error;
};

function request(path, method, data, setting) {
const tokenInfo = uni.getStorageSync("tokenInfo");
const host = setting ? setting.host || defaultHost : defaultHost;
const token = setting ? setting.token || tokenInfo.token : tokenInfo.token;
return new Promise((resolve, reject) => {
uni.request({
url: host + path,
method: method,
data: data,
header: {
Authorization: "Bearer " + token,
},
success: (res) => {
// 状态码非200的处理
if (res.statusCode >= 400) {
const error = errorMsg(res);
uni.showToast({
title: error.msg,
icon: "none",
});
reject(errorMsg(res));
// errorCallback(errorMsg(res))
} else if (res.data.code) {
uni.showToast({
title: res.data.msg,
icon: "none",
});
// reject(errorMsg(res.data.msg))
// errorCallback(res.data)
} else {
resolve(res.data);
// successCallback(res.data)
}
},
});
});
}

export default {
get: (path, data, otherData) => {
return request(path, "get", data, otherData);
},
post: (path, data, otherData) => {
return request(path, "post", data, otherData);
},
request: request,
};

记住上述代码也有几个地方需要注意:

  1. defaultHost 是后端接口的域名部分,如果是有分测试环境真实环境,那么该处最好根据环境判断一下;
  2. 笔者是按照 jwt 规范在 header 中添加的 token 信息,这里的规则也是可以和后端进行协商;
  3. 关于错误码的判断,笔者判断如果后端返回了自定义的错误码时,返回 0 为正常响应,不为 0 则都算异常。这里需要根据实际情况进行调整。

封装了请求后,我们就可以直接调用需要登录态才能访问的接口了。比如获取用户信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div class="my">
<div>用户ID:{{userInfo.id}}</div>
<div>用户昵称:{{userInfo.username}}</div>
<div>用户openId:{{userInfo.openId}}</div>
</div>
</template>

<script setup>
import { ref } from "vue";
import request from "@/common/request.js";

const userInfo = ref({});

request.get("/user/info").then((res) => {
userInfo.value = res;
});
</script>
<style lang="scss" scoped>
.my {
width: 100%;
}
</style>

至此,登录功能完成。

后记

本次任务只是完成了用户登录,如果想获取用户昵称头像甚至手机号,那还需要申请用户授权,比较复杂,笔者将在下篇博客中详细介绍。

关于作者

计算机专业科班出身,8 年+Web 开发经验,多年深耕 Vue2,Vue3 技术栈。全栈开发经验丰富,技能树覆盖从前端工程搭建到部署上线全链路流程。紧跟技术潮流,一直关注着各种新兴技术趋势并积极进行实践探索,追求优雅的开发体验,极致的开发效率,高标准的开发质量。
欢迎批评指正,或者与我交流探讨前端技术。源码尚未公开,联系我可私发。
联系我:imwty2023(微信)