uni-app跨端登录模板:Pinia状态管理与多端认证统一方案
2026/9/14 14:16:48 网站建设 项目流程

简介:这是一份面向前端开发者与uni-app初学者的跨平台登录功能快速实现模板,解决移动应用开发中重复编写登录页、表单验证及状态管理等共性问题。资源包含81个文件,主体为19个Vue组件(如login、reg、pwd等页面及m-input、uni-forms-item等复用组件)、17个JSON配置文件(pages.json、manifest.json等)、14个JS工具脚本(含uqrcode.js、univerify.js等增强能力模块)以及15个Markdown文档(含changelog.md、README说明),整体压缩包仅474KB,轻量易集成。已有4721人学习下载,体现其在实际项目中的高复用价值。用户可直接导入项目使用标准化登录流程,获得完整页面结构、双向数据绑定逻辑、本地存储式记住密码实现、HTTPS网络请求封装、响应式布局适配及uni-module生态(uni-id、uni-popup等)集成范例,尤其适合需要快速交付多端登录功能的中小型App或小程序项目。

1. 这不是又一个“写完就扔”的登录页:uni-app 登录模板真正要解决的是跨端一致性、状态可追溯、错误可拦截这三件事

很多团队在用 uni-app 开发多端应用时,登录页往往是第一个被快速复制粘贴的模块——H5 里抄一份,微信小程序里改个button样式,App 端再加个原生插件调用,最后发现:密码输入框在 iOS 上光标错位、验证码倒计时在支付宝小程序里不触发、token 刷新逻辑在 Android App 里静默失败却无日志。这不是 UI 不统一的问题,而是登录这个关键链路缺乏可复用的状态管理契约、可插拔的认证适配层、可审计的错误传播路径。本篇讲的“基于 uni-app 框架的登录模板”,核心不是组件样式或表单校验,而是构建一个跨平台可收敛、业务逻辑可剥离、异常流可观测的登录基础设施。它适合正在维护 3+ 个端(尤其含 App)、已接入统一用户中心、且开始遭遇「登录成功但首页白屏」「微信授权后跳转丢失参数」这类隐性问题的中型项目团队。模板本身不绑定任何后端协议,但预留了 OAuth2、手机号一键登录、生物认证等主流扩展点。

2. 为什么必须用 Composition API + Pinia 重构登录状态?而不是直接写data()vuex

2.1 登录状态的本质是「跨页面、跨生命周期、跨平台能力」的聚合体

传统data()方式声明isSubmitting,loginForm,errorTips等字段,看似简单,但在实际场景中会迅速失控:

  • 微信小程序中onLoad触发时需预填充上一次失败的手机号,但data()初始化后无法响应式更新;
  • App 端调起原生人脸识别后,回调函数在mounted之外执行,this指向丢失导致this.$refs.form.validate()报错;
  • H5 页面刷新后 token 仍存在 localStorage,但data()中的isLoggedIn未同步还原,导致导航守卫误判。

Pinia 的 store 模块化 + Composition API 的逻辑复用能力,恰好解决这三类问题。它让登录状态成为独立于页面生命周期的「服务实例」,而非依附于某个 Vue 实例的临时数据。

2.2 构建useAuthStore:最小可行的登录状态容器

