Vue全家桶系列之Vuex(三)

1.前言

上篇文章介绍了Vuex的核心概念,这篇文章来说下Vuex的一些进阶内容!

2.模块的命名空间

如果把所有的状态都放在一个store对象里面,当应用变得非常复杂时,store 对象就有可能变得相当臃肿,维护起来也会变得相当复杂,所以我们可以用模块来划分,比如A module,B module,在项目中很有可能A module内部的 action、mutation 和 getter 可能和B module内部的 action、mutation 和 getter方法名定义一样,但是这些方法又都是全局的,那么假如提交一次mutation,A module和B module 的mutation都会执行,这肯定不行,所以可以用模块的命名空间来区分,可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。下面我们来定义A module模拟一下,还是用之前的例子!

import vue from "vue"
import vuex from "vuex"

vue.use(vuex);

let moduleA = {
  namespaced: true,
  state: {
    num: 100,
  },
  getters: {
    filterNum: (state, getters, rootState) => {
      console.log(getters, rootState)
      return state.num > 120 ? 120 : state.num
    }
  },
  mutations: {
    addNumber(state, payload) {
      state.num += payload.n;
    },
    substractNumber(state, payload) {
      state.num -= payload.n;
    }
  },
  actions: {
    addActionA({commit}, payload) {
      setTimeout(() => {
        commit({
          type: "addNumber",
          n:payload.n
        });
      }, 1000)
    },
    addActionB({dispatch}, payload) {
      setTimeout(() => {
        dispatch({
          type: "addActionA",
          n:payload.n
        });
      }, 1000)
    },
    addActionC({dispatch}, payload) {
      setTimeout(() => {
        dispatch({
          type: "addActionB",
          n:payload.n
        });
      }, 1000)
    }
  }
};


let stroe = new vuex.Store({
  modules: {
    moduleA
  }
});

export default stroe;

既然用了模块命名空间,那么组件里面也需要修改

<template>
  <div>
    <h2>GrandSon组件的number值:{{ computedNum }}</h2>
    <!-- GrandSon组件的gettersNumber值{{num1}} -->
    <button @click="addNumber">点击我改变number值</button>
    <button @click="substractNumber">点击我改变number值</button>
  </div>
</template>

<script>
export default {
  name: "GrandSon",
  props: ["childNumber"],
  computed: {
    computedNum() {
      return this.$store.getters["moduleA/filterNum"];
    },
    num() {
      return this.$store.state.moduleA.num;
    }
  },

  methods: {
    addNumber() {
      this.$store.dispatch("moduleA/addActionC", { n: 5 });
    },
    substractNumber() {
       this.$store.commit("moduleA/substractNumber", {
        n: 10,
      });
    }
  },
};
</script>

当然你也可以使用 mapState, mapGetters, mapActions 和 mapMutations 这些函数来绑定带命名空间的模块

<template>
  <div>
    <h2>GrandSon组件的number值:{{ computedNum }}</h2>
    <!-- GrandSon组件的gettersNumber值{{num1}} -->
    <button @click="addNumber({n:5})">点击我改变number值</button>
    <button @click="substractNumber({n:10})">点击我改变number值</button>
  </div>
</template>

<script>
import { mapState, mapGetters, mapActions, mapMutations } from "vuex";
export default {
  name: "GrandSon",
  props: ["childNumber"],
  computed: {
    ...mapGetters({
      computedNum: "moduleA/filterNum",
    }),
    ...mapState({
      num(state) {
        return state.moduleA.num;
      },
    }),
  },

  methods: {
    ...mapActions({
      addNumber: "moduleA/addActionC",
    }),
    ...mapMutations({
      substractNumber: "moduleA/substractNumber",
    }),
  },
};
</script>

或者你还可以这样写,你可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文,这样写会比较容易理解!

<template>
  <div>
    <h2>GrandSon组件的number值:{{ computedNum }}</h2>
    <!-- GrandSon组件的gettersNumber值{{num1}} -->
    <button @click="addNumber({n:5})">点击我改变number值</button>
    <button @click="substractNumber({n:10})">点击我改变number值</button>
  </div>
</template>

