{{wikiTitle}}
前端状态管理说明
复制链接
编辑文档
CRMEB PRO 前端状态管理说明
一、概述
CRMEB PRO项目包含两套前端应用,分别采用不同的状态管理方案:
| 应用 | 框架 | 状态管理 | 位置 |
|---|---|---|---|
| 后台管理端 | Vue CLI | Vuex | view/admin/src/store/ |
| 移动端 | uni-app | Vuex | view/uniapp/store/ |
本文档详细说明两端的状态管理实现和使用方法。
二、移动端(uni-app)状态管理
2.1 目录结构
view/uniapp/store/
├── index.js # Vuex入口文件
├── getters.js # 全局getters
└── modules/ # 模块目录
├── index.js # 模块导出
├── app.js # 应用状态模块(核心)
├── hotWords.js # 热词模块
└── indexData.js # 首页数据模块
2.2 Store入口配置
store/index.js:
import Vue from "vue";
import Vuex from "vuex";
import modules from "./modules";
import getters from "./getters";
Vue.use(Vuex);
const debug = process.env.NODE_ENV !== "production";
export default new Vuex.Store({
modules, // 模块
getters, // 全局getters
strict: debug // 开发环境开启严格模式
});
2.3 全局Getters
store/getters.js:
export default {
// 用户Token
token: state => state.app.token,
// 是否已登录
isLogin: state => !!state.app.token,
// 背景色
backgroundColor: state => state.app.backgroundColor,
// 用户信息
userInfo: state => state.app.userInfo || {},
// 用户ID
uid: state => state.app.uid,
// 首页是否激活
homeActive: state => state.app.homeActive,
// 各类申请手机验证开关
divisionApplyPhoneVerify: state => state.app.division_apply_phone_verify,
brokerageApplyPhoneVerify: state => state.app.brokerage_apply_phone_verify,
channelApplyPhoneVerify: state => state.app.channel_apply_phone_verify,
supplierApplyPhoneVerify: state => state.app.supplier_apply_phone_verify,
// 购物车数量
cartNum: state => state.indexData.cartNum,
// DIY配置
diyProduct: state => state.app.diyProduct,
diyCategory: state => state.app.diyCategory,
// 功能开关
productVideoStatus: state => state.app.productVideoStatus,
brokerageFuncStatus: state => state.app.brokerageFuncStatus,
// 底部导航数据
tabBarData: state => state.app.tabBarData,
// 分销配置
storeBrokerageStatus: state => state.app.store_brokerage_status,
storeBrokeragePrice: state => state.app.store_brokerage_price,
// 活动弹窗数据
activityModalData: state => state.app.activityModalList,
};
2.4 App模块(核心)
store/modules/app.js:
import { getUserInfo } from "@/api/user.js";
import { getNavigation, systemConfigApi } from '@/api/public.js';
import { getDiyVersion } from "@/api/api.js";
import { getActivityModalListApi } from "@/api/activity";
import {
LOGIN_STATUS,
NON_WIFI_AUTOPLAY,
UID,
USER_INFO
} from '@/config/cache';
import Cache from '@/utils/cache';
// ==================== State ====================
const state = {
// 用户Token
token: Cache.get(LOGIN_STATUS) || false,
// 背景色
backgroundColor: "#fff",
// 用户信息
userInfo: Cache.get(USER_INFO) || {},
// 用户ID
uid: Cache.get(UID) || 0,
// 首页状态
homeActive: false,
// 手机号验证状态
phoneStatus: true,
// 自动播放设置
autoplay: Cache.get(NON_WIFI_AUTOPLAY) || false,
// 商品详情可视化配置
diyProduct: {
navList: [0, 1, 2, 3, 4],
openShare: 1,
pictureConfig: 0,
swiperDot: 1,
showPrice: [0, 1],
isOpen: [0, 1, 2],
showSvip: 1,
showRank: 1,
showService: [0, 1, 2, 3],
showReply: 1,
replyNum: 3,
showMatch: 1,
matchNum: 3,
showRecommend: 1,
recommendNum: 12,
menuList: [0, 1, 2],
showCart: 1,
showCommunity: 0,
communityNum: 3,
showPromoter: 1,
showPromoterType: 0,
menuStatus: 0,
showMenuPromoterShare: 0,
system_channel_style: 1
},
// 商品分类可视化配置
diyCategory: {
level: 2,
index: 0
},
// 分销配置
store_brokerage_status: 1,
store_brokerage_price: '',
// 功能开关
productVideoStatus: true,
brokerage_func_status: 0,
// 底部导航数据
tabBarData: {
menuList: [],
navConfig: {}
},
// 用户身份
identity: Cache.get('identity'),
// 各类申请验证开关
division_apply_phone_verify: Cache.get('division_apply_phone_verify') || 0,
brokerage_apply_phone_verify: Cache.get('brokerage_apply_phone_verify') || 0,
channel_apply_phone_verify: Cache.get('channel_apply_phone_verify') || 0,
supplier_apply_phone_verify: Cache.get('supplier_apply_phone_verify') || 0,
// 说明文案
brokerage_apply_explain: '',
division_apply_explain: '',
channel_apply_explain: '',
supplier_apply_explain: '',
// 活动弹窗列表
activityModalList: [],
// 渠道功能开关
channel_func_status: 0,
};
// ==================== Mutations ====================
const mutations = {
// 设置手机号状态
SETPHONESTATUS(state, val) {
state.phoneStatus = val;
},
// 登录
LOGIN(state, opt) {
state.token = opt.token;
Cache.set(LOGIN_STATUS, opt.token, opt.time);
},
// 设置用户ID
SETUID(state, val) {
state.uid = val;
Cache.set(UID, val);
},
// 更新Token
UPDATE_LOGIN(state, token) {
state.token = token;
},
// 退出登录
LOGOUT(state) {
Cache.clear(LOGIN_STATUS);
Cache.clear(UID);
Cache.clear(USER_INFO);
Cache.clear('newcomerGift');
state.token = undefined;
state.uid = undefined;
state.userInfo = {};
},
// 设置背景色
BACKGROUND_COLOR(state, color) {
state.color = color;
document.body.style.backgroundColor = color;
},
// 更新用户信息
UPDATE_USERINFO(state, userInfo) {
state.userInfo = userInfo;
state.identity = userInfo.identity;
Cache.set(USER_INFO, userInfo);
Cache.set('identity', userInfo.identity);
},
// 打开首页
OPEN_HOME(state) {
state.homeActive = true;
},
// 关闭首页
CLOSE_HOME(state) {
state.homeActive = false;
},
// 设置自动播放
SET_AUTOPLAY(state, data) {
state.autoplay = data;
Cache.set(NON_WIFI_AUTOPLAY, data);
},
// 设置底部导航
SET_PAGE_FOOTER(state, data) {
state.tabBarData = data;
},
// 设置基础配置
SET_BASIC_CONFIG(state, data) {
state.diyProduct = data.product_detail;
state.diyCategory = data.product_category;
state.productVideoStatus = data.product_video_status;
state.brokerageFuncStatus = data.brokerage_func_status;
state.store_brokerage_status = data.store_brokerage_statu;
state.store_brokerage_price = data.store_brokerage_price;
state.system_channel_style = data.system_channel_style;
state.division_apply_phone_verify = data.division_apply_phone_verify == 1;
state.brokerage_apply_phone_verify = data.brokerage_apply_phone_verify == 1;
state.channel_apply_phone_verify = data.channel_apply_phone_verify == 1;
state.supplier_apply_phone_verify = data.supplier_apply_phone_verify == 1;
state.brokerage_apply_explain = data.brokerage_apply_explain;
state.division_apply_explain = data.division_apply_explain;
state.channel_apply_explain = data.channel_apply_explain;
state.supplier_apply_explain = data.supplier_apply_explain;
state.channel_func_status = data.channel_func_status;
// 缓存配置
Cache.set('division_apply_phone_verify', data.division_apply_phone_verify == 1);
Cache.set('brokerage_apply_phone_verify', data.brokerage_apply_phone_verify == 1);
Cache.set('channel_apply_phone_verify', data.channel_apply_phone_verify == 1);
Cache.set('supplier_apply_phone_verify', data.supplier_apply_phone_verify == 1);
},
// 设置活动弹窗
SET_ACTIVITY_MODAL(state, data) {
state.activityModalList = data.list;
Cache.clear('closeModalSwiper');
},
};
// ==================== Actions ====================
const actions = {
// 获取用户信息
USERINFO({ state, commit }, force) {
if (state.userInfo !== null && !force) {
return Promise.resolve(state.userInfo);
}
return new Promise((resolve, reject) => {
getUserInfo().then(res => {
commit("UPDATE_USERINFO", res.data);
Cache.set(USER_INFO, res.data);
resolve(res.data);
}).catch(reject);
});
},
// 获取底部导航配置
async getPageFooter({ commit }) {
let res = await getDiyVersion(0);
if (res.status == 200) {
let diyVersion = uni.getStorageSync('diyVersionNav');
if (res.data.version === diyVersion) {
let pageFooter = uni.getStorageSync('pageFooterData');
commit("SET_PAGE_FOOTER", pageFooter);
} else {
uni.setStorageSync('diyVersionNav', res.data.version);
let result = await getNavigation();
if (result.status == 200) {
commit("SET_PAGE_FOOTER", result.data);
uni.setStorageSync('pageFooterData', result.data);
}
}
}
},
// 获取基础配置
async getBasicConfig({ commit }) {
let result = await systemConfigApi();
if (result.status == 200) {
commit("SET_BASIC_CONFIG", result.data);
}
},
// 获取活动弹窗
async getActivityModal({ commit }) {
let result = await getActivityModalListApi();
if (result.status == 200 && result.data.list && result.data.list.length) {
commit("SET_ACTIVITY_MODAL", result.data);
}
}
};
export default {
state,
mutations,
actions
};
2.5 IndexData模块
store/modules/indexData.js:
import Cache from '@/utils/cache';
const state = {
// 购物车数量
cartNum: Cache.get('cartNum') || 0,
};
const mutations = {
// 设置购物车数量
SET_CART_NUM(state, num) {
state.cartNum = num;
Cache.set('cartNum', num);
},
// 增加购物车数量
ADD_CART_NUM(state, num = 1) {
state.cartNum += num;
Cache.set('cartNum', state.cartNum);
},
// 减少购物车数量
REDUCE_CART_NUM(state, num = 1) {
state.cartNum = Math.max(0, state.cartNum - num);
Cache.set('cartNum', state.cartNum);
}
};
const actions = {
// 更新购物车数量
updateCartNum({ commit }, num) {
commit('SET_CART_NUM', num);
}
};
export default {
state,
mutations,
actions
};
2.6 在页面中使用
<template>
<view class="user-page">
<!-- 使用getters -->
<view v-if="isLogin">
<text>欢迎,{{ userInfo.nickname }}</text>
<text>购物车:{{ cartNum }}</text>
</view>
<view v-else>
<button @click="goLogin">请先登录</button>
</view>
</view>
</template>
<script>
import { mapGetters, mapMutations, mapActions } from 'vuex';
export default {
computed: {
// 使用mapGetters映射
...mapGetters([
'isLogin',
'userInfo',
'uid',
'cartNum',
'token'
]),
// 直接访问state
homeActive() {
return this.$store.state.app.homeActive;
}
},
methods: {
// 映射mutations
...mapMutations([
'LOGIN',
'LOGOUT',
'UPDATE_USERINFO',
'SET_CART_NUM'
]),
// 映射actions
...mapActions([
'USERINFO',
'getBasicConfig'
]),
// 登录处理
async handleLogin(token) {
// 调用mutation
this.LOGIN({ token, time: 86400 });
// 获取用户信息
await this.USERINFO(true);
},
// 退出登录
handleLogout() {
this.$store.commit('LOGOUT');
uni.reLaunch({ url: '/pages/index/index' });
},
// 更新购物车
updateCart(num) {
this.$store.commit('SET_CART_NUM', num);
// 或使用映射的方法
this.SET_CART_NUM(num);
},
goLogin() {
uni.navigateTo({
url: '/pages/users/wechat_login/index'
});
}
},
onLoad() {
// 获取基础配置
this.getBasicConfig();
// 如果已登录,获取用户信息
if (this.isLogin) {
this.USERINFO();
}
}
};
</script>
三、后台管理端状态管理
3.1 目录结构
view/admin/src/store/
├── index.js # Store入口
├── getters.js # 全局getters
└── modules/ # 模块目录
├── user.js # 用户模块
├── app.js # 应用模块
├── permission.js # 权限模块
└── tagsView.js # 标签页模块
3.2 Store入口配置
// store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
import getters from './getters';
import user from './modules/user';
import app from './modules/app';
import permission from './modules/permission';
import tagsView from './modules/tagsView';
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
user,
app,
permission,
tagsView
},
getters
});
3.3 User模块示例
// store/modules/user.js
import { login, logout, getInfo } from '@/api/user';
import { getToken, setToken, removeToken } from '@/utils/auth';
import router from '@/router';
const state = {
token: getToken(),
name: '',
avatar: '',
roles: [],
permissions: []
};
const mutations = {
SET_TOKEN: (state, token) => {
state.token = token;
},
SET_NAME: (state, name) => {
state.name = name;
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar;
},
SET_ROLES: (state, roles) => {
state.roles = roles;
},
SET_PERMISSIONS: (state, permissions) => {
state.permissions = permissions;
}
};
const actions = {
// 登录
login({ commit }, userInfo) {
const { username, password } = userInfo;
return new Promise((resolve, reject) => {
login({ username: username.trim(), password })
.then(response => {
const { data } = response;
commit('SET_TOKEN', data.token);
setToken(data.token);
resolve();
})
.catch(error => {
reject(error);
});
});
},
// 获取用户信息
getInfo({ commit }) {
return new Promise((resolve, reject) => {
getInfo()
.then(response => {
const { data } = response;
if (!data) {
reject('验证失败,请重新登录');
}
const { roles, name, avatar, permissions } = data;
commit('SET_ROLES', roles);
commit('SET_NAME', name);
commit('SET_AVATAR', avatar);
commit('SET_PERMISSIONS', permissions);
resolve(data);
})
.catch(error => {
reject(error);
});
});
},
// 退出登录
logout({ commit }) {
return new Promise((resolve, reject) => {
logout()
.then(() => {
commit('SET_TOKEN', '');
commit('SET_ROLES', []);
commit('SET_PERMISSIONS', []);
removeToken();
router.push('/login');
resolve();
})
.catch(error => {
reject(error);
});
});
},
// 清除Token
resetToken({ commit }) {
return new Promise(resolve => {
commit('SET_TOKEN', '');
commit('SET_ROLES', []);
removeToken();
resolve();
});
}
};
export default {
namespaced: true,
state,
mutations,
actions
};
3.4 Permission模块
// store/modules/permission.js
import { constantRoutes, asyncRoutes } from '@/router';
/**
* 判断用户是否有权限访问该路由
*/
function hasPermission(roles, route) {
if (route.meta && route.meta.roles) {
return roles.some(role => route.meta.roles.includes(role));
}
return true;
}
/**
* 递归过滤异步路由
*/
function filterAsyncRoutes(routes, roles) {
const res = [];
routes.forEach(route => {
const tmp = { ...route };
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles);
}
res.push(tmp);
}
});
return res;
}
const state = {
routes: [],
addRoutes: []
};
const mutations = {
SET_ROUTES: (state, routes) => {
state.addRoutes = routes;
state.routes = constantRoutes.concat(routes);
}
};
const actions = {
// 根据角色生成可访问路由
generateRoutes({ commit }, roles) {
return new Promise(resolve => {
let accessedRoutes;
if (roles.includes('admin')) {
// 超级管理员拥有所有权限
accessedRoutes = asyncRoutes || [];
} else {
// 根据角色过滤路由
accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
}
commit('SET_ROUTES', accessedRoutes);
resolve(accessedRoutes);
});
}
};
export default {
namespaced: true,
state,
mutations,
actions
};
3.5 在后台组件中使用
<template>
<div class="dashboard">
<div class="user-info">
<img :src="avatar" class="avatar">
<span>{{ name }}</span>
</div>
<el-button @click="handleLogout">退出登录</el-button>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
computed: {
...mapGetters([
'name',
'avatar',
'roles',
'permissions'
])
},
methods: {
async handleLogout() {
await this.$store.dispatch('user/logout');
this.$router.push('/login');
},
// 检查权限
hasPermission(permission) {
return this.permissions.includes(permission);
}
}
};
</script>
四、状态管理最佳实践
4.1 模块化设计
// 按功能拆分模块
store/
├── modules/
│ ├── user.js # 用户相关
│ ├── cart.js # 购物车
│ ├── order.js # 订单
│ ├── product.js # 商品
│ └── settings.js # 设置
4.2 命名规范
// Mutations - 使用大写下划线
const mutations = {
SET_USER_INFO: (state, info) => {},
UPDATE_CART_NUM: (state, num) => {},
CLEAR_USER_DATA: (state) => {}
};
// Actions - 使用驼峰命名
const actions = {
getUserInfo({ commit }) {},
updateCartNum({ commit }, num) {},
async fetchOrderList({ commit }) {}
};
4.3 持久化存储
// utils/cache.js
const CACHE_PREFIX = 'crmeb_';
export default {
set(key, value, expire = 0) {
const data = {
value,
expire: expire ? Date.now() + expire * 1000 : 0
};
uni.setStorageSync(CACHE_PREFIX + key, JSON.stringify(data));
},
get(key, defaultValue = null) {
const data = uni.getStorageSync(CACHE_PREFIX + key);
if (!data) return defaultValue;
try {
const { value, expire } = JSON.parse(data);
if (expire && Date.now() > expire) {
uni.removeStorageSync(CACHE_PREFIX + key);
return defaultValue;
}
return value;
} catch (e) {
return defaultValue;
}
},
clear(key) {
if (key) {
uni.removeStorageSync(CACHE_PREFIX + key);
} else {
uni.clearStorageSync();
}
}
};
// 在Store中使用
const state = {
token: Cache.get('token') || '',
userInfo: Cache.get('userInfo') || {}
};
const mutations = {
SET_TOKEN(state, token) {
state.token = token;
Cache.set('token', token, 86400); // 24小时过期
}
};
4.4 异步操作处理
const actions = {
// 推荐:使用async/await
async fetchUserInfo({ commit }) {
try {
const res = await getUserInfo();
commit('SET_USER_INFO', res.data);
return res.data;
} catch (error) {
console.error('获取用户信息失败:', error);
throw error;
}
},
// 组合多个action
async initApp({ dispatch }) {
await dispatch('getBasicConfig');
await dispatch('getPageFooter');
if (this.getters.isLogin) {
await dispatch('fetchUserInfo');
}
}
};
五、数据流图示
┌─────────────────────────────────────────────────────────┐
│ Vue组件 │
├─────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────┐ │
│ │ computed / mapGetters │ │
│ │ ┌───────────────────────────────────────────┐ │ │
│ │ │ isLogin, userInfo, cartNum, ... │ │ │
│ │ └───────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ ▲ │
│ │ 读取 │
│ │ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Getters │ │
│ │ ┌───────────────────────────────────────────┐ │ │
│ │ │ token, isLogin, userInfo, ... │ │ │
│ │ └───────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ ▲ │
│ │ 计算 │
│ │ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ State │ │
│ │ ┌───────────────────────────────────────────┐ │ │
│ │ │ { token, userInfo, cartNum, ... } │ │ │
│ │ └───────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ ▲ │
│ │ 修改 │
│ │ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Mutations │ │
│ │ ┌───────────────────────────────────────────┐ │ │
│ │ │ SET_TOKEN, UPDATE_USERINFO, ... │ │ │
│ │ └───────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ ▲ │
│ │ commit │
│ │ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Actions │ │
│ │ ┌───────────────────────────────────────────┐ │ │
│ │ │ USERINFO, getBasicConfig, ... │ │ │
│ │ └───────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ ▲ │
│ │ dispatch │
│ │ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ methods / mapActions │ │
│ │ ┌───────────────────────────────────────────┐ │ │
│ │ │ handleLogin, handleLogout, ... │ │ │
│ │ └───────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
六、常见问题
6.1 刷新页面状态丢失
// 解决方案:持久化关键状态
const state = {
token: Cache.get(LOGIN_STATUS) || false,
userInfo: Cache.get(USER_INFO) || {}
};
// 在App.vue中初始化
onLaunch() {
// 从缓存恢复状态
if (this.$store.getters.token) {
this.$store.dispatch('USERINFO');
}
}
6.2 跨页面状态同步
// 使用uni.$emit和uni.$on
// 页面A - 发送事件
uni.$emit('cartUpdated', { num: 10 });
// 页面B - 监听事件
onLoad() {
uni.$on('cartUpdated', (data) => {
this.$store.commit('SET_CART_NUM', data.num);
});
}
onUnload() {
uni.$off('cartUpdated');
}
6.3 避免直接修改State
// 错误做法
this.$store.state.app.userInfo.name = 'new name';
// 正确做法
this.$store.commit('UPDATE_USERINFO', {
...this.$store.state.app.userInfo,
name: 'new name'
});
6.4 模块化状态访问报错
// 问题:namespaced模块访问方式错误
// 错误写法
this.$store.dispatch('fetchUserInfo');
// 正确写法(带命名空间)
this.$store.dispatch('app/fetchUserInfo');
// 或使用mapActions辅助函数
...mapActions('app', ['fetchUserInfo'])
6.5 异步Action中处理错误
// 建议使用try-catch处理异步错误
async fetchData({ commit }) {
try {
const res = await api.getData();
commit('SET_DATA', res.data);
} catch (error) {
console.error('获取数据失败:', error);
// 可以commit一个错误状态
commit('SET_ERROR', error.message);
}
}
6.6 Getter依赖多个State值
// 复杂getter示例
getters: {
cartTotalPrice: (state, getters) => {
return state.cartList.reduce((total, item) => {
return total + item.price * item.num;
}, 0);
}
}
注意事项
- 严格模式:开发环境建议开启 Vuex 的 strict 模式,可以检测到非 mutation 修改 state 的情况
- 模块命名空间:使用
namespaced: true时,访问 action、mutation、getter 都需要带上模块名前缀 - 缓存与状态同步:使用 Cache 持久化状态时,要注意缓存过期和状态初始化的时机
- 避免状态冗余:可以通过 getter 计算得出的值不要存储在 state 中,保持 state 的精简
- 组件销毁清理:使用
uni.$on监听事件后,必须在onUnload中使用uni.$off取消监听,防止内存泄漏 - 异步操作放Action:所有异步操作(API请求、定时器等)只能在 Action 中执行,Mutation 必须是同步的
- 大型应用模块化:随着应用规模增长,及时将 store 拆分为独立模块,便于维护和协作开发
- 调试工具:Vue Devtools 可以查看 Vuex 状态变化历史,开发时建议安装使用
评论({{cateWiki.comment_num}})
最新
最早
{{cateWiki.page_view_num}}人看过该文档
评论(0)
最新
最早
110人看过
登录/注册
即可发表评论
{{item.user ? item.user.nickname : ''}}
(自评)
{{item.content}}
搜索结果
为您找到{{wikiCount}}条结果
{{item.page_view_num}}
{{item.like ? item.like.like_num : 0}}
{{item.comment ? item.comment.comment_num : 0}}
位置:
{{path.name}}
{{(i+1) == item.catalogue.path_data.length ? '':'/'}}
