vite 插件钩子详解

vite 开发模式下插件的生命周期钩子执行顺序

其中 Vite 会调用一系列与 Rollup 兼容的钩子,这个钩子主要分为三个阶段:

  • 服务器启动阶段: optionsbuildStart 钩子会在服务启动时被调用

  • 请求响应阶段: 当浏览器发起请求时,Vite 内部依次调用 resolveIdloadtransform 钩子

  • 服务器关闭阶段: Vite 会依次执行 buildEndcloseBundle 钩子。

除了以上钩子,其他 Rollup 插件钩子(如 moduleParsedrenderChunk)均不会在 Vite 开发阶段调用。而生产环境下,由于 Vite 直接使用 RollupVite 插件中所有 Rollup 的插件钩子都会生效。

执行顺序

image 2024 11 30 21 38 04 316
  • 服务启动阶段: configVite 独有)、configResolvedVite 独有)、optionsconfigureServer(Vite 独有)、buildStart

  • 请求响应阶段: 如果是 html 文件,仅执行 transformIndexHtml 钩子;对于非 HTML 文件,则依次执行 resolveIdloadtransform 钩子

  • 热更新阶段: 执行 handleHotUpdate 钩子。

  • 服务关闭阶段: 依次执行 buildEndcloseBundle 钩子

应用场景

默认情况下 Vite 插件同时被用于开发环境和生产环境,你可以通过 apply 属性来决定应用场景:

{
  // 'serve' 表示仅用于开发环境,'build'表示仅用于生产环境
  apply: 'serve'
}

apply 参数还可以配置成一个函数,进行更灵活的控制:

apply(config, { command }) {
  // 只用于非 SSR 情况下的生产环境构建
  return command === 'build' && !config.build.ssr
}

应用顺序

可以通过 enforce 属性来指定插件的执行顺序:

{
  // 默认为`normal`,可取值还有`pre`和`post`
  enforce: 'pre'
}
image 2024 11 30 21 40 50 056

vite 插件各个钩子

config

Vite 在读取完配置文件(即 vite.config.ts)之后,会拿到用户导出的配置对象,然后执行 config 钩子。在这个钩子里面,你可以对配置文件导出的对象进行自定义的操作,如下代码所示:

// 返回部分配置(推荐)
const editConfigPlugin = () => ({
  name: 'vite-plugin-modify-config',
  config: () => ({
    alias: {
      react: require.resolve('react')
    }
  })
})

官方推荐的姿势是在 config 钩子中返回一个配置对象,这个配置对象会和 Vite 已有的配置进行深度的合并。

configResolved

Vite 在解析完配置之后会调用 configResolved 钩子,这个钩子一般用来记录最终的配置信息,而不建议再修改配置,用法如下图所示:

const exmaplePlugin = () => {
  let config

  return {
    name: 'read-config',

    configResolved(resolvedConfig) {
      // 记录最终配置
      config = resolvedConfig
    },

    // 在其他钩子中可以访问到配置
    transform(code, id) {
      console.log(config)
    }
  }
}

options

替换或操作传递给 rollup.rollup 的选项对象。返回 null 不会替换任何内容。如果只需要读取选项,则建议使用 buildStart 钩子,因为该钩子可以访问所有 options 钩子的转换考虑后的选项。

configureServer

这个钩子仅在开发阶段会被调用,用于扩展 ViteDev Server,一般用于增加自定义 server 中间件,如下代码所示:

const myPlugin = () => ({
  name: 'configure-server',
  configureServer(server) {
    // 姿势 1: 在 Vite 内置中间件之前执行
    server.middlewares.use((req, res, next) => {
      // 自定义请求处理逻辑
    })
    // 姿势 2: 在 Vite 内置中间件之后执行
    return () => {
      server.middlewares.use((req, res, next) => {
        // 自定义请求处理逻辑
      })
    }
  }
})

buildStart

