yanyue404 / blog

Just blog and not just blog.
https://yanyue404.github.io/blog/
Other
87 stars 13 forks source link

你不知道的 Nuxt Module #263

Open yanyue404 opened 11 months ago

yanyue404 commented 11 months ago

Modules

模块是 Nuxt.js 扩展,可以扩展其核心功能并添加无限的集成。

在使用 Nuxt 开发应用程序时,您很快就会发现框架的核心功能还不够。 Nuxt 可以使用配置选项和插件进行扩展,但是在多个项目中维护这些自定义是繁琐、重复和耗时的。 另一方面,开箱即用支持每个项目的需求将使 Nuxt 非常复杂且难以使用。

这就是 Nuxt 提供高阶模块系统使得扩展核心成为可能的原因之一。模块是启动 Nuxt 时顺序调用的函数。框架等待每个模块完成后再继续。通过这种方式,模块几乎可以定制项目的任何方面。由于 Nuxt 的模块化设计(基于 webpack 的 Tapable ),模块可以轻松地为某些入口点(例如构建器初始化)注册钩子。模块还可以覆盖模板、配置 webpack 加载器、添加 CSS 库以及执行许多其他有用的任务。我们可以使用功能强大的  Hookable Nuxt.js 系统来完成特定事件的任务。

最重要的是,Nuxt 模块可以合并到 npm 包中。这使得跨项目重用并与社区共享成为可能,从而帮助创建高质量附加组件的生态系统。

如果你:

Nuxt.js 模块列表

Nuxt.js 团队提供   官方   模块:

Nuxt.js 社区制作的模块列表可在  https://github.com/topics/nuxt-module  中查询

基础模块

如下所示,一个模块只需要导出一个默认函数即可,当模块需要做一些异步操作时,可以返回 Promise 对象或者传入 callback 参数,此处推荐使用 async/await 直接返回 Promise 对象。

modules/simple.js

export default function SimpleModule(moduleOptions) {
  // Write your code here
}

// REQUIRED if publishing as an npm package
// module.exports.meta = require('./package.json')

在模块中,通常会使用到以下变量:

moduleOptions

这是用户使用modules数组传递对象,我们可以使用它来定制它的行为。

this.options

您可以使用此属性直接访问 Nuxt 选项。这是nuxt.config.js,其中包含所有默认选项,可用于模块之间的共享选项。

this.nuxt

这是对当前 Nuxt 实例的引用。 请参考 Nuxt class docs for available methods.

this

modules 中的 context, 请参考 ModuleContainer 来查看可用的方法。

module.exports.meta

如果要将模块发布为 npm 包,则需要配置此选项。Nuxt 内部使用meta来更好地处理您的包。

nuxt.config.js

export default {
  modules: [
    // Using package name
    "@nuxtjs/axios",

    // Relative to your project srcDir
    "~/modules/awesome.js",

    // Providing options
    ["@nuxtjs/google-analytics", { ua: "X1234567" }],

    // Inline definition
    function () {},
  ],
};

模块需要在 nuxt.config.js 中通过 modules 注册,对于 npm 安装的模块可以直接使用包名,对于项目代码中的模块,可以使用相对路径,使用 @ 别名,将从项目 srcDir 解析,我们还可以为模块提供额外的参数。

编写自己的模块

模块就是函数。它们可以打包为 npm 模块或直接包含在项目源代码中。

export default {
  exampleMsg: "hello",
  modules: [
    // Simple usage
    "~/modules/example",
    // Passing options directly
    ["~/modules/example", { token: "123" }],
  ],
};
export default function ExampleModule(moduleOptions) {
  console.log(moduleOptions.token); // '123'
  console.log(this.options.exampleMsg); // 'hello'

  this.nuxt.hook("ready", async (nuxt) => {
    console.log("Nuxt is ready");
  });
}

// REQUIRED if publishing the module as npm package
module.exports.meta = require("./package.json");

异步模块

并非所有模块都会同步完成所有操作,例如:您可能希望开发一个需要获取某些 API 或执行异步 IO 的模块。为此,Nuxt 支持在异步模块中返回 Promise 或调用回调。

