开发 Nuxt SPA

在开发 Nuxt SPA 时,需要记住一个重要的事项:提供给 asyncDatafetch 方法的 Nuxt 上下文将丢失它们的 reqres 对象,因为这些对象是 Node.js HTTP 对象。在本节中,我们将创建一个简单的用户登录身份验证,你应该已经熟悉了。但是,这次我们将使用 Nuxt SPA 来实现它。我们还将创建一个使用动态路由列出用户的页面,正如我们在第 4 章 “添加视图、路由和过渡” 中所学到的那样。让我们开始吧:

  1. 准备以下 .vue 文件,或者直接从上一章复制:

    -| pages/
    ---| index.vue
    ---| about.vue
    ---| login.vue
    ---| secret.vue
    ---| users/
    -----| index.vue
    -----| _id.vue
  2. 准备带有 store statemutationsactions 和处理用户登录身份验证的 index 文件的 Vuex store,如下所示:

-| store/
---| index.js
---| state.js
---| mutations.js
---| actions.js

正如我们在上一章中提到的,当我们静态生成 Nuxt 通用 SSR 应用程序时,我们将丢失 store 中的 nuxtServerInit action,这在 Nuxt SPA 中也是一样的——我们在客户端将没有这个服务器 action。因此,我们需要一个客户端的 nuxtServerInit action 来模拟服务器端的 nuxtServerInit action。我们将在下一节学习如何做到这一点。

创建客户端的 nuxtServerInit action

这些文件中的方法和属性与我们在过去的练习中使用的相同,除了 nuxtServerInit action:

// store/index.js
const cookie = process.server ? require('cookie') : undefined
export const actions = {
  nuxtServerInit ({ commit }, { req }) {
    if (
      req
      && req.headers
      && req.headers.cookie
      && req.headers.cookie.indexOf('auth') > -1
    ) {
      let auth = cookie.parse(req.headers.cookie)['auth']
      commit('setAuth', JSON.parse(auth))
    }
  }
}

由于 nuxtServerInit 仅由 Nuxt 从服务器端调用,因此 Nuxt SPA 中不涉及服务器。因此,我们需要一个解决方案。我们可以使用 Node.js 的 js-cookie 模块在用户登录时将身份验证数据存储在客户端,这使其成为替换服务器端 cookie 的最佳选择。让我们学习如何实现这一点:

  1. 通过 npm 安装 Node.js 的 js-cookie 模块:

    $ npm i js-cookie
  2. store actions 中创建一个名为 nuxtClientInit 的自定义方法(如果愿意,你可以选择任何你喜欢的名称),以检索 cookie 中的用户数据。然后,当用户刷新浏览器时,将其设置回指定情况下的所需状态:

    // store/index.js
    import cookies from 'js-cookie'
    export const actions = {
      nuxtClientInit ({ commit }, ctx) {
        let auth = cookies.get('auth')
        if (auth) {
          commit('setAuth', JSON.parse(auth))
        }
      }
    }

    你可能还记得,刷新页面时,storenuxtServerInit action 始终在服务器端调用。此 nuxtClientInit 方法也会发生同样的情况;每次刷新页面时,都应该在客户端调用它。但是,它不会自动调用,因此我们可以使用一个插件在 Vue 根实例初始化之前每次都调用它。

  3. /plugins/ 目录中创建一个名为 nuxt-client-init.js 的插件,该插件将通过 store 中的 dispatch 方法调用 nuxtClientInit 方法:

    // plugins/nuxt-client-init.js
    export default async (ctx) => {
      await ctx.store.dispatch('nuxtClientInit', ctx)
    }

    请记住,我们可以在 Vue 根实例初始化之前在插件中访问 Nuxt 上下文。store 被添加到 Nuxt 上下文中,因此,我们可以访问 store actions,而我们在这里感兴趣的是 nuxtClientInit 方法。

  4. 现在,将此插件添加到 Nuxt 配置文件中以安装该插件:

    // nuxt.config.js
    export default {
      plugins: [
        { src: '~/plugins/nuxt-client-init.js', mode: 'client' }
      ]
    }

现在,每次你刷新浏览器时,nuxtClientInit 方法将被调用,并且在 Vue 根实例初始化之前,状态将由此方法重新填充。正如你所见,当我们失去作为通用 JavaScript 应用程序的 Nuxt 的全部功能时,模仿 nuxtServerInit action 并非易事。但是,如果你必须使用 Nuxt SPA,那么可以使用我们刚刚创建的 nuxtClientInit 方法来解决此问题。

接下来,我们将使用 Nuxt 插件创建一些自定义 Axios 实例。这应该是你已经非常熟悉的内容。但是,能够创建自定义 Axios 实例很有用,因为即使我们也有 Nuxt Axios 模块,你也可以在需要时始终回退到 Axios 的原始版本。所以,让我们继续!

使用插件创建多个自定义 Axios 实例