<script>
import { mapState, mapGetters, mapActions, mapMutations } from "vuex";
export default {
  name: "GrandSon",
  props: ["childNumber"],
  computed: {
     ...mapGetters("moduleA",{
      computedNum: "filterNum",
    }),
     ...mapState("moduleA", {
      num(state) {
        return state.num;
      },
    })
  },

  methods: {
     ...mapActions("moduleA", {
      addNumber: "addActionC",
    }),
     ...mapMutations("moduleA",["substractNumber"]),
  },
};
</script>

你还可以通过使用 createNamespacedHelpers 创建基于某个命名空间辅助函数,这样写又简洁又明了,不过缺点是模块多了,需要创建多个命名空间辅助函数!

<template>
  <div>
    <h2>GrandSon组件的number值:{{ computedNum }}</h2>
    <!-- GrandSon组件的gettersNumber值{{num1}} -->
    <button @click="addNumber({n:5})">点击我改变number值</button>
    <button @click="substractNumber({n:10})">点击我改变number值</button>
  </div>
</template>

<script>
import { createNamespacedHelpers } from 'vuex'
//创建命名空间辅助函数namespaceA
let namespaceA = createNamespacedHelpers('moduleA');
export default {
  name: "GrandSon",
  props: ["childNumber"],
  computed: {
     ...(namespace.mapGetters)({
      computedNum: "filterNum",
    }),
     ...(namespace.mapState)(["num"])
  },

  methods: {
     ...(namespace.mapActions)({
      addNumber:"addActionC"
    }),
     ...(namespace.mapMutations)(["substractNumber"])
  },
};
</script>

一般划分模块时,有些时候会划分出公共模块,可以直接把它放到根路径下,这样其他的模块可以访问到公共模块的一些方法,来模拟一下

import vue from "vue"
import vuex from "vuex"

vue.use(vuex);

let moduleA = {...};
let stroe = new vuex.Store({
//公共模块
  state: {
    num: 20
  },
  getters: {
    filterNum: (state, getters) => {
      return state.num
    }
  },
  mutations: {
    addNumber(state, payload) {
      state.num += 2 * (payload.n);
    },
    substractNumber(state, payload) {
      state.num -= 2 * (payload.n);
    }
  },
  actions: {
    addActionC({
      commit
    }, payload) {
      setTimeout(() => {
        commit('moduleA/addNumber',{
          n: payload.n
        });
      }, 100)
    }
  },
  modules: {
    moduleA
  }
});

如果你希望使用全局的state 和 getter,rootState 和 rootGetters 会作为第三和第四参数传入 getter,也会通过 context 对象的属性传入 action。若需要分发全局的 action 或提交 mutation,将 { root: true } 作为第三参数传给 dispatch 或 commit 即可!

import vue from "vue"
import vuex from "vuex"

vue.use(vuex);

let moduleA = {
  namespaced: true,
  state: {
    num: 100,
  },
  getters: {
    filterNum: (state, getters, rootState, rootGetters) => {
    //调用全局命名空间的filterNum方法
      return rootGetters.filterNum + state.num > 120 ? 120 : state.num
    }
  },
  mutations: {
    addNumber(state, payload) {
      state.num += payload.n;
    },
    substractNumber(state, payload) {
      state.num -= payload.n;
    }
  },
  actions: {
    addActionA({
      commit
    }, payload) {
      setTimeout(() => {
        commit({
          type: "addNumber",
          n: payload.n
        });
      }, 100)
    },
    addActionB({
      dispatch
    }, payload) {
      setTimeout(() => {
        dispatch({
          type: "addActionA",
          n: payload.n
        });
      }, 100)
    },
    addActionC({
      dispatch,rootGetters 
    }, payload) {
    	//全局命名空间内分发action 
        dispatch("addActionC",{
          n: payload.n
        },{ root: true });
    }
  }
};

let stroe = new vuex.Store({
  state: {
    num: 20
  },
  getters: {
    filterNum: (state, getters) => {
      return state.num
    }
  },
  mutations: {
    addNumber(state, payload) {
      state.num += 2 * (payload.n);
    },
    substractNumber(state, payload) {
      state.num -= 2 * (payload.n);
    }
  },
  actions: {
    addActionC({
      commit
    }, payload) {
      setTimeout(() => {
        commit('moduleA/addNumber',{
          n: payload.n
        });
      }, 100)
    }
  },
  modules: {
    moduleA
  }
});

export default stroe;