在每个 rollup.rollup 构建上调用。当你需要访问传递给 rollup.rollup() 的选项时,建议使用此钩子,因为它考虑了所有 options 钩子的转换,并且还包含未设置选项的正确默认值。

resolveId

在 Vite 插件系统中,resolveId 是一个非常重要的钩子,用于解决模块的 ID(即模块的路径)。当 Vite 在构建或开发过程中遇到一个导入时,它会调用 resolveId 钩子来解析该模块的实际路径。resolveId 钩子允许插件控制如何找到和处理这些模块。

resolveId 钩子的作用

resolveId 的主要作用是:

  • 解析模块路径:Vite 通过 resolveId 钩子将模块的相对路径或模块别名转换为实际的文件路径或 URL。

  • 自定义模块解析:如果你需要处理一些自定义的导入路径(例如,模块别名、虚拟模块等),可以通过 resolveId 来实现。

  • 支持虚拟模块:虚拟模块是不存在于文件系统中的模块,通常用于通过 Vite 插件生成或修改内容。resolveId 可以将这些虚拟模块映射到相应的文件或内容。

  • 自定义解析逻辑:你可以在 resolveId 中编写自定义的模块解析逻辑,以实现更复杂的行为(例如,处理某些特殊的导入路径,或覆盖默认的解析规则)。

resolveId 的执行时机

resolveId 会在每次模块导入时执行,通常是在 Vite 解析源文件的过程中触发。当 Vite 遇到一个模块的导入语句时,它会调用插件链中的每个插件的 resolveId 钩子,直到找到一个返回非 null 值的插件为止。

resolveId 钩子会在以下几种场景下触发:

  • 解析 JavaScript、TypeScript、CSS、JSON 等模块的导入。

  • 解析组件库、插件中的虚拟模块或文件。

  • 解析带有路径别名的导入(如 @/foo)。

  • 解析扩展名不标准的导入(如 .vue.ts)。

resolveId 钩子的参数

resolveId 钩子接收以下参数:

  1. source (string): 导入语句中的模块路径(即 import 'source' 中的 'source' 部分)。这是插件需要解析的模块路径。

  2. importer (string | undefined): 导入当前模块的文件路径。也就是引发当前导入语句的文件路径。如果当前模块是顶级模块(没有其他模块导入它),该值为 undefined。

  3. isEntry (boolean): 是否是入口文件的导入。如果是入口文件的导入,Vite 会尝试将该模块作为程序的入口。

  4. ssr (boolean): 是否处于 SSR(服务器端渲染)模式。如果为 true,表示当前是 SSR 环境。

resolveId 的返回值

resolveId 必须返回以下几种值之一:

  1. null: 如果插件无法解析该模块路径,则返回 null。这表示模块将交给下一个插件或默认解析方式处理。

  2. 字符串: 如果插件能够解析该模块路径,则返回解析后的路径(例如,绝对路径或相对路径)。返回的路径将作为模块的最终路径使用。

  3. false: 返回 false 表示这个模块不再继续尝试解析。

  4. 对象(可选):可以返回一个包含 id 属性的对象,id 是解析后的路径。可以用于返回更多的元数据。

示例:使用 resolveId 自定义模块解析

以下是一个简单的插件示例,展示了如何在 Vite 中使用 resolveId 钩子自定义模块解析。

示例 1:处理自定义路径别名
import { Plugin } from 'vite';

export default function aliasPlugin(): Plugin {
  return {
    name: 'alias-plugin',
    resolveId(source) {
      // 如果是以 "@/components" 开头的模块路径,则解析为自定义路径
      if (source.startsWith('@/components')) {
        const resolvedPath = source.replace('@', '/src/components');
        return resolvedPath;
      }

      // 对于其他模块,返回 null,交给下一个插件处理
      return null;
    }
  };
}
  • 在此示例中,resolveId 钩子会拦截所有以 @/components 开头的导入路径,并将其转换为 /src/components 目录下的路径。

  • 如果模块路径不符合自定义规则,则返回 null,让 Vite 或其他插件继续处理该路径。

