[Vite]Vite插件生命周期了解

[Vite]Vite插件生命周期了解

Chunk和Bundle的概念

  1. Chunk

    • 在 Vite 中,chunk 通常指的是应用程序中的一个代码片段,它是通过 Rollup 或其他打包工具在构建过程中生成的。每个 chunk 通常包含应用程序的一部分逻辑,可能是一个路由视图、一个组件或一组相关的模块。
    • 与 Webpack 类似,Vite 也支持代码分割(Code Splitting),可以将代码拆分成不同的 chunk 以实现按需加载,从而优化应用的性能和加载时间。
  2. Bundle

    • bundle 是指最终的输出文件,它是通过打包工具将多个模块、库和资源组合在一起形成的。在 Vite 的生产构建中,Rollup 会将应用程序的代码和依赖项打包成一个或多个 bundle。
    • Bundle 可以包含一个或多个 chunk,并且可能还会包括一些额外的元数据和辅助代码,如运行时代码、模块的映射信息等。
    • Vite 的输出 bundle 旨在优化生产环境的加载和执行,通常会进行压缩、树摇(Tree Shaking)和模块的合并等操作。

vite插件钩子

Vite的插件可以有两种形式,一种是vite插件,仅供vite使用;另一种则是rollup通用插件,它不使用 Vite 特有的钩子,让我们简单介绍一下关于这两种插件的生命周期:

所有钩子
  1. apply - 插件的入口点,Vite 会调用每个插件的 apply 函数,传入 Vite 的配置对象。
  2. config - 在 Vite 的配置被最终确定之前,允许插件修改配置。此钩子接收当前配置并应返回新的配置。
  3. configResolved - 在配置被解析并确定后调用,允许插件访问最终的配置。
  4. configureServer - 允许插件配置或修改 Vite 的开发服务器。
  5. transform - 在开发阶段,Vite 调用此钩子来请求插件对特定文件进行转换。
  6. render - 在开发阶段,Vite 调用此钩子来请求插件对 HTML 模板进行渲染。
  7. buildStart - 在构建开始之前调用。
  8. build - 在构建过程中调用,允许插件参与构建流程。
  9. generateBundle - 在构建结束时调用,允许插件访问或修改最终生成的 bundle。
  10. closeBundle - 在构建过程中,当一个 bundle 被写入磁盘后调用。
  11. writeBundle - 在构建过程中,当 bundle 准备写入磁盘时调用。
  12. optimizeDeps - 允许插件优化依赖,例如决定哪些依赖应该被包含在客户端。
  13. load - 允许插件提供一个模块的加载内容,而不是从文件系统中加载。
  14. resolveId - 允许插件介入模块 ID 的解析过程。
  15. shouldHandleRequest - 允许插件决定是否处理特定的请求。
  16. handleHotUpdate - 在 HMR(热模块替换)过程中,允许插件处理更新。
  17. transformIndexHtml - 在开发阶段,允许插件修改 HTML 模板。
  18. enforce - 指定插件应用的时机,可以是 'pre''post',分别表示在 Vite 默认插件之前或之后执行。
1. vite 独有的钩子
  1. enforce :值可以是prepostpre 会较于 post 先执行;
  2. apply :值可以是 buildserve 亦可以是一个函数,指明它们仅在 buildserve 模式时调用;
  3. config(config, env) :可以在 vite 被解析之前修改 vite 的相关配置。钩子接收原始用户配置 config 和一个描述配置环境的变量env;
  4. configResolved(resolvedConfig) :在解析 vite 配置后调用。使用这个钩子读取和存储最终解析的配置。当插件需要根据运行的命令做一些不同的事情时,它很有用。
  5. configureServer(server) :主要用来配置开发服务器,为 dev-server (connect 应用程序) 添加自定义的中间件;
  6. transformIndexHtml(html) :转换 index.html 的专用钩子。钩子接收当前的 HTML 字符串和转换上下文;
  7. handleHotUpdate(ctx):执行自定义HMR更新,可以通过ws往客户端发送自定义的事件;