使用 async/await

请注意,仅在 Node.js > 7.2 中支持使用 async / await。 因此,如果您是模块开发人员,至少要警告用户使用它们时 Node.js 版本不能低于 7.2。 对于大量异步模块或更好的传统支持,您可以使用 bundler 将其转换为兼容较旧的 Node.js 版本或 Promise 方法。

import fse from "fs-extra";

export default async function asyncModule() {
  // You can do async works here using `async`/`await`
  const pages = await fse.readJson("./pages.json");
}

返回 Promise

import axios from "axios";

export default function asyncModule() {
  return axios
    .get("https://jsonplaceholder.typicode.com/users")
    .then((res) => res.data.map((user) => `/users/${user.username}`))
    .then((routes) => {
      // Do something by extending Nuxt routes
    });
}

使用回调

import axios from "axios";

export default function asyncModule(callback) {
  axios
    .get("https://jsonplaceholder.typicode.com/users")
    .then((res) => res.data.map((user) => `/users/${user.username}`))
    .then((routes) => {
      callback();
    });
}

常见模块

优先级最高选项

有时在nuxt.config.js中注册模块时可以使用顶级选项更方便,这允许我们组合多个选项源。

nuxt.config.js

export default {
  modules: [["@nuxtjs/axios", { anotherOption: true }]],

  // axios module is aware of this by using `this.options.axios`
  axios: {
    option1,
    option2,
  },
};

module.js

export default function (moduleOptions) {
  const options = Object.assign({}, this.options.axios, moduleOptions);
  // ...
}

注册插件

通常,我们会在 nuxt.config.js 中配置 plugins 来注册插件,但如果我们想在模块中集成这部分功能,就需要用到this.addPlugin方法。

vantLoadPlugin.js

import { Dialog, Toast } from "vant";
import Vue from "vue";

Vue.use(Toast).use(Dialog);

export default (context, inject) => {
  // 与nuxt.config.js中定义的plugins一样,此处可以选择注入变量到context和vue实例
};

vantLoadModule.js

const path = require("path");

export default async function vantLoadModule() {
  // Registre 'vantLoadPlugin' template
  this.addPluin(path.resolve(__dirname, "vantLoad.js"));
}

模板插件

插件会在 @nuxt/builder 中通过 lodash template 方法进行模板编译,因此可以在注册插件时提供 options 做为模板编译参数使用,此时 addPlugin 参数对象 src 属性为插件地址。

plugin.js

// Set Google Analytics UA
ga('create', '<%= options.ua %>', 'auto')

<% if (options.debug) { %>
// Dev only code
<% } %>

module.js

import path from "path";

export default function nuxtBootstrapVue(moduleOptions) {
  // Register `plugin.js` template
  this.addPlugin({
    src: path.resolve(__dirname, "plugin.js"),
    options: {
      // Nuxt will replace `options.ua` with `123` when copying plugin to project
      ua: 123,
      // conditional parts with dev will be stripped from plugin code on production builds
      debug: this.options.dev,
    },
  });
}

注册 webpack loader

我们可以使用 this.extendBuild 在 nuxt.config.js 中执行与 build.extend 相同的操作。

export default function (moduleOptions) {
    this.extendBuild((config, { isClient, isServer }) => {
      // `.foo` Loader
      config.module.rules.push({
        test: /\.foo$/,
        use: [...]
      })

      // Customize existing loaders
      // Refer to source code for Nuxt internals:
      // https://github.com/nuxt/nuxt/blob/2.x-dev/packages/webpack/src/config/base.js
      const barLoader = config.module.rules.find(rule => rule.loader === 'bar-loader')
  })
}

注册 webpack 插件

我们可以注册 webpack 插件用来在构建期间发出资源。需要了解 webpack 构建原理以及插件开发。

nuxt@2.11.0,对应 webpack 大版本为 v4,开发插件时请注意版本。

