Vite(五)插件API(钩子hook)、HMR API、JavaScript API

Vite(五)插件API(钩子hook)、HMR API、JavaScript API

  • 网址:https://vitejs.cn/guide/why.html

1. 插件 API

Vite 插件扩展了设计出色的 Rollup 接口,带有一些 vite 独有的配置项。因此,你只需要编写一个 Vite 插件,就可以同时为开发环境和生产环境工作

推荐在阅读下面的章节之前,首先阅读下 Rollup 插件文档

约定

如果插件不使用 Vite 特有的钩子,可以实现为 兼容的 Rollup 插件,推荐使用 Rollup 插件名称约定

  • Rollup 插件应该有一个带 rollup-plugin- 前缀、语义清晰的名称。
  • 在 package.json 中包含 rollup-pluginvite-plugin 关键字。

这样,插件也可以用于纯 Rollup 或基于 WMR 的项目。

对于 Vite 专属的插件:

  • Vite 插件应该有一个带 vite-plugin- 前缀、语义清晰的名称。
  • 在 package.json 中包含 vite-plugin 关键字。
  • 在插件文档增加一部分关于为什么本插件是一个 Vite 专属插件的详细说明(如,本插件使用了 Vite 特有的插件钩子)。

如果你的插件只适用于特定的框架,它的名字应该遵循以下前缀格式:

  • vite-plugin-vue- 前缀作为 Vue 插件
  • vite-plugin-react- 前缀作为 React 插件
  • vite-plugin-svelte- 前缀作为 Svelte 插件

简单示例

TIP

通常的惯例是创建一个 Vite/Rollup 插件作为一个返回实际插件对象的工厂函数。该函数可以接受允许用户自定义插件行为的选项。

引入一个虚拟文件

  • 在 ES6 模块系统中,使用 export default 可以导出一个默认值,使用方可以用 import foo from ‘foo’ 而不是 import { foo } from ‘foo’ 来导入这个默认值。
  • 在类型声明文件中,export default 用来导出默认值的类型
  • 注意,只有 functionclassinterface 可以直接默认导出,其他的变量需要先定义出来,再默认导出
export default function myPlugin() {
  const virtualFileId = '@my-virtual-file'

  return {
    name: 'my-plugin', // 必须的,将会显示在 warning 和 error 中
    resolveId(id) {
      if (id === virtualFileId) {
        return virtualFileId
      }
    },
    load(id) {
      if (id === virtualFileId) {
        return `export const msg = "from virtual file"`
      }
    }
  }
}

这使得可以在 JavaScript 中引入这些文件:

import { msg } from '@my-virtual-file'
console.log(msg)

转换自定义文件类型

const fileRegex = /\.(my-file-ext)$/

export default function myPlugin() {
  return {
    name: 'transform-file',
    transform(src, id) {
      if (fileRegex.test(id)) {
        return {
          code: compileFileToJS(src),
          map: null // provide source map if available
        }
      }
    }
  }
}

