接下来介绍一些 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 函数中访问 statecount: computed(() => store.state.count),// 在 computed 函数中访问 getterdouble: computed(() => store.getters.double)}}
}
2.2 访问 Mutation 和 Action
要使用 mutation 和 action 时,只需要在 setup
钩子函数中调用 commit
和 dispatch
函数。
import { useStore } from 'vuex'
export default {setup () {const store = useStore()return {// 使用 mutationincrement: () => store.commit('increment'),// 使用 actionasyncIncrement: () => 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...// 保存状态,用于下一次 mutationprevState = 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, // 自动展开记录的 mutationfilter (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 }// 应用 mutationincrement(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 和参数调用 actionaction({ commit, state }, ...args)// 检查是否没有 mutation 被 dispatchif (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' }]}// 模拟 getterconst 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').defaultconst 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 stateinterface 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}
}
每文一句:书山有路勤为径,学海无涯苦作舟。
本次的分享就到这里,如果本章内容对你有所帮助的话欢迎点赞+收藏。文章有不对的地方欢迎指出,有任何疑问都可以在评论区留言。希望大家都能够有所收获,大家一起探讨、进步!
最后
为大家准备了一个前端资料包。包含54本,2.57G的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。
有需要的小伙伴,可以点击下方卡片领取,无偿分享