2. vite 与 rollup 的通用钩子之构建阶段
  1. options(options) :在服务器启动时被调用:获取、操纵Rollup选项,严格意义上来讲,它执行于属于构建阶段之前;
  2. buildStart(options):在每次开始构建时调用;
  3. resolveId(source, importer, options):在每个传入模块请求时被调用,创建自定义确认函数,可以用来定位第三方依赖;
  4. load(id):在每个传入模块请求时被调用,可以自定义加载器,可用来返回自定义的内容;
  5. transform(code, id):在每个传入模块请求时被调用,主要是用来转换单个模块;
  6. buildEnd(error?: Error):在构建阶段结束后被调用,此处构建结束只是代表所有模块转义完成;
3. vite 与 rollup 的通用钩子之输出阶段
  1. outputOptions(options):接受输出参数;
  2. renderStart(outputOptions, inputOptions):每次 bundle.generate 和 bundle.write 调用时都会被触发;
  3. augmentChunkHash(chunkInfo):用来给 chunk 增加 hash;
  4. renderChunk(code, chunk, options):转译单个的chunk时触发。rollup 输出每一个chunk文件的时候都会调用;
  5. generateBundle(options, bundle, isWrite):在调用 bundle.write 之前立即触发这个 hook;
  6. writeBundle(options, bundle):在调用 bundle.write后,所有的chunk都写入文件后,最后会调用一次 writeBundle;
  7. closeBundle():在服务器关闭时被调用
4. 插件钩子函数 hooks 的执行顺序

vite插件开发钩子函数 (1).png

按照顺序,首先是配置解析相关:

  1. config:vite专有
  2. configResolved :vite专有
  3. options
  4. configureServer :vite专有

接下来是构建阶段的钩子:

  1. buildStart
  2. Resolved
  3. load
  4. transform
  5. buildEnd

然后是输出阶段的钩子:

  1. outputOptions
  2. renderStart
  3. augmentChunkHash
  4. renderChunk
  5. generateBundle
  6. transformIndexHtml
  7. writeBundle
  8. closeBundle
5. 插件的执行顺序
  1. 别名处理Alias
  2. 用户插件设置enforce: 'pre'
  3. vite 核心插件
  4. 用户插件未设置enforce
  5. vite 构建插件
  6. 用户插件设置enforce: 'post'
  7. vite 构建后置插件(minify, manifest, reporting)

举例

统计打包后dist大小

实现一个统计打包后dist大小的插件

主要使用的是closeBundle这个钩子。

import fs from 'fs'
import path from 'path'
import { Plugin } from 'vite'

function getFolderSize(folderPath: string): number {
  if (!fs.existsSync(folderPath) || !fs.lstatSync(folderPath).isDirectory()) {
    return 0
  }

  let totalSize = 0
  const files = fs.readdirSync(folderPath)

  files.forEach(file => {
    const filePath = path.join(folderPath, file)
    const stats = fs.statSync(filePath)
    if (stats.isFile()) {
      totalSize += stats.size
    } else if (stats.isDirectory()) {
      totalSize += getFolderSize(filePath)
    }
  })

  return totalSize
}

function formatBytes(bytes: number, decimals: number = 2): string {
  if (bytes === 0) return '0.00'
  const megabytes = bytes / (1024 * 1024)
  return megabytes.toFixed(decimals)
}

function calculateDistSizePlugin(): Plugin {
  let distPath = ''

  return {
    name: 'calculate-dist-size',
    enforce: 'post' as const,
    apply: 'build' as const,

    configResolved(config) {
      // 可以在这里获取打包输出的目录
      const outDir = config.build.outDir

      distPath = outDir
    },

    closeBundle() {
      if (!distPath) {
        console.error('Fail to get size of dist folder.')
        return
      }
      const distSize = getFolderSize(distPath)
      const formattedSize = formatBytes(distSize)
      console.log(`Size of dist folder: ${formattedSize} MB`)
    }
  }
}

export default calculateDistSizePlugin
自己实现的React热更新+SWC打包插件

这个插件利用 SWC 来编译 JavaScript 和 TypeScript 代码,并在 Vite 开发服务器中提供 React JSX热更新。此外,它还处理构建配置,以确保代码被正确地编译为适用于生产环境的格式。

