六、编写插件和模块

还记得自第 3 章添加 UI 框架以来,您是如何在 Nuxt 应用中编写一些简单插件的吗?正如我们前面提到的,插件本质上是JavaScript 函数。您将始终需要编写自定义函数以适应 web 开发中的情况,本书中我们将创建许多函数。在本章中,我们将更详细地研究如何为您的 Nuxt 应用以及自定义模块创建自定义插件。您将学习在 Vue 应用中创建自定义插件,并在 Nuxt 应用中实现它们。然后,您将学习如何在插件之上创建自定义 Nuxt 模块。您还将学习将现有的 Vue 插件和 Nuxt 模块导入并安装到您的 Nuxt 应用中,这些插件和模块是作为 Vue 和 Nuxt 社区的贡献提供的。学习和理解 Vue 插件和 Nuxt 模块非常重要,无论它们是定制的还是从外部导入的,因为在接下来的章节中我们将经常使用其中的一些插件和 Nuxt 模块。

我们将在本章中介绍的主题如下:

  • 编写 Vue 插件
  • 用 Nuxt 编写全局函数
  • 写入 Nuxt 模块
  • 编写异步 Nuxt 模块
  • 编写 Nuxt 模块代码段

编写 Vue 插件

插件是封装在.js文件中的全局 JavaScript 函数,可以使用Vue.use全局方法安装在应用中。我们在第 4 章中使用了一些 Vue 插件添加了视图、路由和转换,例如vue-routervue-meta。在使用new语句启动根 Vue 之前,必须通过Vue.use方法安装这些插件,如下例所示:

// src/entry.js
import Vue from 'vue'
import Meta from 'vue-meta'

Vue.use(Meta)
new VueRouter({ ... })

您可以通过Vue.use将选项传递到插件中,以这种格式配置插件:

Vue.use(<plugin>, <options>)

例如,我们可以将以下选项传递到vue-meta插件中:

Vue.use(Meta, {
  keyName: metaData, // default => metaInfo
  refreshOnceOnNavigation: true // default => false
})

选项是可选的。这意味着您可以使用插件本身,而无需传入任何插件。Vue.use还可以防止意外注入同一个插件两次或两次以上,因此多次调用插件只会安装一次。

You can check out awesome-vue for a huge collection of community-contributed plugins and libraries at https://github.com/vuejs/awesome-vuecomponents--libraries.

现在,让我们在下一节中探索如何创建 Vue 插件。

在 Vue 中编写自定义插件

编写 Vue 插件相当容易。您只需在插件中使用install方法,即可接受Vue作为第一个参数,options作为第二个参数:

// plugin.js
export default {
  install (Vue, options) {
    // ...
  }
}

让我们为标准 Vue 应用创建一个不同语言的简单自定义问候插件。可通过options参数配置语言;如果未提供选项,则英语将用作默认语言:

  1. /src/目录中创建一个/plugins/文件夹,其中包含一个basic.js文件,代码如下:
// src/plugins/basic.js
export default {
  install (Vue, options) {
    if (options === undefined) {
      options = {}
    }
    let { language } = options
    let languages = {
      'EN': 'Hello!',
      'ES': 'Hola!'
    }
    if (language === undefined) {
      language = 'EN'
    }
    Vue.prototype.$greet = (name) => {
      return languages[language] + ' ' + name
    }
    Vue.prototype.$message = 'Helló Világ!'
  }
}