export default function (moduleOptions) {
  const info = `Built by awesome module - 1.3 alpha on ${Date.now()}`;

  this.options.build.plugins.push({
    apply(compiler) {
      compiler.hooks.emit.tapAsync("emit", (compilation, cb) => {
        // This will generate `.nuxt/dist/info.txt' with contents of info variable.
        // Source can be buffer too
        compilation.assets["info.txt"] = {
          source: () => info,
          size: () => info.length,
        };

        cb();
      });
    },
  });
}

上面的模块为 webpack 添加了一个新 plugin,监听了 compiler 的 emit hooks,在生成资源到 output 目录之前添加了一个新的输出资源 info.txt。

为构建提供扩展配置

我们可以使用this.extendBuild为客户端和服务端的构建配置进行手工的扩展处理,这与nuxt.config.js中的build.extend属性含义相同。该扩展方法会服务端和客户端打包构建分别被调用,该方法的参数如下:

  1. Webpack 配置对象
  2. 构建环境对象,包括这些属性(全部为布尔类型):isDevisClientisServer
export default function (options) {
  // 构建扩展配置,会在nuxt.config.js中build.extend之后执行
  this.context.extendBuild((config, { isClient }) => {
    config.externals = [];
    // 为客户端打包进行扩展配置
    if (isClient) {
      // 开发环境增加source-map
      if (this.envs.PATH_TYPE === "development") {
        config.devtool = "eval-source-map";
      } else {
        // runtime不参与打包
        config.externals.push({
          vue: "Vue",
          vuex: "Vuex",
          "vue-router": "VueRouter",
        });
      }
    }
  });
}

上面的模块为构建提供了扩展配置,在客户端时构建时,开发环境配置 source-map ,非开发环境配置 vue runtime 不参与打包。您还可以使用暴露的 webpack 对象进行更加高级的操作。

nuxt 生命周期注册钩子

this.nuxt :这是对当前 Nuxt 实例的引用。我们可以在某些生命周期事件上注册钩子。

nuxt.hook("ready", async (nuxt) => {
  // Your custom code here
});
nuxt.hook("error", async (error) => {
  // Your custom code here
});
nuxt.hook("close", async (nuxt) => {
  // Your custom code here
});
nuxt.hook("listen", async (server, { host, port }) => {
  // Your custom code here
});

this :模块的上下文。所有模块都将在 ModuleContainer 实例的上下文中调用。

请查看 ModuleContainer 类文档以获取可用的方法。

在指定钩子上执行任务

您的模块可能只需要在特定条件下执行操作,而不仅仅是在 Nuxt 初始化期间。我们可以使用Hookable Nuxt.js 系统来完成特定事件的任务。Nuxt 将等待钩子返回Promise或被定义为async(异步)。

export default function () {
  // Add hook for modules
  this.nuxt.hook('modules:done', moduleContainer => {
    // This will be called when all modules finished loading
  })

  // Add hook for renderer
  this.nuxt.hook('render:before', renderer => {
    // Called after the renderer was created
  })

  // Add hook for build:extendRoutes
  this.nuxt.hook('build:extendRoutes', async routes => {
    // This will be called after routes created
  })

  // Add hook for build:compile
  this.nuxt.hook('build:compile', async {compiler} => {
    // This will be run just before webpack compiler starts
  })

  // Add hook for generate
  this.nuxt.hook('generate:before', async generator => {
    // This will be called before Nuxt generates your pages
  })
}

上面仅列出了一部分 hooks,请参考 Nuxt Internals 了解有关 Nuxt 内部 API 的更多信息。

Nuxt 的 hooks 功能来自 hable 模块,Nuxt 类本身继承了 hable,所以在模块中可以使用 this.nuxt 注册和调用。

export default function () {
  this.nuxt.hook("hook1", async (param) => {
    // 注册 hook1 回调
  });
  // 调用钩子 hook1
  await this.nuxt.callHook("hook1");
}

来个实战

根治 ———— nuxt 项目遇到的,网页重新部署,通知用户。

nuxt.config.js

