基于uniapp+nodejs实现小程序登录功能
本系列教程,以【中二少年工具箱】小程序为案例demo,具体效果请在微信中搜索该小程序查看。或在微信输入框输入 【#小程序://中二少年工具箱/6buitXgPnjHV21r】
一、概述
1.1 技术选型:
小程序端:uniapp
后端:nodejs+midwayjs+typeorm
数据库:mysql
1.2 登录功能实现方案:
1.小程序端调用接口uni.login获取随机code
2.将随机code传递给后端接口,后端接口中调用小程序官方api,获取用户信息
3.后端将用户信息保存到数据库中用户信息表,并将保存结果返回给前端
4.前端缓存用户信息,并显示
二、小程序端实现
代码实现:
function getUserInfoByWx() {
isLoad.value = true
uni.login({
provider: 'weixin', //使用微信登录
success: function (loginRes) {
const userData = {
code: loginRes.code
}
// console.log('userData',userData);
getUserInfoByWxApi(userData).then(res => {
console.log(res)
if (res.success) {
userInfoStore.setUserInfo({
userName: res.data.userName,
openidWx: res.data.openidWx
})
} else {
openMessage({
text: '自动创建用户出错,请点击登录手动创建'
})
}
}).catch(err => {
console.log('eeeeeeeeeeeeeee', err)
openMessage({
text: '登录失败,请联系开发者'
})
})
.finally(() => {
// debugger
isLoad.value = false
uni.$emit('loginFinish');
})
},
fail() {
isLoad.value = false
}
});
}
代码解释:
1.isLoad:前端是否显示正在登录的动画。
2.uni.login:uniapp提供的登录api,可以生成各平台的临时code。
3.getUserInfoByWxApi:调用后端接口,将临时code作为参数传递给后端,后端再调用官方接口完成登录。
4.userInfoStore.setUserInfo登录成功后,在全局状态管理中保存用户信息
上面的代码,大部分都是和前端登录相关的业务代码,真正核心的是生成了临时code并传递给后端,因为调用官方接口只能在后端代码中运行。
三、后端实现
后端代码实现可分为两步,一是调用官方接口,获取小程序官方返回的用户信息;二是根据业务需求,将用户信息保存到我们的数据库中。
controller层代码实现;
@Post('/getUserInfoByWx')
async getUserInfoByWx(@Body() userData: { code: string }) {
const openidRs = await this.loginService.getOpenidWx(userData)
const openidKey = 'openidWx'
const rs = await this.loginService.getUserInfoByPlat(openidRs, openidKey)
return rs
}
上面代码的code就是小程序端传入的临时code,主要用于getOpenidWx方法中,获取调用官方接口后的返回结果。
3.1 调用官方接口
上面代码中的getOpenidWx方法即是调用官方接口:
const openidRs = await this.loginService.getOpenidWx(userData)
具体的service实现:
/*根据临时code,获取wx返回的登录信息*/
async getOpenidWx(userData:{code:string} | any): Promise<any> {
const url = 'https://api.weixin.qq.com/sns/jscode2session';
const data = {
appid: 'wx9cxxxxxxxxxxxxx',
secret: '66bxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
js_code: userData.code,
grant_type: 'authorization_code'
}
const result = await this.httpService.get(url, {params: data})
return result
}
官方的文档如下:
结合文档和我写的接口示例,我为大家总结了关键点:
1.接口通过GET方式访问
2.参数包括appid、secret、js_code、grant_type都是通过url参数的方式传递。
3.appid+secret:开发者的身份认证,通过开发管理平台获取:
4.js_code:小程序端传递来的临时code
5.grant_type:照官网写,别问为什么。
返回结果中我们需要重点关注的参数是openid,这是每一个用户的唯一标识。
3.2 保存用户信息到数据库
上面controller层一共调用了两个方法,一个是上面的调用官方接口,另一个就是保存用户信息到数据库并返回用户信息:
const rs = await this.loginService.getUserInfoByPlat(openidRs, openidKey)
上面是兼容各平台的写法,我们可以忽略openidKey参数,只以微信小程序为例,具体的service实现为:
/**
* 根据各平台的openid获取用户信息,可用于首次登录时,自动注册*/
async getUserInfoByPlat(openidRs,openidKey:string){
// debugger
// 通过三方平台的
let rs:any={}
let findWxUserRs = new WxUser()
if (openidRs.status == 200 && (openidRs.data.openid || openidRs.data.data.openid)) {
const userInfo={}
userInfo[openidKey]=openidRs.data.openid || openidRs.data.data.openid
findWxUserRs = await this.wxUserService.getWxUserByUserInfo(userInfo) || new WxUser()
}
if (findWxUserRs && findWxUserRs.id) {
//用户已注册,获取用户信息
if(!findWxUserRs.userExtraEntity){
// 兼容旧数据,若没有extra信息,则创建
rs = await this.wxUserService.saveWxUser(findWxUserRs)
}else{
rs=findWxUserRs
}
} else {
//用户未注册,则保存并登录
// /*TODO:tt和wxopenid的层级不同,需要改造*/
findWxUserRs[openidKey] = openidRs.data.openid || openidRs.data.data.openid
rs = await this.wxUserService.saveWxUser(findWxUserRs)
}
return rs
}
代码解释:
1.openidRs是调用官方接口的方法返回的用户信息,如果它的status为200,并且openid有值,则说明调用官方接口成功。判断里之所以有两种层级,可能是因为某个平台的返回结果比较奇葩,代码过于久远,我也记不清了。
2.findWxUserRs是以openid作为筛选条件,筛选数据库的用户表,第一版代码不用像我写这么麻烦,我的openid可能分为openidWX,openidTT等等。第一次做这个功能,就在用户表里增加字段openid即可,然后根据这个字段筛选用户表。
3.如果以openid为筛选条件查到了用户信息,说明用户已注册,返回查询到的数据库中的用户信息。不用关心我的userExtraEntity对象判断,我的用户表结构发生过变化,为了兼容旧数据,这里做了判断。
4.如果以openid为筛选条件未查询到信息,则说明用户未注册,应该主动注册用户。注册用户的逻辑就因人而异了,我生成了随机的用户名称和用户id,再加上openid字段,保存了基本的用户信息。
5.保存成功后,返回用户信息。
我们要理清楚一个概念:用户信息。
通过官方接口获取的用户信息,是小程序官方提供返回结果,现阶段对我们最重要的是openid。
通过我们自己业务代码获取的用户信息,是返回数据库中的用户表信息。现阶段最重要的是用户名(userName)+id(表id,在我们业务中的唯一标识)+openid(用户在某小程序中的唯一标识)。
为什么不能用openid代替id成为我们业务表中的唯一标识,因为以后有可能还会集成其他小程序平台,openid在某小程序的各个场景中是唯一标识,但对于我们系统而言,它只是一个业务字段。
三、后端返回结果,前端显示
前端调用接口最终返回的结果,是保存在数据表中的用户信息:用户名+id+openid。
在上面前端代码中,成功调用接口后,主要做了两个操作:
userInfoStore.setUserInfo({
userName: res.data.userName,
openidWx: res.data.openidWx
})
...省略代码
uni.$emit('loginFinish');
代码解释:
1.userInfoStore.setUserInfo:是维护全局状态管理中的用户信息,并且使用pinia做成响应式,只要改变,小程序端的页面就会显示对应用户名。
2.uni.$emit('loginFinish')
:是一个事件通知机制。当登录模块所有操作完成,再触发loginFinish。其他组件中需要等待登录操作完成后才能执行的代码,需要严格控制执行顺序的代码,就在合适的位置插入uni.$on()监听。
总结
博主的大部分demo示例都会放到:中二少年学编程的示例项目。戳链接,查看示例效果。如果链接失效,请手动输入地址:lizetoolbox.top:8080/#/
本文知识点总结:
1.uni.login获取登录随机code,传递给后端接口。
2.后端代码中,调用官方接口获取平台返回的openid
3.将openid更新到数据库的用户信息表,如果没有该用户,则创建。
4.后端接口返回用户信息,在前端显示并执行后续操作
有任何前端项目、demo、教程需求,都可以联系博主,博主会视精力更新,免费的羊毛,不薅白不薅!~