
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 += `
` + 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 是如何搭建的, 以及中间件机制是怎样运行的. 中间件在拦截请求后会调用这一章学到的插件机制, 来进行源码的转化. 敬请期待~