// stores/auth.ts import { defineStore } from 'pinia' import { ref, computed } from 'vue' import type { LoginParams, UserInfo } from '@/types/auth' export const useAuthStore = defineStore('auth', () => { // 响应式状态(跨页面共享) const token = ref<string>('') const userInfo = ref<UserInfo | null>(null) const isLoggingIn = ref<boolean>(false) const loginError = ref<string>('') // 持久化同步(关键!解决刷新丢失问题) const initFromStorage = () => { const storedToken = uni.getStorageSync('auth_token') const storedUser = uni.getStorageSync('auth_user') if (storedToken && storedUser) { token.value = storedToken userInfo.value = JSON.parse(storedUser) as UserInfo } } // 登录主逻辑(解耦平台差异) const login = async (params: LoginParams) => { isLoggingIn.value = true loginError.value = '' try { // 此处对接真实 API,返回 { token, user_info } const res = await uni.request({ url: '/api/v1/login', method: 'POST', data: params, header: { 'Content-Type': 'application/json' } }) if (res.statusCode === 200 && res.data.code === 0) { const { token: t, user_info: u } = res.data.data token.value = t userInfo.value = u // 统一持久化(所有端生效) uni.setStorageSync('auth_token', t) uni.setStorageSync('auth_user', JSON.stringify(u)) return { success: true } } else { throw new Error(res.data.message || '登录失败') } } catch (err) { loginError.value = err instanceof Error ? err.message : '网络异常,请重试' return { success: false, message: loginError.value } } finally { isLoggingIn.value = false } } // 退出登录(清理所有端缓存) const logout = () => { token.value = '' userInfo.value = null uni.removeStorageSync('auth_token') uni.removeStorageSync('auth_user') } // 计算属性暴露给视图 const isLoggedIn = computed(() => !!token.value && !!userInfo.value) return { token, userInfo, isLoggingIn, loginError, isLoggedIn, initFromStorage, login, logout } })

提示initFromStorage必须在应用启动时主动调用(如main.tsuseAuthStore().initFromStorage()),否则页面首次加载时isLoggedInfalse,导致守卫跳转逻辑失效。

2.3 在登录页面中使用:避免this.$store的旧式写法

<!-- pages/login/index.vue --> <template> <view class="login-container"> <u-form :model="form" ref="formRef"> <u-form-item label="手机号" prop="phone"> <u-input v-model="form.phone" placeholder="请输入手机号" /> </u-form-item> <u-form-item label="验证码" prop="code"> <u-input v-model="form.code" placeholder="请输入验证码"> <template #right> <u-button size="small" @click="sendCode" :disabled="countdown > 0" > {{ countdown > 0 ? `${countdown}s` : '获取' }} </u-button> </template> </u-input> </u-form-item> <u-button type="primary" @click="handleSubmit" :loading="authStore.isLoggingIn" > 登录 </u-button> </u-form> </view> </template> <script setup lang="ts"> import { ref, onMounted } from 'vue' import { useAuthStore } from '@/stores/auth' import { validatePhone, validateCode } from '@/utils/validator' const authStore = useAuthStore() const formRef = ref<any>(null) const form = ref({ phone: '', code: '' }) const countdown = ref(0) // 页面挂载时初始化 store 状态 onMounted(() => { authStore.initFromStorage() }) // 发送验证码(此处演示跨端差异处理) const sendCode = async () => { if (!validatePhone(form.value.phone)) { uni.showToast({ title: '手机号格式错误', icon: 'none' }) return } // H5 和小程序走 HTTP 请求,App 端可调用原生短信 SDK const platform = uni.getSystemInfoSync().platform if (platform === 'ios' || platform === 'android') { // 调用原生插件(需提前配置) const res = await uni.callNativePlugin({ name: 'SMSPlugin', method: 'sendCode', params: { phone: form.value.phone } }) if (!res.success) throw new Error(res.message) } else { // 小程序/H5 走 API await uni.request({ url: '/api/v1/sms/send', method: 'POST', data: { phone: form.value.phone } }) } countdown.value = 60 const timer = setInterval(() => { countdown.value-- if (countdown.value <= 0) clearInterval(timer) }, 1000) } // 表单提交 const handleSubmit = async () => { const valid = await formRef.value?.validate?.() if (!valid) return const result = await authStore.login({ phone: form.value.phone, code: form.value.code }) if (result.success) { // 登录成功后跳转(自动携带 token) uni.navigateTo({ url: '/pages/home/index' }) } else { uni.showToast({ title: result.message, icon: 'none' }) } } </script>

注意uni.callNativePlugin是示例写法,实际需按 uni-app 原生插件规范 配置 iOS/Android 原生代码。模板中通过platform判断分支,确保同一份 JS 逻辑在不同端执行不同路径,这才是「跨端一致性」的底层保障。