示例 2:处理虚拟模块
import { Plugin } from 'vite';

export default function virtualModulePlugin(): Plugin {
  return {
    name: 'virtual-module-plugin',
    resolveId(source) {
      // 如果导入的是虚拟模块 "virtual-module",返回一个自定义路径
      if (source === 'virtual-module') {
        return 'virtual:/my-virtual-module.js';
      }

      return null; // 其他路径交给 Vite 默认的解析处理
    },

    load(id) {
      // 当 Vite 请求虚拟模块时,提供该模块的内容
      if (id === 'virtual:/my-virtual-module.js') {
        return `export default 'This is a virtual module.';`;
      }
      return null;
    }
  };
}
  • 在 resolveId 中,我们拦截了对 virtual-module 模块的请求,并返回了一个虚拟模块的路径 virtual:/my-virtual-module.js。

  • 在 load 钩子中,我们处理了该虚拟模块,并返回了它的内容。

示例 3:扩展支持特定文件类型
import { Plugin } from 'vite';
import path from 'path';

export default function customFileTypePlugin(): Plugin {
  return {
    name: 'custom-file-type-plugin',
    resolveId(source) {
      // 假设我们自定义支持 `.txt` 文件的导入
      if (source.endsWith('.txt')) {
        const resolvedPath = path.resolve(__dirname, 'assets', source);
        return resolvedPath;
      }

      return null;
    },

    load(id) {
      // 读取文本文件内容
      if (id.endsWith('.txt')) {
        return `export default ${JSON.stringify('This is a text file.')};`;
      }

      return null;
    }
  };
}
  • resolveId 解析以 .txt 结尾的文件,并返回该文件的绝对路径。

  • 在 load 中返回该文本文件的内容。

load

在 Vite 插件系统中,load 钩子用于在模块加载阶段处理和修改文件内容。它是插件对 Vite 构建过程中的一个重要环节,允许你在文件内容被传递给其他部分(如转译、打包等)之前,进行处理、修改、优化或者生成内容。

load 钩子的作用

load 钩子的主要作用是:

  • 修改文件内容:你可以在此钩子中修改模块的源代码,例如进行内容转换、代码注入等操作。

  • 处理虚拟模块:如果你在 resolveId 中创建了虚拟模块,load 钩子允许你返回该虚拟模块的内容。

  • 自定义文件处理逻辑:你可以根据文件类型、路径等条件对文件进行特殊处理,如对 .txt 文件进行加载,或者对某些模块进行特殊的转换。

load 钩子的执行时机

load 钩子会在 Vite 的构建过程中被触发,它发生在模块被解析之后(resolveId 之后),但是在它被传递给转译器(如 Babel、TypeScript)和打包器(如 Rollup)之前。具体来说,load 会在以下几种情况下触发:

  • 当模块被请求时(例如,导入一个模块)。

  • 当模块的路径已经解析并且准备加载时。

  • 在加载虚拟模块时,Vite 会调用 load 钩子来提供虚拟模块的内容。

load 钩子的参数

load 钩子接收以下参数:

  1. id (string): 请求的模块路径,通常是文件路径或者虚拟模块的路径。这个路径由 resolveId 解析出来。

  2. ssr (boolean): 当前是否处于 SSR(服务器端渲染)模式。如果是 SSR 环境,插件可以返回不同的模块内容或执行特定的操作。

load 的返回值

load 钩子必须返回以下值之一:

  • string: 返回模块的源代码。返回的字符串会被 Vite 后续的处理流程使用。

  • null: 如果当前模块无法被处理或修改,返回 null,表示该模块交给后续的插件或默认流程继续处理。

  • Promise<string | null>: 如果 load 是一个异步函数,它可以返回一个 Promise,Promise 解析的值必须是 string 或 null。

  • { code: string, map?: object }: 除了代码内容,load 还可以返回一个对象,其中包含 code(源代码)和 map(source map)。如果有 source map,Vite 会将它与代码一起传递给后续处理。

