本节主要讲解如何使用 Vuex 以及 Uniapp 的 storage 储存功能来做用户状态保存。

下面先来了解下 storage 的基本概念。

# 利用 storage 存储信息

传统的网页 h5 端数据存储方式(cookie、localStorage、sessionStorage,IndexedDB...),而 App 端无大小限制,不是缓存,是持久化存储。小程序端是自有的 storage 方式,在于用户主动删除或超过一定时间被自动清理,否则数据都一直存在。

Uniapp 集成了小程序,app,h5 的数据缓存,统一了 uni.setStorage(),uni.setStorage() 系列 API ,完成对缓存数据的操作。

在登录这一块功能,传统做法是使用 cookie 的存储机制来判定用户。

使用 cookie:用户登录后由后端生成一个 sessionid 放在 cookie 中返回给客户端,并且服务端一直记录着这个 sessionid ,客户端以后每次请求都会带上这个 sessionid,服务端通过这个 sessionid 来验证身份之类的操作。(存在危险:攻击者拿到了cookie(sessionid)后,就可以完全替代你,客户端以为攻击者是实际用户)。

使用 token:用户登录后由后端返回一个 token 给客户端,客户端将这个 token 存储起来,然后每次客户端请求都需要开发者手动将 token 放在请求 header 中带过去,服务端每次只需要对这个 token 进行验证就能使用 token 中的信息来进行下一步操作了。

Cookie 是 JavaScript 中的浏览器对象,在小程序,App 中并没有 document.cookie 对象,因此不支持读写 cookie,所以使用 Uniapp 开发不能用传统的应用那样通过读取 cookie 来判断是否是登录状态。Uniapp 中有 uni.setStorage() 系列 API 进行数据缓存,而且整个框架的语法糖跟 Vue 类似,那我们可以 uni.setStorage() 以及 Vuex 对登录状态进行管理。

注:Uniapp 设置存储有同步与异步之分,以 Sync 结尾的都是同步缓存操作:

// 异步
uni.setStorage({
    key: 'storage_key',
    data: 'hello',
    success: function () {
        console.log('success');
    }
});

// 同步
try {
    uni.setStorageSync('storage_key', 'hello');
} catch (e) {
    // error
}

有设置缓存 API ,当然还有 API 指定 key 去除缓存 uni.removeStorage(),以及 API 清理本地数据缓存 uni.clearStorage()。

注意 Uniapp 的 Storage 在不同端的实现不同:

  1. 所有 storage 的 API 操作对应有同步异步之分,容易使用混淆。
  2. H5端为 localStorage,浏览器限制5M大小,是缓存概念,可能会被清理
  3. App 端为原生的 plus.storage,无大小限制,不是缓存,持久化
  4. 各个小程序端为其自带的 storage api ,数据存储生命周期跟小程序本身一致,即除用户主动删除或超过一定时间被自动清理,否则数据都一直可用。
  5. 微信小程序单个 key 允许存储的最大数据长度为 1MB,所有数据存储上限为 10MB。
  6. 支付宝小程序单条数据转换成字符串后,字符串长度最大200*1024。同一个支付宝用户,同一个小程序缓存总上限为10MB。
  7. 百度、头条小程序文档未说明大小限制。

根据上面的 storage 的 API 与 Vuex,我们可以先捋一下登录功能逻辑:

img

# 引入 Vuex,定义登录状态及用户信息

在 store/index.js 文件夹目录引入 Vuex,添加状态和分发方法:

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
    state: {
        uerInfo: {  // 用户信息
            hasLogin: false,
            token: '',
            profile: ''  // 简介
        },
    },
    mutations: {
        storeLogin(state, payload) { // 改变登录状态
            const temp = {
                hasLogin: true,
                token: payload.token,
                profile: payload.profile
            }

            state.uerInfo = Object.assign({}, state.uerInfo, temp)

            // 将用户信息保存在本地
            uni.setStorageSync('uerInfo', JSON.stringify(state.uerInfo))

        },
        storeLogout(state) { // 退出登录
            const temp = {
                hasLogin: false,
                token: '',
                profile: {}
            }
            state.uerInfo = Object.assign({}, state.uerInfo, temp)

            uni.removeStorageSync('uerInfo')
        }
    }
})

export default store

Uniapp 中可以直接引入 Vuex 而不需命令行安装依赖,只需要注入到 Vuex 插件 Vue.use(Vuex)。

new Vuex.Store() 对象参数 state 保存用户信息,项目全局共享这些信息。mutations 对象定义方法 storeLogin,storeLogout 暴露出去给项目中的页面组件修改 state 的唯一通道,修改 uerInfo 状态信息只能通过这两个方法。

# 挂载 store ,全局共享状态信息

要想项目全局共享这些信息,需要在入口文件 main.js 文件中导入这个 js,并把 store 挂载到 Vue 中。

import Vue from 'vue'
import App from './App'
import store from './store'

Vue.prototype.$store = store

App.mpType = 'app'

const app = new Vue({
    ...App
})
app.$mount()

挂载到 Vue 后,就可以在页面上使用了。