在这个 spa 模式的练习中,我们需要两个 Axios 实例来向以下地址发起 API 调用:

  • localhost:4000 用于用户身份验证

  • jsonplaceholder.typicode.com 用于获取用户

  1. 通过 npm 安装原始 Axios

    $ npm i axios
  2. 在你需要的页面上创建一个 Axios 实例:

    // pages/users/index.vue
    const instance = axios.create({
      baseURL: '<api-address>',
      timeout: <value>,
      headers: { '<x-custom-header>': '<value>' }
    })

但是直接在页面上创建 Axios 实例并不理想。理想情况下,我们应该能够提取这个实例并在任何地方重用它。通过 Nuxt 插件,我们可以创建提取的 Axios 实例。我们可以遵循两种方法来创建它们。我们将在下一节中介绍第一种方法。

在 Nuxt 配置文件中安装自定义 Axios 插件

在前面的章节中,你学习了如何使用 inject 方法创建一个插件,并通过 Nuxt 配置文件安装该插件。除了使用 inject 方法之外,值得了解的是,我们还可以直接将插件注入到 Nuxt 上下文中。让我们看看如何做到这一点:

  1. /plugins/ 目录中创建一个 axios-typicode.js 文件,导入原始的 axios,并创建实例,如下所示:

    // plugins/axios-typicode.js
    import axios from 'axios'
    const instance = axios.create({
      baseURL: 'https://jsonplaceholder.typicode.com'
    })
    export default (ctx, inject) => {
      ctx.$axiosTypicode = instance
      inject('axiosTypicode', instance)
    }

    正如你所见,在创建 axios 实例后,我们通过 Nuxt 上下文 (ctx) 注入插件,使用 inject 方法,然后将其导出。

  2. Nuxt 配置文件中安装此插件:

    // nuxt.config.js
    export default {
      plugins: [
        { src: '~/plugins/axios-typicode.js', mode: 'client' }
      ]
    }

    你必须将 mode 选项设置为 client,因为我们只需要在客户端使用它。

  3. 你可以从任何你喜欢的地方访问此插件。在本例中,我们想在用户索引页上使用此插件来获取用户列表:

    // pages/users/index.vue
    export default {
      async asyncData({ $axiosTypicode }) {
        let { data } = await $axiosTypicode.get('/users')
        return { users: data }
      }
    }

    在这个插件中,我们将自定义的 axios 实例直接注入到 Nuxt 上下文 (ctx) 中作为 $axiosTypicode,这样我们就可以使用 JavaScript 的解构赋值语法将其解包为 $axiosTypicode 并直接调用它。我们还使用 inject 方法注入了插件,因此我们也使用 ctx.app 调用此插件,如下所示:

    // pages/users/index.vue
    export default {
      async asyncData({ app }) {
        let { data } = await app.$axiosTypicode.get('/users')
        return { users: data }
      }
    }

创建一个自定义的 Axios 插件并不难,不是吗?如果你通过 Nuxt 配置文件安装插件,这意味着它是一个全局 JavaScript 函数,你可以从任何地方访问它。但是,如果你不想将其安装为全局插件,你可以跳过在 Nuxt 配置文件中安装它的步骤。这使我们进入了创建 Nuxt 插件的第二种方法。

手动导入自定义 Axios 插件

创建自定义 Axios 实例的另一种方法完全不涉及 Nuxt 配置文件。我们可以只将自定义实例导出为一个普通的 JavaScript 函数,然后在我们需要它的页面中直接导入它。让我们看看如何做到这一点:

  1. /plugins/ 目录中创建一个 axios-api.js 文件,导入原始的 axios,并创建实例,如下所示:

    // plugins/axios-api.js
    import axios from 'axios'
    export default axios.create({
      baseURL: 'http://localhost:4000',
      withCredentials: true
    })

    正如你所见,我们不再使用 inject 方法;相反,我们直接导出该实例。

  2. 现在,我们可以在需要时手动导入它。在本例中,我们需要在 login action 方法中使用它,如下所示:

    // store/actions.js
    import axios from '~/plugins/axios-api'
    
    async login({ commit }, { username, password }) {
      const { data } = await axios.post('/public/users/login', { username, password })
      //...
    }

    正如你所见,我们必须手动导入此插件,因为它没有插入到 Nuxt 生命周期中。

  3. token 中间件中导入它,并在此 axios 实例上设置 Authorization header,如下所示:

    // middleware/token.js
    import axios from '~/plugins/axios-api'
    
    export default async ({ store, error }) => {
      //...
      axios.defaults.headers.common['Authorization'] = `Bearer:${store.state.auth.token}`
    }

    即使我们必须在使用此方法时手动导入插件,但至少我们将以下设置提取到了一个插件中,我们可以在任何需要的地方重用它:

    {
      baseURL: 'http://localhost:4000',
      withCredentials: true
    }

    你可以在本书 GitHub 仓库的 /chapter-15/frontend/ 中找到 Nuxt SPA 的代码以及这两种方法。

一旦你创建、测试和检查了所有代码和文件,你就可以部署 Nuxt SPA 了。让我们开始吧!