示例:使用 load 钩子

示例 1:处理虚拟模块
import { Plugin } from 'vite';

export default function virtualModulePlugin(): Plugin {
  return {
    name: 'virtual-module-plugin',

    resolveId(source) {
      if (source === 'virtual-module') {
        return 'virtual:/my-virtual-module.js';
      }
      return null;
    },

    load(id) {
      if (id === 'virtual:/my-virtual-module.js') {
        // 返回虚拟模块的内容
        return `export default 'This is a virtual module.';`;
      }
      return null;
    }
  };
}
  • resolveId 钩子拦截了 virtual-module 模块,并将其解析为 virtual:/my-virtual-module.js。

  • 在 load 中,我们提供了该虚拟模块的内容:export default 'This is a virtual module.'。

示例 2:处理 .txt 文件类型
import { Plugin } from 'vite';
import path from 'path';
import fs from 'fs';

export default function textFilePlugin(): Plugin {
  return {
    name: 'text-file-plugin',

    resolveId(source) {
      // 如果是以 .txt 结尾的路径,则进行自定义解析
      if (source.endsWith('.txt')) {
        return path.resolve(__dirname, 'src', source);
      }
      return null;
    },

    load(id) {
      // 如果是自定义解析的 .txt 文件,读取文件内容并返回
      if (id.endsWith('.txt')) {
        const content = fs.readFileSync(id, 'utf-8');
        return `export default ${JSON.stringify(content)};`;
      }
      return null;
    }
  };
}
  • resolveId 解析 .txt 文件,并返回它的绝对路径。

  • load 读取 .txt 文件的内容,并将其作为一个字符串导出。

示例 3:修改 JavaScript 文件内容
import { Plugin } from 'vite';

export default function modifyJSPlugin(): Plugin {
  return {
    name: 'modify-js-plugin',

    load(id) {
      // 如果是 JavaScript 文件,可以对其进行修改
      if (id.endsWith('.js')) {
        return `console.log('This is modified by the plugin');\n` + fs.readFileSync(id, 'utf-8');
      }
      return null;
    }
  };
}
  • load 钩子会拦截所有 .js 文件,并在文件内容的开头插入一行日志:console.log('This is modified by the plugin');

示例 4:返回带 Source Map 的内容
import { Plugin } from 'vite';

export default function jsWithSourceMapPlugin(): Plugin {
  return {
    name: 'js-with-source-map-plugin',

    load(id) {
      if (id.endsWith('.js')) {
        const code = `console.log('Modified JS');`;
        const map = {
          version: 3,
          file: id,
          sources: [id],
          names: [],
          mappings: 'AAAA'
        };
        return { code, map };
      }
      return null;
    }
  };
}
  • load 返回了一个包含 code 和 map 的对象,code 是修改后的代码,map 是对应的 source map。

  • 这使得 Vite 在构建时能够正确地映射修改后的代码到源文件,方便调试和错误追踪。

transform

在 Vite 插件系统中,transform 钩子是用于处理和转换模块代码的关键钩子。它是在构建过程中,模块的源代码被传递给 Vite 的打包工具(如 Rollup)之前执行的。这一钩子让你能够对模块进行转换、注入代码、压缩或者执行其他类似的操作。

transform 钩子的作用

transform 钩子的主要作用是:

  • 修改文件内容:对 JavaScript、TypeScript、CSS 等文件内容进行转换。

  • 支持其他编译器:可以用于将某些语言(如 TypeScript、SASS、SCSS 等)编译成浏览器可以理解的 JavaScript 或 CSS。

  • 代码注入:允许你在文件中插入额外的代码,或者动态修改文件的内容。

transform 钩子的执行时机