来看下执行效果:
在这里插入图片描述
之前之所以用了命名空间,是因为默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的,如果你又想用命名空间,又想在带命名空间的模块注册全局 action,你可以这么写:

//moduleA的actions
actions: {
  addRootAction: {
    root:true,
    handler({
      commit
    }, payload) {
      commit("addNumber", payload);
    }
  }
}

那么组件分发的时候就可以不需要加上命名空间的路径了,直接分发全局的action

...mapActions({
	//如果不是全局的就需要这么写  addNumber: "moduleA/addRootAction",
   addNumber: "addRootAction",
 }),

3.模块的动态注册

有些时候我们需要动态添加状态时,vuex中的数据就不能提前写好,所以需要模块动态注册方式实现,动态注册用store.registerModule,既然有注册就有卸载是把,卸载用store.unregisterModule,你可以通过 store.hasModule(moduleName) 方法检查该模块是否已经被注册到 store!下面来模拟下!
在这里插入图片描述

   <button @click="registerModuleB">注册moudleB模块</button>
   <button @click="unregisterModuleB">卸载moudleB模块</button>
methods:{
    registerModuleB() {
   		 //注册模块B
      this.$store.registerModule("moudleB", {
        state: {
          numB: 20,
        },
      });
      //看下store是否有模块B
      console.log(this.$store.hasModule("moudleB"));
    },
    unregisterModuleB() {
    	//卸载模块B
      this.$store.unregisterModule("moudleB");
       //看下store是否有模块B
      console.log(this.$store.hasModule("moudleB"));
    }
}

点击注册和卸载,看右边打印的结果:
在这里插入图片描述
在注册一个新 module 时,你很有可能想保留过去的 state,你可以通过 preserveState来保留过去的 state:store.registerModule('a', module, { preserveState: true })。当你设置 preserveState: true 时,该模块会被注册,action、mutation 和 getter 会被添加到 store 中,但是 state 不会。这里假设 store 的 state 已经包含了这个 module 的 state 并且你不希望将其覆写,我们来模拟下,还是用上面的例子,假如我注册一个moudleB模块,后面我又注册一个moudleB模块,按正常逻辑应该是后面的覆盖前面的!

  registerModuleB() {
      this.$store.registerModule("moudleB", {
        state: {
          numB: 20,
        },
        mutations: {
          test() {
            console.log("test");
          },
        },
      });

      this.$store.registerModule("moudleB", {
        state: {
          numB: 200,
        },
        mutations: {
          test1() {
            console.log("test1");
          },
        },
      });
      console.log(this.$store.state.moudleB.numB);
    },

点击发现后面的确实覆盖了前面,vue也给了相关的提示说"moudleB"被一个同名的模块覆盖 !
在这里插入图片描述

把 preserveState: true加上再来看下

    registerModuleB() {
      this.$store.registerModule("moudleB", {
        state: {
          numB: 20,
        },
        mutations: {
          test() {
            console.log("test");
          },
        },
      });

      this.$store.registerModule("moudleB", {
        state: {
          numB: 200,
        },
        mutations: {
          test1() {
            console.log("test1");
          },
        },
      },{preserveState: true});
      console.log(this.$store.state.moudleB.numB);
    }

发现state的numB还是20,说明state并没有覆盖保留了下来被保留了下来!并且也没有vue的警告提示了!
在这里插入图片描述

4.vuex严格模式

我们在开发环境下,可以使用vuex的严格模式,因为它会在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到,严格模式会深度监测状态树来检测不合规的状态变更,请确保在发布环境下关闭严格模式,以避免性能损失

  methods: {
    addNumber(){
      this.$store.state.moduleA.num++;
    }
  }

vuex提示报错,大概意思是vuex store state的变化不能是由 mutation 函数以外引起的
在这里插入图片描述

5.vuex插件

了解插件之前,首先来介绍几个不常用的函数,可能你用vuex很少用的上,但是写插件的话就经常用的上了!

5.1 subscribe

这个是一个订阅 store 的 mutation函数,也就是当commit一个mutation之后,这个函数就会执行,这个函数会在每个 mutation 完成后调用,接收 mutation 和经过 mutation 后的状态作为参数,如果改变了状态可能要做一些后续处理就会用到这个函数!

store.subscribe((mutation, state) => {
  console.log(mutation.type)
  console.log(mutation.payload)
})