export default {
  mode: 'universal',
  env: {
    PATH_TYPE: process.env.PATH_TYPE
  },
  /*
   ** Nuxt.js modules
   */
  modules: [
    [
      '~/webUpdateNotification',
      {
        logVersion: true,
        checkInterval: 5 * 60 * 1000, // 5 分钟设置轮询一次
        // 在某些环境下才开启该 Module
        shouldBeEnable: (options) => {
          if (options.env.PATH_TYPE === 'trial') {
            return true
          }
          return false
        }
      }
    ]
  ],
}

webUpdateNotification.js

import { accessSync, constants, readFileSync, writeFileSync } from 'fs'
import { resolve } from 'path'
import {
  DIRECTORY_NAME,
  INJECT_SCRIPT_FILE_NAME,
  INJECT_STYLE_FILE_NAME,
  JSON_FILE_NAME,
  NOTIFICATION_ANCHOR_CLASS_NAME,
  generateJSONFileContent,
  generateJsFileContent,
  getFileHash,
  getVersion,
  get__Dirname
} from '@plugin-web-update-notification/core'

const pluginName = 'WebUpdateNotificationPlugin'

/**
 * It injects the hash into the HTML, and injects the notification anchor and the stylesheet and the
 * script into the HTML
 * @param {string} html - The original HTML of the page
 * @param {string} version - The hash of the current commit
 * @param {Options} options - Options
 * @returns The html of the page with the injected script and css.
 */
function injectPluginHtml(
  html = '',
  version = '',
  options = {},
  { cssFileHash = '', jsFileHash = '' }
) {
  const {
    customNotificationHTML,
    hiddenDefaultNotification,
    injectFileBase = '/'
  } = options

  const versionScript = `<script>window.pluginWebUpdateNotice_version = '${version}';</script>`
  const cssLinkHtml =
    customNotificationHTML || hiddenDefaultNotification
      ? ''
      : `<link rel="stylesheet" href="${injectFileBase}${DIRECTORY_NAME}/${INJECT_STYLE_FILE_NAME}.${cssFileHash}.css">`
  let res = html

  res = res.replace(
    '<head>',
    `<head>
    ${cssLinkHtml}
    <script src="${injectFileBase}${DIRECTORY_NAME}/${INJECT_SCRIPT_FILE_NAME}.${jsFileHash}.js"></script>

    ${versionScript}`
  )

  if (!hiddenDefaultNotification) {
    res = res.replace(
      '</body>',
      `<div class="${NOTIFICATION_ANCHOR_CLASS_NAME}"></div></body>`
    )
  }

  return res
}