transform 钩子在模块加载并且 Vite 开始对其进行转换时触发。它会在模块内容被解析并经过 load 钩子处理之后触发,但在最终交给打包工具(如 Rollup)处理之前执行。也就是说,transform 钩子是 Vite 执行 JavaScript/TypeScript、CSS 以及其他资源转换的地方。

  • transform 是针对模块内容的逐个转换,通常用于对每个文件的源代码进行转换。

  • transform 主要针对的是源文件的内容,尤其是 JavaScript、TypeScript、CSS 等文件,它允许你在这些文件中进行代码的修改和注入。

transform 钩子的参数

transform 钩子接收以下参数:

  1. code (string): 当前模块的源代码,通常是文件内容的字符串。这个参数代表了文件的源代码,可以在 transform 中修改这个内容。

  2. id (string): 请求的模块路径,通常是文件路径或虚拟模块的路径。这个路径是 Vite 用来解析该模块的标识。

  3. options (object): 这个对象包含了 Vite 当前构建的配置选项。你可以根据这些选项来调整 transform 的行为(如是否启用 sourceMap,是否开启 esbuild 转换等)。

  4. ssr (boolean): 当前是否处于 SSR(服务器端渲染)模式。可以用来区分客户端和服务器端的转换需求。如果是 SSR 模式,可能会有不同的代码转换需求。

transform 的返回值

transform 必须返回以下之一:

  • string: 变换后的代码内容。这个字符串将会作为新的模块代码,传递给后续的插件或构建步骤。

  • null: 如果当前模块不需要处理,或者没有做任何变换,返回 null。这意味着插件不处理该文件,交给下一个插件或默认流程继续处理。

  • { code: string, map?: object }: 除了修改后的 code 外,transform 还可以返回一个对象,该对象包含:

    • code: 转换后的代码内容。

    • map (可选): 对应的 source map。map 是一个对象,表示源代码到转换后代码之间的映射。如果你提供了 map,Vite 会在构建时自动生成正确的 source map。

  • Promise<string | null> 或 Promise<{ code: string, map?: object }>: 如果 transform 是一个异步函数,它应该返回一个 Promise,Promise 的解析值可以是上述三种情况之一。

示例:使用 transform 钩子

示例 1:处理 JavaScript 文件
import { Plugin } from 'vite';

export default function jsTransformPlugin(): Plugin {
  return {
    name: 'js-transform-plugin',

    transform(code, id) {
      if (id.endsWith('.js')) {
        // 在所有 JavaScript 文件的顶部插入一行代码
        const transformedCode = `console.log('Transformed by js-transform-plugin');\n` + code;
        return transformedCode;
      }
      return null;
    }
  };
}
  • 该插件会在所有 .js 文件的顶部插入一行代码 console.log('Transformed by js-transform-plugin');。

  • transform 钩子会返回修改后的代码内容。

示例 2:处理 TypeScript 文件
import { Plugin } from 'vite';

export default function tsTransformPlugin(): Plugin {
  return {
    name: 'ts-transform-plugin',

    transform(code, id) {
      if (id.endsWith('.ts')) {
        // 对 TypeScript 代码做一些简单的替换
        const transformedCode = code.replace(/console\.log/g, 'console.debug');
        return transformedCode;
      }
      return null;
    }
  };
}
  • 该插件会将所有 TypeScript 文件中的 console.log 替换为 console.debug。

示例 3:返回带 Source Map 的转换
import { Plugin } from 'vite';

export default function transformWithSourceMapPlugin(): Plugin {
  return {
    name: 'transform-with-source-map-plugin',

    transform(code, id) {
      if (id.endsWith('.js')) {
        // 修改代码内容
        const transformedCode = `console.log('Modified JS with source map');\n` + code;

        // 返回带有 source map 的转换结果
        const map = {
          version: 3,
          file: id,
          sources: [id],
          names: [],
          mappings: 'AAAA'
        };

        return {
          code: transformedCode,
          map
        };
      }
      return null;
    }
  };
}
  • 该插件不仅修改了 .js 文件的代码内容,还为其提供了 source map,以便调试和追踪代码的来源。

