在 Nuxt 中使用 Vuex store

Nuxt 中,Vuex 已经为你安装好了。你只需要确保项目根目录下存在 /store/ 目录。如果你使用 create-nuxt-app 安装你的 Nuxt 项目,这个目录会在项目安装过程中自动生成。在 Nuxt 中,你可以用两种不同的模式创建你的 store

  • Module

  • Classic(已弃用)。

由于 Classic 模式已被弃用,本书将只关注 Module 模式。所以,让我们在接下来的章节开始吧。

你可以在我们的 GitHub 仓库的 /chapter-10/nuxt-universal/ 中找到以下所有 Nuxt 示例的源代码。

使用模块模式

Vue 应用不同,在 Nuxt 中,每个模块(包括根模块)的 namespaced 键默认设置为 true。此外,在 Nuxt 中,你不需要在 store 根目录中组装模块;你只需要在根文件和模块文件中将 state 导出为一个函数,将 mutationsgettersactions 导出为对象即可。让我们通过以下步骤开始:

  1. 创建 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
  2. 创建一个模块,如下所示:

    // 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) { ... }
          }
          ...
        }
      }
    })
  3. 在任何页面的 <script> 块中映射所有的 store stategettermutationaction,如下所示:

    // 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'
        })
      }
    }
  4. <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 应用中将 stateactionsmutationsgetters 分割到不同的文件中。让我们在下一节看看如何做到这一点以及 Nuxt 中的不同之处。

使用模块文件

我们可以将模块中的大文件拆分成单独的文件——state.jsactions.jsmutations.jsgetters.js——用于 store 的根目录和每个模块。让我们通过以下步骤来实现:

  1. store 根目录创建单独的 stateactionsmutationsgetters 文件,如下所示:

    // store/state.js
    export default () => ({
      number: 3
    })
    // store/mutations.js
    export default {
      mutation1 (state) { ... }
    }
  2. 为模块创建单独的 stateactionsmutationsgetters 文件,如下所示:

    // 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 文件,以从单独的文件中组装 stateactionsmutationsgetters

    // 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
            └── ...

因此,Nuxtstore 是开箱即用的,并且为你省去了组装文件和注册模块的一些代码。很棒,不是吗?现在,让我们在下一节进一步探索如何在 Nuxt 中使用 fetch 方法动态地填充 storestate

使用 fetch 方法

我们可以使用 fetch 方法在页面渲染之前填充 storestate。它的工作方式与我们已经介绍过的 asyncData 方法相同——它在每次组件加载之前被调用。它在服务器端只调用一次,然后在客户端导航到其他路由时调用。就像 asyncData 一样,我们可以将 async/awaitfetch 方法一起用于异步数据。它在组件创建之后被调用,因此我们可以在 fetch 方法中通过 this 访问组件实例。因此,我们可以通过 this.$nuxt.context.store 访问 store。让我们通过以下步骤创建一个使用此方法的简单 Nuxt 应用:

  1. 在任何页面中使用 fetch 方法异步地从远程 API 请求用户列表,如下所示:

    // pages/index.vue
    import axios from 'axios'
    export default {
      async fetch () {
        const { store } = this.$nuxt.context
        await store.dispatch('users/getUsers')
      }
    }
  2. 创建一个包含 statemutationsactions 的用户模块,如下所示:

    // 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)
      }
    }

    mutationsactions 中的 setUsers 方法用于将用户列表设置到 state 中,而 removeUser 方法用于一次从 state 中删除一个用户。

  3. stateactions 中的方法映射到页面,如下所示:

    // pages/index.vue
    import { mapState, mapActions } from 'vuex'
    export default {
      computed: {
        ...mapState ('users', {
          users (state) {
            return state.list
          }
        })
      },
      methods: {
        ...mapActions('users', {
          removeUser: 'removeUser'
        })
      }
    }
  4. <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 应用启动之前在服务器端调用。与在服务器端调用然后在后续路由的客户端调用的 asyncDatafetch 方法不同,nuxtServerInit action 方法仅在服务器端调用一次,除非你在浏览器中刷新应用的任何页面。此外,与将 Nuxt 上下文对象作为其第一个参数的 asyncData 方法不同,nuxtServerInit action 方法将其作为其第二个参数。它接收的第一个参数是 store 上下文对象。让我们将这些上下文对象放入下表中:

方法 第一个参数 第二个参数(Nuxt 上下文) 仅服务器端 页面组件 所有组件 仅在 store/index.js 调用次数

asyncData

Nuxt 上下文对象

是/否

每次加载组件(服务器端一次,客户端导航时)

fetch

组件实例 (this)

Nuxt 上下文对象

是/否

每次加载组件(服务器端一次,客户端导航时)

nuxtServerInit

Store 上下文对象

Nuxt 上下文对象

仅在服务器端启动时一次(除非刷新页面)

因此,当我们想从应用的任何页面从服务器端获取数据,然后用服务器数据填充 storestate 时,nuxtServerInit action 方法非常有用——例如,当用户登录我们的应用时,我们存储在服务器端会话中的已认证用户数据。此会话数据可以作为 Express 中的 req.session.authUserKoa 中的 ctx.session.authUser 存储。然后,我们可以通过 req 对象将 ctx.session 传递给 nuxtServerInit

让我们使用这个方法 action 创建一个简单的用户登录应用,并使用你在第八章 “添加服务器端框架” 中了解到的 Koa 作为服务器端 API。在我们可以将任何数据注入会话并使用 nuxtServerIni action 方法创建一个 store 之前,我们只需要对服务器端进行一些修改,我们可以通过以下步骤完成:

  1. 使用 npm 安装会话包 koa-session

    $ npm install koa-session
  2. 导入并注册会话包作为中间件,如下所示:

    // server/middlewares.js
    import session from 'koa-session'
    app.keys = ['some secret hurr']
    app.use(session(app))
  3. 在服务器端创建两个路由,如下所示:

    // 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 用于取消设置认证数据。

  4. 创建包含 authUser 键的 store state 以保存认证数据:

    // store/state.js
    export default () => ({
      authUser: null
    })
  5. 创建一个 mutation 方法,将数据设置到前面 state 中的 authUser 键:

    // store/mutations.js
    export default {
      setUser (state, data) {
        state.authUser = data
      }
    }
  6. 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 方法来填充 storestateloginlogout action 方法用于验证用户登录凭据和取消设置它们。请注意,由于本书使用 Koa 作为服务器 API,因此会话数据存储在 req.ctx 中。如果你使用的是 Express,请使用以下代码:

    actions: {
      nuxtServerInit ({ commit }, { req }) {
        if (req.session.user) {
          commit('user', req.session.user)
        }
      }
    }

    就像 asyncDatafetch 方法一样,nuxtServerInit action 方法也可以是异步的。你只需要返回一个 Promise,或者使用 async/await 语句,以便 Nuxt 服务器等待 action 异步完成,如下所示:

    actions: {
      async nuxtServerInit({ commit }) {
        await commit('setUser', req.ctx.session.authUser)
      }
    }
  7. 创建一个表单以使用 storeaction 方法,如下所示:

    // 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/ 中找到它们的完整版本。

做得好!你终于完成了 NuxtVue 的一个令人兴奋的功能——Vuex store 的学习。这是一个漫长的章节,但它非常重要,因为在接下来的章节中我们将经常回到 Vuex 并使用它。现在,让我们总结一下你在本章中学到的内容。