Vite的插件可以有两种形式,一种是vite插件,仅供vite使用;另一种则是rollup通用插件,它不使用 Vite 特有的钩子,让我们简单介绍一下关于这两种插件的生命周期:
apply
函数,传入 Vite 的配置对象。'pre'
或 'post'
,分别表示在 Vite 默认插件之前或之后执行。enforce
:值可以是pre
或 post
, pre
会较于 post
先执行;apply
:值可以是 build
或 serve
亦可以是一个函数,指明它们仅在 build
或 serve
模式时调用;config(config, env)
:可以在 vite 被解析之前修改 vite 的相关配置。钩子接收原始用户配置 config 和一个描述配置环境的变量env;configResolved(resolvedConfig)
:在解析 vite 配置后调用。使用这个钩子读取和存储最终解析的配置。当插件需要根据运行的命令做一些不同的事情时,它很有用。configureServer(server)
:主要用来配置开发服务器,为 dev-server (connect 应用程序) 添加自定义的中间件;transformIndexHtml(html)
:转换 index.html 的专用钩子。钩子接收当前的 HTML 字符串和转换上下文;handleHotUpdate(ctx)
:执行自定义HMR更新,可以通过ws往客户端发送自定义的事件;options(options)
:在服务器启动时被调用:获取、操纵Rollup选项,严格意义上来讲,它执行于属于构建阶段之前;buildStart(options)
:在每次开始构建时调用;resolveId(source, importer, options)
:在每个传入模块请求时被调用,创建自定义确认函数,可以用来定位第三方依赖;load(id)
:在每个传入模块请求时被调用,可以自定义加载器,可用来返回自定义的内容;transform(code, id)
:在每个传入模块请求时被调用,主要是用来转换单个模块;buildEnd(error?: Error)
:在构建阶段结束后被调用,此处构建结束只是代表所有模块转义完成;outputOptions(options)
:接受输出参数;renderStart(outputOptions, inputOptions)
:每次 bundle.generate 和 bundle.write 调用时都会被触发;augmentChunkHash(chunkInfo)
:用来给 chunk 增加 hash;renderChunk(code, chunk, options)
:转译单个的chunk时触发。rollup 输出每一个chunk文件的时候都会调用;generateBundle(options, bundle, isWrite)
:在调用 bundle.write 之前立即触发这个 hook;writeBundle(options, bundle)
:在调用 bundle.write后,所有的chunk都写入文件后,最后会调用一次 writeBundle;closeBundle()
:在服务器关闭时被调用按照顺序,首先是配置解析相关:
接下来是构建阶段的钩子:
然后是输出阶段的钩子:
enforce: 'pre'
enforce
enforce: 'post'
实现一个统计打包后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
这个插件利用 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
plugin
函数接受一个可能包含用户配置的对象 _options
,并返回一个 Vite 插件数组。target
和 JSX 导入源 jsxImportSource
。include
或 exclude
规则,会创建一个过滤器 filter
,用于决定哪些文件应该被插件处理。transformWithSwc
异步函数接收文件名、代码和 React 配置,调用 SWC 的 transform
API 来编译代码。silenceUseClientWarning
函数用于抑制 Rollup 的某些警告,特别是与 'use client'
指令相关的警告。resolveSwcHelpersDeps
函数尝试解析 @swc/helpers
包中的辅助函数文件列表。name
、apply
、config
、transformIndexHtml
和 transform
属性来定义插件的行为。resolveId
和 load
处理 React 快速刷新的运行时脚本。transform
方法用于对代码进行转换,并添加了 React 快速刷新的相关代码。name
、apply
、config
和 transform
属性。transformIndexHtml
和 transform
方法来支持 React 快速刷新。plugin
函数作为默认导出,使其可以在 Vite 配置中使用。https://juejin.cn/post/7103165205483356168?searchId=20240706212202081D45CDF4733CF7923F#heading-17
https://article.juejin.cn/post/7211745375920586813
https://cn.vitejs.dev/
因篇幅问题不能全部显示,请点此查看更多更全内容
怀疑对方AI换脸可以让对方摁鼻子 真人摁下去鼻子会变形
女子野生动物园下车狼悄悄靠近 后车司机按喇叭提醒
睡前玩8分钟手机身体兴奋1小时 还可能让你“变丑”
惊蛰为啥吃梨?倒春寒来不来就看惊蛰
男子高速犯困开智能驾驶出事故 60万刚买的奔驰严重损毁