export default function WebUpdateNotificationPlugin(moduleOptions = {}) {
  // this 为 modules 中的 context
  // 所有的 nuxt 的配置项
  const options = this.options
  // 对当前 nuxt 实例的引用
  const nuxt = this.nuxt

  /** inject script file hash */
  let jsFileHash = ''
  /** inject css file hash */
  let cssFileHash = ''
  // 版本信息
  let version = ''

  // 留一个函数 shouldBeEnable 可以判断是否开启
  if (typeof moduleOptions.shouldBeEnable === 'function') {
    const enable = moduleOptions.shouldBeEnable(options)
    if (!enable) {
      return
    }
  }

  // Emit assets: 注册 webpack 插件在构建期间发出资源
  options.build.plugins.push({
    apply(compiler) {
      // 仅执行一次标识 (client 和 server 会分别走一次,不用再走服务端一次)
      if (compiler.options.name !== 'client') {
        return
      }

      const { publicPath } = compiler.options.output
      if (moduleOptions.injectFileBase === undefined)
        moduleOptions.injectFileBase =
          typeof publicPath === 'string' ? publicPath : '/'

      const { hiddenDefaultNotification, versionType, customVersion, silence } =
        moduleOptions
      if (versionType === 'custom')
        version = getVersion(versionType, customVersion)
      else version = getVersion(versionType)

      compiler.hooks.emit.tap(pluginName, (compilation) => {
        // const outputPath = compiler.outputPath
        const jsonFileContent = generateJSONFileContent(version, silence)
        // @ts-expect-error
        compilation.assets[`${DIRECTORY_NAME}/${JSON_FILE_NAME}.json`] = {
          source: () => jsonFileContent,
          size: () => jsonFileContent.length
        }
        if (!hiddenDefaultNotification) {
          const injectStyleContent = readFileSync(
            `${get__Dirname()}/${INJECT_STYLE_FILE_NAME}.css`,
            'utf8'
          )
          cssFileHash = getFileHash(injectStyleContent)

          // @ts-expect-error
          compilation.assets[
            `${DIRECTORY_NAME}/${INJECT_STYLE_FILE_NAME}.${cssFileHash}.css`
          ] = {
            source: () => injectStyleContent,
            size: () => injectStyleContent.length
          }
        }

        const filePath = resolve(
          `${get__Dirname()}/${INJECT_SCRIPT_FILE_NAME}.js`
        )

        const injectScriptContent = generateJsFileContent(
          readFileSync(filePath, 'utf8').toString(),
          version,
          moduleOptions // 传入 module 的参数控制
        )
        jsFileHash = getFileHash(injectScriptContent)

        // @ts-expect-error
        compilation.assets[
          `${DIRECTORY_NAME}/${INJECT_SCRIPT_FILE_NAME}.${jsFileHash}.js`
        ] = {
          source: () => injectScriptContent,
          size: () => injectScriptContent.length
        }
      })
    }
  })

  // Hook on generation finished
  nuxt.hook('generate:done', async (generator) => {
    const htmlFilePath = resolve(
      generator.distPath,
      moduleOptions.indexHtmlFilePath || './index.html' // 可以自定义写入要 inject 的 html 地址
    )
    try {
      accessSync(htmlFilePath, constants.F_OK)

      let html = readFileSync(htmlFilePath, 'utf8')
      html = injectPluginHtml(html, version, moduleOptions, {
        jsFileHash,
        cssFileHash
      })
      writeFileSync(htmlFilePath, html)
    } catch (error) {
      console.error(error)
      console.error(
        `${pluginName} failed to inject the plugin into the HTML file. index.html(${htmlFilePath}) not found.`
      )
    }
  })
}

调试

教你调试 nuxt modules。

由于文档并不能全面了解 Nuxt 构建过程,所以很多时候需要查看 Nuxt 源码,这里提供些小技巧给大家。

我们可以在项目目录中执行 nuxt 命令,其实是因为npm install nuxt 过程中在 ~/node_modules/.bin 下添加了 nuxt 启动文件,内容如下所示:

~/node_modules/.bin/nuxt

#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")

case `uname` in
    *CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w "$basedir"`;;
esac

if [ -x "$basedir/node" ]; then
  "$basedir/node"  "$basedir/../nuxt/bin/nuxt.js" "$@"
  ret=$?
else
  node  "$basedir/../nuxt/bin/nuxt.js" "$@"
  ret=$?
fi
exit $ret

可以看到最终执行了 /../nuxt/bin/nuxt.js

~/node_modules/nuxt/bin/nuxt.js

#!/usr/bin/env node
const suffix = require('../package.json').name.includes('-edge') ? '-edge' : ''
require('@nuxt/cli' + suffix).run()
  .catch((error) => {
    require('consola').fatal(error)
    process.exit(2)
  })

又调用了 @nuxt/cli ,查看 @nuxt/cli package.json 中 main 属性可知,入口文件为 ./dist/cli.js。结合 cli.js 中的逻辑可以确定,nuxt 入口文件就在这里,根据 nuxt 命令参数的不同,会分别调用 ./cli-dev.js 和 ./cli-generate.js

虽然确定了入口文件,但仅通过 console 的方式很难调试代码,还好 vscode 内置了调试功能,可以方便的调试 JS 代码。

  1. 在需要调试的代码行最前面添加断点。
  2. 在 vscode 控制台右上角下拉选项中选择 JavaScript Debug Terminal
  3. 在打开的 Debug Terminal 中执行构建命令会自动进行调试模式,如执行 npm run serve
  4. 然后就可以愉快的调试了

参考

这里还有更多关于 nuxt