
Vite 源码解析(3) - 插件篇
无论是 webpack 还是 vite, 插件机制都极大的扩展了这些打包器的深度和广度, 这一篇我们深入 Vite 的插件机制, 了解它是如何运作的.
第一篇文章我们讲了在配置中, 通过 `resolvePlugins`
来聚合和解析一票插件, 留了一个未展开讲的函数 `createPluginContainer`
, 我们知道 vite 插件扩展了设计出色的 rollup 接口, 因此为了和 rollup 插件打通, vite 搞了个 `createPluginContainer`
函数, 其实这个函数来自 wmr, 是 preact 打通 rollup 的包, vite 拿这个重新卷了一下.
这篇文章我们先来学习一下 `createPluginContainer`
的源码, 然后通过 `@rollup/plugin-alias`
, `plugins/esbuild`
这两个库, 来看一看 `createPluginContainer`
是怎样与 rollup hooks 打通的.
createPluginContainer
整体来讲, `createPluginContainer`
可以划分为 `前置逻辑`
, `Context`
, `TransformContext`
, `container`
和, 其中我们重点讲的是后三个. 当 vite 在构建到某个阶段时, 会调用 `container`
中的钩子函数, 这些钩子函数会遍历你所有的插件, 如果你的某些插件在这个钩子中要做些什么, 那就执行它, 否则就跳过它, 而这些钩子函数是和 roullup 插件体系打通的. 而 `Context`
和 `TransformContext`
相当于一个工厂, 将 vite 的插件转换成 rollup 可用的.
前置代码
const isDebug = process.env.DEBUG; const seenResolves: Record<string, true | undefined> = {}; const debugResolve = createDebugger("vite:resolve"); const debugPluginResolve = createDebugger("vite:plugin-resolve", { onlyWhenFocused: "vite:plugin", }); const debugPluginTransform = createDebugger("vite:plugin-transform", { onlyWhenFocused: "vite:plugin", }); const debugSourcemapCombineFlag = "vite:sourcemap-combine"; const isDebugSourcemapCombineFocused = process.env.DEBUG?.includes( debugSourcemapCombineFlag ); const debugSourcemapCombineFilter = process.env.DEBUG_VITE_SOURCEMAP_COMBINE_FILTER; const debugSourcemapCombine = createDebugger("vite:sourcemap-combine", { onlyWhenFocused: true, }); // --------------------------------------------------------------------------- const watchFiles = new Set<string>(); // TODO: use import() // createRequire 上次说了, 用来加载 cjs 文件 const _require = createRequire(import.meta.url); // get rollup version // 先拿到 node_modules/rollup/package,json const rollupPkgPath = resolve(_require.resolve("rollup"), "../../package.json"); // 我们看到主要是取当前 rollup 的版本信息 const minimalContext: MinimalPluginContext = { meta: { rollupVersion: JSON.parse(fs.readFileSync(rollupPkgPath, "utf-8")).version, watchMode: true, }, }; // 不兼容的插件暴露 warning. function warnIncompatibleMethod(method: string, plugin: string) { logger.warn( colors.cyan(`[plugin:${plugin}] `) + colors.yellow( `context method ${colors.bold( `${method}()` )} is not supported in serve mode. This plugin is likely not vite-compatible.` ) ); } // throw when an unsupported ModuleInfo property is accessed, // so that incompatible plugins fail in a non-cryptic way. // 这里很优雅的用到了 Proxy 和 Reflect, 用于拦截 module.info 的 getter // 配合下面 getModuleInfo 这个函数看 const ModuleInfoProxy: ProxyHandler<ModuleInfo> = { get(info: any, key: string) { if (key in info) { return info[key]; } throw Error(`[vite] The "${key}" property of ModuleInfo is not supported.`); }, }; // same default value of "moduleInfo.meta" as in Rollup const EMPTY_OBJECT = Object.freeze({}); function getModuleInfo(id: string) { const module = moduleGraph?.getModuleById(id); if (!module) { return null; } // 如果没有 module.info, 就给 module.info 加上代理, 且 // module.info 只有 id, meta 两个字段 // 一旦你访问非 id, meta 字段, 比如 module.info.xxx 就会报错 if (!module.info) { module.info = new Proxy( { id, meta: module.meta || EMPTY_OBJECT } as ModuleInfo, ModuleInfoProxy ); } return module.info; } // 更新模块的 meta 信息 function updateModuleInfo(id: string, { meta }: { meta?: object | null }) { if (meta) { const moduleInfo = getModuleInfo(id); if (moduleInfo) { moduleInfo.meta = { ...moduleInfo.meta, ...meta }; } } }
Context
关于 Context 和 TransformContext 我们简单说, 它实现了 rollup 的 PluginContext 接口, 可以把它理解为一个工厂, 是将 vite 的插件转换成 rollup 可用的.
// we should create a new context for each async hook pipeline so that the // active plugin in that pipeline can be tracked in a concurrency-safe manner. // using a class to make creating new contexts more efficient class Context implements PluginContext { meta = minimalContext.meta; ssr = false; _scan = false; _activePlugin: Plugin | null; _activeId: string | null = null; _activeCode: string | null = null; _resolveSkips?: Set<Plugin>; _addedImports: Set<string> | null = null; constructor(initialPlugin?: Plugin) { this._activePlugin = initialPlugin || null; } // 这里用到了 acorn 这个库, 用来将代码字符串转换为 AST // rollup, webpack 都用这个 parse(code: string, opts: any = {}) { return parser.parse(code, { sourceType: "module", ecmaVersion: "latest", locations: true, ...opts, }); } // 解析路径 // src/container/Home.tsx -> /User/xxxx/vite-example/src/container/Home.tsx async resolve( id: string, importer?: string, options?: { skipSelf?: boolean } ) { // 收集需要跳过的插件 let skip: Set<Plugin> | undefined; if (options?.skipSelf && this._activePlugin) { skip = new Set(this._resolveSkips); skip.add(this._activePlugin); } // 关于 resolveId 下面会讲到 // 它就是获取当前模块在文件系统的绝对路径 // 我们会跳过 skip 包含的插件 let out = await container.resolveId(id, importer, { skip, ssr: this.ssr, scan: this._scan, }); // 如果没有找到, 或者被跳过, 就返回 null // 否则返回路径 if (typeof out === "string") out = { id: out }; return out as ResolvedId | null; } // 获取模块信息 getModuleInfo(id: string) { return getModuleInfo(id); } // 获取所有模块路径 getModuleIds() { return moduleGraph ? moduleGraph.idToModuleMap.keys() : Array.prototype[Symbol.iterator](); } addWatchFile(id: string) { watchFiles.add(id); (this._addedImports || (this._addedImports = new Set())).add(id); if (watcher) ensureWatchedFile(watcher, id, root); } getWatchFiles() { return [...watchFiles]; } // 未实现 emitFile(assetOrFile: EmittedFile) { warnIncompatibleMethod(`emitFile`, this._activePlugin!.name); return ""; } // 未实现 setAssetSource() { warnIncompatibleMethod(`setAssetSource`, this._activePlugin!.name); } // 未实现 getFileName() { warnIncompatibleMethod(`getFileName`, this._activePlugin!.name); return ""; } warn( e: string | RollupError, position?: number | { column: number; line: number } ) { const err = formatError(e, position, this); const msg = buildErrorMessage( err, [colors.yellow(`warning: ${err.message}`)], false ); logger.warn(msg, { clear: true, timestamp: true, }); } error( e: string | RollupError, position?: number | { column: number; line: number } ): never { // error thrown here is caught by the transform middleware and passed on // the the error middleware. throw formatError(e, position, this); } }
TransformContext
TransformContext 在 Context 的基础上加上了对 sourcemap 的处理. 具体可以看下面的 transform 部分.
class TransformContext extends Context { filename: string; originalCode: string; originalSourcemap: SourceMap | null = null; sourcemapChain: NonNullable<SourceDescription["map"]>[] = []; combinedMap: SourceMap | null = null; constructor(filename: string, code: string, inMap?: SourceMap | string) { super(); this.filename = filename; this.originalCode = code; if (inMap) { this.sourcemapChain.push(inMap); } } _getCombinedSourcemap(createIfNull = false) { if ( debugSourcemapCombineFilter && this.filename.includes(debugSourcemapCombineFilter) ) { debugSourcemapCombine("----------", this.filename); debugSourcemapCombine(this.combinedMap); debugSourcemapCombine(this.sourcemapChain); debugSourcemapCombine("----------"); } let combinedMap = this.combinedMap; for (let m of this.sourcemapChain) { if (typeof m === "string") m = JSON.parse(m); if (!("version" in (m as SourceMap))) { // empty, nullified source map combinedMap = this.combinedMap = null; this.sourcemapChain.length = 0; break; } if (!combinedMap) { combinedMap = m as SourceMap; } else { combinedMap = combineSourcemaps(cleanUrl(this.filename), [ { ...(m as RawSourceMap), sourcesContent: combinedMap.sourcesContent, }, combinedMap as RawSourceMap, ]) as SourceMap; } } if (!combinedMap) { return createIfNull ? new MagicString(this.originalCode).generateMap({ includeContent: true, hires: true, source: cleanUrl(this.filename), }) : null; } if (combinedMap !== this.combinedMap) { this.combinedMap = combinedMap; this.sourcemapChain.length = 0; } return this.combinedMap; } getCombinedSourcemap() { return this._getCombinedSourcemap(true) as SourceMap; } }
container
const container: PluginContainer = { options: await(async () => {})(), // 从依赖图中取得模块信息, 上面介绍了 getModuleInfo, async buildStart() {}, async resolveId(rawId, importer = join(root, "index.html"), options) {}, async load(id, ssr) {}, async transform(code, id, inMap, ssr) {}, watchChange(id, event = "update") {}, async close() {}, }; return container;
上面我们讲到 rollup 的钩子, 而 vite 用到了如下几个:
- options
- buildStart
- resolveId
- load
- transform 下面我们依次看他们做了些什么.
options
options 是一个立即执行函数, 也是构建过程中执行的第一个钩子. 它会遍历执行所有插件的 options 方法, 最终返回合并后的 options.
const container: PluginContainer = { options: await(async () => { // 在 vite 配置文件中可以配置 build.rollupOptions // https://rollupjs.org/guide/en/#big-list-of-options let options = rollupOptions; for (const plugin of plugins) { if (!plugin.options) continue; options = (await plugin.options.call(minimalContext, options)) || options; } // 扩展 Acorn 的 Parser, 比如: // rollupOptions: { // acornInjectPlugins: [ importAssertions ], // } if (options.acornInjectPlugins) { parser = acorn.Parser.extend( ...(arraify(options.acornInjectPlugins) as any) ); } return { acorn, acornInjectPlugins: [], ...options, }; })(), };
buildStart
它的作用是就是开始构建之前获取上面 options 钩子的配置项, 用于其他钩子函数使用. 因为用了 Promise.all, 一个凉了就全部失败.
const container: PluginContainer = { async buildStart() { await Promise.all( plugins.map((plugin) => { if (plugin.buildStart) { return plugin.buildStart.call( new Context(plugin) as any, container.options as NormalizedInputOptions ); } }) ); }, };
resolveId
下一章我们会讲到 transformMiddleware 这个中间件, 它用于拦截并处理模块请求, 其中就包含调用 resolveId 钩子函数, 我们在代码中会直接引用 node_modules 中的库, 也会相对引用其他模块的源码, 也会使用 alias 等等. 因此, 这个钩子就是用于解析文件路径, 变成绝对路径.
/** * @param rawId 代码中使用的路径, 比如 import { foo } from '../bar.js', 那 rawId 就是 '../bar.js' * @param importer 导入模块的位置, 默认是 index.html 那一级的路径 * @param options 配置 * @returns Promise<PartialResolvedId | null> */ const container: PluginContainer = { async resolveId(rawId, importer = join(root, "index.html"), options) { const skip = options?.skip; // Set<Plugin> | undefined const ssr = options?.ssr; const scan = !!options?.scan; const ctx = new Context(); ctx.ssr = !!ssr; // 是否为服务端渲染 ctx._scan = scan; // 是否需要扫描 ctx._resolveSkips = skip; // 需要跳过的模块集合 const resolveStart = isDebug ? performance.now() : 0; let id: string | null = null; const partial: Partial<PartialResolvedId> = {}; for (const plugin of plugins) { if (!plugin.resolveId) continue; // 没有 resolveId 钩子就跳过 if (skip?.has(plugin)) continue; // 如果需要跳过, 就跳过 // 此时这个插件就是当前正在执行 resolveId 钩子函数的 ctx._activePlugin = plugin; const pluginResolveStart = isDebug ? performance.now() : 0; // 调用 resolveId 钩子函数 const result = await plugin.resolveId.call(ctx as any, rawId, importer, { ssr, scan, }); // 如果没有返回值继续调用剩余插件的 resolveId 钩子函数 if (!result) continue; if (typeof result === "string") { id = result; } else { id = result.id; Object.assign(partial, result); } isDebug && debugPluginResolve( timeFrom(pluginResolveStart), plugin.name, prettifyUrl(id, root) ); // resolveId() is hookFirst - first non-null result is returned. // 拿到 id 后, 就可以终止 resolveId 钩子函数了 // 也就是说只要有一个插件的 resolveId 方法返回有效值, 就终止循环 break; } if (isDebug && rawId !== id && !rawId.startsWith(FS_PREFIX)) { const key = rawId + id; // avoid spamming if (!seenResolves[key]) { seenResolves[key] = true; debugResolve( `${timeFrom(resolveStart)} ${colors.cyan(rawId)} -> ${colors.dim(id)}` ); } } // 最终返回一个对象, 对象内有一个属性 id, 值是解析后的绝对路径 // external 是指引用的外部 url, 比如像 react, react-dom 走了 CDN if (id) { partial.id = isExternalUrl(id) ? id : normalizePath(id); return partial as PartialResolvedId; } else { return null; } }, };
load
在 resolveId 钩子结束后, 我们就拿到了当前文件的绝对路径, 接下来就是调用 load 钩子函数, 可以获取到文件的内容. 在 rollup 中, 如果你执行了 load 函数, 返回 null, 说明你没有对源码进行改变; 否则返回 `SourceDescription`
类型. 因此在下面的代码中, 我们也看到如果 result 是 Object 类型, 那么需要执行 `updateModuleInfo`
函数.
和 resolveId 类似, 只要有一个插件的 load 方法返回了 `SourceDescription`
类型的 result 就终止遍历.
const container: PluginContainer = { async load(id, options) { const ssr = options?.ssr; const ctx = new Context(); ctx.ssr = !!ssr; for (const plugin of plugins) { if (!plugin.load) continue; ctx._activePlugin = plugin; const result = await plugin.load.call(ctx as any, id, { ssr }); if (result != null) { if (isObject(result)) { updateModuleInfo(id, result); } return result; } } return null; }, };
transform
transform 似乎是我们最常用的钩子, 它用于转换源码, 像 JSON, YAML, 图片等非 JavaScript 文件都需要在这里进行转换. 前段时间写了个插件, 叫做 rollup-plugin-toml, 它可以将 toml 文件转换为 ESM 模块.
export interface SourceDescription extends Partial<PartialNull<ModuleOptions>> { ast?: AcornNode; code: string; map?: SourceMapInput; } const container: PluginContainer = { /** * @param code 文件源码 * @param id 文件路径 * @param options sourcemap 相关 * @returns {SourceDescription | null} */ async transform(code, id, options) { const inMap = options?.inMap; const ssr = options?.ssr; const ctx = new TransformContext(id, code, inMap as SourceMap); ctx.ssr = !!ssr; for (const plugin of plugins) { if (!plugin.transform) continue; ctx._activePlugin = plugin; // 当前正在使用的插件 ctx._activeId = id; // 当前正在处理的模块路径 ctx._activeCode = code; // 当前正在处理的源码字符串 const start = isDebug ? performance.now() : 0; let result: TransformResult | string | undefined; try { // 调用插件的 transform 方法 result = await plugin.transform.call(ctx as any, code, id, { ssr }); } catch (e) { ctx.error(e); } if (!result) continue; isDebug && debugPluginTransform( timeFrom(start), plugin.name, prettifyUrl(id, root) ); if (isObject(result)) { // 说明源码发生了变动 if (result.code !== undefined) { // 复写源码 code = result.code; if (result.map) { if (isDebugSourcemapCombineFocused) { // @ts-expect-error inject plugin name for debug purpose result.map.name = plugin.name; } ctx.sourcemapChain.push(result.map); } } // 由于结果变了, 需要更新模块信息 updateModuleInfo(id, result); } else { // 由于 transform 也可以返回字符串, 那么这个 result 就是 transform 之后的源码. code = result; } } // 返回新的 code 和 map 供下个插件使用 return { code, map: ctx._getCombinedSourcemap(), }; }, };
close
不多说, 我们直接摘抄 rollup 的原话: the last one is always `buildEnd`
. If there is a build error, `closeBundle`
will be called after that. 翻译一下就是在 build 最后会调用 `buildEnd`
钩子, 如果发生了构建错误, 最后还会额外调用 `closeBundle`
钩子.
const container: PluginContainer = { async close() { if (closed) return; const ctx = new Context(); await Promise.all( plugins.map((p) => p.buildEnd && p.buildEnd.call(ctx as any)) ); await Promise.all( plugins.map((p) => p.closeBundle && p.closeBundle.call(ctx as any)) ); closed = true; }, };
@rollup/plugin-alias
最后我们看两个插件, 作为对 `createPluginContainer`
的一个复习. 首先看一下 `@rollup/plugin-alias`
.
我们在平时开发中, 为了防止 `../../../xxx.js`
这种路径引用, 一般都会配置 `alias`
配置, 比如:
module.exports = { plugins: [ alias({ entries: [ { find: 'utils', replacement: '../../../utils' }, { find: 'batman-1.0.0', replacement: './joker-1.5.0' } { find:/^i18n\!(.*)/, replacement: '$1.js' } ] }), alias({ entries: { '@': path.resolve(__dirname, 'src') } }); ] };
这样的好处是, 当我们引用 `../../../xxx.js`
时, 可以写成 `@/xxx.js`
, 代码会非常的干净. 想必大家也能猜出, 这个插件最重要地方的是对 resolveId 这个钩子的处理, 我们来学习下这块.
function alias(options = {}) { // 获取别名列表 const entries = getEntries(options); if (entries.length === 0) { return { name: "alias", resolveId: () => null, }; } return { name: "alias", async buildStart(inputOptions) { await Promise.all( [ ...(Array.isArray(options.entries) ? options.entries : []), options, ].map( ({ customResolver }) => customResolver && typeof customResolver === "object" && typeof customResolver.buildStart === "function" && customResolver.buildStart.call(this, inputOptions) ) ); }, resolveId(importee, importer, resolveOptions) { if (!importer) { return null; } // First match is supposed to be the correct one // 找到第一个匹配的配置 const matchedEntry = entries.find((entry) => matches(entry.find, importee) ); if (!matchedEntry) { return null; } // @containers/Home.tsx -> src/containers/Home.tsx const updatedId = importee.replace( matchedEntry.find, matchedEntry.replacement ); // 如果提供了自定义的 resolve 算法, 那就用自定义的 if (matchedEntry.resolverFunction) { return matchedEntry.resolverFunction.call( this, updatedId, importer, resolveOptions ); } // 否则用默认的 resolve 方法, 这个上面讲 Context 的时候说到了 return this.resolve( updatedId, importer, // 当然上面有一点没说明的就是 skipSelf: true // 这句话的意思是这个 id 已经将要被解析成绝对路径了 // 如果后面有插件还想解析这个 id, 直接用现成了, 跳过就好 Object.assign({ skipSelf: true }, resolveOptions) ).then((resolved) => resolved || { id: updatedId }); }, }; }
vite:esbuild
上面我们讲到了一个使用 resolveId 的实例. 这次我们讲下 vite 内置的 `vite:esbuild`
. 我们知道 vite 快的一个根本原因就是用了 esbuild. esbuild 在 vite 开发环境体现在以下三个阶段:
- 依赖预构建: 这也是 vite 冷启为什么快的原因, 我们在第六章会讲到
- ESM 请求源码: 也就是我们一会儿要讲到了, 在前端请求源码时, 会受到 transformMiddleware 的拦截, 我们知道源码是无法被浏览器直接使用的, 所以这里就会通过 rollup 进行构建, 而在开发环境, vite 在 rollup 中使用了 esbuild 插件
- HMR: 和上一条类似, 都是使用 esbuild 加速构建过程
`vite:esbuild`
中最核心的就是 transform 钩子, 它调用了 esbuild 的 transform 函数, 该函数用于转换单个文件, 可以用来压缩 JavaScript, 把 ts/tsx 转换成 JavaScript, 或者把高版本 ECMAScript 转成低版本. 它返回一个 Promise, 当 Promise 被 resolve 时, 返回一个`TransformResult`
类型的对象; 如果被 reject, 就返回一个`TransformFailure`
类型的对象.
This function transforms a single JavaScript file. It can be used to minify JavaScript, convert TypeScript/JSX to JavaScript, or convert newer JavaScript to older JavaScript. It returns a promise that is either resolved with a "TransformResult" object or rejected with a "TransformFailure" object. 我们简单过一下代码, 了解一下一般我们会在 transform 钩子做什么. 至于 esbuild 的 transform 函数怎么运作的, 超纲了...
export async function transformWithEsbuild( code: string, filename: string, options?: TransformOptions, inMap?: object ): Promise<ESBuildTransformResult> { let loader = options?.loader; // loader 是 esbuild Transform API 中的一个参数, 它一般就是文件的扩展名, 比如 .js, .css, .tsx 等等 // 它用于告知 esbuild 用什么 loader 来处理指定格式的文件 if (!loader) { // if the id ends with a valid ext, use it (e.g. vue blocks) // otherwise, cleanup the query before checking the ext const ext = path .extname(/\.\w+$/.test(filename) ? filename : cleanUrl(filename)) .slice(1); if (ext === "cjs" || ext === "mjs") { loader = "js"; } else { loader = ext as Loader; } } // tsconfigRaw 也是 esbuild Transform API 中的一个参数, 它以字符串的形式传入 tsconfig.json 的配置 // https://esbuild.github.io/api/#tsconfig-raw // 如果你传入了这个参数, 那么 esbuild 就不会去读取 tsconfig.json 文件了 let tsconfigRaw = options?.tsconfigRaw; // if options provide tsconfigraw in string, it takes highest precedence if (typeof tsconfigRaw !== "string") { // these fields would affect the compilation result // https://esbuild.github.io/content-types/#tsconfig-json const meaningfulFields: Array<keyof TSCompilerOptions> = [ "target", "jsxFactory", "jsxFragmentFactory", "useDefineForClassFields", "importsNotUsedAsValues", "preserveValueImports", ]; // 如果是 ts 或者 tsx, 就找 tsconfig.json 的配置 // 如果有 meaningfulFields 的这六个, 就把它缓存到 compilerOptionsForFile 对象中 const compilerOptionsForFile: TSCompilerOptions = {}; if (loader === "ts" || loader === "tsx") { const loadedTsconfig = await loadTsconfigJsonForFile(filename); const loadedCompilerOptions = loadedTsconfig.compilerOptions ?? {}; for (const field of meaningfulFields) { if (field in loadedCompilerOptions) { // @ts-ignore TypeScript can't tell they are of the same type compilerOptionsForFile[field] = loadedCompilerOptions[field]; } } } // 最终合并成一个新的 tsconfigRaw tsconfigRaw = { ...tsconfigRaw, compilerOptions: { ...compilerOptionsForFile, ...tsconfigRaw?.compilerOptions, }, }; } const resolvedOptions = { sourcemap: true, // ensure source file name contains full query sourcefile: filename, ...options, loader, tsconfigRaw, } as ESBuildOptions; delete resolvedOptions.include; delete resolvedOptions.exclude; delete resolvedOptions.jsxInject; try { // 这里就是 esbuild 真正转换文件的核心了, 有兴趣可以去看 esbuild 的源码 // 对于绝大多数 transfrom 钩子的实现, 我们大多都是读取文件内容, 然后通过三方库来转换, 然后再写入文件 // 比如我写的 [rollup-plugin-toml](https://github.com/YanceyOfficial/rollup-plugin-toml) // 实际用到的转换器是 @iarna/toml 这个库 const result = await transform(code, resolvedOptions); let map: SourceMap; if (inMap && resolvedOptions.sourcemap) { const nextMap = JSON.parse(result.map); nextMap.sourcesContent = []; map = combineSourcemaps(filename, [ nextMap as RawSourceMap, inMap as RawSourceMap, ]) as SourceMap; } else { map = resolvedOptions.sourcemap ? JSON.parse(result.map) : { mappings: "" }; } if (Array.isArray(map.sources)) { map.sources = map.sources.map((it) => toUpperCaseDriveLetter(it)); } return { ...result, map, }; } catch (e: any) { debug(`esbuild error with options used: `, resolvedOptions); // patch error information if (e.errors) { e.frame = ""; e.errors.forEach((m: Message) => { e.frame += `\n` + prettifyMessage(m, code); }); e.loc = e.errors[0].location; } throw e; } }
总结
这一章我们了解了 vite 的插件机制, vite 的插件本质需要在 rollup 体系下执行, 因此 vite 封装了 createPluginContainer 函数, 通过它, vite 插件才得以在 rollup 的各个 hooks 中被调用. 此外, 我们还通过两个例子, 来了解插件是如何实现钩子函数的. 通过 vite:esbuild 这个例子, 我们知道了 vite 快的原因之一, 就是源码是按需编译的, 且编译使用了 esbuild. 下一章我们将重点学习 vite 的开发 server 是如何搭建的, 以及中间件机制是怎样运行的. 中间件在拦截请求后会调用这一章学到的插件机制, 来进行源码的转化. 敬请期待~

PREVIOUS POST
Vite 源码解析(2) - 模块依赖图

NEXT POST
Vite 源码解析(4) - 开发服务篇