如果有多个订阅 store 的 mutation函数它会从上往下执行,但是你想某个订阅函数第一个执行,可以加上prepend: true属性让它第一个执行,当然你也可以写在最上面,它就第一个执行

 store.subscribe((mutation, state) => {
   console.log(1);
  })
  store.subscribe((mutation, state) => {
    console.log(2);
  })

在这里插入图片描述

 store.subscribe((mutation, state) => {
   console.log(1);
  })
  store.subscribe((mutation, state) => {
    console.log(2);
  },{prepend: true})

在这里插入图片描述
如果要取消订阅,调用此方法返回的函数即可停止订阅!

  let offSubscribe  =  store.subscribe((mutation, state) => {
   console.log(1);
  })
 
  offSubscribe();
5.2 subscribeAction

这个和subscribe类似,subscribeAction接受的参数是action和当前的 store,要注意一点是subscribeAction的订阅函数会在action的所有代码之前执行!subscribe的订阅函数mutation之后执行!(默认行为是之前),如果你也想改成 action之后执行也可以,需要加个after属性(3.1.0 以上版本支持)!

 store.subscribeAction({
   after: (action, state) => {
     console.log(action.type)
     console.log(action.payload)
   }
 })

subscribeAction 也可以指定一个 error 处理函数以捕获分发 action 的时候被抛出的错误。该函数会从第三个参数接收到一个 error 对象(3.4.0 以上版本支持)

store.subscribeAction({
  error: (action, state, error) => {
    console.log(`error action ${action.type}`)
    console.error(error)
  }
})

如果需要订阅函数第一个执行也是通过加prepend: true,要停止订阅,调用此方法返回的函数即可停止订阅,这些和subscribe一样

5.3 replaceState

这个函数是用于替换 store 的根状态,我有些时候希望改变这个状态后直接替换掉stroe状态,比如登录界面,用户登录了,如果刷新页面是不是又要输入用户信息,这样很不方面,一个解决方案就是使用loacalStorage随时储存state,刷新页面出发created则判断localStorage.state是否有内容,有则覆盖原state!

store.replaceState(state)
5.3 watch

这个方法的作用就是监听state或getters的变化,它实际上跟vue实例的watch作用差不多,vue实例的watch监听更倾向于监听data的变化,vuex实例的watch监听state的代码更容易维护!

store.watch((state,getter) => {
    return state
  }, (newValue) => {
   //todo  state改变之后的处理
  //...
  },{deep:true})
5.4 手写一个插件

还是用之前的例子,但是状态做完处理之后需要保存到localStorage,如果刷新,还是最新改变之后的状态,类似一个状态持久化插件!

let localStoragePlugin = store => {
  // 当 store 初始化后调用
  store.subscribe((mutation, state) => {
    let data = state;
    localStorage && localStorage.setItem("data", JSON.stringify(data))
  })
}

上面代码意思是当状态发生改变后,如果浏览器支持localStorage就保存到localStorage里面 ,当然你也可以用watch,注意的是这个是状态对象,所以要深度监听才能监听到状态的改变!

let localStoragePlugin = store => {
  store.watch((state) => {
    return state
  }, (newValue) => {
    let data = newValue;
    localStorage && localStorage.setItem("data", JSON.stringify(data));
  },{deep:true})
}

那么组件created时候也需要从localStorage去取这个状态,并且替换到vuex的根状态!

//GrandSon.vue
  created() {
    let localState = JSON.parse("data" && window.localStorage.getItem("data")),
      storeState = this.$store.state;
    if (localState) {
      // 通过 Vuex 内置的 store.replaceState 方法修改 store.state,{ ...storeState, ...localState }是es6的扩展运算符写法,意思是合并对象
      this.$store.replaceState({ ...storeState, ...localState });
    }
  }

那么一个简单的插件写好之后,需要注册到vuex实例上!

import vue from "vue"
import vuex from "vuex"
let stroe = new vuex.Store({
  strict: true,
  state: {
    //...
  },
  getters: {
    //...
  },
  mutations: {
    //...
  },
  actions: {
    //...
  },
  modules: {
    moduleA
    // moduleB
  },
  plugins: [localStoragePlugin ]
});

