在 Nuxt 中使用 Vuex store
在 Nuxt
中,Vuex
已经为你安装好了。你只需要确保项目根目录下存在 /store/
目录。如果你使用 create-nuxt-app
安装你的 Nuxt
项目,这个目录会在项目安装过程中自动生成。在 Nuxt
中,你可以用两种不同的模式创建你的 store
:
-
Module
-
Classic(已弃用)。
由于 Classic
模式已被弃用,本书将只关注 Module
模式。所以,让我们在接下来的章节开始吧。
你可以在我们的 |
使用模块模式
与 Vue
应用不同,在 Nuxt
中,每个模块(包括根模块)的 namespaced
键默认设置为 true
。此外,在 Nuxt
中,你不需要在 store
根目录中组装模块;你只需要在根文件和模块文件中将 state
导出为一个函数,将 mutations
、getters
和 actions
导出为对象即可。让我们通过以下步骤开始:
-
创建
store
根目录,如下所示:// store/index.js export const state = () => ({ number: 3 }) export const mutations = { mutation1 (state) { ... } } export const getters = { getter1 (state, getter) { ... } } export const actions = { action1 ({ state, commit }) { ... } }
在
Nuxt
中,Vuex
的严格模式在开发环境下默认设置为true
,并在生产模式下自动关闭,但你可以在开发环境下禁用它,如下所示:// store/index.js export const strict = false
-
创建一个模块,如下所示:
// store/module1.js export const state = () => ({ number: 1 }) export const mutations = { mutation1 (state) { ... } } export const getters = { getter1 (state, getter, rootState) { ... } } export const actions = { action1 ({ state, commit, rootState }) { ... } }
然后,就像我们在上一节中在
Vue
应用中手动做的那样,store
将会自动生成,如下所示:new Vuex.Store({ state: () => ({ number: 3 }), mutations: { mutation1 (state) { ... } }, getters: { getter1 (state, getter) { ... } }, actions: { action1 ({ state, commit }) { ... } }, modules: { module1: { namespaced: true, state: () => ({ number: 1 }), mutations: { mutation1 (state) { ... } } ... } } })
-
在任何页面的
<script>
块中映射所有的store state
、getter
、mutation
和action
,如下所示:// pages/index.vue import { mapState, mapGetters, mapActions } from 'vuex' export default { computed: { ...mapState({ numberRoot: state => state.number, }), ...mapState('module1', { numberModule1: state => state.number, }), ...mapGetters({ getNumberRoot: 'getter1' }), ...mapGetters('module1', { getNumberModule1: 'getter1' }) }, methods: { ...mapActions({ doNumberRoot:'action1' }), ...mapActions('module1', { doNumberModule1:'action1' }) } }
-
在
<template>
块中显示计算属性和提交mutation
的方法,如下所示:// pages/index.vue <p>{{ numberRoot }}, {{ getNumberRoot }}</p> <button v-on:click="doNumberRoot">x 2 (root)</button> <p>{{ numberModule1 }}, {{ getNumberModule1 }}</p> <button v-on:click="doNumberModule1">x 2 (module1)</button>
你应该在屏幕上看到以下初始结果,当你点击模板中显示的先前按钮时,它们将被改变:
3, 3 1, 1
正如我们之前提到的,在 Nuxt
中你不需要在 store
根目录中组装模块,因为如果你使用以下结构,Nuxt
会为你 “组装” 它们:
// chapter-10/nuxt-universal/module-mode/
└── store
├── index.js
├── module1.js
├── module2.js
└── ...
但是,如果你像我们在 Vue
应用中那样,使用以下结构在 store
根目录中手动组装模块:
// chapter-10/vuex-sfc/structuring-modules/basic/
└── store
├── index.js
├── ...
└── modules
├── module1.js
└── module2.js
你将在 Nuxt
应用中收到以下错误:
ERROR [vuex] module namespace not found in mapState(): module1/
ERROR [vuex] module namespace not found in mapGetters(): module1/
要修复这些错误,你需要显式地告诉 Nuxt
这些模块的存放位置:
export default {
computed: {
..mapState('modules/module1', {
numberModule1: state => state.number,
}),
...mapGetters('modules/module1', {
getNumberModule1: 'getter1'
})
},
methods: {
...mapActions('modules/module1', {
doNumberModule1:'action1'
})
}
}
就像 Vue
应用中的 Vuex
一样,我们也可以在 Nuxt
应用中将 state
、actions
、mutations
和 getters
分割到不同的文件中。让我们在下一节看看如何做到这一点以及 Nuxt
中的不同之处。
使用模块文件
我们可以将模块中的大文件拆分成单独的文件——state.js
、actions.js
、mutations.js
和 getters.js
——用于 store
的根目录和每个模块。让我们通过以下步骤来实现:
-
为
store
根目录创建单独的state
、actions
、mutations
和getters
文件,如下所示:// store/state.js export default () => ({ number: 3 }) // store/mutations.js export default { mutation1 (state) { ... } }
-
为模块创建单独的
state
、actions
、mutations
和getters
文件,如下所示:// store/module1/state.js export default () => ({ number: 1 }) // store/module1/mutations.js export default { mutation1 (state) { ... } }
同样,在
Nuxt
中,我们不需要像在Vue
应用中那样使用index.js
来组装这些单独的文件。只要我们使用以下结构,Nuxt
就会为我们完成这项工作:// chapter-10/nuxt-universal/module-files/ └── store ├── state.js ├── action.js └── ... ├── module1 │ ├── state.js │ ├── mutations.js │ └── ... └── module2 ├── state.js ├── mutations.js └── ...
我们可以将其与我们在
Vue
应用中使用的以下结构进行比较,在该结构中,我们需要为store
根目录和每个模块创建一个index.js
文件,以从单独的文件中组装state
、actions
、mutations
和getters
:// chapter-10/vuex-sfc/structuring-modules/advanced/ └── store ├── index.js ├── action.js └── ... ├── module1 │ ├── index.js │ ├── state.js │ ├── mutations.js │ └── ... └── module2 ├── index.js ├── state.js ├── mutations.js └── ...
因此,Nuxt
中 store
是开箱即用的,并且为你省去了组装文件和注册模块的一些代码。很棒,不是吗?现在,让我们在下一节进一步探索如何在 Nuxt
中使用 fetch
方法动态地填充 store
的 state
。
使用 fetch 方法
我们可以使用 fetch
方法在页面渲染之前填充 store
的 state
。它的工作方式与我们已经介绍过的 asyncData
方法相同——它在每次组件加载之前被调用。它在服务器端只调用一次,然后在客户端导航到其他路由时调用。就像 asyncData
一样,我们可以将 async/await
与 fetch
方法一起用于异步数据。它在组件创建之后被调用,因此我们可以在 fetch
方法中通过 this
访问组件实例。因此,我们可以通过 this.$nuxt.context.store
访问 store
。让我们通过以下步骤创建一个使用此方法的简单 Nuxt 应用:
-
在任何页面中使用
fetch
方法异步地从远程API
请求用户列表,如下所示:// pages/index.vue import axios from 'axios' export default { async fetch () { const { store } = this.$nuxt.context await store.dispatch('users/getUsers') } }
-
创建一个包含
state
、mutations
和actions
的用户模块,如下所示:// store/users/state.js export default () => ({ list: {} }) // store/users/mutations.js export default { setUsers (state, data) { state.list = data }, removeUser (state, id) { let found = state.list.find(todo => todo.id === id) state.list.splice(state.list.indexOf(found), 1) } } // store/users/actions.js export default { setUsers ({ commit }, data) { commit('setUsers', data) }, removeUser ({ commit }, id) { commit('removeUser', id) } }
mutations
和actions
中的setUsers
方法用于将用户列表设置到state
中,而removeUser
方法用于一次从state
中删除一个用户。 -
将
state
和actions
中的方法映射到页面,如下所示:// pages/index.vue import { mapState, mapActions } from 'vuex' export default { computed: { ...mapState ('users', { users (state) { return state.list } }) }, methods: { ...mapActions('users', { removeUser: 'removeUser' }) } }
-
在
<template>
块中循环并显示用户列表,如下所示:// pages/index.vue <li v-for="(user, index) in users" v-bind:key="user.id"> {{ user.name }} <button class="button" von:click="removeUser(user.id)">Remove</button> </li>
当你在浏览器中加载应用时,你应该在屏幕上看到用户列表,并且你可以点击 “Remove” 按钮来删除一个用户。我们也可以在
actions
中使用async/await
来获取远程数据,如下所示:// store/users/actions.js import axios from 'axios' export const actions = { async getUsers ({ commit }) { const { data } = await axios.get('https://jsonplaceholder.typicode.com/users') commit('setUsers', data) } }
然后,我们可以像这样 dispatch
getUsers
action:// pages/index.vue export default { async fetch () { const { store } = this.$nuxt.context await store.dispatch('users/getUsers') } }
除了在 Nuxt
中使用 fetch
方法获取和填充 state
之外,我们还可以使用 nuxtServerInit
action,它只在 Nuxt
中可用。让我们在下一节继续了解它。
使用 nuxtServerInit action
与仅在页面级组件中可用的 asyncData
方法和在所有 Vue
组件(包括页面级组件)中可用的 fetch
方法不同,nuxtServerInit
action 是 Nuxt
store 中保留的 store
action 方法,只有在定义时才可用。它只能在 store
根目录的 index.js
文件中定义,并且仅在 Nuxt
应用启动之前在服务器端调用。与在服务器端调用然后在后续路由的客户端调用的 asyncData
和 fetch
方法不同,nuxtServerInit
action 方法仅在服务器端调用一次,除非你在浏览器中刷新应用的任何页面。此外,与将 Nuxt
上下文对象作为其第一个参数的 asyncData
方法不同,nuxtServerInit
action 方法将其作为其第二个参数。它接收的第一个参数是 store
上下文对象。让我们将这些上下文对象放入下表中:
方法 | 第一个参数 | 第二个参数(Nuxt 上下文) | 仅服务器端 | 页面组件 | 所有组件 | 仅在 store/index.js | 调用次数 |
---|---|---|---|---|---|---|---|
asyncData |
Nuxt 上下文对象 |
无 |
是/否 |
是 |
否 |
否 |
每次加载组件(服务器端一次,客户端导航时) |
fetch |
组件实例 (this) |
Nuxt 上下文对象 |
是/否 |
是 |
是 |
否 |
每次加载组件(服务器端一次,客户端导航时) |
nuxtServerInit |
Store 上下文对象 |
Nuxt 上下文对象 |
是 |
否 |
否 |
是 |
仅在服务器端启动时一次(除非刷新页面) |
因此,当我们想从应用的任何页面从服务器端获取数据,然后用服务器数据填充 store
的 state
时,nuxtServerInit
action 方法非常有用——例如,当用户登录我们的应用时,我们存储在服务器端会话中的已认证用户数据。此会话数据可以作为 Express
中的 req.session.authUser
或 Koa
中的 ctx.session.authUser
存储。然后,我们可以通过 req
对象将 ctx.session
传递给 nuxtServerInit
。
让我们使用这个方法 action
创建一个简单的用户登录应用,并使用你在第八章 “添加服务器端框架” 中了解到的 Koa
作为服务器端 API
。在我们可以将任何数据注入会话并使用 nuxtServerIni
action 方法创建一个 store
之前,我们只需要对服务器端进行一些修改,我们可以通过以下步骤完成:
-
使用
npm
安装会话包koa-session
:$ npm install koa-session
-
导入并注册会话包作为中间件,如下所示:
// server/middlewares.js import session from 'koa-session' app.keys = ['some secret hurr'] app.use(session(app))
-
在服务器端创建两个路由,如下所示:
// server/routes.js router.post('/login', async (ctx, next) => { let request = ctx.request.body || {} if (request.username === 'demo' && request.password === 'demo') { ctx.session.authUser = { username: 'demo' } ctx.body = { username: 'demo' } } else { ctx.throw(401) } }) router.post('/logout', async (ctx, next) => { delete ctx.session.authUser ctx.body = { ok: true } })
在上面的代码中,我们使用
/login
路由将认证的用户数据authUser
通过会话注入到Koa
上下文ctx
中,而/logout
用于取消设置认证数据。 -
创建包含
authUser
键的store state
以保存认证数据:// store/state.js export default () => ({ authUser: null })
-
创建一个
mutation
方法,将数据设置到前面state
中的authUser
键:// store/mutations.js export default { setUser (state, data) { state.authUser = data } }
-
在
store
根目录中创建一个index.js
文件,包含以下actions
:// store/index.js export const actions = { nuxtServerInit({ commit }, { req }) { if (req.ctx.session && req.ctx.session.authUser) { commit('setUser', req.ctx.session.authUser) } }, async login({ commit }, { username, password }) { const { data } = await axios.post('/api/login', { username, password }) commit('setUser', data.data) }, async logout({ commit }) { await axios.post('/api/logout') commit('setUser', null) } }
在上面的代码中,
nuxtServerInit
action 方法用于从服务器访问会话数据,并通过提交setUser
mutation 方法来填充store
的state
。login
和logout
action 方法用于验证用户登录凭据和取消设置它们。请注意,由于本书使用Koa
作为服务器API
,因此会话数据存储在req.ctx
中。如果你使用的是Express
,请使用以下代码:actions: { nuxtServerInit ({ commit }, { req }) { if (req.session.user) { commit('user', req.session.user) } } }
就像
asyncData
和fetch
方法一样,nuxtServerInit
action 方法也可以是异步的。你只需要返回一个Promise
,或者使用async/await
语句,以便Nuxt
服务器等待action
异步完成,如下所示:actions: { async nuxtServerInit({ commit }) { await commit('setUser', req.ctx.session.authUser) } }
-
创建一个表单以使用
store
的action
方法,如下所示:// pages/index.vue <form v-on:submit.prevent="login"> <input v-model="username" type="text" name="username" /> <input v-model="password" type="password" name="password" /> <button class="button" type="submit">Login</button> </form> <script> export default { data() { return { username: '', password: '' } }, methods: { async login() { await this.$store.dispatch('login', { username: this.username, password: this.password }) }, async logout() { await this.$store.dispatch('logout') } } } </script>
我们简化了前面的代码和步骤 6 中的代码以适应此页面,但你可以在我们的
GitHub
仓库/chapter-10/nuxt-universal/nuxtServerInit/
中找到它们的完整版本。
做得好!你终于完成了 Nuxt
和 Vue
的一个令人兴奋的功能——Vuex store
的学习。这是一个漫长的章节,但它非常重要,因为在接下来的章节中我们将经常回到 Vuex
并使用它。现在,让我们总结一下你在本章中学到的内容。