通用钩子

  • 简单的说,钩子就是程序。这些程序可以在特定的时间被调用。 你既然用CI框架,那么应该知道,在框架初始化的过程中,有一些特殊的时间点,比如 (1)、框架初始化之前的时间点。 (2)、控制器初始化之前的时间点 (3)、控制器运行之后的时间点 当然还有其他的时间点。 在每一个时间点,你都可以埋下一些钩子(可以是一段程序,或者一个函数)。框架中都会有一个专门处理钩子的类库(比如CI中的hooks.php),这个Hook类就会在程序运行的特定点检查是不是有特定的钩子,如果有钩子,就执行这个钩子。 说了这么多,可能还是有点抽象。打个比方: 你下班回家的整个流程看做是框架的执行流程,正常情况下,你下班直接走回家就可以。如果你老妈给你打电话说,如果你路过邮局,刚好有咱家的快递,带回家,这个过程,就比较类似钩子(检查钩子,如果有则执行之)。 如果你做过一些前端的工作,这个钩子就恰似“事件驱动”的模式。

  • 所谓Hook机制,是从Windows编程中流行开的一种技术。其主要思想是提前在可能增加功能的地方埋好(预设)一个钩子,这个钩子并没有实际的意义,当我们需要重新修改或者增加这个地方的逻辑的时候,把扩展的类或者方法挂载到这个点即可

  • 讲到“钩子”,一定要提前说明的是一种设计模式,那就是行为型设计模式中的模板方法模式,明白了它的话,会让我们更容易理解钩子。

    • 模板方法模式是一种基于继承的代码复用,它是一种类行为型模式,在其结构中只存在父类与子类之间的继承关系。通过使用模板方法模式,可以将一些复杂流程的实现步骤封装在一系列基本方法中,在抽象父类中提供一个称之为模板方法的方法来定义这些基本方法的执行次序,而通过其子类来实现某些步骤,从而使得相同的算法框架可以有不同的执行结果。
    • 要注意的是,在处理的过程中,要遵循反向控制接口(“好莱坞原则”),这个原则是指父类调用子类的操作,而子类不调用父类的操作。好莱坞原则于模版方法设计模式紧密相关,因为它在父类中实现,除了templateMethod方法外,父类的其他方法都是抽象和受保护的方法。所以,尽管客户实例化一个具体类,但是它调用了父类中实现的方法。
    • 有的时候,模板方法函数中可能有一个不想要的步骤,例如,在我们购买商品的时候要计算最终价格,商品价格+运费+服务产生费用,不过在有些活动中,顾客商品价格满100元就可以免运费。这里就要使用到模板方法的钩子。
    • 也就是说,在模板方法设计模式中,利用钩子可以将一个方法作为模板方法的一部分,不过不一定会用到这个方法。换句话说,它是方法的一部分,不过它包含一个钩子,可以处理例外的情况,子类可以为算法增加一个可选元素,这样以来,尽管仍然按照父类模板方法建立的顺序执行,但是有可能并不完全按照模板方法期望的那样动作。
  • 优点
    1.提高代码复用性 ,将相同部分的代码放在抽象的父类中
    2.提高了拓展性 ,将不同的代码放入不同的子类中,通过对子类的扩展增加新的行为
    3.实现了反向控制,通过一个父类调用其子类的操作,通过对子类的扩展增加新的行为,实现了反向控制 & 符合“开闭原则”

  • 缺点
    引入了抽象类,每一个不同的实现都需要一个子类来实现,导致类的个数增加,从而增加了系统实现的复杂度

在开发中,Vite 开发服务器会创建一个插件容器来调用 Rollup 构建钩子,与 Rollup 如出一辙。

以下钩子在服务器启动时被调用:

以下钩子会在每个传入模块请求时被调用:

以下钩子在服务器关闭时被调用:

请注意 moduleParsed 钩子 不是 在开发中被调用的,因为 Vite 为了性能会避免完整的 AST 解析。

Output Generation Hooks(除了 closeBundle) 不是 在开发中被调用的。你可以认为 Vite 的开发服务器只调用了 rollup.rollup() 而没有调用 bundle.generate().

Vite 独有钩子

Vite 插件也可以提供钩子来服务于特定的 Vite 目标。这些钩子会被 Rollup 忽略。

config

  • 类型: (config: UserConfig, env: { mode: string, command: string }) => UserConfig | null | void

  • 种类: sync, sequential

    在被解析之前修改 Vite 配置。钩子接收原始用户配置(命令行选项指定的会与配置文件合并)和一个描述配置环境的变量,包含正在使用的 modecommand它可以返回一个将被深度合并到现有配置中的部分配置对象,或者直接改变配置(如果默认的合并不能达到预期的结果)

    示例

    // 返回部分配置(推荐)
    const partialConfigPlugin = () => ({
      name: 'return-partial',
      config: () => ({
        alias: {
          foo: 'bar'
        }
      })
    })
    
    // 直接改变配置(应仅在合并不起作用时使用)
    const mutateConfigPlugin = () => ({
      name: 'mutate-config',
      config(config, { command }) {
        if (command === 'build') {
          config.root = __dirname
        }
      }
    })
    

    注意

    用户插件在运行这个钩子之前会被解析,因此在 config 钩子中注入其他插件不会有任何效果。

configResolved

  • 类型: (config: ResolvedConfig) => void

  • 种类: sync, sequential

    在解析 Vite 配置后调用使用这个钩子读取和存储最终解析的配置。当插件需要根据运行的命令做一些不同的事情时,它也很有用

    示例:

    const exmaplePlugin = () => {
      let config
    
      return {
        name: 'read-config',
    
        configResolved(resolvedConfig) {
          // 存储最终解析的配置
          config = resolvedConfig
        },
    
        // 使用其他钩子存储的配置
        transform(code, id) {
          if (config.command === 'serve') {
            // serve: 用于启动开发服务器的插件
          } else {
            // build: 调用 Rollup 的插件
          }
        }
      }
    }
    