import { Output, ParserConfig, ReactConfig, transform } from '@swc/core'
import { readFileSync, readdirSync } from 'fs'
import { SourceMapPayload } from 'module'
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
import { BuildOptions, PluginOption, UserConfig, createFilter } from 'vite'
import { createRequire } from 'node:module'
import { ViteOptions } from './type'
import { runtimePublicPath, preambleCode, refreshContentRE } from './const'

const _dirname = typeof __dirname !== 'undefined' ? __dirname : dirname(fileURLToPath(import.meta.url))

const _resolve = typeof global.require !== 'undefined' ? global.require.resolve : createRequire(import.meta.url).resolve

const plugin = (_options?: ViteOptions): PluginOption[] => {
  const options = {
    ..._options,
    target: _options?.target || 'es2017',
    jsxImportSource: _options?.jsxImportSource || 'react'
  }

  // vite配置中的buildTarget
  const buildTarget = options.target

  const filter = options.include || options.exclude ? createFilter(options.include, options.exclude) : null

  // 核心函数:根据配置调用SWC编译代码
  const transformWithSwc = async (fileName: string, code: string, reactConfig: ReactConfig) => {
    if ((!filter && fileName.includes('node_modules')) || (filter && !filter(fileName))) return

    const decorators = true
    const parser: ParserConfig | undefined = fileName.endsWith('.tsx')
      ? { syntax: 'typescript', tsx: true, decorators }
      : fileName.endsWith('.ts')
        ? { syntax: 'typescript', tsx: false, decorators }
        : fileName.endsWith('.jsx')
          ? { syntax: 'ecmascript', jsx: true }
          : fileName.endsWith('.mdx')
            ? // JSX is required to trigger fast refresh transformations, even if MDX already transforms it
              { syntax: 'ecmascript', jsx: true }
            : undefined
    if (!parser) return

    let result: Output
    try {
      const swcTransformConfig: any = {
        // 允许被配置文件覆盖
        swcrc: true,
        rootMode: 'upward-optional',
        filename: fileName,
        minify: false,
        jsc: {
          // target: buildTarget,
          parser,
          transform: {
            useDefineForClassFields: false,
            react: {
              ...reactConfig,
              useBuiltins: true
            }
          }
        },
        env: {
          targets: {
            safari: '11',
            edge: '79',
            chrome: '73'
          },
          mode: 'usage',
          coreJs: '3.36'
        }
      }

      // 两者不兼容,只能取其一
      if (swcTransformConfig.env && swcTransformConfig.jsc.target) {
        delete swcTransformConfig.jsc.target
      }

      result = await transform(code, swcTransformConfig)
    } catch (e: any) {
      // 输出错误信息
      const message: string = e.message
      const fileStartIndex = message.indexOf('╭─[')
      if (fileStartIndex !== -1) {
        const match = message.slice(fileStartIndex).match(/:(\d+):(\d+)]/)
        if (match) {
          e.line = match[1]
          e.column = match[2]
        }
      }
      throw e
    }

    return result
  }

  const silenceUseClientWarning = (userConfig: UserConfig): BuildOptions => ({
    rollupOptions: {
      onwarn(warning, defaultHandler) {
        if (warning.code === 'MODULE_LEVEL_DIRECTIVE' && warning.message.includes('use client')) {
          return
        }
        if (userConfig.build?.rollupOptions?.onwarn) {
          userConfig.build.rollupOptions.onwarn(warning, defaultHandler)
        } else {
          defaultHandler(warning)
        }
      }
    }
  })

  const resolveSwcHelpersDeps = () => {
    let helperList: string[] = []
    try {
      const file = _resolve('@swc/helpers')

      if (file) {
        const dir = dirname(file)
        const files = readdirSync(dir)
        helperList = files.map(file => join(dir, file))
      }
    } catch (e) {
      console.error(e)
    }
    return helperList
  }

  return [
    // dev时热更新1:加载热更新功能
    {
      name: 'vite:swc:resolve-runtime',
      apply: 'serve',
      enforce: 'pre', // Run before Vite default resolve to avoid syscalls
      resolveId: id => (id === runtimePublicPath ? id : undefined),
      load: id => (id === runtimePublicPath ? readFileSync(join(_dirname, 'refresh-runtime.js'), 'utf-8') : undefined)
    },

    // dev时热更新2:热更新核心插件
    {
      name: 'vite:swc',
      apply: 'serve',
      config: userConfig => {
        const userOptimizeDepsConfig = userConfig?.optimizeDeps?.disabled
        const optimizeDepsDisabled = userOptimizeDepsConfig === true || userOptimizeDepsConfig === 'dev'

        // 预编译列表
        const optimizeDeps = !optimizeDepsDisabled
          ? ['react', `${options.jsxImportSource}/jsx-dev-runtime`, ...resolveSwcHelpersDeps()]
          : undefined
        return {
          esbuild: false,
          optimizeDeps: {
            include: optimizeDeps,
            esbuildOptions: {
              target: buildTarget,
              supported: {
                decorators: true // esbuild 0.19在使用target为es2017时,预构建会报错,这里假定目标浏览器支持装饰器,避开报错
              }
            }
          },
          resolve: {
            dedupe: ['react', 'react-dom']
          }
        }
      },
      transformIndexHtml: (_, config) => [
        {
          tag: 'script',
          attrs: { type: 'module' },
          children: preambleCode.replace('__PATH__', config.server!.config.base + runtimePublicPath.slice(1))
        }
      ],
      async transform(code, _id, transformOptions) {
        const id = _id.split('?')[0]

        const result = await transformWithSwc(id, code, {
          refresh: !transformOptions?.ssr,
          development: true,
          runtime: 'automatic',
          importSource: options.jsxImportSource
        })
        if (!result) return

        if (transformOptions?.ssr || !refreshContentRE.test(result.code)) {
          return result
        }

        result.code = /*js*/ `
          import * as RefreshRuntime from "${runtimePublicPath}";
          if (!window.$RefreshReg$) throw new Error("React refresh preamble was not loaded. Something is wrong.");
          const prevRefreshReg = window.$RefreshReg$;
          const prevRefreshSig = window.$RefreshSig$;
          window.$RefreshReg$ = RefreshRuntime.getRefreshReg("${id}");
          window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;

          ${result.code}

          window.$RefreshReg$ = prevRefreshReg;
          window.$RefreshSig$ = prevRefreshSig;
          RefreshRuntime.__hmr_import(import.meta.url).then((currentExports) => {
            RefreshRuntime.registerExportsForReactRefresh("${id}", currentExports);
            import.meta.hot.accept((nextExports) => {
              if (!nextExports) return;
              const invalidateMessage = RefreshRuntime.validateRefreshBoundaryAndEnqueueUpdate(currentExports, nextExports);
              if (invalidateMessage) import.meta.hot.invalidate(invalidateMessage);
            });
          });
        `

        if (result.map) {
          const sourceMap: SourceMapPayload = JSON.parse(result.map)
          sourceMap.mappings = ';;;;;;;;' + sourceMap.mappings
          return { code: result.code, map: sourceMap }
        } else {
          return { code: result.code }
        }
      }
    },

    // 打包时候使用的插件
    {
      name: 'vite:swc',
      apply: 'build',
      enforce: 'post', // Run before esbuild
      config: userConfig => ({
        build: {
          ...silenceUseClientWarning(userConfig),
          target: buildTarget
        },
        resolve: {
          dedupe: ['react', 'react-dom']
        }
      }),
      transform: (code, _id) =>
        transformWithSwc(_id.split('?')[0], code, {
          runtime: 'automatic',
          importSource: options.jsxImportSource
        })
    }
  ]
}