F12把localStorage打开,再来看下效果:
在这里插入图片描述
当然这样插件很多,我们可以直接用别人的,上面只是写了一个非常简单的插件作为例子,有很多方面没考虑全面,我们可以用别人写好的vuex-persistedstate插件,使用起来非常简单,默认保存到localStorage,引入之后,然后在插件里面注册就可以直接使用了!(详细的用法可以点击这里

import vue from "vue"
import vuex from "vuex"
import createPersistedState from "vuex-persistedstate"
vue.use(vuex);
let stroe = new vuex.Store({
  strict: true,
  state: {
    //...
  },
  getters: {
    //...
  },
  mutations: {
    //...
  },
  actions: {
    //...
  },
  modules: {
    moduleA
    // moduleB
  },
  plugins: [createPersistedState()]
});

6.vuex表单处理

先看下面代码:

<template>
  <div>
    <input type="text" v-model="num"  @input="updateMessage"/>
  </div>
</template>

<script>
import { createNamespacedHelpers } from "vuex";
let namespace = createNamespacedHelpers("moduleA");
export default {
  name: "GrandSon",
  computed: {
    ...namespace.mapState(["num"]),
  },
  methods:{
    updateMessage(e) {
      this.$store.state.moduleA.num = e.target.value;
    },
  }
};
</script>

当我改变input输入框的值会报错,在用户输入时,v-model 会试图直接修改状态。在严格模式中,由于这个修改不是在 mutation 函数中执行的, 这里会抛出一个错误(一样的错误,vuex严格模式说到过)。
在这里插入图片描述
用“Vuex 的思维”去解决这个问题的方法是:给 <input> 中绑定 value,然后侦听 input 或者 change 事件,在事件回调中调用一个方法

<template>
  <div>
   <input type="text" :value="num" @input="updateMessage" />
  </div>
</template>

<script>
import { createNamespacedHelpers } from "vuex";
let namespace = createNamespacedHelpers("moduleA");
export default {
  name: "GrandSon",
  computed: {
    ...namespace.mapState(["num"]),
  },
  methods: {
   updateMessage(e) {
       this.$store.commit("moduleA/updateMessage", e.target.value);
    }
  }
};
</script>

然后需要在mutations添加一个事件类型 (type) 和 一个 回调函数 (handler)

import vue from "vue"
import vuex from "vuex"
vue.use(vuex);
let moduleA = {
  namespaced: true,
  state: {
    num: 100,
  },
  mutations: {
  	updateMessage (state, payload) {
      state.num = payload;
      //改变状态之后,打印出状态
      console.log(state.num)
    }
  }
};

let stroe = new vuex.Store({
  strict: true,
  modules: {
    moduleA
  }
});

看下面效果,注意右边打印的状态也发生了变化!
在这里插入图片描述

另一个方法是使用带有 setter 的双向绑定计算属性,这样写的话会更简洁(推荐使用这种方式处理)

```javascript
<template>
  <div>
   <input type="text" :value="num" @input="updateMessage" />
  </div>
</template>

<script>
export default {
  name: "GrandSon",
  computed: {
    num: {
     get() {
        return this.$store.state.moduleA.num;
      },
      set(value) {
        this.$store.commit("moduleA/updateMessage", value);
      }
    }
  }
};
</script>

当输入值发生改变会调用set方法提交mutation触发updateMessage回调函数改变状态,因为get方法里面依赖了状态,所以状态发生改变又会执行get方法,然后把最新的状态通过组件渲染到视图上,效果和上面的效果是一样的!
在这里插入图片描述

7.总结

我们再来回顾下流程图:
在这里插入图片描述
学到这里,这个流程图已经能完全看懂了吧,首先请求后端的接口(Backend Api),通过组件的Dispatch actions(分发actions)执行action方法,然后action不能直接改变状态,需要commit Mutations,可以通过Devtools工具来进行调试或者做时光旅行,提交Mutations之后状态发生改变就需要通知组件渲染,然后改变之后的状态就呈现在视图上,这个是vuex的一个基本流程,好了,vuex说到这里就差不多了,其实这些知识已经完全够正常的vuex开发了,还有一些测试啊,热重载啊(为了让开发更加便捷,配置了就热重载不会刷新页面而是动态地加载或热重载配置了的模块),有兴趣的可以官网去研究下,这里就不详细说了,下篇文章会讨论Axios,如果觉得写好的,可以点个赞,随便点个关注,谢谢!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值