示例 4:根据 SSR 模式做不同的处理
import { Plugin } from 'vite';

export default function ssrTransformPlugin(): Plugin {
  return {
    name: 'ssr-transform-plugin',

    transform(code, id, options) {
      if (options.ssr) {
        // 如果是 SSR 模式,做一些不同的转换
        return code.replace('window', 'globalThis');
      }
      return code;
    }
  };
}
  • 该插件根据是否处于 SSR 模式,做不同的转换。如果是 SSR 模式,它会将 window 替换为 globalThis,以保证代码能够在 Node.js 环境中运行。

transformIndexHtml

在 Vite 中,transformIndexHtml 是一个特殊的插件钩子,专门用于对生成的 index.html 文件进行处理。它允许开发者在 Vite 构建过程中,修改、注入或自定义 HTML 文件,通常用于在页面中插入自定义的 <script>、<link> 标签,或者修改某些现有内容。

transformIndexHtml 钩子的作用

transformIndexHtml 主要用于:

  • 注入自定义的 HTML 代码:比如插入外部资源(如 JS、CSS 文件)、内联脚本、元数据等。

  • 修改 <head> 或 <body> 内容:在 HTML 文件的 <head> 或 <body> 部分注入额外的元素(如 <meta>、<title>、<script>、<link> 等)。

  • 修改 HTML 结构:可以在构建过程中修改生成的 index.html 文件的结构,例如替换某些默认模板内容。

  • 处理 HTML 模板:可以针对模板引擎(如 Pug、Handlebars 等)做一些特定的 HTML 预处理。

transformIndexHtml 钩子的执行时机

  • transformIndexHtml 在 Vite 打包生成 HTML 页面时触发,通常在构建完成后、生成最终的 index.html 文件之前。它的主要目的是对 index.html 文件进行修改、增强或动态注入内容。

  • 如果你想在 HTML 文件中添加资源链接(如脚本、样式表、favicon、动态标签等),可以在这个钩子中进行操作。

transformIndexHtml 钩子的参数

transformIndexHtml 钩子接收两个参数:

  1. html (string): 当前的 index.html 文件的原始内容。这个参数表示 HTML 文件的源代码,可以对其进行修改和注入。

  2. env (object): 当前的构建环境信息。包含一些与构建过程相关的配置选项,如是否是生产模式 (prod),或者是否启用了某些插件等。

transformIndexHtml 的返回值

transformIndexHtml 必须返回修改后的 html 字符串,或者返回一个对象来修改 HTML 的某些部分。具体返回值的类型可以是:

  1. string: 修改后的 index.html 内容。

  2. Promise<string>: 如果钩子是异步的,返回一个 Promise,解析值为修改后的 html 字符串。

  3. { html: string, inject: { head: string[], body: string[] } }: 返回一个对象,允许你指定更多详细的注入内容,包括:

    • html: 修改后的 HTML 内容。

    • inject: 一个对象,允许你分别注入 head 和 body 部分的内容,格式如下:

      • head: 注入到 <head> 部分的 HTML 代码。

      • body: 注入到 <body> 部分的 HTML 代码。

示例:使用 transformIndexHtml 插件钩子

示例 1:注入外部脚本和样式
import { Plugin } from 'vite';

export default function injectScriptsPlugin(): Plugin {
  return {
    name: 'inject-scripts-plugin',

    transformIndexHtml(html) {
      return html.replace(
        '</head>',
        `
          <script src="https://cdn.example.com/some-library.js"></script>
          <link rel="stylesheet" href="https://cdn.example.com/style.css">
        </head>`
      );
    }
  };
}
  • 该插件会在 index.html 的 </head> 标签前注入一个外部的 <script> 和 <link> 标签。

示例 2:修改 <title> 标签内容
import { Plugin } from 'vite';

