UniApp 实战:集成手机号、微信与QQ登录功能
前言
在当今移动互联网迅速发展的时代,用户对于应用的便捷性和安全性提出了更高的要求。为了满足这些需求,开发者们不断探索更加友好和高效的用户认证方式。传统的用户名密码登录虽然简单直接,但随着人们对隐私保护意识的增强以及对操作简便性的追求,越来越多的应用开始转向使用手机号验证码登录及第三方平台(如微信
、QQ
)登录等更为安全快捷的方式。
对于移动应用开发而言,UniApp
框架因其一次开发多端运行的特点而广受青睐。它不仅能够显著减少开发成本和周期,还能保证不同平台上的一致性体验。因此,在UniApp
中实现多种登录方式的支持变得尤为重要。这不仅可以提升用户体验,还可以为应用程序吸引更多的潜在用户群体。
本篇博客将基于UniApp
框架,深入探讨如何实现手机号验证码登录、微信登录以及QQ登录
的功能集成。我们将从环境搭建、接口调用到代码实现等多个方面进行详细介绍,并分享一些实用技巧和注意事项。无论你是刚刚接触UniApp
的新手,还是希望优化现有项目的资深开发者,相信都能从中获得有价值的参考信息。
实现
第一步、实现登录页面样式
代码:
<template>
<view class="app">
<!-- mixin.js 中的 navBack(),不能少了括号,不然会传递event对象 -->
<text class="back-btn iconfont icon-close" @click="navBack()"></text>
<view class="left-top-sign">LOGIN</view>
<view class="welcome">欢迎回来!</view>
<!-- 手机号登录 -->
<view class="input-content">
<view class="input-item">
<text class="tit">手机号码</text>
<view class="row">
<input v-model="mobile" type="number" maxlength="11" placeholder="请输入手机号码"
placeholder-style="color: #909399" />
</view>
</view>
<view class="input-item">
<text class="tit">验证码</text>
<view class="row">
<input v-model="code" type="number" maxlength="6" placeholder="请输入手机验证码" placeholder-style="color: #909399" />
<wyk-code :mobile="mobile" templateCode="SMS_19999999"></wyk-code>
</view>
</view>
<button type="primary" :loading="loading" @click="login">登录</button>
</view>
<!-- #ifndef H5 -->
<view class="other-wrapper">
<view class="line center"><text class="tit">社交账号登录</text>
</view>
<view class="row">
<image @click="toProviderLogin('weixin')" class="icon" src="/static/share/weixin.png"></image>
<!-- #ifdef APP-PLUS -->
<image @click="toProviderLogin('qq')" class="icon" src="/static/share/qq.png"></image>
<!-- #endif -->
</view>
</view>
<!-- #endif -->
<!-- #ifdef APP-PLUS || MP-WEIXIN -->
<!-- #endif -->
<!-- 协议 -->
<view class="agreement center">
<text class="iconfont icon-roundcheckfill" :class="{active: agreement}" @click="checkAgreement"></text>
<text @click="checkAgreement">请认真阅读并同意</text>
<text class="tit" @click="navTo('/pages/public/xieyi')">《用户服务协议》</text>
<text class="tit" @click="navTo('/pages/public/yinsi')">《隐私权政策》</text>
</view>
</view>
</template>
<script>
import api from '@/api/system.js'
export default {
data() {
return {
mobile: '', // 用户名
code: '', // 验证码
agreement: false, // 是否同意协议
loading: false, // 登录中
}
},
methods: {
//同意协议
checkAgreement() { this.agreement = !this.agreement},
// 手机号登录
async login() {},
// 获取应用内的认证授权信息
async getServiceUserInfo(reqData) {}
}
}
</script>
<style lang="scss">
.app {
padding-top: 200rpx;
}
/* 关闭按钮 */
.back-btn {
position: absolute;
left: 20rpx;
top: calc(var(--status-bar-height) + 20rpx);
z-index: 90;
padding: 20rpx;
font-size: 39rpx;
color: #606266;
}
.left-top-sign {
font-size: 120rpx;
color: #f8f8f8;
}
.welcome {
position: relative;
top: -90rpx;
padding-left: 60rpx;
font-size: 46rpx;
color: #555;
text-shadow: 1px 0px 1px rgba(0, 0, 0, .3);
}
.input-content {
padding: 0 60rpx;
}
.input-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
padding-left: 30rpx;
background: #f8f6fc;
height: 120rpx;
border-radius: 4px;
margin-bottom: 50rpx;
&:last-child {
margin-bottom: 0;
}
.row {
width: 100%;
}
.tit {
height: 50rpx;
line-height: 56rpx;
font-size: 26rpx;
color: #606266;
}
input {
flex: 1;
height: 60rpx;
font-size: 30rpx;
color: #303133;
width: 100%;
}
}
button[type=primary] {
background-color: $mxg-color-primary !important;
}
/* 其他登录方式 */
.other-wrapper {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 20rpx;
margin-top: 80rpx;
.weixin {
border: 0;
}
.line {
margin-bottom: 40rpx;
.tit {
margin: 0 32rpx;
font-size: 24rpx;
color: #606266;
}
&:before,
&:after {
content: '';
width: 160rpx;
height: 0;
border-top: 1px solid #e0e0e0;
}
}
.icon {
width: 80rpx;
height: 80rpx;
margin: 0 50rpx;
}
}
.agreement {
position: absolute;
left: 0;
bottom: 10rpx;
width: 750rpx;
height: 90rpx;
font-size: 24rpx;
color: #999;
.iconfont {
font-size: 36rpx;
color: #ccc;
margin-right: 8rpx;
&.active {
color: $mxg-color-primary;
}
}
.tit {
color: #40a2ff;
}
}
</style>
第二步、创建 获取验证码按钮子组件
创建@/components/mxg-code/wyk-code.vue
,并添加相应的逻辑:
<template>
<view class="mxg-code center">
<text class="code-btn" @click="sendSmsCode">
{{codeDuration ? codeDuration + 's' : '获取验证码' }}
</text>
</view>
</template>
<script>
let interval = null
import api from '@/api/system.js'
export default {
props: {
mobile: { // 手机号
type: String,
default: '18888888888'
},
templateCode: { // 短信模板代码
type: String,
default: ''
}
},
data() {
return {
codeDuration: null, // 倒计时时长
}
},
methods: {
// 发送短信验证码
async sendSmsCode() {
// 有倒计时,则已发送
if (this.codeDuration) {
uni.showModal({
content: `请在${this.codeDuration}秒后重试`,
showCancel: false
})
return
}
// 校验手机号
if (!this.$util.checkStr(this.mobile, 'mobile')) {
this.$util.msg('手机号码格式不正确')
return
}
// 更换手机号与原手机号一致,不允许发送
if (this.$store && this.mobile === this.$store.state.userInfo.mobile) {
this.$util.msg('新手机号与当前绑定的手机号不能相同')
return
}
try {
// 发送短信验证码
uni.showLoading({
mask: true
})
const data = {
mobile: this.mobile,
templateCode: this.templateCode
}
const res = await api.sendSmsCode(data)
uni.hideLoading()
// 响应结果
this.$util.msg(res.message)
if (res.code !== 20000){
return
}
// 倒计时
this.codeDuration = 60
interval = setInterval(() => {
this.codeDuration--
if (this.codeDuration === 0) {
if (interval) {
clearInterval(interval)
interval = null
}
}
}, 1000)
} catch (e) {
uni.hideLoading()
this.$util.msg('验证码发送失败')
console.log('验证码发送失败', e)
}
},
}
}
</script>
<style lang="scss">
.mxg-code {
width: 160rpx;
background-color: #345dc2;
border-radius: 10rpx;
}
.code-btn {
padding: 10rpx 0;
font-size: 25rpx;
color: #FFFFFF;
}
</style>
第三步、实现手机号登录成功后使用Vuex进行用户状态管理
// 手机号登录
async login() {
// 协议
if (!this.agreement) {
this.$util.msg("请阅读并统一服务及隐私协议");
return
}
// 手机号验证码判断
const {
mobile,
code
} = this
if (this.$util.checkStr(mobile, 'mobile')) {
this.$util.msg("请输入有效手机号码")
}
if (this.$util.checkStr(code, 'mobileCode')) {
this.$util.msg("请输入有效验证码")
}
// 调用登录接口
this.loading = true
uni.showLoading({
title: '登陆中',
mask: true
})
const res = await api.login({
mobile,
code
})
this.loading = false
uni.hideLoading()
if (res.code === 20000) {
this.loginSuccessCallBack(res.data)
} else {
this.$util.msg(res.message)
}
},
// 登录成功的回调
loginSuccessCallBack(data) {
this.$util.msg("登录成功")
// 状态管理保存用户信息
this.$store.commit('setToken', data)
setTimeout(() => {
this.navBack()
}, 500)
},
第四步、创建并使用Vuex
管理登录状态
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
// 声明状态变量
state: {
userInfo: {}, // 用户信息
accessToken: "" //访问令牌"
},
// store的计算属性
getters: {
hasLogin(state) {
return !!state.accessToken
}
},
mutations: {
setState(state, data) {
for (let key in data) {
// 每个对象的key作为状态名, value作为状态值
state[key] = data[key]
}
},
// 更新用户登录状态
setToken(state, data) {
// 解构属性,
const {
userInfo,
access_token
} = data
// 状态赋值保存
if (userInfo) {
state.userInfo = userInfo
uni.setStorageSync('userInfo', userInfo)
}
if (access_token) {
state.accessToken = access_token
uni.setStorageSync('mxgEducationToken', access_token)
}
},
// 退出登录
logout(state) {
// 状态清空
state.userInfo = {}
state.accessToken = ''
// 移除本地数据
uni.removeStorageSync('userInfo')
uni.removeStorageSync('mxgEducationToken')
}
}
})
export default store
main.js
中引入vuex
全局状态管理
// 状态管理文件
import store from './store'
// 挂载Vue实例上,this.$store 获取
Vue.prototype.$store = store;
const app = new Vue({
store,
...App
})
第五步、使用vuex
对用户信息进行渲染
<template>
<view>
<!-- #ifndef MP -->
<!-- 头部空出的距离 -->
<view class="status_bar"></view>
<!-- #endif -->
<!-- 状况用户信息 -->
<view class="my-header">
<view class="header-content space-between center" @click="hasLogin? navTo('/pages/my/user', {login: true}): navTo('/pages/auth/login')">
<view class="left row center">
<image :src="userInfo.imageUrl||'../../static/logo.png'" class="header-image" mode=""></image>
<view class="header-info column" v-if="hasLogin">
<text class="nickname">{{userInfo.nickName}}</text>
<text class="username">用户名:{{userInfo.username}}</text>
</view>
<view class="header-info" v-else>
<text class="nickname">请登录</text>
</view>
</view>
<text class="iconfont icon-right"></text>
</view>
</view>
<!-- 功能列表 -->
<wyk-list :list="list"></wyk-list>
</view>
</template>
<script>
import {
mapState,
mapGetters
} from 'vuex'
import list from '@/config/my-list-bar.js'
export default {
computed: {
// 结构状态作为计算属性
...mapState(['userInfo']),
...mapGetters(['hasLogin'])
},
data() {
return {
list: list() // 功能列表数据
}
},
methods: {
}
}
</script>
第七步、启动应用初始化登录状态
重新打开应用时,在 App.vue
中获取本地保存的 accessToken
和 userInfo
信息 ,如果有则认为是登录状态,没有则为未登录状态,得到用户信息后需要同步改变 vuex
的状态,使所有页面都能共享登陆状态与用户信息。
<script>
export default {
onLaunch: function() {
this.initLogin()
},
onShow: function() {
console.log('App Show')
},
onHide: function() {
console.log('App Hide')
},
methods: {
initLogin() {
const userInfo = uni.getStorageSync('userInfo');
const accessToken = uni.getStorageSync("mxgEducationToken")
if (userInfo && accessToken) {
this.$store.commit('setState', {
userInfo,
accessToken
})
}
}
}
}
</script>
关键点解释:
onLaunch
:这是应用首次启动时触发的生命周期钩子。在这个例子中,当应用启动时会调用initLogin
方法来检查是否有保存的用户信息和访问令牌,并根据情况初始化用户的登录状态。onShow
和onHide
:这两个是应用可见性变化时触发的钩子。onShow
在应用从前台被切到后台后再次回到前台时触发;onHide
则是在应用将要退到后台时触发。它们可以用来执行清理工作或恢复某些资源。methods
:这部分定义了组件内的方法。initLogin
方法用于尝试自动登录用户,如果之前已经登录过(即本地存储中有用户信息和有效的访问令牌),那么就直接设置这些信息到Vuex
的状态管理器中,以便其他页面可以直接使用。uni.getStorageSync
:这是一个同步版本的API
,用来获取本地缓存的数据。这里的'userInfo'
和'mxgEducationToken'
是存储在本地缓存中的键名,用来存放用户信息和访问令牌。this.$store.commit
:这是Vuex
的状态变更方式,通过提交mutation
来改变store
中的状态。这里假设有一个名为setState的mutation
,它接受一个对象参数,包含userInfo
和accessToken
,用于更新全局状态。
第八步、在 mxin.js
中的 navTo
方法里,增加判断未登录跳转到登录页
第九步、微信
与QQ
第三方授权登录【参考】
调用 uni.login
实现第三方授权登录,授权登录后,通过第三方响应的授权信息,再进行请求后台进行应用内认证授权,获取应用内认证信息。
实现
在login.vue
的 toProviderLogin
方法中进行调用 uni.login
实现第三方授权登录
// 微信、QQ提供商登录
toProviderLogin(provider) {
// 协议
if (!this.agreement) {
this.$util.msg("请阅读并统一服务及隐私协议");
return
}
uni.showLoading({
mask: true
}),
uni.login({
provider,
// #ifdef MP-ALIPAY
scopes: 'auth_user', // 支付宝小程序要设置主动授权
// #endif
success: (res) => {
console.log("授权成功", res)
// #ifdef APP-PLUS
const data = {
userInfo: res.authResult
}
// #endif
// #ifdef MP-WEIXIN
const data = {
code: res.code
}
// #endif
// 2. 授权成功后,请求自己的后台认证接口,来完成自己平台的认证
this.getServiceUserInfo(data)
uni.hideLoading()
},
fail: (err) => {
this.$util.msg("授权登陆失败")
uni.hideLoading()
}
})
},
.授权登录后,通过第三方响应的授权信息,再进行请求后台进行应用内认证授权,获取应用认证信息token
等信息
// 获取应用内的认证授权信息
async getServiceUserInfo(reqData) {
// 1. 调用后台服务接口应用内登录,获取用户信息和token
const {
code,
message,
data
} = await api.loginByProvider(reqData)
uni.hideLoading()
if (code !== 20000) {
this.$util.msg(message)
return
}
// 2. 判断是否绑定了手机号
if (data.userInfo.mobile && data.access_token) {
// 2.1 已绑定:更新 store 中的登录状态为已登录
this.loginSuccessCallBack(data)
} else {
this.$util.msg('授权成功,请绑定手机号')
// 2.2 未绑定:则跳转手机号绑定页 /pages/auth/bindMobile
this.navTo('/pages/auth/bind-mobile?data=' + JSON.stringify(data))
}
}
第十步、退出登录功能实现
点击个人资料页面的 退出登录 按钮,触发 logout
方法
// 退出登录
async logout() {
uni.showModal({
title: '确定退出登录?',
content: '退出后不会删除任何历史数据',
success: async (res) => {
if (res.confirm) {
const {
code,
message
} = await authApi.logout(this.$store.state.accessToken)
if (code === 20000) {
this.$util.msg('成功退出登录')
this.$store.commit('logout')
setTimeout(() => {
this.navBack()
}, 300)
} else {
this.$util.msg(message)
}
}
}
})
},
效果:
第十一步、完善请求头带上令牌和路由拦截
登录成功后,后台会响应访问令牌 accessToken
,如果当前有访问令牌,则在请求头带上访问令牌,这样针后台会认证访问令牌是否有效,如果令牌有效,才可以访问需要登录后的接口,不然就访问失败。找到 request.js
文件,添加代码:
代码:
import {
msg
} from './util.js'
// 导入成功后,后台会相应访问令牌accessToken,如果当前有访问令牌,则在请求头上面带上令牌
import store from '@/store'
// 基础URL
// #ifndef H5
// 非h5端,
let BASE_URL = 'https://mock.mengxuegu.com/mock/63fcbc2d7c016026ff2b8cd8/education-app'
// #endif
// #ifdef H5
// h5, 进行代理转发
let BASE_URL = '/api' // 'http://39.108.187.100:6001'
// #endif
const request = (options = {}) => {
// 2.判断请求头带上访问令牌
const accessToken = store.state.accessToken
if(accessToken){
options.header = {'Authorization':`Bearer${accessToken}`}
}
// resolve 正常响应,reject异常响应
return new Promise((resolve, reject) => {
uni.request({
url: BASE_URL + options.url,
method: options.method || 'GET',
data: options.data || {},
timeout: 8000, // 8秒超时时间,单位ms
// 3. 添加请求头
header:options.header,
success: (res) => {
// console.log('res', res.data)
resolve(res.data)
},
fail: (err) => {
// console.log('err', err)
msg('请求接口失败')
reject(err)
}
})
})
}
// 导出
export default request
效果图
APP端口第三方授权QQ登录效果
APP端口第三方授权微信登录效果
APP端口第三方手机号验证码登录效果
微信小程序端手机号登录效果
微信小程序端微信登录效果
退出登录功能
完结
通过上述步骤,我们已经能够在UniApp
项目中集成手机号登录、微信登录(包括微信小程序)、QQ
登录以及安全退出登录的功能。希望这篇博客能帮助你更好地理解和实现这些特性。如果你有任何疑问或者遇到了困难,请随时查阅官方文档或寻求社区的帮助。祝你开发顺利!