3. 如何让登录模板支持微信小程序、App、H5 三端差异化行为?

3.1 微信小程序:wx.login+code2Session的安全接入模式

微信小程序不能直接传明文密码,必须走wx.login获取临时 code,再由后端调用微信接口换取 openid。登录模板需提供wechatLogin()方法:

// stores/auth.ts(续) // 在 useAuthStore 内部添加 const wechatLogin = async () => { try { // 微信小程序专属 API const { code } = await uni.login({ provider: 'weixin' }) // 用 code 换取用户标识(此步骤必须在服务端完成,前端只传 code) const res = await uni.request({ url: '/api/v1/wechat/login', method: 'POST', data: { code } }) if (res.statusCode === 200 && res.data.code === 0) { const { token, user_info } = res.data.data token.value = token userInfo.value = user_info uni.setStorageSync('auth_token', token) uni.setStorageSync('auth_user', JSON.stringify(user_info)) return { success: true } } else { throw new Error(res.data.message || '微信登录失败') } } catch (err) { loginError.value = err instanceof Error ? err.message : '微信授权失败' return { success: false, message: loginError.value } } }

关键点uni.login({ provider: 'weixin' })返回的code仅能使用一次,且有效期 5 分钟,必须立即传给后端。前端绝不自行调用微信jscode2session接口(涉及appidsecret泄露风险)。

3.2 App 端:集成原生生物认证与设备指纹

App 需支持 FaceID/TouchID,并将设备 ID 作为登录凭证一部分。模板通过uni.getProvider检测能力并封装:

// utils/biometric.ts export const checkBiometricSupport = (): Promise<{ supported: boolean; type: 'face' | 'fingerprint' }> => { return new Promise((resolve) => { uni.getProvider({ service: 'biometric', success: (res) => { if (res.provider.length > 0) { // iOS 返回 ['face'],Android 可能返回 ['fingerprint'] resolve({ supported: true, type: res.provider[0] as 'face' | 'fingerprint' }) } else { resolve({ supported: false, type: 'fingerprint' }) } }, fail: () => resolve({ supported: false, type: 'fingerprint' }) }) }) } // stores/auth.ts(续) const biometricLogin = async () => { const { supported, type } = await checkBiometricSupport() if (!supported) { uni.showToast({ title: '设备不支持生物认证', icon: 'none' }) return { success: false } } try { const res = await uni.startSmsVerification({ // 注意:startSmsVerification 是示例名,实际需用 uni-app 支持的原生生物认证 API // 如 iOS 用 LocalAuthentication,Android 用 BiometricPrompt promptMessage: `请使用${type === 'face' ? '面容' : '指纹'}验证` }) if (res.success) { // 生物认证通过后,获取设备唯一标识 const deviceInfo = await uni.getSystemInfo() const deviceId = deviceInfo.deviceId || deviceInfo.model // 携带 deviceId 发起登录 const apiRes = await uni.request({ url: '/api/v1/biometric/login', method: 'POST', data: { device_id: deviceId } }) if (apiRes.statusCode === 200) { // 同步 token 和用户信息 token.value = apiRes.data.token userInfo.value = apiRes.data.user_info uni.setStorageSync('auth_token', apiRes.data.token) uni.setStorageSync('auth_user', JSON.stringify(apiRes.data.user_info)) return { success: true } } } } catch (err) { loginError.value = '生物认证失败,请重试' return { success: false } } }

3.3 H5 端:兼容第三方 OAuth2(如企业微信、钉钉)

H5 场景下常需跳转到企业微信扫码登录。模板提供oauthRedirect()方法,避免硬编码 redirect_uri:

// stores/auth.ts(续) const oauthRedirect = (provider: 'ww' | 'dd' | 'qywx') => { const redirectUri = encodeURIComponent(uni.getStorageSync('base_url') + '/callback/oauth') const state = Math.random().toString(36).substr(2, 9) // 防 CSRF uni.setStorageSync(`oauth_state_${provider}`, state) let url = '' if (provider === 'ww') { url = `https://open.weixin.qq.com/connect/qrconnect?appid=YOUR_CORPID&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_login&state=${state}#wechat_redirect` } else if (provider === 'dd') { url = `https://oapi.dingtalk.com/connect/qrconnect?appid=YOUR_APPKEY&response_type=code&scope=openid&redirect_uri=${redirectUri}&state=${state}` } if (url) { window.location.href = url } }

参数说明state用于防止跨站请求伪造(CSRF),回调页/callback/oauth必须校验该值;redirect_uri必须与企业后台配置的完全一致(包括协议、域名、路径),否则微信/钉钉会拒绝跳转。

4. 登录失败时如何精准定位问题?三类错误的捕获与上报策略

4.1 网络层错误:区分超时、连接拒绝、HTTP 状态码

uni-app 的uni.request默认不抛出网络异常,需手动判断:

// utils/request.ts(登录模板配套工具) export const safeRequest = (options: UniApp.RequestOptions) => { return new Promise((resolve, reject) => { uni.request({ ...options, timeout: 10000, // 统一设置超时时间 success: (res) => { // HTTP 4xx/5xx 归为业务错误,非网络错误 if (res.statusCode >= 400) { const error = { type: 'http_error' as const, statusCode: res.statusCode, data: res.data, requestUrl: options.url } // 上报至监控系统(如 Sentry) reportError(error) reject(error) } else { resolve(res) } }, fail: (err) => { // 网络层错误:超时、无网络、SSL 证书错误等 const error = { type: 'network_error' as const, message: err.errMsg || '网络请求失败', requestUrl: options.url, timestamp: Date.now() } reportError(error) reject(error) } }) }) } // 在 login 方法中替换 uni.request 为 safeRequest const login = async (params: LoginParams) => { try { const res = await safeRequest({ url: '/api/v1/login', method: 'POST', data: params }) // ...后续处理 } catch (err) { if (err.type === 'network_error') { loginError.value = '网络不稳定,请检查网络后重试' } else if (err.type === 'http_error') { loginError.value = err.data?.message || '服务器繁忙,请稍后再试' } } }

4.2 表单校验错误:结构化错误提示而非字符串拼接

避免uni.showToast({ title: '手机号不能为空' })这种弱提示,改为字段级反馈:

// utils/validator.ts export const validatePhone = (phone: string): boolean => { return /^1[3-9]\d{9}$/.test(phone) } export const validateCode = (code: string): boolean => { return /^\d{6}$/.test(code) } // 在登录页面中 const handleSubmit = async () => { const errors: Record<string, string> = {} if (!validatePhone(form.value.phone)) { errors.phone = '请输入正确的手机号' } if (!validateCode(form.value.code)) { errors.code = '验证码为6位数字' } if (Object.keys(errors).length > 0) { // 使用 u-form 的 setRules 动态设置错误(uView 组件库) formRef.value?.setRules?.({ phone: [{ required: true, message: errors.phone }], code: [{ required: true, message: errors.code }] }) return } // ...继续登录逻辑 }

4.3 Token 失效错误:拦截 401 并触发静默刷新

当 API 返回 401 时,不应直接登出,而应尝试用 refresh_token 刷新:

// stores/auth.ts(续) // 添加 refresh token 逻辑 const refreshToken = async (): Promise<boolean> => { try { const refresh_token = uni.getStorageSync('refresh_token') if (!refresh_token) return false const res = await safeRequest({ url: '/api/v1/token/refresh', method: 'POST', data: { refresh_token } }) if (res.statusCode === 200) { const { token: newToken } = res.data.data token.value = newToken uni.setStorageSync('auth_token', newToken) return true } return false } catch { return false } } // 全局请求拦截器(在 main.ts 中注册) uni.addInterceptor('request', { invoke(args) { const authStore = useAuthStore() if (authStore.token && args.url && !args.url.includes('/login')) { args.header = { ...args.header, Authorization: `Bearer ${authStore.token}` } } } }) uni.addInterceptor('fail', { invoke(err) { if (err.statusCode === 401) { // 尝试刷新 token const authStore = useAuthStore() if (authStore.refreshToken()) { // 刷新成功,重发原请求(需保存原请求参数) console.log('token 刷新成功,重试请求') } else { // 刷新失败,强制登出 authStore.logout() uni.navigateTo({ url: '/pages/login/index' }) } } } })