export default plugin
  1. 导入依赖:代码开始部分导入了所需的 Node.js 内置模块和第三方库,以及 Vite 和 SWC 的相关 API。
  2. 定义插件函数plugin 函数接受一个可能包含用户配置的对象 _options,并返回一个 Vite 插件数组。
  3. 配置选项合并:函数内部,用户配置与默认配置合并,以设置插件的行为,例如编译目标 target 和 JSX 导入源 jsxImportSource
  4. 创建过滤器:如果用户提供了 includeexclude 规则,会创建一个过滤器 filter,用于决定哪些文件应该被插件处理。
  5. SWC 转换函数transformWithSwc 异步函数接收文件名、代码和 React 配置,调用 SWC 的 transform API 来编译代码。
  6. 错误处理:在 SWC 转换过程中,如果出现错误,会尝试提取错误消息中的错误行和列,并重新抛出格式化后的错误。
  7. 静默警告silenceUseClientWarning 函数用于抑制 Rollup 的某些警告,特别是与 'use client' 指令相关的警告。
  8. 解析 SWC 辅助依赖resolveSwcHelpersDeps 函数尝试解析 @swc/helpers 包中的辅助函数文件列表。
  9. 定义 Vite 插件对象:返回的插件数组中包括两个主要的插件对象,一个用于开发环境,另一个用于构建环境。
  10. 开发环境插件
    • 设置了 nameapplyconfigtransformIndexHtmltransform 属性来定义插件的行为。
    • 使用 resolveIdload 处理 React 快速刷新的运行时脚本。
    • transform 方法用于对代码进行转换,并添加了 React 快速刷新的相关代码。
  11. 构建环境插件
    • 设置了 nameapplyconfigtransform 属性。
    • 在构建配置中应用了静默警告配置,并指定了编译目标。
  12. React 快速刷新:在开发环境中,插件通过修改 transformIndexHtmltransform 方法来支持 React 快速刷新。
  13. 导出默认:最后,plugin 函数作为默认导出,使其可以在 Vite 配置中使用。

