[Vite]vite-plugin-react和vite-plugin-react-swc插件原理了解
共同的作用
- JSX 支持:插件为 React 应用程序中的 JSX 语法提供支持,确保它可以被正确地转换为 JavaScript。
- Fast Refresh:提供热更新功能,当应用程序在开发服务器上运行时,可以快速地看到更改的效果,而不需要手动刷新页面。
- 装饰器支持:如果项目中使用了 TypeScript 装饰器,插件会正确处理它们。
- Source Maps:生成源代码映射,以便于在浏览器中调试源代码。
- Virtual DOM 的导入:确保 React 和 ReactDOM 的虚拟 DOM 导入被正确处理。
- 开发模式与生产模式的区分:在开发模式下,插件会提供更多的辅助功能,如 React 快速刷新;而在生产模式下,它会专注于代码的最小化和优化。
- ESLint 集成:可选地集成 ESLint,以在开发过程中提供代码质量和一致性的检查。
- 配置 Rollup:在构建过程中,插件可能会配置 Rollup 以适应 React 应用程序的特定需求。
- 兼容性:确保 React 应用程序可以在目标浏览器中运行,即使这些浏览器不支持最新的 JavaScript 特性。
- 自定义配置:允许用户通过 Vite 配置文件传递自定义选项给插件,例如指定 JSX 工厂函数或模式等。
vite-plugin-react
源码地址
https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/src/index.ts
逻辑解读
- 类型导入:代码开始部分导入了 TypeScript 类型,用于确保插件的类型正确性。
- 依赖懒加载:
loadBabel
函数用于按需加载 Babel,因为 Babel 只在开发模式下或当特定的插件被使用时才需要。 - 配置选项:
Options
接口定义了插件的配置选项,允许用户自定义包括include
、exclude
、jsxImportSource
、jsxRuntime
等。 - 插件数组导出:
viteReact
函数返回一个插件数组,这些插件在 Vite 构建过程中执行不同的任务。 - 创建 Babel 插件配置:
viteBabel
插件对象配置了如何使用 Babel 来转换 React 代码,包括 JSX 支持和快速刷新。 - React 快速刷新:
viteReactRefresh
插件对象处理与 React 快速刷新相关的逻辑,包括在index.html
中注入必要的脚本。 - 构建配置:在
config
函数中,插件可以修改 Vite 配置,例如设置esbuild
选项或optimizeDeps
。 - 配置解析:
configResolved
函数在 Vite 配置解析完成后调用,用于确定是否处于生产模式、项目根路径等。 - 转换函数:
transform
函数是 Vite 插件中的核心,用于实际的代码转换工作。它使用 Babel 来转换 JSX 语法,并在开发模式下添加 React 快速刷新的包装器。 - 快速刷新逻辑:根据配置和代码内容,插件决定是否需要对代码进行快速刷新包装。
- Babel 插件加载:
loadPlugin
函数用于加载 Babel 插件,确保插件的异步加载。 - Babel 选项创建:
createBabelOptions
函数用于创建 Babel 的选项对象,它可以从用户配置或默认值中初始化。 - 编译器检测:
hasCompiler
和hasCompilerWithDefaultRuntime
函数用于检测 Babel 插件列表中是否包含特定的编译器插件。 - 构建时警告处理:
silenceUseClientWarning
函数用于抑制 Rollup 的某些警告。 - 静态资源服务:
resolveId
和load
函数用于处理静态资源的请求,例如 React 快速刷新运行时脚本。 - 转换 HTML:
transformIndexHtml
函数用于修改index.html
,注入快速刷新的脚本。 - 插件 API:
ViteReactPluginApi
类型定义了插件可以提供的 API,允许其他插件通过reactBabel
钩子来修改 Babel 配置。 - 默认导出:最后,
viteReact
函数作为默认导出,使其可以在 Vite 配置中使用。
总结来说,@vitejs/plugin-react
插件的主要功能是为 React 应用程序提供 Vite 构建和开发服务器的集成支持。它包括 JSX 语法的转换、React 组件的快速刷新(热更新)、以及对 React 运行时的配置。通过插件的配置选项,用户可以根据项目需求定制化插件的行为。
// eslint-disable-next-line import/no-duplicates
import type * as babelCore from '@babel/core'
// eslint-disable-next-line import/no-duplicates
import type { ParserOptions, TransformOptions } from '@babel/core'
import { createFilter } from 'vite'
import type {
BuildOptions,
Plugin,
PluginOption,
ResolvedConfig,
UserConfig,
} from 'vite'
import {
addClassComponentRefreshWrapper,
addRefreshWrapper,
preambleCode,
runtimeCode,
runtimePublicPath,
} from './fast-refresh'
// lazy load babel since it's not used during build if plugins are not used
let babel: typeof babelCore | undefined
async function loadBabel() {
if (!babel) {
babel = await import('@babel/core')
}
return babel
}
export interface Options {
include?: string | RegExp | Array<string | RegExp>
exclude?: string | RegExp | Array<string | RegExp>
/**
* Control where the JSX factory is imported from.
* https://esbuild.github.io/api/#jsx-import-source
* @default 'react'
*/
jsxImportSource?: string
/**
* Note: Skipping React import with classic runtime is not supported from v4
* @default "automatic"
*/
jsxRuntime?: 'classic' | 'automatic'
/**
* Babel configuration applied in both dev and prod.
*/
babel?:
| BabelOptions
| ((id: string, options: { ssr?: boolean }) => BabelOptions)
}
export type BabelOptions = Omit<
TransformOptions,
| 'ast'
| 'filename'
| 'root'
| 'sourceFileName'
| 'sourceMaps'
| 'inputSourceMap'
>
/**
* The object type used by the `options` passed to plugins with
* an `api.reactBabel` method.
*/
export interface ReactBabelOptions extends BabelOptions {
plugins: Extract<BabelOptions['plugins'], any[]>
presets: Extract<BabelOptions['presets'], any[]>
overrides: Extract<BabelOptions['overrides'], any[]>
parserOpts: ParserOptions & {
plugins: Extract<ParserOptions['plugins'], any[]>
}
}
type ReactBabelHook = (
babelConfig: ReactBabelOptions,
context: ReactBabelHookContext,
config: ResolvedConfig,
) => void
type ReactBabelHookContext = { ssr: boolean; id: string }
export type ViteReactPluginApi = {
/**
* Manipulate the Babel options of `@vitejs/plugin-react`
*/
reactBabel?: ReactBabelHook
}
const reactCompRE = /extends\s+(?:React\.)?(?:Pure)?Component/
const refreshContentRE = /\$Refresh(?:Reg|Sig)\$\(/
const defaultIncludeRE = /\.[tj]sx?$/
const tsRE = /\.tsx?$/
export default function viteReact(opts: Options = {}): PluginOption[] {
// Provide default values for Rollup compat.
let devBase = '/'
const filter = createFilter(opts.include ?? defaultIncludeRE, opts.exclude)
const jsxImportSource = opts.jsxImportSource ?? 'react'
const jsxImportRuntime = `${jsxImportSource}/jsx-runtime`
const jsxImportDevRuntime = `${jsxImportSource}/jsx-dev-runtime`
let isProduction = true
let projectRoot = process.cwd()
let skipFastRefresh = false
let runPluginOverrides:
| ((options: ReactBabelOptions, context: ReactBabelHookContext) => void)
| undefined
let staticBabelOptions: ReactBabelOptions | undefined
// Support patterns like:
// - import * as React from 'react';
// - import React from 'react';
// - import React, {useEffect} from 'react';
const importReactRE = /\bimport\s+(?:\*\s+as\s+)?React\b/
const viteBabel: Plugin = {
name: 'vite:react-babel',
enforce: 'pre',
config() {
if (opts.jsxRuntime === 'classic') {
return {
esbuild: {
jsx: 'transform',
},
}
} else {
return {
esbuild: {
jsx: 'automatic',
jsxImportSource: opts.jsxImportSource,
},
optimizeDeps: { esbuildOptions: { jsx: 'automatic' } },
}
}
},
configResolved(config) {
devBase = config.base
projectRoot = config.root
isProduction = config.isProduction
skipFastRefresh =
isProduction ||
config.command === 'build' ||
config.server.hmr === false
if ('jsxPure' in opts) {
config.logger.warnOnce(
'[@vitejs/plugin-react] jsxPure was removed. You can configure esbuild.jsxSideEffects directly.',
)
}
const hooks: ReactBabelHook[] = config.plugins
.map((plugin) => plugin.api?.reactBabel)
.filter(defined)
if (hooks.length > 0) {
runPluginOverrides = (babelOptions, context) => {
hooks.forEach((hook) => hook(babelOptions, context, config))
}
} else if (typeof opts.babel !== 'function') {
// Because hooks and the callback option can mutate the Babel options
// we only create static option in this case and re-create them
// each time otherwise
staticBabelOptions = createBabelOptions(opts.babel)
}
},
async transform(code, id, options) {
if (id.includes('/node_modules/')) return
const [filepath] = id.split('?')
if (!filter(filepath)) return
const ssr = options?.ssr === true
const babelOptions = (() => {
if (staticBabelOptions) return staticBabelOptions
const newBabelOptions = createBabelOptions(
typeof opts.babel === 'function'
? opts.babel(id, { ssr })
: opts.babel,
)
runPluginOverrides?.(newBabelOptions, { id, ssr })
return newBabelOptions
})()
const plugins = [...babelOptions.plugins]
const isJSX = filepath.endsWith('x')
const useFastRefresh =
!skipFastRefresh &&
!ssr &&
(isJSX ||
(opts.jsxRuntime === 'classic'
? importReactRE.test(code)
: code.includes(jsxImportDevRuntime) ||
code.includes(jsxImportRuntime)))
if (useFastRefresh) {
plugins.push([
await loadPlugin('react-refresh/babel'),
{ skipEnvCheck: true },
])
}
if (opts.jsxRuntime === 'classic' && isJSX) {
if (!isProduction) {
// These development plugins are only needed for the classic runtime.
plugins.push(
await loadPlugin('@babel/plugin-transform-react-jsx-self'),
await loadPlugin('@babel/plugin-transform-react-jsx-source'),
)
}
}
// Avoid parsing if no special transformation is needed
if (
!plugins.length &&
!babelOptions.presets.length &&
!babelOptions.configFile &&
!babelOptions.babelrc
) {
return
}
const parserPlugins = [...babelOptions.parserOpts.plugins]
if (!filepath.endsWith('.ts')) {
parserPlugins.push('jsx')
}
if (tsRE.test(filepath)) {
parserPlugins.push('typescript')
}
const babel = await loadBabel()
const result = await babel.transformAsync(code, {
...babelOptions,
root: projectRoot,
filename: id,
sourceFileName: filepath,
// Required for esbuild.jsxDev to provide correct line numbers
// This crates issues the react compiler because the re-order is too important
// People should use @babel/plugin-transform-react-jsx-development to get back good line numbers
retainLines: hasCompiler(plugins)
? false
: !isProduction && isJSX && opts.jsxRuntime !== 'classic',
parserOpts: {
...babelOptions.parserOpts,
sourceType: 'module',
allowAwaitOutsideFunction: true,
plugins: parserPlugins,
},
generatorOpts: {
...babelOptions.generatorOpts,
decoratorsBeforeExport: true,
},
plugins,
sourceMaps: true,
})
if (result) {
let code = result.code!
if (useFastRefresh) {
if (refreshContentRE.test(code)) {
code = addRefreshWrapper(code, id)
} else if (reactCompRE.test(code)) {
code = addClassComponentRefreshWrapper(code, id)
}
}
return { code, map: result.map }
}
},
}
// We can't add `react-dom` because the dependency is `react-dom/client`
// for React 18 while it's `react-dom` for React 17. We'd need to detect
// what React version the user has installed.
const dependencies = ['react', jsxImportDevRuntime, jsxImportRuntime]
const staticBabelPlugins =
typeof opts.babel === 'object' ? opts.babel?.plugins ?? [] : []
if (hasCompilerWithDefaultRuntime(staticBabelPlugins)) {
dependencies.push('react/compiler-runtime')
}
const viteReactRefresh: Plugin = {
name: 'vite:react-refresh',
enforce: 'pre',
config: (userConfig) => ({
build: silenceUseClientWarning(userConfig),
optimizeDeps: {
include: dependencies,
},
resolve: {
dedupe: ['react', 'react-dom'],
},
}),
resolveId(id) {
if (id === runtimePublicPath) {
return id
}
},
load(id) {
if (id === runtimePublicPath) {
return runtimeCode
}
},
transformIndexHtml() {
if (!skipFastRefresh)
return [
{
tag: 'script',
attrs: { type: 'module' },
children: preambleCode.replace(`__BASE__`, devBase),
},
]
},
}
return [viteBabel, viteReactRefresh]
}
viteReact.preambleCode = preambleCode
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 loadedPlugin = new Map<string, any>()
function loadPlugin(path: string): any {
const cached = loadedPlugin.get(path)
if (cached) return cached
const promise = import(path).then((module) => {
const value = module.default || module
loadedPlugin.set(path, value)
return value
})
loadedPlugin.set(path, promise)
return promise
}
function createBabelOptions(rawOptions?: BabelOptions) {
const babelOptions = {
babelrc: false,
configFile: false,
...rawOptions,
} as ReactBabelOptions
babelOptions.plugins ||= []
babelOptions.presets ||= []
babelOptions.overrides ||= []
babelOptions.parserOpts ||= {} as any
babelOptions.parserOpts.plugins ||= []
return babelOptions
}
function defined<T>(value: T | undefined): value is T {
return value !== undefined
}
function hasCompiler(plugins: ReactBabelOptions['plugins']) {
return plugins.some(
(p) =>
p === 'babel-plugin-react-compiler' ||
(Array.isArray(p) && p[0] === 'babel-plugin-react-compiler'),
)
}
// https://gist.github.com/poteto/37c076bf112a07ba39d0e5f0645fec43
function hasCompilerWithDefaultRuntime(plugins: ReactBabelOptions['plugins']) {
return plugins.some(
(p) =>
p === 'babel-plugin-react-compiler' ||
(Array.isArray(p) &&
p[0] === 'babel-plugin-react-compiler' &&
p[1]?.runtimeModule === undefined),
)
}
vite-plugin-react-swc
核心:它使用SWC来替代Babel进行打包,速度快了很多。
源码地址
https://github.com/vitejs/vite-plugin-react-swc/blob/main/src/index.ts
逻辑解读
- 导入依赖:代码开始部分导入了 Node.js 的内置模块和第三方库,如
fs
、path
、url
等,以及@swc/core
和 Vite 的类型定义。 - 定义插件选项:
Options
类型定义了插件的配置选项,包括jsxImportSource
、tsDecorators
、plugins
、devTarget
和parserConfig
。 - react 函数:这是一个工厂函数,用于创建 Vite 插件数组。它接受用户配置并返回配置好的插件对象。
- 处理 HMR:代码检查了服务器是否启用了热模块替换(HMR),如果没有启用,则设置
hmrDisabled
标志。 - 创建插件对象:
- 对象
vite:react-swc:resolve-runtime
用于解析 React 刷新运行时的路径,并提供相应的代码。 - 对象
vite:react-swc
包含了多个属性和方法,用于处理开发服务器上的特定行为,如config
、configResolved
、transformIndexHtml
和transform
。
- 对象
- React 快速刷新:在
transform
方法中,插件会检查代码是否包含 React 组件或刷新相关的代码。如果是,它将修改代码以支持 React 快速刷新。 - Source Map 处理:对于支持快速刷新的代码,插件会修改 Source Map,以确保源代码映射正确。
- 构建时的配置:当插件应用于构建时,它会配置 SWC 以使用特定的目标和插件选项进行代码转换。
- transformWithOptions 函数:这是一个异步函数,用于执行实际的代码转换。它接受文件 ID、代码、目标、选项和 React 配置,然后调用 SWC 的
transform
方法。 - silenceUseClientWarning 函数:这个函数用于抑制 Rollup 的警告,特别是与
"use client"
相关的警告。 - 导出默认:最后,
react
函数作为默认导出,使其可以在 Vite 配置中使用。
import { readFileSync } from "fs";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
import { SourceMapPayload } from "module";
import {
Output,
ParserConfig,
ReactConfig,
JscTarget,
transform,
} from "@swc/core";
import { PluginOption, UserConfig, BuildOptions } from "vite";
import { createRequire } from "module";
const runtimePublicPath = "/@react-refresh";
const preambleCode = `import { injectIntoGlobalHook } from "__PATH__";
injectIntoGlobalHook(window);
window.$RefreshReg$ = () => {};
window.$RefreshSig$ = () => (type) => type;`;
const _dirname =
typeof __dirname !== "undefined"
? __dirname
: dirname(fileURLToPath(import.meta.url));
const resolve = createRequire(
typeof __filename !== "undefined" ? __filename : import.meta.url,
).resolve;
const reactCompRE = /extends\s+(?:React\.)?(?:Pure)?Component/;
const refreshContentRE = /\$Refresh(?:Reg|Sig)\$\(/;
type Options = {
/**
* Control where the JSX factory is imported from.
* @default "react"
*/
jsxImportSource?: string;
/**
* Enable TypeScript decorators. Requires experimentalDecorators in tsconfig.
* @default false
*/
tsDecorators?: boolean;
/**
* Use SWC plugins. Enable SWC at build time.
* @default undefined
*/
plugins?: [string, Record<string, any>][];
/**
* Set the target for SWC in dev. This can avoid to down-transpile private class method for example.
* For production target, see https://vitejs.dev/config/build-options.html#build-target
* @default "es2020"
*/
devTarget?: JscTarget;
/**
* Override the default include list (.ts, .tsx, .mts, .jsx, .mdx).
* This requires to redefine the config for any file you want to be included.
* If you want to trigger fast refresh on compiled JS, use `jsx: true`.
* Exclusion of node_modules should be handled by the function if needed.
*/
parserConfig?: (id: string) => ParserConfig | undefined;
};
const isWebContainer = globalThis.process?.versions?.webcontainer;
const react = (_options?: Options): PluginOption[] => {
let hmrDisabled = false;
const options = {
jsxImportSource: _options?.jsxImportSource ?? "react",
tsDecorators: _options?.tsDecorators,
plugins: _options?.plugins
? _options?.plugins.map((el): typeof el => [resolve(el[0]), el[1]])
: undefined,
devTarget: _options?.devTarget ?? "es2020",
parserConfig: _options?.parserConfig,
};
return [
{
name: "vite:react-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,
},
{
name: "vite:react-swc",
apply: "serve",
config: () => ({
esbuild: false,
optimizeDeps: {
include: [`${options.jsxImportSource}/jsx-dev-runtime`],
esbuildOptions: { jsx: "automatic" },
},
}),
configResolved(config) {
if (config.server.hmr === false) hmrDisabled = true;
const mdxIndex = config.plugins.findIndex(
(p) => p.name === "@mdx-js/rollup",
);
if (
mdxIndex !== -1 &&
mdxIndex >
config.plugins.findIndex((p) => p.name === "vite:react-swc")
) {
throw new Error(
"[vite:react-swc] The MDX plugin should be placed before this plugin",
);
}
if (isWebContainer) {
config.logger.warn(
"[vite:react-swc] SWC is currently not supported in WebContainers. You can use the default React plugin instead.",
);
}
},
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 refresh = !transformOptions?.ssr && !hmrDisabled;
const result = await transformWithOptions(
id,
code,
options.devTarget,
options,
{
refresh,
development: true,
runtime: "automatic",
importSource: options.jsxImportSource,
},
);
if (!result) return;
if (!refresh) return result;
const hasRefresh = refreshContentRE.test(result.code);
if (!hasRefresh && !reactCompRE.test(result.code)) return result;
const sourceMap: SourceMapPayload = JSON.parse(result.map!);
sourceMap.mappings = ";;" + sourceMap.mappings;
result.code = `import * as RefreshRuntime from "${runtimePublicPath}";
${result.code}`;
if (hasRefresh) {
sourceMap.mappings = ";;;;;;" + sourceMap.mappings;
result.code = `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;
`;
}
result.code += `
RefreshRuntime.__hmr_import(import.meta.url).then((currentExports) => {
RefreshRuntime.registerExportsForReactRefresh("${id}", currentExports);
import.meta.hot.accept((nextExports) => {
if (!nextExports) return;
const invalidateMessage = RefreshRuntime.validateRefreshBoundaryAndEnqueueUpdate("${id}", currentExports, nextExports);
if (invalidateMessage) import.meta.hot.invalidate(invalidateMessage);
});
});
`;
return { code: result.code, map: sourceMap };
},
},
options.plugins
? {
name: "vite:react-swc",
apply: "build",
enforce: "pre", // Run before esbuild
config: (userConfig) => ({
build: silenceUseClientWarning(userConfig),
}),
transform: (code, _id) =>
transformWithOptions(_id.split("?")[0], code, "esnext", options, {
runtime: "automatic",
importSource: options.jsxImportSource,
}),
}
: {
name: "vite:react-swc",
apply: "build",
config: (userConfig) => ({
build: silenceUseClientWarning(userConfig),
esbuild: {
jsx: "automatic",
jsxImportSource: options.jsxImportSource,
tsconfigRaw: {
compilerOptions: { useDefineForClassFields: true },
},
},
}),
},
];
};
const transformWithOptions = async (
id: string,
code: string,
target: JscTarget,
options: Options,
reactConfig: ReactConfig,
) => {
const decorators = options?.tsDecorators ?? false;
const parser: ParserConfig | undefined = options.parserConfig
? options.parserConfig(id)
: id.endsWith(".tsx")
? { syntax: "typescript", tsx: true, decorators }
: id.endsWith(".ts") || id.endsWith(".mts")
? { syntax: "typescript", tsx: false, decorators }
: id.endsWith(".jsx")
? { syntax: "ecmascript", jsx: true }
: id.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 {
result = await transform(code, {
filename: id,
swcrc: false,
configFile: false,
sourceMaps: true,
jsc: {
target,
parser,
experimental: { plugins: options.plugins },
transform: {
useDefineForClassFields: true,
react: reactConfig,
},
},
});
} 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);
}
},
},
});
export default react;