Vue3状态管理器Vuex4.0的进阶使用方案!
前言
大家好,我是CoderBin。Vuex4 是 Vue3 的一个状态管理库,它可以将项目所需的数据统一进行存储,并且任意组件都可以通过 Vuex 的方法去访问到数据,修改数据。关于 Vuex4 的基本使用可以前往:# 全面拥抱Vue3,Vuex4 最新详解教程!
接下来介绍一些 Vue4 中的一些进阶使用,希望对大家有所帮助,谢谢。
如果文中有不对、疑惑的地方,欢迎在评论区留言指正????
一、项目结构
Vuex 并不限制你的代码结构。但是,它规定了一些需要遵守的规则:
应用层级的状态应该集中到单个 store 对象中。
提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。
异步逻辑都应该封装到 action 里面。
只要你遵守以上规则,如何组织代码随你便。如果你的 store 文件太大,只需将 action、mutation 和 getter 分割到单独的文件。
对于大型应用,我们会希望把 Vuex 相关代码分割到模块中。下面是项目结构示例:
├── index.html ├── main.js ├── api │ └── ... # 抽取出API请求 ├── components │ ├── App.vue │ └── ... └── store ├── index.js # 我们组装模块并导出 store 的地方 ├── actions.js # 根级别的 action ├── mutations.js # 根级别的 mutation └── modules ├── cart.js # 购物车模块 └── products.js # 产品模块 复制代码
二、组合式API
可以通过调用 useStore
函数,来在 setup
钩子函数中访问 store。这与在组件中使用选项式 API 访问 this.$store
是等效的。
import { useStore } from 'vuex' export default { setup () { const store = useStore() } } 复制代码
2.1 访问 State 和 Getter
为了访问 state 和 getter,需要创建 computed
引用以保留响应性,这与在选项式 API 中创建计算属性等效。
import { computed } from 'vue' import { useStore } from 'vuex' export default { setup () { const store = useStore() return { // 在 computed 函数中访问 state count: computed(() => store.state.count), // 在 computed 函数中访问 getter double: computed(() => store.getters.double) } } } 复制代码
2.2 访问 Mutation 和 Action
要使用 mutation 和 action 时,只需要在 setup
钩子函数中调用 commit
和 dispatch
函数。
import { useStore } from 'vuex' export default { setup () { const store = useStore() return { // 使用 mutation increment: () => store.commit('increment'), // 使用 action asyncIncrement: () => store.dispatch('asyncIncrement') } } } 复制代码
三、插件
Vuex 的 store 接受 plugins
选项,这个选项暴露出每次 mutation 的钩子。Vuex 插件就是一个函数,它接收 store 作为唯一参数:
const myPlugin = (store) => { // 当 store 初始化后调用 store.subscribe((mutation, state) => { // 每次 mutation 之后调用 // mutation 的格式为 { type, payload } }) } 复制代码
然后像这样使用:
const store = createStore({ // ... plugins: [myPlugin] }) 复制代码
3.1 在插件内提交 Mutation
在插件中不允许直接修改状态——类似于组件,只能通过提交 mutation 来触发变化。
通过提交 mutation,插件可以用来同步数据源到 store。例如,同步 websocket 数据源到 store(下面是个大概例子,实际上 createWebSocketPlugin
方法可以有更过选项来完成复杂任务):
export default function createWebSocketPlugin (socket) { return (store) => { socket.on('data', data => { store.commit('receiveData', data) }) store.subscribe(mutation => { if (mutation.type === 'UPDATE_DATA') { socket.emit('update', mutation.payload) } }) } } 复制代码
const plugin = createWebSocketPlugin(socket) const store = createStore({ state, mutations, plugins: [plugin] }) 复制代码
3.2 生成 State 快照
有时候插件需要获得状态的“快照”,比较改变的前后状态。想要实现这项功能,你需要对状态对象进行深拷贝:
const myPluginWithSnapshot = (store) => { let prevState = _.cloneDeep(store.state) store.subscribe((mutation, state) => { let nextState = _.cloneDeep(state) // 比较 prevState 和 nextState... // 保存状态,用于下一次 mutation prevState = nextState }) } 复制代码
生成状态快照的插件应该只在开发阶段使用,使用 webpack 或 Browserify,让构建工具帮我们处理:
const store = createStore({ // ... plugins: process.env.NODE_ENV !== 'production' ? [myPluginWithSnapshot] : [] }) 复制代码
上面插件会默认启用。在发布阶段,你需要使用 webpack 的 DefinePlugin 或者是 Browserify 的 envify 使 process.env.NODE_ENV !== 'production'
为 false
。
3.3 内置 Logger 插件
Vuex 自带一个日志插件用于一般的调试:
import { createLogger } from 'vuex' const store = createStore({ plugins: [createLogger()] }) 复制代码
createLogger
函数有几个配置项:
const logger = createLogger({ collapsed: false, // 自动展开记录的 mutation filter (mutation, stateBefore, stateAfter) { // 若 mutation 需要被记录,就让它返回 true 即可 // 顺便,`mutation` 是个 { type, payload } 对象 return mutation.type !== "aBlocklistedMutation" }, actionFilter (action, state) { // 和 `filter` 一样,但是是针对 action 的 // `action` 的格式是 `{ type, payload }` return action.type !== "aBlocklistedAction" }, transformer (state) { // 在开始记录之前转换状态 // 例如,只返回指定的子树 return state.subTree }, mutationTransformer (mutation) { // mutation 按照 { type, payload } 格式记录 // 我们可以按任意方式格式化 return mutation.type }, actionTransformer (action) { // 和 `mutationTransformer` 一样,但是是针对 action 的 return action.type }, logActions: true, // 记录 action 日志 logMutations: true, // 记录 mutation 日志 logger: console, // 自定义 console 实现,默认为 `console` }) 复制代码
日志插件还可以直接通过 <script>
标签引入,它会提供全局方法 createVuexLogger
。
要注意,logger 插件会生成状态快照,所以仅在开发环境使用。
四、严格模式
开启严格模式,仅需在创建 store 的时候传入 strict: true
:
const store = createStore({ // ... strict: true }) 复制代码
在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。
4.1 开发环境与发布环境
不要在发布环境下启用严格模式!严格模式会深度监测状态树来检测不合规的状态变更——请确保在发布环境下关闭严格模式,以避免性能损失。
类似于插件,我们可以让构建工具来处理这种情况:
const store = createStore({ // ... strict: process.env.NODE_ENV !== 'production' }) 复制代码
五、表单处理
当在严格模式中使用 Vuex 时,在属于 Vuex 的 state 上使用 v-model
会比较棘手:
<input v-model="obj.message"> 复制代码
假设这里的 obj
是在计算属性中返回的一个属于 Vuex store 的对象,在用户输入时,v-model
会试图直接修改 obj.message
。在严格模式中,由于这个修改不是在 mutation 函数中执行的, 这里会抛出一个错误。
用“Vuex 的思维”去解决这个问题的方法是:给 <input>
中绑定 value,然后侦听 input
或者 change
事件,在事件回调中调用一个方法:
<input :value="message" @input="updateMessage"> 复制代码
// ... computed: { ...mapState({ message: state => state.obj.message }) }, methods: { updateMessage (e) { this.$store.commit('updateMessage', e.target.value) } } 复制代码
下面是 mutation 函数:
// ... mutations: { updateMessage (state, message) { state.obj.message = message } } 复制代码
5.1 双向绑定的计算属性
必须承认,这样做比简单地使用“v-model
+ 局部状态”要啰嗦得多,并且也损失了一些 v-model
中很有用的特性。另一个方法是使用带有 setter 的双向绑定计算属性:
<input v-model="message"> 复制代码
// ... computed: { message: { get () { return this.$store.state.obj.message }, set (value) { this.$store.commit('updateMessage', value) } } } 复制代码
六、测试
我们主要想针对 Vuex 中的 mutation 和 action 进行单元测试。
6.1 测试 Mutation
Mutation 很容易被测试,因为它们仅仅是一些完全依赖参数的函数。这里有一个小技巧,如果你使用了 ES2015 模块,且将 mutation 定义在了 store.js
文件中,那么除了模块的默认导出外,你还应该将 mutation 进行命名导出:
const state = { ... } // `mutations` 作为命名输出对象 export const mutations = { ... } export default createStore({ state, mutations }) 复制代码
下面是用 Mocha + Chai 测试一个 mutation 的例子(实际上你可以用任何你喜欢的测试框架):
// mutations.js export const mutations = { increment: state => state.count++ } 复制代码
// mutations.spec.js import { expect } from 'chai' import { mutations } from './store' // 解构 `mutations` const { increment } = mutations describe('mutations', () => { it('INCREMENT', () => { // 模拟状态 const state = { count: 0 } // 应用 mutation increment(state) // 断言结果 expect(state.count).to.equal(1) }) }) 复制代码
6.2 测试 Action
Action 应对起来略微棘手,因为它们可能需要调用外部的 API。当测试 action 的时候,我们需要增加一个 mocking 服务层——例如,我们可以把 API 调用抽象成服务,然后在测试文件中用 mock 服务回应 API 调用。为了便于解决 mock 依赖,可以用 webpack 和 inject-loader 打包测试文件。
下面是一个测试异步 action 的例子:
// actions.js import shop from '../api/shop' export const getAllProducts = ({ commit }) => { commit('REQUEST_PRODUCTS') shop.getProducts(products => { commit('RECEIVE_PRODUCTS', products) }) } 复制代码
// actions.spec.js // 使用 require 语法处理内联 loaders。 // inject-loader 返回一个允许我们注入 mock 依赖的模块工厂 import { expect } from 'chai' const actionsInjector = require('inject-loader!./actions') // 使用 mocks 创建模块 const actions = actionsInjector({ '../api/shop': { getProducts (cb) { setTimeout(() => { cb([ /* mocked response */ ]) }, 100) } } }) // 用指定的 mutations 测试 action 的辅助函数 const testAction = (action, args, state, expectedMutations, done) => { let count = 0 // 模拟提交 const commit = (type, payload) => { const mutation = expectedMutations[count] try { expect(mutation.type).to.equal(type) expect(mutation.payload).to.deep.equal(payload) } catch (error) { done(error) } count++ if (count >= expectedMutations.length) { done() } } // 用模拟的 store 和参数调用 action action({ commit, state }, ...args) // 检查是否没有 mutation 被 dispatch if (expectedMutations.length === 0) { expect(count).to.equal(0) done() } } describe('actions', () => { it('getAllProducts', done => { testAction(actions.getAllProducts, null, {}, [ { type: 'REQUEST_PRODUCTS' }, { type: 'RECEIVE_PRODUCTS', payload: { /* mocked response */ } } ], done) }) }) 复制代码
如果在测试环境下有可用的 spy (比如通过 Sinon.JS),你可以使用它们替换辅助函数 testAction
:
describe('actions', () => { it('getAllProducts', () => { const commit = sinon.spy() const state = {} actions.getAllProducts({ commit, state }) expect(commit.args).to.deep.equal([ ['REQUEST_PRODUCTS'], ['RECEIVE_PRODUCTS', { /* mocked response */ }] ]) }) }) 复制代码
6.3 测试 Getter
如果你的 getter 包含很复杂的计算过程,很有必要测试它们。Getter 的测试与 mutation 一样直截了当。
测试一个 getter 的示例:
// getters.js export const getters = { filteredProducts (state, { filterCategory }) { return state.products.filter(product => { return product.category === filterCategory }) } } 复制代码
// getters.spec.js import { expect } from 'chai' import { getters } from './getters' describe('getters', () => { it('filteredProducts', () => { // 模拟状态 const state = { products: [ { id: 1, title: 'Apple', category: 'fruit' }, { id: 2, title: 'Orange', category: 'fruit' }, { id: 3, title: 'Carrot', category: 'vegetable' } ] } // 模拟 getter const filterCategory = 'fruit' // 获取 getter 的结果 const result = getters.filteredProducts(state, { filterCategory }) // 断言结果 expect(result).to.deep.equal([ { id: 1, title: 'Apple', category: 'fruit' }, { id: 2, title: 'Orange', category: 'fruit' } ]) }) }) 复制代码
6.4 执行测试
如果你的 mutation 和 action 编写正确,经过合理地 mocking 处理之后这些测试应该不依赖任何浏览器 API,因此你可以直接用 webpack 打包这些测试文件然后在 Node 中执行。换种方式,你也可以用 mocha-loader
或 Karma + karma-webpack
在真实浏览器环境中进行测试。
6.4.1 在 Node 中执行测试
创建以下 webpack 配置(配置好 .babelrc
):
// webpack.config.js module.exports = { entry: './test.js', output: { path: __dirname, filename: 'test-bundle.js' }, module: { loaders: [ { test: /.js$/, loader: 'babel-loader', exclude: /node_modules/ } ] } } 复制代码
然后:
webpack mocha test-bundle.js 复制代码
6.4.2 在浏览器中测试
安装
mocha-loader
。把上述 webpack 配置中的
entry
改成'mocha-loader!babel-loader!./test.js'
。用以上配置启动
webpack-dev-server
。访问
localhost:8080/webpack-dev-server/test-bundle
。
6.4.3 使用 Karma + karma-webpack 在浏览器中执行测试
详见 Vue Loader 的文档。
七、热重载
使用 webpack 的 Hot Module Replacement API,Vuex 支持在开发过程中热重载 mutation、module、action 和 getter。你也可以在 Browserify 中使用 browserify-hmr 插件。
对于 mutation 和模块,你需要使用 store.hotUpdate()
方法:
// store.js import { createStore } from 'vuex' import mutations from './mutations' import moduleA from './modules/a' const state = { ... } const store = createStore({ state, mutations, modules: { a: moduleA } }) if (module.hot) { // 使 action 和 mutation 成为可热重载模块 module.hot.accept(['./mutations', './modules/a'], () => { // 获取更新后的模块 // 因为 babel 6 的模块编译格式问题,这里需要加上 `.default` const newMutations = require('./mutations').default const newModuleA = require('./modules/a').default // 加载新模块 store.hotUpdate({ mutations: newMutations, modules: { a: newModuleA } }) }) } 复制代码
7.1 动态模块热重载
如果你仅使用模块,你可以使用 require.context
来动态地加载或热重载所有的模块。
// store.js import { createStore } from 'vuex' // 加载所有模块。 function loadModules() { const context = require.context("./modules", false, /([a-z_]+).js$/i) const modules = context .keys() .map((key) => ({ key, name: key.match(/([a-z_]+).js$/i)[1] })) .reduce( (modules, { key, name }) => ({ ...modules, [name]: context(key).default }), {} ) return { context, modules } } const { context, modules } = loadModules() const store = new createStore({ modules }) if (module.hot) { // 在任何模块发生改变时进行热重载。 module.hot.accept(context.id, () => { const { modules } = loadModules() store.hotUpdate({ modules }) }) } 复制代码
八、TypeScript 支持
Vuex 提供了类型声明,因此可以使用 TypeScript 定义 store,并且不需要任何特殊的 TypeScript 配置。请遵循 Vue 的基本 TypeScript 配置来配置项目。
但是,如果你使用 TypeScript 来编写 Vue 组件,则需要遵循一些步骤才能正确地为 store 提供类型声明。
8.1 Vue 组件中 $store
属性的类型声明
Vuex 没有为 this.$store
属性提供开箱即用的类型声明。如果你要使用 TypeScript,首先需要声明自定义的模块补充 module augmentation 。
为此,需要在项目文件夹中添加一个声明文件来声明 Vue 的自定义类型 ComponentCustomProperties
:
// vuex.d.ts import { Store } from 'vuex' declare module '@vue/runtime-core' { // 声明自己的 store state interface State { count: number } // 为 `this.$store` 提供类型声明 interface ComponentCustomProperties { $store: Store<State> } } 复制代码
8.2 useStore
组合式函数类型声明
当使用组合式 API 编写 Vue 组件时,您可能希望 useStore
返回类型化的 store。为了 useStore
能正确返回类型化的 store,必须执行以下步骤:
定义类型化的
InjectionKey
。将 store 安装到 Vue 应用时提供类型化的
InjectionKey
。将类型化的
InjectionKey
传给useStore
方法。
让我们逐步解决这个问题。首先,使用 Vue 的 InjectionKey
接口和自己的 store 类型定义来定义 key :
// store.ts import { InjectionKey } from 'vue' import { createStore, Store } from 'vuex' // 为 store state 声明类型 export interface State { count: number } // 定义 injection key export const key: InjectionKey<Store<State>> = Symbol() export const store = createStore<State>({ state: { count: 0 } }) 复制代码
然后,将 store 安装到 Vue 应用时传入定义好的 injection key。
// main.ts import { createApp } from 'vue' import { store, key } from './store' const app = createApp({ ... }) // 传入 injection key app.use(store, key) app.mount('#app') 复制代码
最后,将上述 injection key 传入 useStore
方法可以获取类型化的 store。
// vue 组件 import { useStore } from 'vuex' import { key } from './store' export default { setup () { const store = useStore(key) store.state.count // 类型为 number } } 复制代码
本质上,Vuex 将store 安装到 Vue 应用中使用了 Vue 的 Provide/Inject
特性,这就是 injection key 是很重要的因素的原因。
8.2.1 简化 useStore
用法
引入 InjectionKey
并将其传入 useStore
使用过的任何地方,很快就会成为一项重复性的工作。为了简化问题,可以定义自己的组合式函数来检索类型化的 store :
// store.ts import { InjectionKey } from 'vue' import { createStore, useStore as baseUseStore, Store } from 'vuex' export interface State { count: number } export const key: InjectionKey<Store<State>> = Symbol() export const store = createStore<State>({ state: { count: 0 } }) // 定义自己的 `useStore` 组合式函数 export function useStore () { return baseUseStore(key) } 复制代码
现在,通过引入自定义的组合式函数,不用提供 injection key 和类型声明就可以直接得到类型化的 store:
// vue 组件 import { useStore } from './store' export default { setup () { const store = useStore() store.state.count // 类型为 number } } 复制代码
每文一句:书山有路勤为径,学海无涯苦作舟。
本次的分享就到这里,如果本章内容对你有所帮助的话欢迎点赞+收藏。文章有不对的地方欢迎指出,有任何疑问都可以在评论区留言。希望大家都能够有所收获,大家一起探讨、进步!
作者:CoderBin
链接:https://juejin.cn/post/7171626512972513317
来源:https://www.77cxw.com/