参考文章和地址

https://juejin.cn/post/7103165205483356168?searchId=20240706212202081D45CDF4733CF7923F#heading-17

https://article.juejin.cn/post/7211745375920586813

https://cn.vitejs.dev/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/779247.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

5个实用的文章生成器,高效输出优质文章

在自媒体时代,优质内容的持续输出是吸引读者、提升影响力的关键。然而,对于许多自媒体创作者来说,频繁的创作难免会遭遇灵感枯竭、创作不出文章的困扰。此时,文章生成器便成为了得力的助手。文章生成器的优势能够快速自动生成高质…

C++怎么解决不支持字符串枚举?

首先,有两种方法:使用命名空间和字符串常量与使用 enum class 和辅助函数。 表格直观展示 特性使用命名空间和字符串常量使用 enum class 和辅助函数类型安全性低 - 编译器无法检查字符串有效性,运行时发现错误高 - 编译期类型检查&#xf…

SCI论文发表:构建清晰论文框架的10个原则 (附思维导图,建议收藏)

我是娜姐 迪娜学姐 ,一个SCI医学期刊编辑,探索用AI工具提效论文写作和发表。 论文框架是什么?对我们完成一篇论文有哪些作用? 之前娜姐分享过一篇深圳湾实验室周耀旗教授关于论文写作的文章,他提出的第一个重要原则就…

Linux笔记之二

Linux笔记之二 一、文件属性学习二、软链接和硬链接1.软链接2.硬链接 三、Vim编辑器四、账号管理总结 一、文件属性学习 Linux 系统是一种典型的多用户系统,不同的用户处于不同的地位,拥有不同的权限。为了保护系统的安全性,Linux系统对不同…

前后端分离系统

前后端分离是一种现代软件架构模式,特别适用于Web应用开发,它强调将用户界面(前端)与服务器端应用逻辑(后端)相分离。两者通过API接口进行数据交互。这种架构模式的主要优势在于提高开发效率、维护性和可扩…

【LInux】从动态库的加载深入理解页表机制

💐 🌸 🌷 🍀 🌹 🌻 🌺 🍁 🍃 🍂 🌿 🍄🍝 🍛 🍤 📃个人主页 :阿然成长日记 …

Xilinx FPGA:vivado关于同步fifo的两个小实验

一、实验一:在同步fifo里写一个读一个(写入是8个位宽,读出是16个位宽) 程序: timescale 1ns / 1ps //要求写一个读一个 //读写时钟一致,写是8个位宽,读是16个位宽 module sync_fifo_test(inpu…

Nuxt框架中内置组件详解及使用指南(一)

title: Nuxt框架中内置组件详解及使用指南(一) date: 2024/7/6 updated: 2024/7/6 author: cmdragon excerpt: 本文详细介绍了Nuxt框架中的两个内置组件和的使用方法与功能。确保包裹的内容仅在客户端渲染,适用于处理浏览器特定功能或异步…