在这个简单的插件中,我们还添加了一个名为$message的实例属性,该属性带有匈牙利语的默认“Hello World!”值(Helló Világ!,当该插件用于组件时,可以修改该属性。注意,{ language } = options是 ES6 编写language = options.language的方式。此外,我们应该在方法和属性前面加上$,因为这是一种惯例。

  1. 按如下方式安装和配置此插件:
// src/entry.js
import PluginSample from './plugins/basic'
Vue.use(PluginBasic, {
  language: 'ES'
})
  1. 然后,我们可以在任何 Vue 组件中全局使用该插件,如下例所示:
// src/components/home.vue
<p>{{ $greet('John') }}</p>
<p>{{ $message }}</p>
<p>{{ messages }}</p>

export default {
  data () {
    let helloWorld = []
    helloWorld.push(this.$message)

    this.$message = 'Ciao mondo!'
    helloWorld.push(this.$message)

    return { messages: helloWorld }
  }
}

因此,当您在浏览器上运行应用时,您应该在屏幕上获得以下输出:

Hola! John
Ciao mondo!
[ "Helló Világ!", "Ciao mondo!" ]

您也可以在插件中使用componentdirective,如下例所示:

// src/plugins/component.js
export default {
  install (Vue, options) {
    Vue.component('custom-component', {
     // ...
    })
  }
}

// src/plugins/directive.js
export default {
  install (Vue, options) {
    Vue.directive('custom-directive', {
      bind (el, binding, vnode, oldVnode) {
        // ...
      }
    })
  }
}

我们也可以使用Vue.mixin()向所有组件注入一个插件,如下所示:

// src/plugins/plugin-mixin.js
export default {
  install (Vue, options) {
    Vue.mixin({
      // ...
    })
  }
}

You can find the preceding example Vue app in /chapter-6/vue/webpack/ in our GitHub repository.

就这样。创建一个可以在 Vue 应用中安装和使用的 Vue 插件非常简单,不是吗?在 Nuxt 应用中呢?如何在 Nuxt 应用中安装前面的自定义 Vue 插件?让我们在下一节中找到答案。

将 Vue 插件导入 Nuxt

这个过程在 Nuxt 应用中的工作原理基本相同。所有插件都要在根 Vue 启动之前运行。因此,如果我们想使用 Vue 插件,就像前面的示例插件一样,我们需要在启动 Nuxt 应用之前设置插件。让我们将自定义的basic.js插件复制到 Nuxt 应用的/plugins/目录中,然后执行以下步骤进行安装:

  1. /plugins/目录下创建一个basic-import.js文件导入basic.js,如下所示:
// plugins/basic-import.js
import Vue from 'vue'
import PluginSample from './basic'

Vue.use(PluginSample)

我们这次用Vue.use方法安装插件时跳过了选项。

  1. basic-import.js的文件路径添加到 Nuxt 配置文件中的plugins选项中,如下所示:
export default {
  plugins: [
    '~/plugins/basic-import',
  ]
}
  1. 在您喜欢的任何页面中使用此插件–就像我们在 Vue 应用中所做的那样,例如:
// pages/index.vue
<p>{{ $greet('Jane') }}</p>
<p>{{ $message }}</p>
<p>{{ messages }}</p>

export default {
  data () {
    let helloWorld = []
    helloWorld.push(this.$message)

    this.$message = 'Olá Mundo!'
    helloWorld.push(this.$message)

    return { messages: helloWorld }
  }
}
  1. 在浏览器上运行 Nuxt 应用,您将在屏幕上获得以下输出:
Hello! Jane
Olá Mundo!
[ "Helló Világ!", "Olá Mundo!" ]

这次我们为$greet方法获得了英文版的“Hello!”,因为我们在安装插件时没有设置任何语言选项。此外,您将在本索引页的<template>块中获得$message的“OláMundo!”;您将在其他页面(例如,/about/contact)上看到“HelloóVilág!”,因为我们只在索引页面的this.$message = 'Olá Mundo!'上设置了葡萄牙语版本的“Hello World!”。

正如我们在本章开头提到的,有大量社区贡献的 Vue 插件可能对您的 Nuxt 应用有用,但有些插件可能只在浏览器中工作,因为它们缺少 SSR(服务器端渲染)支持。因此,在下一节中,我们将研究如何解决这类插件。

导入不支持 SSR 的外部 Vue 插件

我们已经在 Nuxt 中预装了一些 Vue 插件,如vue-routervue-metavuexvue-server-renderer。按照我们在上一节中安装自定义 Vue 插件的步骤,可以轻松地对未安装的插件进行分类。以下是我们如何在 Nuxt 应用中使用vue-notifications的示例:

  1. 使用 npm 安装插件:
$ npm i vue-notification
  1. 导入并注入插件,就像我们对自定义插件所做的那样:
// plugins/vue-notifications.js
import Vue from 'vue'
import VueNotifications from 'vue-notifications'

Vue.use(VueNotifications)
  1. 包括 Nuxt 配置文件的文件路径:
// nuxt.config.js:
export default {
  plugins: ['~/plugins/vue-notifications']
}

对于不支持 SSR 的插件,或者当您只想在客户端使用此插件时,您可以使用plugins选项中的mode: 'client'选项来确保此插件不会在服务器端执行,如下例所示:

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

正如您所见,安装 Vue 插件只需三个步骤,无论是外部插件还是自定义插件。因此,简而言之,Vue 插件是通过使用Vue.use方法和在插件内部公开install方法注入 Vue 实例的全局 JavaScript 函数。但在 Nuxt 本身中,还有其他创建全局函数的方法,可以将其注入 Nuxt 上下文(context和 Vue 实例($root),而无需使用install方法。我们将在接下来的章节中探讨这些方法。

For more information about vue-notifications, please visit https://github.com/euvl/vue-notification.

用 Nuxt 编写全局函数

在 Nuxt 中,我们可以通过将“插件”或全局函数注入以下三项来创建它们:

  • Vue 实例(在客户端):
// plugins/<function-name>.js
import Vue from 'vue'
Vue.prototype.$<function-name> = () => {
  //...
}
  • Nuxt 上下文(在服务器端):
// plugins/<function-name>.js
export default (context, inject) => {
  context.app.$<function-name> = () => {
    //...
  }
}
  • Vue 实例和 Nuxt 上下文:
// plugins/<function-name>.js
export default (context, inject) => {
  inject('<function-name>', () => {
    //...
  })
}

使用上述格式,您可以轻松地为应用编写全局函数。在接下来的部分中,我们将指导您完成一些示例函数。让我们开始吧。

将函数注入 Vue 实例

在本例中,我们将创建一个函数,用于将两个数字相加,例如,1+2=3。我们将通过以下步骤将此函数注入 Vue 实例:

  1. 创建一个.js文件,导入vue,并将该功能附加到/plugins/目录中的vue.prototype
// plugins/vue-injections/sum.js
import Vue from 'vue'
Vue.prototype.$sum = (x, y) => x + y
  1. 将【Nuxt】属性文件添加到【NUT0】中:
// nuxt.config.js
export default {
  plugins: ['~/plugins/vue-injections/sum']
}
  1. 可以在任何地方使用该功能,例如:
// pages/vue-injections.vue
<p>{{ this.$sum(1, 2) }}</p>
<p>{{ sum }}</p>

export default {
  data () {
    return {
      sum: this.$sum(2, 3)
    }
  }
}
  1. 在浏览器上运行页面,屏幕上应显示以下输出(即使在刷新页面时):
3
5

将函数注入 Nuxt 上下文

在本例中,我们将创建一个用于对数字进行平方运算的函数,例如,5*5=25。我们将通过以下步骤将此函数注入 Nuxt 上下文:

  1. 创建一个.js文件并将该函数附加到context.app
// plugins/ctx-injections/square.js
export default ({ app }, inject) => {
  app.$square = (x) => x * x
}
  1. 将功能文件路径添加到 Nuxt 配置文件中的plugins选项:
// nuxt.config.js
export default {
  plugins: ['~/plugins/ctx-injections/square']
}
  1. 在您可以访问上下文的任何页面上使用该功能,例如,在asyncData方法中:
// pages/ctx-injections.vue
<p>{{ square }}</p>

export default {
  asyncData (context) {
    return {
      square: context.app.$square(5)
    }
  }
}
  1. 在浏览器上运行页面,屏幕上应显示以下输出(即使在刷新页面时):
25

请注意,asyncData总是在页面组件启动之前被调用,并且在此方法中您无法访问this。因此,您不能在asyncData方法中使用注入 Vue 实例的函数($root),就像我们在上一个示例中创建的$sum函数一样(我们将在第 8 章添加服务器端框架中更详细地了解asyncData。同样,我们不能在 Vue 生命周期挂钩/方法(例如,mountedupdated等)内调用上下文注入函数,如本例中的$square函数。但是,如果您想要一个可以从thiscontext使用的函数,那么我们将在下一节中通过将此类函数注入 Vue 实例和 Nuxt 上下文来了解如何做到这一点。

将函数同时注入 Vue 实例和 Nuxt 上下文

在本例中,我们将创建一个将两个数字相乘的函数,例如,2*3=6。我们将通过以下步骤将此函数注入 Vue 实例和 Nuxt 上下文:

  1. 创建一个.js文件并使用inject函数封装您的函数:
// plugins/combined-injections/multiply.js
export default ({ app }, inject) => {
  inject('multiply', (x, y) => x  y)
}

Note that $ is prefixed automatically to your function, so you don't have to worry about adding it to your function.

  1. 将【Nuxt】属性文件添加到【NUT0】中:
// nuxt.config.js
export default {
  plugins: ['~/plugins/combined-injections/multiply']
}
  1. 在您可以访问contextthis(Vue 实例)的任何页面上使用该功能,例如:
// pages/combined-injections.vue
<p>{{ this.$multiply(4, 3) }}</p>
<p>{{ multiply }}</p>

export default {
  asyncData (context) {
    return { multiply: context.app.$multiply(2, 3) }
  }
}
  1. 在浏览器上运行页面,屏幕上应显示以下输出(即使在刷新页面时):
12
6

您可以在任何 Vue 生命周期挂钩中使用此功能,例如,如下所示:

mounted () {
  console.log(this.$multiply(5, 3))
}

您应该在浏览器的控制台上获得15的输出。此外,您还可以从 Vuex store 中的actionsmutations对象/属性中的this访问此功能,我们将在第 10 章中介绍,添加 Vuex store

  1. 创建一个.js文件,将以下函数封装在actionsmutations对象中:
// store/index.js
export const state = () => ({
  xNumber: 1,
  yNumber: 3
})

export const mutations = {
  changeNumbers (state, newValue) {
    state.xNumber = this.$multiply(3, 8)
    state.yNumber = newValue
  }
}

export const actions = {
  setNumbers ({ commit }) {
    const newValue = this.$multiply(9, 6)
    commit('changeNumbers', newValue)
  }
}
  1. 在您喜欢的任何页面上使用前面的 storeaction方法,即setNumbers,例如以下示例:
// pages/combined-injections.vue
<p>{{ $store.state }}</p>
<button class="button" v-on:click="updateStore">Update Store</button>

export default {
  methods: {
    updateStore () {
      this.$store.dispatch('setNumbers')
    }
  }
}
  1. 在浏览器上运行页面,屏幕上应显示以下输出(即使在刷新页面时):
{ "xNumber": 1, "yNumber": 3 }
  1. 单击“更新门店”按钮,门店默认状态和前面的编号将更改如下:
{ "xNumber": 24, "yNumber": 54 }

那太好了。通过这种方式,我们可以编写一个在客户端和服务器端都能工作的插件。但有时,我们需要可以在服务器端或客户端独占使用的函数。为此,我们必须指导 Nuxt 如何具体运行我们的函数。因此,让我们在下一节中了解如何做到这一点。

注入仅客户端或仅服务器插件

在本例中,我们将创建一个用于两个数字相除的函数,例如 8/2=4,以及另一个用于两个数字相减的函数,例如 8-2=6。我们将第一个函数注入 Vue 实例,使其仅用于客户端,而第二个函数注入 Nuxt 上下文,使其仅用于服务器端:

  1. 创建两个函数,并在其后面添加.client.js.server.js,如下所示:
// plugins/name-conventions/divide.client.js
import Vue from 'vue'
Vue.prototype.$divide = (x, y) => x / y

// plugins/name-conventions/subtract.server.js
export default ({ app }, inject) => {
  inject('subtract', (x, y) => x - y)
}

追加了.client.js的功能文件只在客户端运行,追加了.server.js的功能文件只在服务器端运行。

  1. 将函数文件路径添加到 Nuxt 配置文件中的plugins属性:
// nuxt.config.js:
export default {
  plugins: [
    '~/plugins/name-conventions/divide.client.js',
    '~/plugins/name-conventions/subtract.server.js'
  ]
}
  1. 在您喜欢的任何页面上使用这些插件,例如:
// pages/name-conventions.vue
<p>{{ divide }}</p>
<p>{{ subtract }}</p>

export default {
  data () {
    let divide = ''
    if (process.client) {
      divide = this.$divide(8, 2)
    }
    return { divide }
  },
  asyncData (context) {
    let subtract = ''
    if (process.server) {
      subtract = context.app.$subtract(10, 4)
    }
    return { subtract }
  }
}
  1. 在浏览器上运行该页面,您将在屏幕上获得以下输出:
4
6

请注意,当您第一次在浏览器上运行页面时,甚至刷新页面时,都会得到上述结果。但在第一次加载后,如果您通过<nuxt-link>导航到此页面,您将在屏幕上获得以下输出:

4

另外,请注意,我们必须将$divide方法包装在process.clientif 条件中,因为它是只在客户端发生的函数。如果删除process.clientIf 条件,浏览器中会出现服务器端错误:

this.$divide is not a function

$subtract方法也是如此:我们必须将其包装在process.serverif 条件中,因为它是只在服务器端发生的函数。如果删除process.serverIf 条件,则浏览器会出现客户端错误:

this.$subtract is not a function

如果每次使用时条件都阻塞,那么将函数包装在process.serverprocess.client中可能并不理想。但是您不需要在仅在客户端调用的 Vue 生命周期挂钩/方法上使用process.clientif 条件,例如mounted挂钩。因此,您可以在不使用 if 条件的情况下安全地使用仅客户端函数,如下例所示:

mounted () {
  console.log(this.$divide(8, 2))
}

您将在浏览器控制台中获得一个输出4。下表显示了八个 Vue 生命周期挂钩/方法,值得一提的是,在 Nuxt 应用中,两侧仅调用其中两个:

| 服务器和客户端 | 仅限客户 | |

  • 之前创建()
  • 创建()

|

  • 安装前()
  • 安装前()
  • 更新前()
  • 更新前()
  • 销毁前()
  • 销毁前()

|

请注意,我们在 Vue 和 Nuxt 应用中使用的data方法在两侧都被调用,就像asyncData方法一样。因此,您可以使用专门为客户端使用而设计的$divide方法,在仅限客户端列表下的钩子中没有 if 条件。而使用专门为服务器端使用而设计的$subtract方法,您可以安全地在nuxtServerInit操作中不使用 if 条件,如下例所示:

export const actions = {
  nuxtServerInit ({ commit }, context) {
    console.log(context.app.$subtract(10, 4))
  }
}

当你的应用在服务器端运行时,即使你刷新一个页面(任何页面),你也会得到一个6的输出。值得一提的是,Nuxt 上下文只能通过以下方法访问:nuxtServerInitasyncDatanuxtServerInit操作作为第二个参数访问上下文,而asyncData方法作为第一个参数访问上下文。我们将在第 10 章中介绍nuxtServerInit操作,添加 Vuex 存储,但现在在下一节中,我们将研究在nuxtServerInit操作之后注入 Nuxt 上下文的 JavaScript 函数,但在 Vue 实例和插件之前,以及在$root之前以及刚才学习的 Nuxt 上下文注入函数。这种类型的函数称为 Nuxt 模块,在本章末尾,您将知道如何编写这些模块。我们走吧。

写入 Nuxt 模块

模块只是启动 Nuxt 时执行的顶级 JavaScript 函数。Nuxt 将按顺序调用每个模块,并等待所有模块完成,然后继续调用要注入到$root和 Nuxt 上下文中的 Vue 实例、Vue 插件和全局函数。因为模块在它们之前被调用(如 Vue 实例等),所以我们可以使用模块覆盖模板、配置网页包加载程序、添加 CSS 库以及执行应用所需的其他任务。除此之外,模块还可以打包到 npm 包中,并与 Nuxt 社区共享。您可以查看 Nuxt 社区制作的生产就绪模块的以下链接:

https://github.com/nuxt-community/awesome-nuxt#official

让我们尝试一下 Axios 模块,它是一个与 Axios 集成的模块(https://github.com/axios/axios 用于 Nuxt。它附带了一些功能,例如自动设置客户端和服务器端的基本 URL。我们将在接下来的章节中发现它的一些特点。如果您想了解更多关于本模块的信息,请访问https://axios.nuxtjs.org/ 。现在,让我们通过以下步骤了解如何使用此模块:

  1. 使用 npm 安装它:
$ npm install @nuxtjs/axios
  1. 在 Nuxt 配置文件中配置它:
// nuxt.config.js
module.exports = {
  modules: [
    '@nuxtjs/axios'
  ]
}
  1. 在任何地方使用它,例如,在页面上的asyncData方法中:
// pages/index.vue
async asyncData ({ $axios }) {
  const ip = await $axios.$get('http://icanhazip.com')
  console.log(ip)
}

您也可以在mounted方法(或createdupdated等)中使用,如下所示:

// pages/index.vue
async mounted () {
  const ip = await this.$axios.$get('http://icanhazip.com')
  console.log(ip)
}

每次导航到/about页面时,您都应该在浏览器控制台上看到您的 IP 地址。您应该注意到,现在您可以像使用 vanilla Axios 一样发送 HTTP 请求,而无需在需要时导入它,因为它现在是通过模块全局注入的。太好了,不是吗?接下来,我们将从一个基本模块开始,指导您编写模块。

编写基本模块

正如我们已经提到的,模块是函数,它们可以选择打包为 npm 模块。这是创建模块所需的基本结构:

// modules/basic.js
export default function (moduleOptions) {
  // ....
}

您只需要在项目根目录中创建一个/modules/目录,然后开始为模块编写代码。请注意,如果要将模块发布为 npm 包,则必须包含以下行:

module.exports.meta = require('./package.json')

如果要创建模块并将其发布为 npm 包,请遵循 Nuxt 社区中的以下模板:

https://github.com/nuxt-community/module-template/tree/master/template

无论您是为 Nuxt 社区创建模块还是仅为自己的项目创建模块,每个模块都可以访问以下内容:

  • 模块选项:

我们可以将 JavaScript 对象中的一些选项从配置文件传递给模块,例如:

// nuxt.config.js
export default {
  modules: [
    ['~/modules/basic/module', { language: 'ES' }],
  ]
}

然后您可以在模块函数的第一个参数中访问前面的选项moduleOptions,如下所示:

// modules/basic/module.js
export default function (moduleOptions) {
  console.log(moduleOptions)
}

您将从配置文件中获得以下传递的选项:

{
  language: 'ES'
}
  • 配置选项:

我们还可以创建一个自定义选项(例如,tokenproxybasic),并将一些特定选项传递给模块(该自定义选项可用于在模块之间共享),如下例所示:

// nuxt.config.js
export default {
  modules: [
    ['~/modules/basic/module'],
  ],
  basic: { // custom option
    option1: false,
    option2: true,
  }
}

然后您可以使用this.options访问前面的自定义选项,如图所示:

// modules/basic/module.js
export default function (moduleOptions) {
  console.log(this.options['basic'])
}

您将从配置文件中获得以下传递的选项:

{
  option1: false,
  option2: true
}

我们还可以将moduleOptionsthis.options组合如下:

// modules/basic/module.js
export default function (moduleOptions) {
  const options = {
    ...this.options['basic'],
    ...moduleOptions
  }
  console.log(options)
}

您将从配置文件中获得以下组合选项:

{
  option1: false,
  option2: true
}
  • Nuxt 实例:

您可以使用this.nuxt访问 Nuxt 实例。请访问以下链接了解可用的方法(例如,hook方法,我们可以使用该方法在启动 Nuxt 时针对特定事件创建特定任务):

https://nuxtjs.org/api/internals-nuxt

  • ModuleContainer实例:

您可以使用this访问ModuleContainer实例。请访问以下链接了解可用的方法(例如,addPlugin方法,我们在模块中经常使用该方法注册插件):

https://nuxtjs.org/api/internals-module-container

  • module.exports.meta代码行:

如前所述,如果您将模块发布为 npm 包,那么这一行是必需的。但在本书中,我们将指导您完成为项目创建模块的步骤。让我们从以下步骤创建一个真正基本的模块开始:

  1. 使用以下代码创建一个module文件:
// modules/basic/module.js
const { resolve } = require('path')

export default function (moduleOptions) {
  const options = {
    ...this.options['basic'],
    ...moduleOptions
  }

  // Add plugin.
  this.addPlugin({
    src: resolve(__dirname, 'plugin.js'),
    fileName: 'basic.js',
    options
  })
}
  1. 使用以下代码创建一个plugin文件:
// modules/basic/plugin.js
var options = []

<% if (options.option1 === true) { %>
  options.push('option 1')
<% } %>

<% if (options.option2 === true) { %>
  options.push('option 2')
<% } %>

<% if (options.language === 'ES') { %>
  options.push('language ES')
<% } %>

const basic = function () {
  return options
}

export default ({ app }, inject) => {
  inject('basic', basic)
}

Note that the <%= %> symbols are interpolation delimiters used by Lodash for interpolating data properties in the template function. We will cover them again later in this chapter. For more information about the Lodash template function, please visit https://lodash.com/docs/4.17.15#template.

  1. 仅在 Nuxt 配置文件中包含模块文件路径(/modules/basic/module.js,并提供一些带有basic自定义选项的选项,如下所示:
// nuxt.config.js
export default {
  modules: [
    ['~/modules/basic/module', { language: 'ES' }],
  ],

  basic: {
    option1: false,
    option2: true,
  }
}
  1. 可以在任何您喜欢的地方使用它,例如:
// pages/index.vue
mounted () {
  const basic = this.$basic()
  console.log(basic)
}
  1. 每次访问主页时,应在浏览器控制台上看到以下输出:
["option 2", "language ES"]

注意module.js如何处理高级配置细节,例如语言和选项。负责注册plugin.js文件,完成实际工作。如您所见,该模块是插件的包装器。我们将在以下章节中对此进行更多研究。

Note that if you are writing modules only for build time and development, then use the buildModules option in the Nuxt config file to register your modules, instead of using the modules option for the Node.js runtime. For more information about this option, please visit https://nuxtjs.org/guide/modules#build-only-modules and https://nuxtjs.org/api/configuration-modules.

编写异步 Nuxt 模块

例如,如果您需要在模块中使用Promise对象,使用 HTTP 客户端从远程 API 获取一些异步数据,那么 Nuxt 完全可以支持这一点。下面是一些可以用来编写异步模块的选项。

使用异步/等待

您可以在模块中使用 ES6 async/await 与 Axios,Axios 是我们自第 4 章开始使用的 HTTP 客户端,添加视图、路由和转换,如以下示例所示:

// modules/async-await/module.js
import axios from 'axios'

export default async function () {
  let { data } = await axios.get(
   'https://jsonplaceholder.typicode.com/posts')
  let routes = data.map(post => '/posts/' + post.id)
  console.log(routes)
}

// nuxt.config.js
modules: [
  ['~/modules/async-await/module']
]

在前面的示例中,我们使用 Axios 的get方法从远程 API JSONPlaceholder(获取所有帖子 https://jsonplaceholder.typicode.com/ 。第一次启动 Nuxt 应用时,您应该会看到以下输出打印到终端:

[
  '/posts/1',
  '/posts/2',
  '/posts/3',
  ...
]

还愿

您可以在模块中使用承诺链并返回Promise对象,如下例所示:

// modules/promise-sample/module.js
import axios from 'axios'

export default function () {
  return axios.get('https://jsonplaceholder.typicode.com/comments')
    .then(res => res.data.map(comment => '/comments/' + comment.id))
    .then(routes => {
      console.log(routes)
    })
}

// nuxt.config.js
modules: [
  ['~/modules/promise-sample/module']
]

在本例中,我们使用 Axios 的get方法从远程 API 获取所有注释。然后我们使用then方法chainPromise进行处理并打印结果。第一次启动 Nuxt 应用时,您应该会看到以下输出打印到终端:

[
  '/comments/1',
  '/comments/2',
  '/comments/3',
  ...
]

You can find these two examples in /chapter-6/nuxt-universal/modules/async/ in our GitHub repository.

有了这两个异步选项和从前面部分学到的基本模块编写技能,您可以轻松地开始创建 Nuxt 模块。在下一节中,我们将通过编写模块的小段(片段来查看更多示例。

编写 Nuxt 模块代码段

在本主题中,我们将把我们创建的自定义模块分解为小块——片段。

You can find all the following code in /chapter-6/nuxt-universal/module-snippets/ in our GitHub repository.

使用顶级选项

还记得我们在编写基本模块一节中提到的可以传递到模块中的配置选项吗?模块选项是在 Nuxt 配置文件中注册模块的顶级选项。我们甚至可以组合来自不同模块的多个选项,并且可以共享它们的选项。让我们在以下步骤中尝试一个同时使用@nuxtjs/axios@nuxtjs/proxy的示例:

  1. 使用 npm 将这两个模块安装在一起:
$ npm i @nuxtjs/axios
$ npm i @nuxtjs/proxy

这两个模块进行了很好的集成,以防止 CORS 问题,我们将在本书后面开发跨域应用时看到并讨论这些问题。不需要手动注册@nuxtjs/proxy模块,但需要在package.json文件的依赖项中。

  1. 注册@nuxtjs/axios模块,并在 Nuxt 配置文件中设置这两个模块的顶级选项:
// nuxt.config.js
export default {
  modules: [
    '@nuxtjs/axios'
  ],
  axios: {
    proxy: true
  },
  proxy: {
    '/api/': { target: 'https://jsonplaceholder.typicode.com/', 
     pathRewrite: {'^/api/': ''} },
  }
}

axios自定义选项中的proxy: true选项告诉 Nuxt 使用@nuxtjs/proxy模块作为代理。proxy自定义选项中的/api/: {...}选项告知@nuxtjs/axios模块使用https://jsonplaceholder.typicode.com/ 作为 API 服务器的目标地址,pathRewrite选项告知@nuxtjs/axios模块在 HTTP 请求时从地址中删除/api/,因为没有与/api的路由在目标 API 中。

  1. 接下来,在任何组件中无缝地使用它们,如以下示例所示:
// pages/index.vue
<template>
  <ul>
    <li v-for="user in users">
      {{ user.name }}
    </li>
  </ul>
</template>

<script>
export default {
  async asyncData({ $axios }) {
    const users = await $axios.$get('/api/users')
    return { users }
  }
}
</script>

将这两个模块结合使用,我们现在可以在请求方法(例如,getpostput中只写较短的 API 地址,例如/api/users而不是https://jsonplaceholder.typicode.com/users。这使得我们的代码更加整洁,因为我们不必在每次调用时编写完整的 URL。请注意,我们在 Nuxt 配置文件中配置的/api/地址将添加到对 API 端点的所有请求中。所以我们使用pathRewrite,正如我们已经解释过的,在发送请求时删除它。

You can find out more info and top-level options provided by these two modules at the following links:

您可以在 GitHub 存储库中找到我们刚刚在/chapter-6/nuxt-universal/module-snippets/top-level/中创建的示例模块片段。

使用 addPlugin 助手

还记得我们在编写基本模块一节中介绍的ModuleContainer实例和this.addPlugin助手方法吗?在本例中,我们将使用此帮助器创建一个提供插件的模块,该插件为bootstrap-vue,将在 Vue 实例中注册。让我们通过以下步骤创建此模块片段:

  1. 安装 Bootstrap 和 BootstrapVue:
$ npm i bootstrap-vue
$ npm i bootstrap
  1. 创建插件文件导入vuebootstrap-vue,然后使用use方法注册bootstrap-vue
// modules/bootstrap/plugin.js
import Vue from 'vue'
import BootstrapVue from 'bootstrap-vue/dist/bootstrap-vue.esm'

Vue.use(BootstrapVue)
  1. 创建一个模块文件以添加我们刚才使用addPlugin方法创建的插件文件:
// modules/bootstrap/module.js
import path from 'path'

export default function (moduleOptions) {
  this.addPlugin(path.resolve(__dirname, 'plugin.js'))
}
  1. 在 Nuxt 配置文件中添加此bootstrap模块的文件路径:
// nuxt.config.js
export default {
  modules: [
    ['~/modules/bootstrap/module']
  ]
}
  1. 开始在您喜欢的任何组件上使用bootstrap-vue;例如,让我们创建一个按钮来切换引导中的警报文本,如下所示:
// pages/index.vue
<b-button @click="toggle">
  {{ show ? 'Hide' : 'Show' }} Alert
</b-button>
<b-alert v-model="show">
  Hello {{ name }}!
</b-alert>

import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

export default {
  data () {
    return {
      name: 'BootstrapVue',
      show: true
    }
  }
}

有了这个模块代码段,我们不必每次需要在组件上导入bootstrap-vue,因为它已经通过前面的代码段模块全局添加。我们只需要导入它的 CSS 文件。在使用示例中,我们使用 Bootstrap 的自定义<b-button>组件切换 Bootstrap 的自定义<b-alert>组件。然后,<b-button>组件将在该按钮上切换文本“隐藏”或“显示”。

For more information on BootstrapVue, please visit https://bootstrap-vue.js.org/. You can find the example module snippet we just created in /chapter-6/nuxt-universal/module-snippets/provide-plugin/ in our GitHub repository.

使用 Lodash 模板

同样,这是我们在编写基本模块一节中创建的自定义模块中熟悉的内容——利用 Lodash 模板通过使用 if 条件块更改已注册插件的输出。同样,Lodash 模板是代码块,我们可以使用<%= %>插值定界符对数据属性进行插值。让我们在以下步骤中尝试另一个简单示例:

  1. 创建插件文件导入axios并添加 if 条件块,确保axios提供了请求 URL,并在您的 Nuxt app 以dev模式(npm run dev运行时在终端上打印请求结果进行调试:
// modules/users/plugin.js
import axios from 'axios'

let users = []
<% if (options.url) { %>
  users = axios.get('<%= options.url %>')
<% } %>

<% if (options.debug) { %>
  // Dev only code
  users.then((response) => {
    console.log(response);
  })
  .catch((error) => {
    console.log(error);
  })
<% } %>

export default ({ app }, inject) => {
  inject('getUsers', async () => {
    return users
  })
}
  1. 创建一个module文件,添加我们刚才使用addPlugin方法创建的插件文件,使用options选项将请求 URL 和this.options.dev的布尔值传递给该插件:
// modules/users/module.js
import path from 'path'

export default function (moduleOptions) {
  this.addPlugin({
    src: path.resolve(__dirname, 'plugin.js'),
    options: {
      url: 'https://jsonplaceholder.typicode.com/users',
      debug: this.options.dev
    }
  })
}
  1. 将此模块的文件路径添加到 Nuxt 配置文件:
// nuxt.config.js
export default {
  modules: [
      ['~/modules/users/module']
    ]
}
  1. 开始在您喜欢的任何组件上使用$getUsers方法,如下例所示:
// pages/index.vue
<li v-for="user in users">
  {{ user.name }}
</li>

export default {
  async asyncData({ app }) {
    const { data: users } = await app.$getUsers()
    return { users }
  }
}

在前面的示例中,当将插件复制到项目中时,Nuxt 将options.url替换为https://jsonplaceholder.typicode.com/usersoptions.debug的 if 条件块将从生产版本的插件代码中剥离,因此在生产模式下(npm run buildnpm run start下),您不会在终端上看到console.log输出。

You can find the example module snippet we just created in /chapter-6/nuxt-universal/module-snippets/template-plugin/ in our GitHub repository.

添加 CSS 库

使用 addPlugin helper部分的模块片段示例中,我们创建了一个模块,允许我们在应用中全局使用bootstrap-vue插件,而无需使用import语句来要求此插件,如下例所示:

// pages/index.vue
<b-button size="sm" @click="toggle">
  {{ show ? 'Hide' : 'Show' }} Alert
</b-button>

import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
export default {
  //...
}

它看起来相当整洁,因为我们不必每次都导入bootstrap-vue,而只需导入 CSS 样式。但是,通过模块将样式添加到应用的全局 CSS 堆栈中,我们仍然可以节省几行代码。让我们创建一个新示例,看看如何在以下步骤中做到这一点:

  1. 创建一个模块文件,其中包含一个名为optionsconst变量,用于将模块和顶级选项传递给插件文件,以及一个 if 条件块,用于确定是否使用普通 JavaScriptpush方法将 CSS 文件push传递给 Nuxt 配置文件中的css选项:
// modules/bootstrap/module.js
import path from 'path'
export default function (moduleOptions) {
  const options = Object.assign({}, this.options.bootstrap, 
    moduleOptions)

  if (options.styles !== false) {
    this.options.css.push('bootstrap/dist/css/bootstrap.css')
    this.options.css.push('bootstrap-vue/dist/bootstrap-vue.css')
  }

  this.addPlugin({
    src: path.resolve(__dirname, 'plugin.js'),
    options
  })
}
  1. 创建一个注册了bootstrap-vue插件的插件文件,以及一个 if 条件 Lodash 模板块,以打印从模块文件处理的选项:
// modules/bootstrap/plugin.js
import Vue from 'vue'
import BootstrapVue from 'bootstrap-vue/dist/bootstrap-vue.esm'

Vue.use(BootstrapVue)

<% if (options.debug) { %>
  <% console.log (options) %>
<% } %>
  1. 将模块的文件路径添加到 Nuxt 配置文件,模块选项指定是否禁用模块文件中的 CSS 文件。另外,添加顶级选项bootstrap,将布尔值传递给debug选项:
// nuxt.config.js
export default {
  modules: [
    ['~/modules/bootstrap/module', { styles: true }]
  ],

  bootstrap: {
    debug: process.env.NODE_ENV === 'development' ? true : false
  }
}
  1. 从组件中删除 CSS 文件:
// pages/index.vue
<script>
- import 'bootstrap/dist/css/bootstrap.css'
- import 'bootstrap-vue/dist/bootstrap-vue.css'
export default {
  //...
}
</script>

因此,最后,我们可以在我们的组件中使用bootstrap-vue插件及其 CSS 文件,而无需导入所有这些文件。下面是另一个通过模块代码段将字体 Awesomecss选项推送到 Nuxt 配置文件的快速示例:

// modules/bootstrap/module.js
export default function (moduleOptions) {
  if (moduleOptions.fontAwesome !== false) {
    this.options.css.push('font-awesome/css/font-awesome.css')
  }
}

If you want to find out more information about Font Awesome, please visit https://fontawesome.com/.

You can find the example module snippet we just created in /chapter-6/nuxt-universal/module-snippets/css-lib/ in our GitHub repository.

注册自定义网页包加载程序

当我们想在 Nuxt 中扩展 webpack 配置时,我们通常在nuxt.config.js中使用build.extend进行扩展。但我们可以通过使用this.extendBuild和以下模块/加载程序模板的模块来实现这一点:

export default function (moduleOptions) {
  this.extendBuild((config, { isClient, isServer }) => {
    //...
  })
}

例如,假设我们想用svg-transform-loader扩展我们的网页包配置,这是一个网页包加载器,用于在 SVG 图像中添加或修改标记和属性。此加载程序的主要目的是允许我们对 SVG 图像使用fillstroke和其他操作。我们也可以在 CSS、Sass、Less、手写笔或 PostCSS 中使用它;例如,如果要用白色填充 SVG 图像,可以使用fillfff(CSS 颜色白色代码)添加到图像中,如下所示:

.img {
  background-image: url('./img.svg?fill=fff');
}

如果您想在 Sass 中使用变量strokeSVG 图像,可以按如下方式操作:

$stroke-color: fff;

.img {
  background-image: url('./img.svg?stroke={$stroke-color}');
}

让我们创建一个示例模块,将此加载程序注册到 Nuxt webpack 默认配置,以便我们可以通过以下步骤操作 Nuxt 应用中的 SVG 图像:

  1. 使用 npm 安装加载程序:
$ npm i svg-transform-loader
  1. 使用前面提供的模块/加载程序模板创建模块文件,如下所示:
// modules/svg-transform-loader/module.js
export default function (moduleOptions) {
  this.extendBuild((config, { isClient, isServer }) => {
    //...
  })
}
  1. this.extendBuild的回调中,添加以下行以查找文件加载器,并从其现有规则测试中删除svg
const rule = config.module.rules.find(
  r => r.test.toString() === '/\\.(png|jpe?g|gif|svg|webp)$/i'
)
rule.test = /\.(png|jpe?g|gif|webp)$/i
  1. svg-transform-loader加载器的push前面的代码块之后,将以下代码块添加到默认网页配置的模块规则中:
config.module.rules.push({
  test: /\.svg(\?.)?$/, // match img.svg and img.svg?param=value
  use: [
    'url-loader',
    'svg-transform-loader'
  ]
})

模块现在已经完成,我们可以继续步骤 5

  1. 将此模块的文件路径添加到 Nuxt 配置文件:
// nuxt.config.js
export default {
  modules: [
    ['~/modules/svg-transform-loader/module']
  ]
}
  1. 开始转换组件中的任何 SVG 图像,例如:
// pages/index.vue
<template>
  <div>
    <div class="background"></div>
    <img src=img/bug.svg?stroke=red&stroke-
     width=4&fill=blue">
  </div>
</template>

<style lang="less">
.background {
   height: 100px;
   width: 100px;
   border: 4px solid red;
   background-image: url('~img/bug.svg?stroke=red&stroke-
    width=2');
}
</style>

You can find out more information about svg-transform-loader at https://www.npmjs.com/package/svg-transform-loader. If you want to learn more about the rule test, and to see the full content of the Nuxt default webpack config, please visit the following links:

You can find the example module snippet we just created in /chapter-6/nuxt-universal/module-snippets/webpack-loader/ in our GitHub repository.

注册自定义网页包插件

Nuxt 模块不仅允许我们注册网页包加载器,还允许我们在以下模块/插件架构中使用this.options.build.plugins.push注册网页包插件:

export default function (moduleOptions) {
  this.options.build.plugins.push({
    apply(compiler) {
      compiler.hooks.<hookType>.<tap>('<PluginName>', (param) => {
        //...
      })
    }
  })
}

<tap>取决于吊钩类型;它可以是tapAsynctapPromise或只是tap。让我们按照以下步骤通过 Nuxt 模块创建一个非常简单的“Hello World”网页包插件:

  1. 使用我们为打印“Hello World!”提供的模块/插件体系结构创建模块文件,如下所示:
// modules/hello-world/module.js
export default function (moduleOptions) {
  this.options.build.plugins.push({
    apply(compiler) {
      compiler.hooks.done.tap('HelloWordPlugin', (stats) => {
        console.log('Hello World!')
      })
    }
  })
}

请注意,stats(统计信息)在点击done钩子时作为参数传递。

  1. 将此模块的文件路径添加到 Nuxt 配置文件:
// nuxt.config.js
export default {
 modules: [
 ['~/modules/hello-world/module']
}
  1. 使用$ npm run dev运行 Nuxt 应用,您应该会在终端上看到“Hello World!”。

请注意,apply方法、compilerhooks和 TAP 都是构建网页包插件的关键部分。

If you are new to webpack plugins and want to learn more about how to develop plugins for webpack, please visit https://webpack.js.org/contribute/writing-a-plugin/.

You can find the example module snippet we just created in /chapter-6/nuxt-universal/module-snippets/webpack-plugin/ in our GitHub repository.

在特定挂钩上创建任务

如果在启动 Nuxt 时需要对特定生命周期事件执行某些任务(例如,当所有模块都完成加载时),则可以创建一个模块并使用hook方法侦听该事件,然后执行该任务。考虑下面的例子:

  • 如果要在所有模块完成加载后执行某些操作,请尝试以下操作:
export default function (moduleOptions) {
  this.nuxt.hook('modules:done', moduleContainer => {
    //...
  })
}
  • 如果要在创建渲染器后执行某些操作,请尝试以下操作:
export default function (moduleOptions) {
  this.nuxt.hook('render:before', renderer => {
    //...
  })
}
  • 如果要在编译器(默认为 webpack)启动之前执行某些操作,请尝试以下操作:
export default function (moduleOptions) {
  this.nuxt.hook('build:compile', async ({ name, compiler }) => {
    //...
  })
}
  • 如果要在 Nuxt 生成页面之前执行某些操作,请尝试以下操作:
export default function (moduleOptions) {
  this.nuxt.hook('generate:before', async generator => {
    //...
  })
}
  • 如果要在 Nuxt 就绪时执行某些操作,请尝试以下操作:
export default function (moduleOptions) {
  this.nuxt.hook('ready', async nuxt => {
    //...
  })
}

让我们创建一个简单的模块,按照以下步骤监听modules:done钩子/事件:

  1. 创建模块文件,在加载所有模块时打印'All modules are loaded'
// modules/tasks/module.js
export default function (moduleOptions) {
  this.nuxt.hook('modules:done', moduleContainer => {
    console.log('All modules are loaded')
  })
}
  1. 创建更多用于打印'Module 1''Module 2''Module 3'等的模块,如下所示:
// modules/module1.js
export default function (moduleOptions) {
  console.log('Module 1')
}
  1. 将钩子模块和其他模块的文件路径添加到 Nuxt 配置文件:
// nuxt.config.js
export default {
  modules: [
    ['~/modules/tasks/module'],
    ['~/modules/module3'],
    ['~/modules/module1'],
    ['~/modules/module2']
  ]
}
  1. 使用$ npm run dev运行 Nuxt 应用,您应该会在终端上看到以下输出:
Module 3
Module 1
Module 2
All modules are loaded

您可以看到,钩子模块总是最后打印,而其余的则是根据modules选项中的顺序打印的。

钩子模块可以是异步的,无论您是使用async/ await函数还是返回Promise

For more information about the preceding hooks and other hooks in the Nuxt's life cycle events, please visit the following links:

You can find the example module snippet we just created in /chapter-6/nuxt-universal/module-snippets/hooks/ in our GitHub repository.

总结

在本章中,我们成功地介绍了 Nuxt 中的插件和模块。您已经了解到,从技术上讲,它们是 JavaScript 函数,您可以为项目创建这些函数,或者从外部源导入它们。此外,您还学习了如何在 Nuxt 环境中创建全局函数,方法是将它们注入到 Nuxt 应用的 Vue 实例或 Nuxt 上下文中,或两者都注入,以及创建仅客户端和仅服务器的函数。最后,您已经学会了使用addPlugin助手创建用于添加 JavaScript 库的模块片段,全局添加 CSS 库,使用 Lodash 模板有条件地更改已注册插件的输出,将网页包加载程序和插件添加到 Nuxt 默认网页包配置,以及使用 Nuxt 生命周期事件挂钩创建任务,例如modules:done

在下一章中,我们将探索 Vue 表单并将其添加到 Nuxt 应用中。您将了解v-model如何在 HTML 元素中工作,例如texttextareacheckboxradioselect。您将学习如何在 Vue 应用中验证这些元素,绑定默认数据和动态数据,并使用修改器(如.lazy.trim)修改或强制输入值。您还将学习使用 Vue 插件vee-validate验证它们,然后将它们应用于 Nuxt 应用。我们将引导您顺利完成所有这些领域。所以请继续关注。