configureServer

  • 类型: (server: ViteDevServer) => (() => void) | void | Promise<(() => void) | void>

  • 种类: async, sequential

  • 此外请看 ViteDevServer

    是用于配置开发服务器的钩子。最常见的用例是在内部 connect 应用程序中添加自定义中间件:

    const myPlugin = () => ({
      name: 'configure-server',
      configureServer(server) {
        server.middlewares.use((req, res, next) => {
          // 自定义请求处理...
        })
      }
    })
    

    注入后置中间件

    configureServer 钩子将在内部中间件被安装前调用,所以自定义的中间件将会默认会比内部中间件早运行。如果你想注入一个在内部中间件 之后 运行的中间件,你可以从 configureServer 返回一个函数,将会在内部中间件安装后被调用:

    const myPlugin = () => ({
      name: 'configure-server',
      configureServer(server) {
        // 返回一个在内部中间件安装后被调用的后置钩子
        return () => {
          server.middlewares.use((req, res, next) => {
            // 自定义请求处理...
          })
        }
      }
    })
    

    存储服务器访问

    在某些情况下,其他插件钩子可能需要访问开发服务器实例(例如访问 websocket 服务器、文件系统监视程序或模块图)。这个钩子也可以用来存储服务器实例以供其他钩子访问:

    const myPlugin = () => {
      let server
      return {
        name: 'configure-server',
        configureServer(_server) {
          server = _server
        },
        transform(code, id) {
          if (server) {
            // 使用 server...
          }
        }
      }
    }
    

    注意 configureServer 在运行生产版本时不会被调用,所以其他钩子需要注意防止它的缺失。

transformIndexHtml

  • 类型: IndexHtmlTransformHook | { enforce?: 'pre' | 'post' transform: IndexHtmlTransformHook }

  • 种类: async, sequential

    转换 index.html 的专用钩子。钩子接收当前的 HTML 字符串和转换上下文。上下文在开发期间暴露ViteDevServer实例,在构建期间暴露 Rollup 输出的包

    这个钩子可以是异步的,并且可以返回以下其中之一:

    • 经过转换的 HTML 字符串
    • 注入到现有 HTML 中的标签描述符对象数组({ tag, attrs, children })。每个标签也可以指定它应该被注入到哪里(默认是在 <head> 之前)
    • 一个包含 { html, tags } 的对象

    Basic Example

    const htmlPlugin = () => {
      return {
        name: 'html-transform',
        transformIndexHtml(html) {
          return html.replace(
            /<title>(.*?)<\/title>/,
            `<title>Title replaced!</title>`
          )
        }
      }
    }
    

    完整钩子签名:

    type IndexHtmlTransformHook = (
      html: string,
      ctx: {
        path: string
        filename: string
        server?: ViteDevServer
        bundle?: import('rollup').OutputBundle
        chunk?: import('rollup').OutputChunk
      }
    ) =>
      | IndexHtmlTransformResult
      | void
      | Promise<IndexHtmlTransformResult | void>
    
    type IndexHtmlTransformResult =
      | string
      | HtmlTagDescriptor[]
      | {
          html: string
          tags: HtmlTagDescriptor[]
        }
    
    interface HtmlTagDescriptor {
      tag: string
      attrs?: Record<string, string>
      children?: string | HtmlTagDescriptor[]
      /**
       * 默认: 'head-prepend'
       */
      injectTo?: 'head' | 'body' | 'head-prepend' | 'body-prepend'
    }
    

handleHotUpdate

  • 类型: (ctx: HmrContext) => Array<ModuleNode> | void | Promise<Array<ModuleNode> | void>

    执行自定义 HMR 更新处理。钩子接收一个带有以下签名的上下文对象:

    interface HmrContext {
      file: string
      timestamp: number
      modules: Array<ModuleNode>
      read: () => string | Promise<string>
      server: ViteDevServer
    }
    
    • modules 是受更改文件影响的模块数组。它是一个数组,因为单个文件可能映射到多个服务模块(例如 Vue 单文件组件)。
    • read 这是一个异步读函数,它返回文件的内容。之所以这样做,是因为在某些系统上,文件更改的回调函数可能会在编辑器完成文件更新之前过快地触发,并 fs.readFile 直接会返回空内容。传入的 read 函数规范了这种行为。

    钩子可以选择:

    • 过滤和缩小受影响的模块列表,使 HMR 更准确。

    • 返回一个空数组,并通过向客户端发送自定义事件来执行完整的自定义 HMR 处理:

      handleHotUpdate({ server }) {
        server.ws.send({
          type: 'custom',
          event: 'special-update',
          data: {}
        })
        return []
      }
      

      客户端代码应该使用 HMR API 注册相应的处理器(这应该被被相同插件的 transform 钩子注入):

      if (import.meta.hot) {
        import.meta.hot.on('special-update', (data) => {
          // 执行自定义更新
        })
      }
      