ubuntu 22 安装 lua 环境 编译lua cjson 模块

在 windows 下使用 cygwin 编译 lua 和 cjson 简直就是灾难,最后还是到 ubuntu 下完成了。 1、下载lua源码(我下载的 5.1 版本,后面还有一个小插曲), 直接解压编译,遇到一个 readline.h not found 的问题,需要安装 re…

MySQL篇三:数据类型

文章目录 前言1. 数值类型1.1 tinyint类型1.2 bit类型1.3 小数类型1.3.1 float1.3.2 decimal 2. 字符串类型2.1 char2.2 varchar2.3 char和varchar比较 3. 日期类型4. enum和set 前言 数据类型分类: 1. 数值类型 1.1 tinyint类型 在MySQL中,整型可以指…

论文略读:Learning and Forgetting Unsafe Examples in Large Language Models

随着发布给公众的大语言模型(LLMs)数量的增加,迫切需要了解这些模型从第三方定制的微调数据中学习的安全性影响。论文研究了在包含不安全内容的噪声定制数据上微调的LLMs的行为,这些数据集包含偏见、毒性和有害性 发现虽然对齐的L…

【Unity数据存储】Unity中使用SqLite数据库进行数据持久化

👨‍💻个人主页:元宇宙-秩沅 👨‍💻 hallo 欢迎 点赞👍 收藏⭐ 留言📝 加关注✅! 👨‍💻 本文由 秩沅 原创 👨‍💻 专栏交流🧧&…

Ubuntu 22.04远程自动登录桌面环境

如果需要远程自动登录桌面环境,首先需要将Ubuntu的自动登录打开,在【settings】-【user】下面 然后要设置【Sharing】进行桌面共享,Ubuntu有自带的桌面共享功能,不需要另外去安装xrdp或者vnc之类的工具了 点开【Remote Desktop】…

解决IDEA每次新建项目都需要重新配置maven的问题

每次打开IDEA都要重新配置maven,这是因为在DEA中分为项目设置和全局设置,这个时候我们就需要去到全局中设置maven了。我用的是IntelliJ IDEA 2023.3.4 (Ultimate Edition),以此为例。 第一步:打开一个空的IDEA,选择左…

大学生电子设计大赛超全资料分享

超全大学生电子设计大赛项目合集免费分享 电赛竞赛资料大全,新增竞赛空间电子设计资料。包含嵌入式硬件和软件开发的学习资料,包括PCB教程,单片机例程, 单片机课程设计毕业设计参考资料、项目设计方案,源码和开发文档…

孟德尔随机化 --痛风与酒精消耗量

写在前面 最近看了微信公众号,jimmy谈到生信与基础之间,个人觉得生信与基础技术是无高低之分的,本质上都是科研中为了证实结果的一个工具。生信的实质是用计算机分析数据,接触基础比较好,感觉是实验操作。分析上游为实…

初学嵌入式是弄linux还是单片机?

在开始前刚好我有一些资料,是我根据网友给的问题精心整理了一份「单片机的资料从专业入门到高级教程」, 点个关注在评论区回复“666”之后私信回复“666”,全部无偿共享给大家!!!1、先入门了51先学了89c52…

Windows安装超好用的截图工具——Snipaste

1、下载 官网:https://zh.snipaste.com/ 2、安装 (1)解压下载的压缩包 (2)选中Snipaste.exe文件,右键发送到 -- > 桌面快捷方式 (3)双击桌面Snipaste图标,桌面右下…

3.js - 裁剪平面(clipIntersection:交集、并集)

看图 代码 // ts-nocheck// 引入three.js import * as THREE from three// 导入轨道控制器 import { OrbitControls } from three/examples/jsm/controls/OrbitControls// 导入lil.gui import { GUI } from three/examples/jsm/libs/lil-gui.module.min.js// 导入tween import …

docker部署onlyoffice,开启JWT权限校验Token

原来的部署方式 之前的方式是禁用了JWT: docker run -itd -p 8080:80 --name docserver --network host -e JWT_ENABLEDfalse --restartalways onlyoffice/documentserver:8 新的部署方式 参考文档:https://helpcenter.onlyoffice.com/installation/…