阅读 80

Vue3状态管理器Vuex4.0的进阶使用方案!

前言

大家好,我是CoderBin。Vuex4 是 Vue3 的一个状态管理库,它可以将项目所需的数据统一进行存储,并且任意组件都可以通过 Vuex 的方法去访问到数据,修改数据。关于 Vuex4 的基本使用可以前往:# 全面拥抱Vue3,Vuex4 最新详解教程!

接下来介绍一些 Vue4 中的一些进阶使用,希望对大家有所帮助,谢谢。

如果文中有不对、疑惑的地方,欢迎在评论区留言指正????

一、项目结构

Vuex 并不限制你的代码结构。但是,它规定了一些需要遵守的规则:

  1. 应用层级的状态应该集中到单个 store 对象中。

  2. 提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。

  3. 异步逻辑都应该封装到 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 在浏览器中测试

  1. 安装 mocha-loader

  2. 把上述 webpack 配置中的 entry 改成 'mocha-loader!babel-loader!./test.js'

  3. 用以上配置启动 webpack-dev-server

  4. 访问 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,必须执行以下步骤:

  1. 定义类型化的 InjectionKey

  2. 将 store 安装到 Vue 应用时提供类型化的 InjectionKey 。

  3. 将类型化的 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/


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