插件顺序

一个 Vite 插件可以额外指定一个 enforce 属性(类似于 webpack 加载器)来调整它的应用顺序。enforce 的值可以是prepost。解析后的插件将按照以下顺序排列:

  • Alias
  • 带有 enforce: 'pre' 的用户插件
  • Vite 内置插件
  • 没有 enforce 值的用户插件
  • Vite 构建用的插件
  • 带有 enforce: 'post' 的用户插件

Rollup 插件兼容性

相当数量的 Rollup 插件将直接作为 Vite 插件工作(例如:@rollup/plugin-alias@rollup/plugin-json),但并不是所有的,因为有些插件钩子在非构建式的开发服务器上下文中没有意义。

一般来说,只要一个 Rollup 插件符合以下标准,那么它应该只是作为一个 Vite 插件:

  • 没有使用 moduleParsed 钩子。
  • 它在打包钩子和输出钩子之间没有很强的耦合。

如果一个 Rollup 插件只在构建阶段有意义,则在 build.rollupOptions.plugins 下指定即可。

你也可以用 Vite 独有的属性来扩展现有的 Rollup 插件:

// vite.config.js
import example from 'rollup-plugin-example'
export default {
  plugins: [
    {
      ...example(),
      enforce: 'post',
      apply: 'build'
    }
  ]
}

查看 Vite Rollup 插件 获取兼容的官方 rollup 插件列表及其使用指南。

路径规范化

Vite 会在解析路径时使用 POSIX 分隔符( / )标准化路径,同时也适用于 Windows 的分卷。而另一方面,Rollup 在默认情况下保持解析的路径不变,因此解析的路径在 Windows 中会使用 win32 分隔符( \ )。然而,Rollup 插件会从 @rollup/pluginutils 中使用一个 normalizePath 工具函数,它在执行比较之前将分隔符转换为 POSIX。所以意味着当这些插件在 Vite 中使用时,includeexclude 两个配置模式,以及与已解析路径比较相似的路径会正常工作。

所以对于 Vite 插件来说,在将路径与已解析的路径进行比较时,首先规范化路径以使用 POSIX 分隔符是很重要的。从 vite 模块中也导出了一个等效的 normalizePath 工具函数。

import { normalizePath } from 'vite'
normalizePath('foo\\bar') // 'foo/bar'
normalizePath('foo/bar') // 'foo/bar'

2. HMR API

注意

这里是客户端 HMR API。若要在插件中处理 HMR 更新,详见 handleHotUpdate.

手动 HMR API 主要用于框架和工具作者。作为最终用户,HMR 可能已经在特定于框架的启动器模板中为你处理过了

Vite 通过特殊的 import.meta.hot 对象暴露手动 HMR API

interface ImportMeta {
  readonly hot?: {
    readonly data: any

    accept(): void
    accept(cb: (mod: any) => void): void
    accept(dep: string, cb: (mod: any) => void): void
    accept(deps: string[], cb: (mods: any[]) => void): void

    dispose(cb: (data: any) => void): void
    decline(): void
    invalidate(): void

    on(event: string, cb: (...args: any[]) => void): void
  }
}

必需的条件守卫

首先,请确保用一个条件语句守护所有 HMR API 的使用,这样代码就可以在生产环境中被 tree-shaking 优化:

if (import.meta.hot) {
  // HMR 代码
}

hot.accept(cb)

要接收模块自身,应使用 import.meta.hot.accept,参数为接收已更新模块的回调函数

export const count = 1

if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    console.log('updated: count is now ', newModule.count)
  })
}

“接受” 热更新的模块被认为是 HMR 边界

请注意,Vite 的 HMR 实际上并不替换最初导入的模块:如果 HMR 边界模块从某个依赖重新导出其导入,则它负责更新这些重新导出的模块(这些导出必须使用 let)。此外,边界模块链上的导入者将不会收到更新。

这种简化的 HMR 实现对于大多数开发用例来说已经足够了,同时允许我们跳过生成代理模块的昂贵工作。

