diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index dec5aa1f8..dbfc8a059 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -9,6 +9,11 @@ module.exports = { title: 'Vuex', description: 'Centralized State Management for Vue.js' }, + '/zh/': { + lang: 'zh-CN', + title: 'Vuex', + description: 'Vue.js 的中心化状态管理方案' + }, '/ptbr/': { lang: 'pt-BR', title: 'Vuex', @@ -20,7 +25,7 @@ module.exports = { ['link', { rel: 'icon', href: `/logo.png` }], ['link', { rel: 'apple-touch-icon', href: `/icons/apple-touch-icon-152x152.png` }], ['link', { rel: 'mask-icon', href: '/icons/safari-pinned-tab.svg', color: '#3eaf7c' }], - ['meta', { name: 'msapplication-TileImage', content: '/icons/msapplication-icon-144x144.png' }], + ['meta', { name: 'msapplication-TileImage', content: '/icons/msapplication-icon-144x144.png' }] ], themeConfig: { @@ -90,6 +95,65 @@ module.exports = { ] }, + '/zh/': { + label: '简体中文', + selectText: '选择语言', + editLinkText: '在 GitHub 上编辑此页', + lastUpdated: '最近更新时间', + + nav: [ + { text: '指南', link: '/zh/guide/' }, + { text: 'API 参考', link: '/zh/api/' }, + { text: '更新记录', link: 'https://github.com/vuejs/vuex/releases' }, + { + text: 'v4.x', + items: [ + { text: 'v3.x', link: 'https://vuex.vuejs.org/zh' } + ] + } + ], + + sidebar: [ + { + text: '介绍', + children: [ + { text: 'Vuex 是什么?', link: '/zh/' }, + { text: '安装', link: '/zh/installation' }, + { text: '开始', link: '/zh/guide/' } + ] + }, + { + text: '核心概念', + children: [ + { text: 'State', link: '/zh/guide/state' }, + { text: 'Getter', link: '/zh/guide/getters' }, + { text: 'Mutation', link: '/zh/guide/mutations' }, + { text: 'Action', link: '/zh/guide/actions' }, + { text: 'Module', link: '/zh/guide/modules' } + ] + }, + { + text: '进阶', + children: [ + { text: '项目结构', link: '/zh/guide/structure' }, + { text: '组合式 API', link: '/zh/guide/composition-api' }, + { text: '插件', link: '/zh/guide/plugins' }, + { text: '严格模式', link: '/zh/guide/strict' }, + { text: '表单处理', link: '/zh/guide/forms' }, + { text: '测试', link: '/zh/guide/testing' }, + { text: '热重载', link: '/zh/guide/hot-reload' }, + { text: 'TypeScript 支持', link: '/zh/guide/typescript-support' }, + ] + }, + { + text: '迁移指南', + children: [ + { text: '从 3.x 迁移到 4.0', link: '/zh/guide/migrating-to-4-0-from-3-x' } + ] + } + ] + }, + '/ptbr/': { label: 'Português', selectText: 'Idiomas', diff --git a/docs/zh/api/index.md b/docs/zh/api/index.md new file mode 100644 index 000000000..9a7e7343e --- /dev/null +++ b/docs/zh/api/index.md @@ -0,0 +1,392 @@ +--- +sidebar: auto +--- + +# API 参考 + +## Store + +### createStore + +- `createStore(options: StoreOptions): Store` + + 创建一个 store 实例。 + + ```js + import { createStore } from 'vuex' + + const store = createStore({ ...options }) + ``` + +## Store 构造器选项 + +### state + +- 类型: `Object | Function` + + Vuex store 实例的根 state 对象。[详细介绍](../guide/state.md) + + 如果你传入返回一个对象的函数,其返回的对象会被用作根 state。这在你想要重用 state 对象,尤其是对于重用 module 来说非常有用。[详细介绍](../guide/modules.md#模块重用) + +### mutations + +- 类型: `{ [type: string]: Function }` + + 在 store 上注册 mutation,处理函数总是接受 `state` 作为第一个参数(如果定义在模块中,则为模块的局部状态),`payload` 作为第二个参数(可选)。 + + [详细介绍](../guide/mutations.md) + +### actions + +- 类型: `{ [type: string]: Function }` + + 在 store 上注册 action。处理函数总是接受 `context` 作为第一个参数,`payload` 作为第二个参数(可选)。 + + `context` 对象包含以下属性: + + ``` js + { + state, // 等同于 `store.state`,若在模块中则为局部状态 + rootState, // 等同于 `store.state`,只存在于模块中 + commit, // 等同于 `store.commit` + dispatch, // 等同于 `store.dispatch` + getters, // 等同于 `store.getters` + rootGetters // 等同于 `store.getters`,只存在于模块中 + } + ``` + + 同时如果有第二个参数 `payload` 的话也能够接收。 + + [详细介绍](../guide/actions.md) + +### getters + +- 类型: `{ [key: string]: Function }` + +在 store 上注册 getter,getter 方法接受以下参数: + + ``` + state, // 如果在模块中定义则为模块的局部状态 + getters, // 等同于 store.getters + ``` + + 当定义在一个模块里时会特别一些: + + ``` + state, // 如果在模块中定义则为模块的局部状态 + getters, // 等同于 store.getters + rootState // 等同于 store.state + rootGetters // 所有 getters + ``` + + 注册的 getter 暴露为 `store.getters`。 + + [详细介绍](../guide/getters.md) + +### modules + +- 类型: `Object` + + 包含了子模块的对象,会被合并到 store,大概长这样: + + ``` js + { + key: { + state, + namespaced?, + mutations, + actions?, + getters?, + modules? + }, + ... + } + ``` + + 与根模块的选项一样,每个模块也包含 `state` 和 `mutations` 选项。模块的状态使用 key 关联到 store 的根状态。模块的 mutation 和 getter 只会接收 module 的局部状态作为第一个参数,而不是根状态,并且模块 action 的 `context.state` 同样指向局部状态。 + + [详细介绍](../guide/modules.md) + +### plugins + +- 类型: `Array` + + 一个数组,包含应用在 store 上的插件方法。这些插件直接接收 store 作为唯一参数,可以监听 mutation(用于外部地数据持久化、记录或调试)或者提交 mutation (用于内部数据,例如 websocket 或 某些观察者) + + [详细介绍](../guide/plugins.md) + +### strict + +- 类型: `boolean` +- 默认值: `false` + + 使 Vuex store 进入严格模式,在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误。 + + [详细介绍](../guide/strict.md) + +### devtools + +- 类型:`boolean` + + 为某个特定的 Vuex 实例打开或关闭 devtools。对于传入 `false` 的实例来说 Vuex store 不会订阅到 devtools 插件。对于一个页面中有多个 store 的情况非常有用。 + + ``` js + { + devtools: false + } + ``` + +## Store 实例属性 + +### state + +- 类型: `Object` + + 根状态,只读。 + +### getters + +- 类型: `Object` + + 暴露出注册的 getter,只读。 + +## Store 实例方法 + +### commit + +- `commit(type: string, payload?: any, options?: Object)` +- `commit(mutation: Object, options?: Object)` + + 提交 mutation。`options` 里可以有 `root: true`,它允许在[命名空间模块](../guide/modules.md#命名空间)里提交根的 mutation。[详细介绍](../guide/mutations.md) + +### dispatch + +- `dispatch(type: string, payload?: any, options?: Object): Promise` +- `dispatch(action: Object, options?: Object): Promise` + + 分发 action。`options` 里可以有 `root: true`,它允许在[命名空间模块](../guide/modules.md#命名空间)里分发根的 action。返回一个解析所有被触发的 action 处理器的 Promise。[详细介绍](../guide/actions.md) + +### replaceState + +- `replaceState(state: Object)` + +  替换 store 的根状态,仅用状态合并或时光旅行调试。 + +### watch + +- `watch(fn: Function, callback: Function, options?: Object): Function` + +  响应式地侦听 `fn` 的返回值,当值改变时调用回调函数。`fn` 接收 store 的 state 作为第一个参数,其 getter 作为第二个参数。最后接收一个可选的对象参数表示 Vue 的 [`vm.$watch`](https://cn.vuejs.org/v2/api/#vm-watch) 方法的参数。 + +  要停止侦听,调用此方法返回的函数即可停止侦听。 + +### subscribe + +- `subscribe(handler: Function, options?: Object): Function` + + 订阅 store 的 mutation。`handler` 会在每个 mutation 完成后调用,接收 mutation 和经过 mutation 后的状态作为参数: + + ``` js + store.subscribe((mutation, state) => { + console.log(mutation.type) + console.log(mutation.payload) + }) + ``` + + 默认情况下,新的处理函数会被添加到其链的尾端,因此它会在其它之前已经被添加了的处理函数之后执行。这一行为可以通过向 `options` 添加 `prepend: true` 来覆写,即把处理函数添加到其链的最开始。 + + ``` js + store.subscribe(handler, { prepend: true }) + ``` +  要停止订阅,调用此方法返回的函数即可停止订阅。 + + 通常用于插件。[详细介绍](../guide/plugins.md) + +### subscribeAction + +- `subscribeAction(handler: Function, options?: Object): Function` + + 订阅 store 的 action。`handler` 会在每个 action 分发的时候调用并接收 action 描述和当前的 store 的 state 这两个参数: + + ``` js + store.subscribeAction((action, state) => { + console.log(action.type) + console.log(action.payload) + }) + ``` + + 默认情况下,新的处理函数会被添加到其链的尾端,因此它会在其它之前已经被添加了的处理函数之后执行。这一行为可以通过向 `options` 添加 `prepend: true` 来覆写,即把处理函数添加到其链的最开始。 + + ``` js + store.subscribeAction(handler, { prepend: true }) + ``` + + 要停止订阅,调用此方法返回的函数即可停止订阅。 + + `subscribeAction` 也可以指定订阅处理函数的被调用时机应该在一个 action 分发*之前*还是*之后* (默认行为是*之前*): + + ``` js + store.subscribeAction({ + before: (action, state) => { + console.log(`before action ${action.type}`) + }, + after: (action, state) => { + console.log(`after action ${action.type}`) + } + }) + ``` + + `subscribeAction` 也可以指定一个 `error` 处理函数以捕获分发 action 的时候被抛出的错误。该函数会从第三个参数接收到一个 `error` 对象。 + + ``` js + store.subscribeAction({ + error: (action, state, error) => { + console.log(`error action ${action.type}`) + console.error(error) + } + }) + ``` + + 该 `subscribeAction` 方法常用于插件。[详细介绍](../guide/plugins.md) + +### registerModule + +- `registerModule(path: string | Array, module: Module, options?: Object)` + + 注册一个动态模块。[详细介绍](../guide/modules.md#模块动态注册) + + `options` 可以包含 `preserveState: true` 以允许保留之前的 state。用于服务端渲染。 + +### unregisterModule + +- `unregisterModule(path: string | Array)` + + 卸载一个动态模块。[详细介绍](../guide/modules.md#模块动态注册) + +### hasModule + +- `hasModule(path: string | Array): boolean` + + 检查该模块的名字是否已经被注册。[详细介绍](../guide/modules.md#模块动态注册) + +### hotUpdate + +- `hotUpdate(newOptions: Object)` + + 热替换新的 action 和 mutation。[详细介绍](../guide/hot-reload.md) + +## 组件绑定的辅助函数 + +### mapState + +- `mapState(namespace?: string, map: Array | Object): Object` + + 为组件创建计算属性以返回 Vuex store 中的状态。[详细介绍](../guide/state.md#mapstate-辅助函数) + + 第一个参数是可选的,可以是一个命名空间字符串。[详细介绍](../guide/modules.md#带命名空间的绑定函数) + + 对象形式的第二个参数的成员可以是一个函数。`function(state: any)` + +### mapGetters + +- `mapGetters(namespace?: string, map: Array | Object): Object` + + 为组件创建计算属性以返回 getter 的返回值。[详细介绍](../guide/getters.md#mapgetters-辅助函数) + + 第一个参数是可选的,可以是一个命名空间字符串。[详细介绍](../guide/modules.md#带命名空间的绑定函数) + +### mapActions + +- `mapActions(namespace?: string, map: Array | Object): Object` + + 创建组件方法分发 action。[详细介绍](../guide/actions.md#在组件中分发-action) + + 第一个参数是可选的,可以是一个命名空间字符串。[详细介绍](../guide/modules.md#带命名空间的绑定函数) + + 对象形式的第二个参数的成员可以是一个函数。`function(dispatch: function, ...args: any[])` + +### mapMutations + +- `mapMutations(namespace?: string, map: Array | Object): Object` + + 创建组件方法提交 mutation。[详细介绍](../guide/mutations.md#在组件中提交-mutation) + + 第一个参数是可选的,可以是一个命名空间字符串。[详细介绍](../guide/modules.md#带命名空间的绑定函数) + + 对象形式的第二个参数的成员可以是一个函数。`function(commit: function, ...args: any[])` + +### createNamespacedHelpers + +- `createNamespacedHelpers(namespace: string): Object` + + 创建基于命名空间的组件绑定辅助函数。其返回一个包含 `mapState`、`mapGetters`、`mapActions` 和 `mapMutations` 的对象。它们都已经绑定在了给定的命名空间上。[详细介绍](../guide/modules.md#带命名空间的绑定函数) + +## 组合式函数 + +### useStore + +- `useStore(injectKey?: InjectionKey> | string): Store;` + + 在 `setup` 钩子函数中调用该方法可以获取注入的 store。当使用组合式 API 时,可以通过调用该方法检索 store。 + + ```js + import { useStore } from 'vuex' + + export default { + setup () { + const store = useStore() + } + } + ``` + + TypeScript 用户可以使用 injection key 来检索已经定义了类型的 store。为了使其工作,在将 store 实例安装到 Vue 应用中时,必须定义 injection key 并将其与 store 一起传递给 Vue 应用。 + + 首先,使用 Vue 的 `InjectionKey` 接口声明一个 injection key。 + + ```ts + // store.ts + import { InjectionKey } from 'vue' + import { createStore, Store } from 'vuex' + + export interface State { + count: number + } + + export const key: InjectionKey> = Symbol() + + export const store = createStore({ + state: { + count: 0 + } + }) + ``` + + 然后,将定义好的 key 作为第二个参数传递给 `app.use` 方法。 + + ```ts + // main.ts + import { createApp } from 'vue' + import { store, key } from './store' + + const app = createApp({ ... }) + + app.use(store, key) + + app.mount('#app') + ``` + + 最后,将 key 传递给 `useStore` 方法以获取指定类型的 store 实例。 + + ```ts + // vue 组件 + import { useStore } from 'vuex' + import { key } from './store' + + export default { + setup () { + const store = useStore(key) + + store.state.count // 类型为 number + } + } + ``` diff --git a/docs/zh/guide/actions.md b/docs/zh/guide/actions.md new file mode 100644 index 000000000..ed58c30cb --- /dev/null +++ b/docs/zh/guide/actions.md @@ -0,0 +1,179 @@ +# Action + + + +Action 类似于 mutation,不同在于: + +- Action 提交的是 mutation,而不是直接变更状态。 +- Action 可以包含任意异步操作。 + +让我们来注册一个简单的 action: + +``` js +const store = createStore({ + state: { + count: 0 + }, + mutations: { + increment (state) { + state.count++ + } + }, + actions: { + increment (context) { + context.commit('increment') + } + } +}) +``` + +Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 `context.commit` 提交一个 mutation,或者通过 `context.state` 和 `context.getters` 来获取 state 和 getters。当我们在之后介绍到 [Modules](modules.md) 时,你就知道 context 对象为什么不是 store 实例本身了。 + +实践中,我们会经常用到 ES2015 的[参数解构](https://github.com/lukehoban/es6features#destructuring)来简化代码(特别是我们需要调用 `commit` 很多次的时候): + +``` js +actions: { + increment ({ commit }) { + commit('increment') + } +} +``` + +### 分发 Action + +Action 通过 `store.dispatch` 方法触发: + +``` js +store.dispatch('increment') +``` + +乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 **mutation 必须同步执行**这个限制么?Action 就不受约束!我们可以在 action 内部执行**异步**操作: + +``` js +actions: { + incrementAsync ({ commit }) { + setTimeout(() => { + commit('increment') + }, 1000) + } +} +``` + +Actions 支持同样的载荷方式和对象方式进行分发: + +``` js +// 以载荷形式分发 +store.dispatch('incrementAsync', { + amount: 10 +}) + +// 以对象形式分发 +store.dispatch({ + type: 'incrementAsync', + amount: 10 +}) +``` + +来看一个更加实际的购物车示例,涉及到**调用异步 API** 和**分发多重 mutation**: + +``` js +actions: { + checkout ({ commit, state }, products) { + // 把当前购物车的物品备份起来 + const savedCartItems = [...state.cart.added] + // 发出结账请求,然后乐观地清空购物车 + commit(types.CHECKOUT_REQUEST) + // 购物 API 接受一个成功回调和一个失败回调 + shop.buyProducts( + products, + // 成功操作 + () => commit(types.CHECKOUT_SUCCESS), + // 失败操作 + () => commit(types.CHECKOUT_FAILURE, savedCartItems) + ) + } +} +``` + +注意我们正在进行一系列的异步操作,并且通过提交 mutation 来记录 action 产生的副作用(即状态变更)。 + +### 在组件中分发 Action + +你在组件中使用 `this.$store.dispatch('xxx')` 分发 action,或者使用 `mapActions` 辅助函数将组件的 methods 映射为 `store.dispatch` 调用(需要先在根节点注入 `store`): + +``` js +import { mapActions } from 'vuex' + +export default { + // ... + methods: { + ...mapActions([ + 'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')` + + // `mapActions` 也支持载荷: + 'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)` + ]), + ...mapActions({ + add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')` + }) + } +} +``` + +### 组合 Action + +Action 通常是异步的,那么如何知道 action 什么时候结束呢?更重要的是,我们如何才能组合多个 action,以处理更加复杂的异步流程? + +首先,你需要明白 `store.dispatch` 可以处理被触发的 action 的处理函数返回的 Promise,并且 `store.dispatch` 仍旧返回 Promise: + +``` js +actions: { + actionA ({ commit }) { + return new Promise((resolve, reject) => { + setTimeout(() => { + commit('someMutation') + resolve() + }, 1000) + }) + } +} +``` + +现在你可以: + +``` js +store.dispatch('actionA').then(() => { + // ... +}) +``` + +在另外一个 action 中也可以: + +``` js +actions: { + // ... + actionB ({ dispatch, commit }) { + return dispatch('actionA').then(() => { + commit('someOtherMutation') + }) + } +} +``` + +最后,如果我们利用 [async / await](https://tc39.github.io/ecmascript-asyncawait/),我们可以如下组合 action: + +``` js +// 假设 getData() 和 getOtherData() 返回的是 Promise + +actions: { + async actionA ({ commit }) { + commit('gotData', await getData()) + }, + async actionB ({ dispatch, commit }) { + await dispatch('actionA') // 等待 actionA 完成 + commit('gotOtherData', await getOtherData()) + } +} +``` + +> 一个 `store.dispatch` 在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。 diff --git a/docs/zh/guide/composition-api.md b/docs/zh/guide/composition-api.md new file mode 100644 index 000000000..cf5b83fea --- /dev/null +++ b/docs/zh/guide/composition-api.md @@ -0,0 +1,62 @@ +# 组合式API + +可以通过调用 `useStore` 函数,来在 `setup` 钩子函数中访问 store。这与在组件中使用选项式 API 访问 `this.$store` 是等效的。 + +```js +import { useStore } from 'vuex' + +export default { + setup () { + const store = useStore() + } +} +``` + +## 访问 State 和 Getter + +为了访问 state 和 getter,需要创建 `computed` 引用以保留响应性,这与在选项式 API 中创建计算属性等效。 + +```js +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) + } + } +} +``` + +## 访问 Mutation 和 Action + +要使用 mutation 和 action 时,只需要在 `setup` 钩子函数中调用 `commit` 和 `dispatch` 函数。 + +```js +import { useStore } from 'vuex' + +export default { + setup () { + const store = useStore() + + return { + // 使用 mutation + increment: () => store.commit('increment'), + + // 使用 action + asyncIncrement: () => store.dispatch('asyncIncrement') + } + } +} +``` + +## 示例 + +查看[组合式 API 案例](https://github.com/vuejs/vuex/tree/4.0/examples/composition),以便了解使用 Vuex 和 Vue 的组合式 API 的应用案例。 diff --git a/docs/zh/guide/forms.md b/docs/zh/guide/forms.md new file mode 100644 index 000000000..919095f78 --- /dev/null +++ b/docs/zh/guide/forms.md @@ -0,0 +1,62 @@ +# 表单处理 + + + +当在严格模式中使用 Vuex 时,在属于 Vuex 的 state 上使用 `v-model` 会比较棘手: + +``` html + +``` + +假设这里的 `obj` 是在计算属性中返回的一个属于 Vuex store 的对象,在用户输入时,`v-model` 会试图直接修改 `obj.message`。在严格模式中,由于这个修改不是在 mutation 函数中执行的, 这里会抛出一个错误。 + +用“Vuex 的思维”去解决这个问题的方法是:给 `` 中绑定 value,然后侦听 `input` 或者 `change` 事件,在事件回调中调用一个方法: + +``` html + +``` +``` js +// ... +computed: { + ...mapState({ + message: state => state.obj.message + }) +}, +methods: { + updateMessage (e) { + this.$store.commit('updateMessage', e.target.value) + } +} +``` + +下面是 mutation 函数: + +``` js +// ... +mutations: { + updateMessage (state, message) { + state.obj.message = message + } +} +``` + +### 双向绑定的计算属性 + +必须承认,这样做比简单地使用“`v-model` + 局部状态”要啰嗦得多,并且也损失了一些 `v-model` 中很有用的特性。另一个方法是使用带有 setter 的双向绑定计算属性: + +``` html + +``` +``` js +// ... +computed: { + message: { + get () { + return this.$store.state.obj.message + }, + set (value) { + this.$store.commit('updateMessage', value) + } + } +} +``` diff --git a/docs/zh/guide/getters.md b/docs/zh/guide/getters.md new file mode 100644 index 000000000..10af41eba --- /dev/null +++ b/docs/zh/guide/getters.md @@ -0,0 +1,122 @@ +# Getter + + + +有时候我们需要从 store 中的 state 中派生出一些状态,例如对列表进行过滤并计数: + +``` js +computed: { + doneTodosCount () { + return this.$store.state.todos.filter(todo => todo.done).length + } +} +``` + +如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它——无论哪种方式都不是很理想。 + +Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。 + +::: warning 注意 +从 Vue 3.0 开始,getter 的结果不再像计算属性一样会被缓存起来。这是一个已知的问题,将会在 3.1 版本中修复。详情请看 [PR #1878](https://github.com/vuejs/vuex/pull/1883)。 +::: + +Getter 接受 state 作为其第一个参数: + +``` js +const store = createStore({ + state: { + todos: [ + { id: 1, text: '...', done: true }, + { id: 2, text: '...', done: false } + ] + }, + getters: { + doneTodos (state) => { + return state.todos.filter(todo => todo.done) + } + } +}) +``` + +### 通过属性访问 + +Getter 会暴露为 `store.getters` 对象,你可以以属性的形式访问这些值: + +``` js +store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }] +``` + +Getter 也可以接受其他 getter 作为第二个参数: + +``` js +getters: { + // ... + doneTodosCount (state, getters) { + return getters.doneTodos.length + } +} +``` + +``` js +store.getters.doneTodosCount // -> 1 +``` + +我们可以很容易地在任何组件中使用它: + +``` js +computed: { + doneTodosCount () { + return this.$store.getters.doneTodosCount + } +} +``` + +注意,getter 在通过属性访问时是作为 Vue 的响应式系统的一部分缓存其中的。 + +### 通过方法访问 + +你也可以通过让 getter 返回一个函数,来实现给 getter 传参。在你对 store 里的数组进行查询时非常有用。 + +```js +getters: { + // ... + getTodoById: (state) => (id) => { + return state.todos.find(todo => todo.id === id) + } +} +``` + +``` js +store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false } +``` + +注意,getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。 + +### `mapGetters` 辅助函数 + +`mapGetters` 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性: + +``` js +import { mapGetters } from 'vuex' + +export default { + // ... + computed: { + // 使用对象展开运算符将 getter 混入 computed 对象中 + ...mapGetters([ + 'doneTodosCount', + 'anotherGetter', + // ... + ]) + } +} +``` + +如果你想将一个 getter 属性另取一个名字,使用对象形式: + +``` js +...mapGetters({ +  // 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount` + doneCount: 'doneTodosCount' +}) +``` diff --git a/docs/zh/guide/hot-reload.md b/docs/zh/guide/hot-reload.md new file mode 100644 index 000000000..7ba5db6fc --- /dev/null +++ b/docs/zh/guide/hot-reload.md @@ -0,0 +1,85 @@ +# 热重载 + +使用 webpack 的 [Hot Module Replacement API](https://webpack.js.org/guides/hot-module-replacement/),Vuex 支持在开发过程中热重载 mutation、module、action 和 getter。你也可以在 Browserify 中使用 [browserify-hmr](https://github.com/AgentME/browserify-hmr/) 插件。 + +对于 mutation 和模块,你需要使用 `store.hotUpdate()` 方法: + +``` js +// 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 + } + }) + }) +} +``` + +参考热重载示例 [counter-hot](https://github.com/vuejs/vuex/tree/dev/examples/counter-hot)。 + +## 动态模块热重载 + +如果你仅使用模块,你可以使用 `require.context` 来动态地加载或热重载所有的模块。 + +```js +// 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 + }) + }) +} +``` diff --git a/docs/zh/guide/index.md b/docs/zh/guide/index.md new file mode 100644 index 000000000..c2b457c15 --- /dev/null +++ b/docs/zh/guide/index.md @@ -0,0 +1,66 @@ +# 开始 + + + +每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的**状态 (state)**。Vuex 和单纯的全局对象有以下两点不同: + +1. Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。 + +2. 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地**提交 (commit) mutation**。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。 + +### 最简单的 Store + +:::tip 提示 +我们将在后续的文档示例代码中使用 ES2015 语法。如果你还没能掌握 ES2015,[你得抓紧了](https://babeljs.io/docs/learn-es2015/)! +::: + +[安装](../installation.md) Vuex 之后,让我们来创建一个 store。创建过程直截了当——仅需要提供一个初始 state 对象和一些 mutation: + +``` js +import { createApp } from 'vue' +import { createStore } from 'vuex' + +// 创建一个新的 store 实例 +const store = createStore({ + state () { + return { + count: 0 + } + }, + mutations: { + increment (state) { + state.count++ + } + } +}) + +const app = createApp({ /* 根组件 */ }) + +// 将 store 实例作为插件安装 +app.use(store) +``` + +现在,你可以通过 `store.state` 来获取状态对象,并通过 `store.commit` 方法触发状态变更: + +``` js +store.commit('increment') + +console.log(store.state.count) // -> 1 +``` + +在 Vue 组件中, 可以通过 `this.$store` 访问store实例。现在我们可以从组件的方法提交一个变更: + +``` js +methods: { + increment() { + this.$store.commit('increment') + console.log(this.$store.state.count) + } +} +``` + +再次强调,我们通过提交 mutation 的方式,而非直接改变 `store.state.count`,是因为我们想要更明确地追踪到状态的变化。这个简单的约定能够让你的意图更加明显,这样你在阅读代码的时候能更容易地解读应用内部的状态改变。此外,这样也让我们有机会去实现一些能记录每次状态改变,保存状态快照的调试工具。有了它,我们甚至可以实现如时间穿梭般的调试体验。 + +由于 store 中的状态是响应式的,在组件中调用 store 中的状态简单到仅需要在计算属性中返回即可。触发变化也仅仅是在组件的 methods 中提交 mutation。 + +接下来,我们将会更深入地探讨一些核心概念。让我们先从 [State](state.md) 概念开始。 diff --git a/docs/zh/guide/migrating-to-4-0-from-3-x.md b/docs/zh/guide/migrating-to-4-0-from-3-x.md new file mode 100644 index 000000000..774eb8eae --- /dev/null +++ b/docs/zh/guide/migrating-to-4-0-from-3-x.md @@ -0,0 +1,116 @@ +# 从 3.x 迁移到 4.0 + +几乎所有的 Vuex 4 API 都与 Vuex 3 保持不变。但是,仍有一些非兼容性变更需要注意。 + +- [非兼容性变更](#非兼容性变更) + - [安装过程](#安装过程) + - [TypeScript 支持](#TypeScript-支持) + - [打包产物已经与 Vue 3 配套](#打包产物已经与-Vue-3-配套) + - [“createLogger”函数从核心模块导出](#“createLogger”函数从核心模块导出) +- [新特性](#新特性) + - [全新的“useStore”组合式函数](#全新的“usestore”组合式函数) + +## 非兼容性变更 + +### 安装过程 + +为了与 Vue 3 初始化过程保持一致,Vuex 的安装方式已经改变了。用户现在应该使用新引入的 `createStore` 方法来创建 store 实例。 + +```js +import { createStore } from 'vuex' + +export const store = createStore({ + state () { + return { + count: 1 + } + } +}) +``` + +要将 Vuex 安装到 Vue 实例中,需要用 `store` 替代之前的 Vuex 传递给 `use` 方法。 + +```js +import { createApp } from 'vue' +import { store } from './store' +import App from './App.vue' + +const app = createApp(App) + +app.use(store) + +app.mount('#app') +``` + +:::tip 提示 +从技术上讲这并不是一个非兼容性变更,仍然可以使用 `new Store(...)` 语法,但是建议使用上述方式以保持与 Vue 3 和 Vue Router Next 的一致。 +::: + +### TypeScript 支持 + +为了修复 [issue #994](https://github.com/vuejs/vuex/issues/994),Vuex 4 删除了 `this.$store` 在 Vue 组件中的全局类型声明。当使用 TypeScript 时,必须声明自己的[模块补充(module augmentation)](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation)。 + +将下面的代码放到项目中,以允许 `this.$store` 能被正确的类型化: + +```ts +// vuex-shim.d.ts + +import { ComponentCustomProperties } from 'vue' +import { Store } from 'vuex' + +declare module '@vue/runtime-core' { + // 声明自己的 store state + interface State { + count: number + } + + interface ComponentCustomProperties { + $store: Store + } +} +``` + +在 [TypeScript 支持](./typescript-support)章节可以了解到更多。 + +### 打包产物已经与 Vue 3 配套 + +下面的打包产物分别与 Vue 3 的打包产物配套: + +- `vuex.global(.prod).js` + - 通过` + +``` + +### npm + +``` bash +npm install vuex@next --save +``` + +### Yarn + +``` bash +yarn add vuex@next --save +``` + +### Promise + +Vuex 依赖 [Promise](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Using_promises)。如果你支持的浏览器并没有实现 Promise (比如 IE),那么你可以使用一个 polyfill 的库,例如 [es6-promise](https://github.com/stefanpenner/es6-promise)。 + +你可以通过 CDN 将其引入: + +``` html + +``` + +然后 `window.Promise` 会自动可用。 + +如果你喜欢使用诸如 npm 或 Yarn 等包管理器,可以按照下列方式执行安装: + +``` bash +npm install es6-promise --save # npm +yarn add es6-promise # Yarn +``` + +或者更进一步,将下列代码添加到你使用 Vuex 之前的一个地方: + +``` js +import 'es6-promise/auto' +``` + +### 自己构建 + +如果需要使用 dev 分支下的最新版本,您可以直接从 GitHub 上克隆代码并自己构建。 + +``` bash +git clone https://github.com/vuejs/vuex.git node_modules/vuex +cd node_modules/vuex +yarn +yarn build +```