export default function modifyTitlePlugin(): Plugin {
  return {
    name: 'modify-title-plugin',

    transformIndexHtml(html) {
      return html.replace(
        /<title>(.*?)<\/title>/,
        `<title>Custom Title - ${process.env.VITE_APP_NAME}</title>`
      );
    }
  };
}
  • 该插件会替换 index.html 中的 <title> 标签内容,将标题动态替换为 Custom Title + 从环境变量中读取的应用名称。

示例 3:根据环境变量动态注入内容
import { Plugin } from 'vite';

export default function envInjectPlugin(): Plugin {
  return {
    name: 'env-inject-plugin',

    transformIndexHtml(html, { env }) {
      if (env.MODE === 'production') {
        return html.replace(
          '</body>',
          `<script>console.log('Production mode activated');</script></body>`
        );
      } else {
        return html.replace(
          '</body>',
          `<script>console.log('Development mode');</script></body>`
        );
      }
    }
  };
}
  • 该插件根据 Vite 构建的环境变量(如 MODE)在 index.html 的 <body> 结束前注入不同的 JavaScript 代码。如果是生产环境,注入一条消息;如果是开发环境,注入另一条消息。

示例 4:使用 inject 对象注入 <head> 和 <body> 内容
import { Plugin } from 'vite';

export default function injectHeadAndBody(): Plugin {
  return {
    name: 'inject-head-body-plugin',

    transformIndexHtml(html) {
      return {
        html,
        inject: {
          head: `<meta name="description" content="My Custom Vite App">`,
          body: `<script>console.log('Injected after body!');</script>`
        }
      };
    }
  };
}
  • 该插件会将 <meta> 标签注入到 <head> 部分,同时将一段 JavaScript 代码注入到 <body> 的结束标签前。

示例 5:动态修改 index.html 内容
import { Plugin } from 'vite';

export default function dynamicHtmlPlugin(): Plugin {
  return {
    name: 'dynamic-html-plugin',

    transformIndexHtml(html) {
      const version = process.env.npm_package_version;
      return html.replace(
        '<body>',
        `<body><h1>Welcome to my app! Version: ${version}</h1>`
      );
    }
  };
}
  • 该插件会动态修改 index.html 文件,在 <body> 标签开始的位置插入一个显示版本号的 <h1> 标签。

handleHotUpdate

在 Vite 中,handleHotUpdate 是一个重要的插件钩子,它用于处理热模块替换(HMR,Hot Module Replacement)。HMR 是开发过程中非常关键的特性,允许在不刷新整个页面的情况下更新单个模块的内容,从而提高开发效率,避免页面的重新加载。

handleHotUpdate 钩子的作用

handleHotUpdate 钩子的主要作用是处理和响应热更新事件。具体来说,插件通过该钩子可以:

  • 拦截模块更新:当某个文件发生变化时,handleHotUpdate 会被调用,插件可以在此时决定是否处理这个变化。

  • 触发额外的操作:比如根据文件变化执行某些额外的操作,如重新加载模块、更新缓存、刷新页面等。

  • 修改更新的模块:插件可以在此时修改或决定热更新的具体行为,比如通过自定义的更新逻辑来替代默认行为。

  • 传递模块信息:插件可以通过 ctx.modules 向 Vite 提供相关的模块信息,以确保 HMR 正常工作。

handleHotUpdate 的参数

handleHotUpdate 接收一个参数,通常是一个上下文对象 ctx,其包含以下属性:

  1. file (string):

    • 被修改的文件路径,通常是 .js、.ts、.vue、.css 等资源文件的路径。

    • 如果是处理非模块文件(如 .html),文件路径也会被提供。

  2. server (ViteDevServer):

    • 当前的 Vite 开发服务器实例,可以用来访问一些 Vite 内部功能,比如模块图(module graph)、HMR 模块等。

  3. modules (Array<Module>):

    • 变更的模块列表。这个属性是一个数组,包含当前正在热更新的所有模块对象。

  4. update (boolean):

    • 一个布尔值,表示是否是模块的更新操作。如果为 true,表示当前文件是一个已经被热更新的模块。