注意uni.addInterceptorfail拦截器需在main.ts中尽早注册,确保所有请求都被捕获。refreshToken成功后需重新发起原请求,此处简化为日志提示,实际项目中建议封装retryRequest方法。

5. 进阶技巧:如何让登录模板支持「记住我」和「多账号切换」?

5.1 「记住我」开关:控制 token 过期策略与存储方式

默认uni.setStorageSync是永久存储,但「记住我」应区分长期/短期 token:

场景Token 存储位置过期策略清理时机
未勾选记住我uni.setStorageSync后端返回 short_token(2h)关闭 App 或页面刷新
勾选记住我uni.setStorageSync后端返回 long_token(30天)用户主动登出或 token 失效
// 登录方法增加 rememberMe 参数 const login = async (params: LoginParams, rememberMe: boolean = false) => { // ...请求逻辑不变 if (res.data.data.token) { const storageKey = rememberMe ? 'auth_long_token' : 'auth_token' uni.setStorageSync(storageKey, res.data.data.token) // 同时存储 rememberMe 状态,用于下次自动勾选 uni.setStorageSync('remember_me', rememberMe) } } // 页面 mounted 时读取 rememberMe 状态 onMounted(() => { authStore.initFromStorage() const rememberMe = uni.getStorageSync('remember_me') if (rememberMe !== null) { form.value.rememberMe = rememberMe } })

5.2 多账号切换:隔离不同用户的本地缓存

当用户需要在「工作账号」和「个人账号」间切换时,不能简单覆盖auth_token,而应按用户 ID 建立命名空间:

// utils/storage.ts export const setUserData = (userId: string, key: string, value: any) => { const namespace = `user_${userId}_${key}` uni.setStorageSync(namespace, value) } export const getUserData = <T>(userId: string, key: string): T | null => { const namespace = `user_${userId}_${key}` const data = uni.getStorageSync(namespace) return data ? JSON.parse(data) : null } // 修改 login 方法 const login = async (params: LoginParams) => { // ...API 请求 if (res.data.data.token && res.data.data.user_info?.id) { const userId = res.data.data.user_info.id setUserData(userId, 'token', res.data.data.token) setUserData(userId, 'user_info', res.data.data.user_info) // 当前活跃用户 ID 存入全局 uni.setStorageSync('active_user_id', userId) } } // 切换账号时 const switchUser = (userId: string) => { const token = getUserData<string>(userId, 'token') const userInfo = getUserData<UserInfo>(userId, 'user_info') if (token && userInfo) { authStore.token = token authStore.userInfo = userInfo uni.setStorageSync('active_user_id', userId) } }

5.3 登录态验证:在 App 启动时执行最小化健康检查

避免用户打开 App 时看到首页再闪退到登录页,应在onLaunch阶段预检:

// App.vue <script> export default { onLaunch() { const authStore = useAuthStore() authStore.initFromStorage() // 如果有 token,发起轻量级校验(不卡住启动) if (authStore.token) { setTimeout(() => { uni.request({ url: '/api/v1/auth/verify', method: 'GET', success: (res) => { if (res.statusCode !== 200) { authStore.logout() uni.navigateTo({ url: '/pages/login/index' }) } }, fail: () => { authStore.logout() uni.navigateTo({ url: '/pages/login/index' }) } }) }, 100) } } } </script>

参数说明setTimeout延迟 100ms 执行校验,确保不影响冷启动速度;/api/v1/auth/verify接口只需返回{ code: 0 }即可,无需返回完整用户信息,降低首屏压力。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询