在vue中遇到共享数据,会带来下面的多个问题:
- 如何保证数据的唯一性?
- 如果数据不唯一,则会浪费大量的内存资源,降低运行效率
- 如果数据不唯一,就可能出现不统一的数据,难以维护
- 某个组件改动数据后,如何让其他用到该数据的组件知道数据变化了?
- 事件总线貌似可以解决该问题,但需要在组件中手动的维护监听,极其不方便,而且事件总线的目的在于「通知」,而不是「共享数据」
一种比较容易想到的方案,就是把所有的共享数据全部提升到根组件,然后通过属性不断下发,当某个组件需要修改数据时,又不断向上抛出事件,直到根组件完成对数据的修改。
这种方案的缺陷也非常明显:
- 需要编写大量的代码层层下发数据,很多组件被迫拥有了自己根本不需要的数据
- 需要编写大量的代码层层上抛事件,很多组件被迫注册了自己根本处理不了的事件
基于上面的问题,我们可以简单的设置一个独立的数据仓库。
-
组件需要什么共享数据,可以自由的从仓库中获取,需要什么拿什么。
-
组件可以自由的改变仓库中的数据,仓库的数据变化后,会自动通知用到对应数据的组件更新
要实现这一切,可以选择vuex
创建仓库
先安装vuex
# 为了保证和课程一致,请安装3.6.2版本
npm i vuex@3.6.2
安装vuex后,可以通过下面的代码创建一个数据仓库,在大部分情况下,一个工程仅需创建一个数据仓库
import Vuex from 'vue';
import Vue from 'vue';
Vue.use(Vuex); // 应用vuex插件
const store = new Vuex.Store({
// 仓库的配置
state: {
// 仓库的初始状态(数据)
count: 0,
},
});
export default store;
仓库创建好后,你可以使用store.state来访问仓库中的数据
如果希望在vue中方便的使用仓库数据,需要将vuex作为插件安装
// store.js
import Vuex from 'vue';
import Vue from 'vue';
Vue.use(Vuex); // 安装Vuex插件
const store = new Vuex({
// 仓库的配置
state: {
// 仓库的初始状态(数据)
count: 0,
},
});
export default store;
// main.js
import Vue from 'vue';
import App from './App.vue';
import store from './store.js';
new Vue({
store, // 向vue中注入仓库
render: (h) => h(App),
}).$mount('#app');
之后,在vue组件中,可以通过实例的$store属性访问到仓库
Vuex会自动将配置的状态数据设置为响应式数据,当数据变化时,依赖该数据的组件会自动渲染。
数据的变更
尽管可以利用数据响应式的特点直接变更数据,但这样的做法在大型项目中会遇到问题
如果有一天,你发现某个共享数据是错误的,而有一百多个组件都有可能变更过这块数据,你该如何知道是哪一步数据变更出现了问题?
为了能够更好的跟踪数据的变化,vuex强烈建议使用mutation来更改数据
const store = new Vuex({
// 仓库的配置
state: {
// 仓库的初始状态(数据)
count: 0,
},
mutations: {
/**
* 每个mutation是一个方法,它描述了数据在某种场景下的变化
* increase mutation描述了数据在增加时应该发生的变化
* 参数state为当前的仓库数据
*/
increase(state) {
state.count++;
},
decrease(state) {
state.count--;
},
/**
* 求n次幂
* 该mutation需要一个额外的参数来提供指数
* 我们把让数据产生变化时的附加信息称之为负荷(负载) payload
* payload可以是任何类型,数字、字符串、对象均可
* 在该mutation中,我们约定payload为一个数字,表示指数
*/
power(state, payload) {
state.count **= payload;
},
},
});
当我们有了mutation后,就不应该直接去改动仓库的数据了
而是通过store.commit方法提交一个mutation,具体做法是
同步操作用下面的方法
store.commit('mutation的名字', payload);
现在,我们可以通过vue devtools观测到数据的变化了
**特别注意: **
-
mutation中不得出现异步操作在实际开发的规范中,甚至要求不得有副作用操作
副作用操作包括:
- 异步
- 更改或读取外部环境的信息,例如
localStorage、location、DOM等
-
提交
mutation是数据改变的唯一原因
异步处理
如果在vuex中要进行异步操作,需要使用action
const store = new Vuex({
state: {
count: 0,
},
mutations: {
increase(state) {
state.count++;
},
decrease(state) {
state.count--;
},
power(state, payload) {
state.count **= payload;
},
},
actions: {
/**
* ctx: 类似于store的对象
* payload: 本次异步操作的额外信息
*/
asyncPower(ctx, payload) {
setTimeout(function () {
ctx.commit('power', payload);
}, 1000);
},
},
});
异步操作用下面的方法 异步操作的方法放在action里面,action里面的方法用dispatch调用。
// 其中payload 是参数
this.$store.dispatch("asyncPower",payload);
简单使用仓库有三个步骤
- 完成store 里面的 state,mutations,action 里面的代码,导出这个配置对象
- 在main.js 上面配置stroe 对象
- 在组件里面使用 store.state里面的数据可以直接访问使用。但是不能直接修改,其中stroe完成后所有的操作都要通过mutations 或者而actions的方法去访问而不是直接修改state里面的数据。
vue
watch(函数,回调函数,watch的配置),第一个参数是监控函数的返回值。这种监控注意要自己销毁。其中store里面也有watch函数和Vue.prototype 一样。
created() {
this.unWatch = this.$store.watch(
() => this.$store.getters["loginUser/status"],
(status) => {
if (status !== "loading") {
this.$router
.push(this.$route.query.returnurl || "/home")
.catch(() => {});
}
},
{
immediate: true,
}
);
},
destroyed() {
this.unWatch();
},
vuex
在vuex里面的state 相当于组件里面的data,而vuex 的get相当于 组件里面的computed. 最常见的做法是从组件里面取vuex取state和get的东西,这样就出现了mapState 和 mapGetters 这俩辅助工具来帮我们快速和简写一些东西,这俩仅此而已,当然我们也可以直接自己去取也是可以的,使用方法有很多种,具体的使用方法可以见下面的文档。
getters getters 类似于组件里面的计算属性。
mapState mapGetters
computed: mapState("loginUser", ["loading"]),
上面其中loginUser 是文件名,loading 是state里面的一个值。
其中mapState导出的结构类似于下面,所以直接取可以通过this.$store.state.loginUser.loading 这种方式去取。
/**
* {
* loading(){
* return this.$store.state.loginUser.loading
* },
* user(){
* return this.$store.state.loginUser.user
* }
* }
*
* */
mapGetters 其实mapGetters和mapState用起来基本一样。
computed: {
...mapGetters("loginUser", ["status"]),
...mapState("loginUser", ["user"]),
},
modules
modules 核心是为了解决将不同的数据放在不同的文件中,有利于让代码更清晰。还可以避免不同的模块里面的get state里面的方法重名的问题。 modules
登录逻辑,鉴权和路由守卫
用户模块逻辑示意图
路由总体示意图
鉴权守卫逻辑示意图
整体来说逻辑是从箭头位置开始,思路其实在路由跳转前进行一系列逻辑处理,具体见下面的代码
import VueRouter from "vue-router";
import routes from "./routes";
import Vue from "vue";
import store from "../store";
Vue.use(VueRouter);
const router = new VueRouter({
routes,
mode: "history",
});
router.beforeEach((to, from, next) => {
// 每当导航切换时(包含刷新页面的第一次),该函数会运行
// from:之前的路由对象 (this.$route)
// to: 即将进入的路由对象 (this.$route)
// next: 确认导航的一个函数 调用该函数(无参),就会直接进入to, 调用该函数(传入参数),根据传入参数进入新的导航
if (to.meta.auth) {
// 需要鉴权,进入鉴权流程
const status = store.getters["loginUser/status"];
if (status === "loading") {
// 加载中,无法确定是否已经登录
next({
path: "/loading",
query: {
returnurl: to.fullPath,
},
});
} else if (status === "login") {
// 登录过了
next();
} else {
// 未登录
alert("该页面需要登录,你还没有登录,请先登录");
next({
path: "/login",
query: {
returnurl: to.fullPath,
},
});
}
} else {
next();
}
});
export default router;
import Home from "../views/Home.vue";
import Login from "../views/Login.vue";
import News from "../views/News.vue";
import User from "../views/User.vue";
import Loading from "../views/Loading.vue";
export default [
{
path: "/",
component: Home,
},
{ path: "/loading", component: Loading },
{
path: "/news",
component: News,
meta: {
auth: true,
},
},
{ path: "/login", component: Login },
{
path: "/user",
component: User,
meta: {
auth: true,
},
},
];
上面有一个细节就是如果在一开始在请求服务器判断是否登录,可能需要等待,然后这时跳转到了一个loading界面,这时就需要在loading界面监听是否处理成功,处理成功就跳转原来的界面,这是就需要把原来界面的路径通过url的路径带过来。类似的还有就是如果当前界面需要登录,需要先跳转到登录页面,然后再调回到原来的界面,这是到登录页面也需要传递参数过来,这时就有问题了,有时只路径完全匹配才能高亮显示,这时候就需要使用按照下面的方式去处理。
#nav a.router-link-exact-active {
color: #42b983;
}
<router-link v-else to="/login" exact-path>Login</router-link>
exact-path 文档见下面。 exact-path