handleHotUpdate 的返回值

handleHotUpdate 必须返回一个数组,这些数组中的每个元素都是一个模块对象(Module)。返回的模块列表将会被 Vite 用来处理 HMR 更新。

  • 返回的数组:ctx.modules,这是一个可以通过 HMR 更新的模块列表。如果插件希望某些模块被热更新处理,必须把这些模块推送到该数组中。

  • 如果插件不做任何处理,或者决定不更新模块,可以返回空数组或不返回任何内容。

handleHotUpdate 的典型用途

  • 拦截 HMR 更新:有些情况下你可能希望拦截某些文件的热更新,比如某些文件并不需要更新、或者需要更新时进行额外的操作(如重新加载资源、缓存清理等)。

  • 修改模块的热更新行为:你可以通过此钩子来自定义 HMR 的处理逻辑,例如重载某个组件或模块,甚至在更新时手动刷新页面。

  • 触发页面重新加载:对于某些资源(如 index.html 或其他关键文件),插件可能会在 HMR 更新时要求浏览器刷新页面,以确保最新的内容被应用。

示例:

使用 handleHotUpdate 处理文件更新
import { Plugin } from 'vite';

export default function myHmrPlugin(): Plugin {
  return {
    name: 'my-hmr-plugin',

    handleHotUpdate(ctx) {
      const { file, server, modules } = ctx;

      // 判断文件是否是某个特定模块
      if (file.endsWith('.css')) {
        console.log(`CSS file updated: ${file}`);
        // 在 HMR 更新时执行自定义的处理逻辑
        // 例如,重新加载相关的模块或清理缓存

        // 可以通过 `ctx.modules` 向 Vite 提供模块信息,以便触发 HMR 更新
        return modules;
      }

      // 对于其他类型的更新,可以做类似的处理
      if (file.endsWith('.js')) {
        console.log(`JavaScript file updated: ${file}`);
        return modules; // 返回需要更新的模块
      }

      return []; // 如果不需要处理这个文件,可以返回空数组
    }
  };
}
  • 该插件拦截所有 .css 和 .js 文件的更新,记录日志并返回模块列表,触发 Vite 对该模块的热更新。

  • 如果文件类型不是 .css 或 .js,则不做任何处理,返回空数组。

使用 handleHotUpdate 强制页面刷新
import { Plugin } from 'vite';

export default function forcePageReloadPlugin(): Plugin {
  return {
    name: 'force-page-reload-plugin',

    handleHotUpdate(ctx) {
      const { file, server } = ctx;

      // 强制更新 `index.html` 或其他关键资源时刷新页面
      if (file.endsWith('index.html')) {
        console.log('index.html updated, reloading the page...');
        // 通过 server.reload 强制刷新页面
        server.ws.send({
          type: 'full-reload',
          path: file
        });
      }

      return [];
    }
  };
}
  • 当 index.html 文件被更新时,插件会强制页面刷新。通过向 Vite 服务器发送一个 full-reload 消息,Vite 会重新加载整个页面。

自定义处理文件的热更新
import { Plugin } from 'vite';

export default function customHmrPlugin(): Plugin {
  return {
    name: 'custom-hmr-plugin',

    handleHotUpdate(ctx) {
      const { file, server } = ctx;

      // 如果是某个特殊的文件,进行特殊处理
      if (file.endsWith('.vue')) {
        // 在更新 Vue 文件时,做一些特定的操作,比如清除缓存、强制重新渲染组件等
        console.log('Vue component updated:', file);

        // 手动触发某个模块的更新
        return [server.moduleGraph.getModuleById(file)];
      }

      return [];
    }
  };
}
  • 当 .vue 文件更新时,插件会返回特定的模块,通知 Vite 对其进行更新处理。