首先是用户的登录:

import { mapMutations } from 'vuex';
import { apiLogin } from '@/apis/account.js'
export default {
	methods: {
		...mapMutations(['storeLogin']),
		bindLogin() {
			const params = {
				phone: this.account,
				password: this.password
			};
			apiLogin(params).then(res => {
				// 不成功信息提示
				if (res.data.code !== 200) {
					uni.showToast({
					    icon: 'none',
					    title: res.data.msg,
					});
					return false
				}

				// 登录成功后改变store登录状态,并进入首页
				// 在store存储token,profile
				const temp = {
					token: res.access_token,
					profile: res.data.profile
				}
				this.storeLogin(temp)

				uni.switchTab({
					url: '/pages/index/index'
				});
			})
		},
	}
}

用户经过登录页登录后就可以在账号页面共享登录状态了。

# 账号页面共享登录状态与用户信息

登录前与登录后的页面状态:

img

img

通过 Vuex 中保存的用户信息判断是否已经登录,从而显示不同的内容。关键代码:

<!-- pages/account/index.vue -->
<!-- 未登录状态 -->
<view v-if="!userInfo.hasLogin" class="empty-user">
    <view>登录Uniapp Music</view>
    <view>手机电脑多端同步,尽享海量高品质音乐</view>
    <navigator class="btn" url="/pages/subpages/account/login">
        立即登录
    </navigator>
</view>
<!-- 登录状态 -->
<template v-if="userInfo.hasLogin">
    <view class="userinfo-box flex-box">
        <view class="avator">
            <image v-if="userInfo.profile.avatarUrl" class="img" :src="userInfo.profile.avatarUrl"></image>
            <view v-else class="no-img">
                上传头像
            </view>
        </view>
        <view class="flex-item">
            <view class="fl">
                <view class="name">{{userInfo.profile.nickname}}</view>
                <view class="level">lv1</view>
            </view>
            <image class="sign fr" src="/static/image/account/a_03.png"></image>
        </view>
    </view>
<template>
<!-- 退出登录 -->
<view v-if="userInfo.hasLogin" class="logout" @click="confirmOut">
    退出登录
</view>
import { mapState, mapMutations } from 'vuex';
import { apiLogout } from '@/apis/account.js';
export default {
		data() {
				return {};
		},
		computed:{
				...mapState(['userInfo'])
		},
		methods: {
				// 退出登录
				...mapMutations(['storeLogout']),
				// 退出登录 清空缓存
	confirmOut() {
		uni.showModal({
			title: 'Uniapp Music',
			content: '确定退出当前账号吗?',
			cancelColor: '#007aff',
			success: res => {
				if (res.confirm) {
					this.confirmLogout()
				} else if (res.cancel) {
					console.log('用户点击取消');
				}
			}
		});
	},
	// 确定退出
	confirmLogout() {
		apiLogout().then(res => {
			this.storeLogout()
			// 到登录页
			uni.navigateTo({
				url: '/pages/subpages/account/login'
			})
		})
	},
		}
}

# 关键点:再次进入应用

刷新页面或者退出后页面再次进入应用后的判断。这里我们根据进入应用触发应用生命周期 onLaunch 来做判定。当进入应用时在 App.vue 中判断本地缓存中是否有 uerInfo,根据 uerInfo 里面的 token 判断登录状态。

import { mapMutations } from 'vuex';
export default {
	onLaunch() {
		console.log('App Launch')
		uni.getStorage({
			key: 'userInfo',
			success: res => {
				console.log(res.data)
				// 此处仅做演示
				// 跟后台校验token的有效性,判定是否在登录状态。如果token失效,需重新登录。app端不强制用户登录,可以游客身份登录,可以进一步优化流程
				// uni.request({
				//  url: '',    // 验证token有效性的api
				//  header: {
				//     "Token":res.data.token
				//  },
				// 	method: "POST",
				// 	success: response => {
				// 		if (response.data.code === 200) {
				// 			this.storeLogin(e.data);
				// 		} else {  // 验证无效清除用户原有缓存数据
				// 			this.storeLogout()
				// 		}
				// 	}
				// })
				this.storeLogin(JSON.parse(res.data))
			}
		});
	},
	methods: {
		...mapMutations(['storeLogin', 'storeLogout'])
	}
}

验证 token 有效性后拿到用户信息,如同再次登录一样,存储用户信息在状态管理,使所有页面都能共享登录状态与用户信息。

可以将 token 放在请求 header 中带过去,服务端每次只需要对这个 token 进行验证就能验证用户了。

// utils/request/index.js
import store from '@/store/index.js'

const reqInterceptor = async (options) => {
  options.header = {  // 头部塞入token 进行验证
    ...options.header,
    token: store.state.userInfo.token
  }
}

# 小结

  1. 关键在于如何存储验证 token 信息,如何与后台交换用户信息;
  2. 将 token 放在请求 header 中,服务端每次只需要对这个 token 进行验证就能验证当前用户。
  3. 本章代码 uni-course-实战开发登录状态 (opens new window)。
阅读全文