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

embedded/2024/9/24 3:27:37/

[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 = 0const 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.outDirdistPath = 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).resolveconst plugin = (_options?: ViteOptions): PluginOption[] => {const options = {..._options,target: _options?.target || 'es2017',jsxImportSource: _options?.jsxImportSource || 'react'}// vite配置中的buildTargetconst buildTarget = options.targetconst 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))) returnconst decorators = trueconst 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 }: undefinedif (!parser) returnlet result: Outputtry {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.messageconst 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 syscallsresolveId: 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?.disabledconst optimizeDepsDisabled = userOptimizeDepsConfig === true || userOptimizeDepsConfig === 'dev'// 预编译列表const optimizeDeps = !optimizeDepsDisabled? ['react', `${options.jsxImportSource}/jsx-dev-runtime`, ...resolveSwcHelpersDeps()]: undefinedreturn {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) returnif (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.mappingsreturn { code: result.code, map: sourceMap }} else {return { code: result.code }}}},// 打包时候使用的插件{name: 'vite:swc',apply: 'build',enforce: 'post', // Run before esbuildconfig: 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/


http://www.ppmy.cn/embedded/56735.html

相关文章

5、MP4解复用---AAC+H264

MP4 MP4同样是一种容器格式,是由一个一个Box组成,每个Box又分为Header与Data,Data又包含很多子Box,具体的MP4文件结构也看过,内部Box结构比较复杂,一般不写MP4解释器的话,Box结构不用了解太细&a…

Ubuntu22.04使用/etc/rc.local开机启动程序

前言 因为在工作中,Ubuntu系统经常使用到开机启动,为了方便之后使用,特此介绍下如何使用/etc/rc.local来开机启动程序,步骤如下,针对Ubuntu20.04或Ubuntu22.04系统都有效。 使用root权限编辑/lib/systemd/system/rc-…

Windows ipconfig命令详解,Windows查看IP地址信息

「作者简介」:冬奥会网络安全中国代表队,CSDN Top100,就职奇安信多年,以实战工作为基础著作 《网络安全自学教程》,适合基础薄弱的同学系统化的学习网络安全,用最短的时间掌握最核心的技术。 ipconfig 1、基…

华为OD机考题(HJ41 称砝码)

前言 经过前期的数据结构和算法学习,开始以OD机考题作为练习题,继续加强下熟练程度。有需要的可以同步练习下。 描述 现有n种砝码,重量互不相等,分别为 m1,m2,m3…mn ; 每种砝码对应的数量为 x1,x2,x3...xn 。现在要…

MySQL:高效的索引

数据库索引 1. 索引介绍1.1 创建索引1.2 查看索引 2. 索引应用2.1 前缀索引2.2 全文索引2.3 复合索引2.4 复合索引中的列顺序2.5 建立最佳索引2.6 使用索引排序2.7 覆盖索引 3. 维护索引4. 建立性能数据库 索引对大型和高并发数据库非常有用,因为它可以显著提升查询…

【云原生监控】Prometheus 普罗米修斯从搭建到使用详解

目录 一、前言 二、服务监控概述 2.1 什么是微服务监控 2.2 微服务监控指标 2.3 微服务监控工具 三、Prometheus概述 3.1 Prometheus是什么 3.2 Prometheus 特点 3.3 Prometheus 架构图 3.3.1 Prometheus核心组件 3.3.2 Prometheus 工作流程 3.4 Prometheus 应用场景…

Redis的两种持久化方案

Redis 提供了多种持久化机制来保证数据在发生意外情况下(如断电或服务器崩溃)不丢失。以下是几种主要的 Redis 持久化方案及其特点: 1. RDB (Redis Database Backup) RDB 是 Redis 创建的数据库快照,它可以将数据集快照以二进制…

2 极/2 零 (2P2Z) 补偿器

极/2 零 (2P2Z) 补偿器是模拟 II 型控制器的数字实现。它是一种滤波器,通过考虑两个极点和一个零点,将特定的增益和相位升压引入系统。您必须战略性地选择每个极点和零点的频率位置,这将有助于实现所需的系统性能。在该…