hot.accept(deps, cb)

模块也可以接受直接依赖项的更新,而无需重新加载自身

import { foo } from './foo.js'

foo()

if (import.meta.hot) {
  import.meta.hot.accept('./foo.js', (newFoo) => {
    // 回调函数接收到更新后的'./foo.js' 模块
    newFoo.foo()
  })

  // 也可以接受一个依赖模块的数组:
  import.meta.hot.accept(
    ['./foo.js', './bar.js'],
    ([newFooModule, newBarModule]) => {
      // 回调函数接收一个更新后模块的数组
    }
  )
}

hot.dispose(cb)

一个接收自身的模块或一个期望被其他模块接收的模块可以使用 hot.dispose 来清除任何由其更新副本产生的持久副作用

function setupSideEffect() {}

setupSideEffect()

if (import.meta.hot) {
  import.meta.hot.dispose((data) => {
    // 清理副作用
  })
}

hot.data

import.meta.hot.data 对象在同一个更新模块的不同实例之间持久化。它可以用于将信息从模块的前一个版本传递到下一个版本。

hot.decline()

调用 import.meta.hot.decline() 表示此模块不可热更新,如果在传播 HMR 更新时遇到此模块,浏览器应该执行完全重新加载。

hot.invalidate()

现在调用 import.meta.hot.invalidate() 只是重新加载页面。

hot.on(event, cb)

监听自定义 HMR 事件。自定义 HMR 事件可以由插件发送。更多细节详见 handleHotUpdate

3. JavaScript API

Vite 的 JavaScript API 是完全类型化的,我们推荐使用 TypeScript 或者在 VSCode 中启用 JS 类型检查来利用智能提示和类型校验

createServer

类型签名

async function createServer(inlineConfig?: InlineConfig): Promise<ViteDevServer>

使用示例

const { createServer } = require('vite')

;(async () => {
  const server = await createServer({
    // 任何合法的用户配置选项,加上 `mode` 和 `configFile`
    configFile: false,
    root: __dirname,
    server: {
      port: 1337
    }
  })
  await server.listen()
})()

InlineConfig

InlineConfig 接口扩展了 UserConfig 并添加了以下属性:

  • configFile:指明要使用的配置文件。如果没有设置,Vite 将尝试从项目根目录自动解析。设置为 false 可以禁用自动解析功能。

ViteDevServer

interface ViteDevServer {
  /**
   * 被解析的 vite 配置对象
   */
  config: ResolvedConfig
  /**
   * 一个 connect 应用实例
   * - 可以用于将自定义中间件附加到开发服务器。
   * - 还可以用作自定义http服务器的处理函数
      或作为中间件用于任何 connect 风格的 Node.js 框架
   *
   * https://github.com/senchalabs/connect#use-middleware
   */
  middlewares: Connect.Server
  /**
   * 本机 node http 服务器实例
   */
  httpServer: http.Server | null
  /**
   * chokidar 监听器实例
   * https://github.com/paulmillr/chokidar#api
   */
  watcher: FSWatcher
  /**
   * web socket 服务器,带有 `send(payload)` 方法
   */
  ws: WebSocketServer
  /**
   * Rollup 插件容器,可以针对给定文件运行插件钩子
   */
  pluginContainer: PluginContainer
  /**
   * 跟踪导入关系、url 到文件映射和 hmr 状态的模块图。
   */
  moduleGraph: ModuleGraph
  /**
   * 以代码方式解析、加载和转换 url 并获取结果
   * 而不需要通过 http 请求管道。
   */
  transformRequest(
    url: string,
    options?: TransformOptions
  ): Promise<TransformResult | null>
  /**
   * 启动服务器
   */
  listen(port?: number, isRestart?: boolean): Promise<ViteDevServer>
  /**
   * 停止服务器
   */
  close(): Promise<void>
}

build

类型校验

async function build(
  inlineConfig?: InlineConfig
): Promise<RollupOutput | RollupOutput[]>

使用示例

const path = require('path')
const { build } = require('vite')

;(async () => {
  await build({
    root: path.resolve(__dirname, './project'),
    build: {
      base: '/foo/',
      rollupOptions: {
        // ...
      }
    }
  })
})()

resolveConfig

类型校验

async function resolveConfig(
  inlineConfig: InlineConfig,
  command: 'build' | 'serve',
  defaultMode?: string
): Promise<ResolvedConfig>

版权声明:本文为qq_43456781原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。