renderChunks
await renderChunks(chunks, outputBundle, this.pluginDriver, this.outputOptions, this.inputOptions.onwarn); 函数内部主要逻辑可以分为以下六步:
- 设置入口 chunk 的 preliminaryFileName
- 执行chunk.render() 生成 magicStringBundle
- 生成 chunkGraph
- 执行 transformChunksAndGenerateContentHashes (它内部执行了 'renderChunk' 钩子函数转换 chunk 并生成 code 和 sourcemap)
- 执行 generateFinalHashes 用于生成 bundle.fileName 的 hash 值
- 执行 addChunksToBundle ,实际上是执行 bundle[finalFileName] = chunk.generateOutputChunk() 来更新 bundle
renderChunks 主要作用是生成最终的 outputBundle。例如:
{index.js: {…}, acorn-bf6b1c54.js: {…}}
最终 bundle.generate 函数内部将 outputBundleBase 返回。
它的代码定义在: src/utils/renderChunks.ts
export async function renderChunks(
chunks: Chunk[],
bundle: OutputBundleWithPlaceholders,
pluginDriver: PluginDriver,
outputOptions: NormalizedOutputOptions,
onwarn: WarningHandler
) {
//设置入口 chunk 的 preliminaryFileName
reserveEntryChunksInBundle(chunks);
//chunk.render()
const renderedChunks = await Promise.all(chunks.map(chunk => chunk.render()));
// 生成chunkGraph:{acorn-!~{001}~.js: {…}, index.js: {…}}
const chunkGraph = getChunkGraph(chunks);
/**
* transformChunksAndGenerateContentHashes 方法
* 它内部执行了 'renderChunk' 钩子函数转换 chunk 并生成 code 和 sourcemap 。
*/
const {
nonHashedChunksWithPlaceholders,
renderedChunksByPlaceholder,
hashDependenciesByPlaceholder
} = await transformChunksAndGenerateContentHashes(
renderedChunks,
chunkGraph,
outputOptions,
pluginDriver,
onwarn
);
//generateFinalHashes 用于生成bundle的hash值
//hashesByPlaceholder Map: { !~{001}~ => bf6b1c54 }
const hashesByPlaceholder = generateFinalHashes(
renderedChunksByPlaceholder,
hashDependenciesByPlaceholder,
bundle
);
// 执行 bundle[finalFileName] = chunk.generateOutputChunk() 更新 bundle 。
addChunksToBundle(
renderedChunksByPlaceholder,
hashesByPlaceholder,
bundle,
nonHashedChunksWithPlaceholders,
pluginDriver,
outputOptions
);
}
设置入口 chunk 的 preliminaryFileName
reserveEntryChunksInBundle(chunks); 的目的是为了设置入口 chunk 的 preliminaryFileName。
function reserveEntryChunksInBundle(chunks: Chunk[]) {
for (const chunk of chunks) {
if (chunk.facadeModule && chunk.facadeModule.isUserDefinedEntryPoint) {
// reserves name in bundle as side effect if it does not contain a hash
chunk.getPreliminaryFileName();
}
}
}
getPreliminaryFileName(): PreliminaryFileName {
if (this.preliminaryFileName) {
return this.preliminaryFileName;
}
let fileName: string;
let hashPlaceholder: string | null = null;
const { chunkFileNames, entryFileNames, file, format, preserveModules } = this.outputOptions;
if (file) {
fileName = basename(file);
} else if (this.fileName !== null) {
fileName = this.fileName;
} else {
const [pattern, patternName] =
preserveModules || this.facadeModule?.isUserDefinedEntryPoint
? [entryFileNames, 'output.entryFileNames']
: [chunkFileNames, 'output.chunkFileNames'];
fileName = renderNamePattern(
typeof pattern === 'function' ? pattern(this.getPreRenderedChunkInfo()) : pattern,
patternName,
{
format: () => format,
hash: size =>
hashPlaceholder || (hashPlaceholder = this.getPlaceholder(patternName, size)),
name: () => this.getChunkName()
}
);
if (!hashPlaceholder) {
fileName = makeUnique(fileName, this.bundle);
}
}
if (!hashPlaceholder) {
this.bundle[fileName] = FILE_PLACEHOLDER;
}
// Caching is essential to not conflict with the file name reservation above
return (this.preliminaryFileName = { fileName, hashPlaceholder });
}
chunk.render()
const renderedChunks = await Promise.all(chunks.map(chunk => chunk.render()));
chunk.render 方法会返回一个 ChunkRenderResult 对象。
export interface ChunkRenderResult {
chunk: Chunk;
magicString: MagicStringBundle;
preliminaryFileName: PreliminaryFileName;
usedModules: Module[];
}
class Chunk{
//...
async render(): Promise<ChunkRenderResult> {
const {
dependencies,
exportMode,
facadeModule,
inputOptions: { onwarn },
outputOptions,
pluginDriver,
snippets
} = this;
const { format, hoistTransitiveImports, preserveModules } = outputOptions;
// for static and dynamic entry points, add transitive dependencies to this
// chunk's dependencies to avoid loading latency
// 设置入口chunk 的 dependencies (设置this.dependencies)
if (hoistTransitiveImports && !preserveModules && facadeModule !== null) {
for (const dep of dependencies) {
if (dep instanceof Chunk) this.inlineChunkDependencies(dep);
}
}
// preliminaryFileName => { fileName: 'index.js', hashPlaceholder: null }
const preliminaryFileName = this.getPreliminaryFileName();
const { accessedGlobals, indent, magicString, renderedSource, usedModules, usesTopLevelAwait } =
this.renderModules(preliminaryFileName.fileName);
//当前 render 的 dependencies,即 imported 信息
const renderedDependencies = [...this.getRenderedDependencies().values()];
//当前 render 的 exports,即 exported 信息
const renderedExports = exportMode === 'none' ? [] : this.getChunkExportDeclarations(format);
let hasExports = renderedExports.length > 0;
let hasDefaultExport = false;
//判断是否有默认导出
for (const { reexports } of renderedDependencies) {
if (reexports?.length) {
hasExports = true;
if (reexports.some(reexport => reexport.reexported === 'default')) {
hasDefaultExport = true;
break;
}
}
}
if (!hasDefaultExport) {
for (const { exported } of renderedExports) {
if (exported === 'default') {
hasDefaultExport = true;
break;
}
}
}
const { intro, outro, banner, footer } = await createAddons(
outputOptions,
pluginDriver,
this.getRenderedChunkInfo()
);
/**
* finalisers[format] 其实执行的是最终的打包 format 的function。
* 它的目的是为了对 renderedSource 做进一步的处理。
* 例如当 format === 'es' 的时候且 bundle 引入了外部的依赖,则需要执行 magicString.prepend()方法在 bundle 头部加上 import 语句。
* 如果有导出变量则需要执行 magicString.append() 方法将导出语句加在 bundle 的尾部。
*/
finalisers[format](
renderedSource,
{
accessedGlobals,
dependencies: renderedDependencies,
exports: renderedExports,
hasDefaultExport,
hasExports,
id: preliminaryFileName.fileName,
indent,
intro,
isEntryFacade: preserveModules || (facadeModule !== null && facadeModule.info.isEntry),
isModuleFacade: facadeModule !== null,
namedExportsMode: exportMode !== 'default',
onwarn,
outro,
snippets,
usesTopLevelAwait
},
outputOptions
);
if (banner) magicString.prepend(banner);
if (footer) magicString.append(footer);
return {
chunk: this,
magicString,
preliminaryFileName,
usedModules
};
}
/**
* inlineChunkDependencies 主要是将所有的外部依赖都收集起来
* 举例子,最终生成 bundle 的时候,如果 bundle 引入了第三方的js库(acorn),
* 则需要在文件的顶部插入 import { a as acorn } from './acorn-bf6b1c54.js';
*/
private inlineChunkDependencies(chunk: Chunk): void {
for (const dep of chunk.dependencies) {
if (this.dependencies.has(dep)) continue;
this.dependencies.add(dep);
if (dep instanceof Chunk) {
this.inlineChunkDependencies(dep);
}
}
}
}
this.renderModules(preliminaryFileName.fileName) 是 "generate" 阶段的关键方法,this.renderModules 会调用 module.render() 生成 source,然后执行 magicString.addSource(source) 将多个 source 拼接成 bundle。
执行 this.renderModules 内部会得到如下一个对象。因为篇幅有限,下一节内容我们会对这个函数继续深入分析内部逻辑。
{
accessedGlobals, // 访问过的全局变量
indent, // 缩进,默认’\t‘
magicString, // MagicStringBundle
renderedSource, // magicString.trim()
usedModules, // 被包含的模块(不含手动模块(manualChunks))
usesTopLevelAwait // 是否允许顶层的 “await”, 默认 false
}
chunk.render() 内部主要逻辑:
- 设置入口chunk 的 dependencies (设置this.dependencies)
- 执行 this.renderModules 生成 magicStringBundle
- 执行 finalisers[format] 对 magicStringBundle 进行加工
chunkGraph
const chunkGraph = getChunkGraph(chunks) 会生成 chunk 图。例如:
{acorn-!~{001}~.js: {…}, index.js: {…}}
getChunkGraph 方法定义:
function getChunkGraph(chunks: Chunk[]) {
/**
Object.fromEntries() 执行与 Object.entries 互逆的操作。例如:
const map = new Map([ ['foo', 'bar'], ['baz', 42] ]);
const obj = Object.fromEntries(map);
console.log(obj); // { foo: "bar", baz: 42 }
*/
return Object.fromEntries(
chunks.map(chunk => {
const renderedChunkInfo = chunk.getRenderedChunkInfo();
return [renderedChunkInfo.fileName, renderedChunkInfo];
})
);
}
transformChunksAndGenerateContentHashes
const {
nonHashedChunksWithPlaceholders,
renderedChunksByPlaceholder,
hashDependenciesByPlaceholder
} = await transformChunksAndGenerateContentHashes(
renderedChunks,
chunkGraph,
outputOptions,
pluginDriver,
onwarn
);
transformChunksAndGenerateContentHashes 方法内部调用了 transformChunk 方法。transformChunk 执行了 'renderChunk' 钩子函数转换 chunk 并生成 code 和 sourcemap。
async function transformChunksAndGenerateContentHashes(
renderedChunks: ChunkRenderResult[],
chunkGraph: Record<string, RenderedChunk>,
outputOptions: NormalizedOutputOptions,
pluginDriver: PluginDriver,
onwarn: WarningHandler
) {
const nonHashedChunksWithPlaceholders: RenderedChunkWithPlaceholders[] = [];
const renderedChunksByPlaceholder = new Map<string, RenderedChunkWithPlaceholders>();
const hashDependenciesByPlaceholder = new Map<string, HashResult>();
const placeholders = new Set<string>();
for (const {
preliminaryFileName: { hashPlaceholder }
} of renderedChunks) {
if (hashPlaceholder) placeholders.add(hashPlaceholder);
}
await Promise.all(
renderedChunks.map(
async ({
chunk,
preliminaryFileName: { fileName, hashPlaceholder },
magicString,
usedModules
}) => {
const transformedChunk = {
chunk,
fileName,
...(await transformChunk(
magicString,
fileName,
usedModules,
chunkGraph,
outputOptions,
pluginDriver,
onwarn
))
};
const { code } = transformedChunk;
if (hashPlaceholder) {
const hash = createHash();
// To create a reproducible content-only hash, all placeholders are
// replaced with the same value before hashing
const { containedPlaceholders, transformedCode } =
replacePlaceholdersWithDefaultAndGetContainedPlaceholders(code, placeholders);
hash.update(transformedCode);
const hashAugmentation = pluginDriver.hookReduceValueSync(
'augmentChunkHash',
'',
[chunk.getRenderedChunkInfo()],
(augmentation, pluginHash) => {
if (pluginHash) {
augmentation += pluginHash;
}
return augmentation;
}
);
if (hashAugmentation) {
hash.update(hashAugmentation);
}
renderedChunksByPlaceholder.set(hashPlaceholder, transformedChunk);
hashDependenciesByPlaceholder.set(hashPlaceholder, {
containedPlaceholders,
contentHash: hash.digest('hex')
});
} else {
nonHashedChunksWithPlaceholders.push(transformedChunk);
}
}
)
);
return {
hashDependenciesByPlaceholder,
nonHashedChunksWithPlaceholders,
renderedChunksByPlaceholder
};
}
transformChunk 方法定义:
async function transformChunk(
magicString: MagicStringBundle,
fileName: string,
usedModules: Module[],
chunkGraph: Record<string, RenderedChunk>,
options: NormalizedOutputOptions,
outputPluginDriver: PluginDriver,
onwarn: WarningHandler
) {
let map: SourceMap | null = null;
const sourcemapChain: DecodedSourceMapOrMissing[] = [];
// 执行 'renderChunk' 钩子函数,对源码进行加工处理并返回
let code = await outputPluginDriver.hookReduceArg0(
'renderChunk',
[magicString.toString(), chunkGraph[fileName], options, { chunks: chunkGraph }],
(code, result, plugin) => {
if (result == null) return code;
if (typeof result === 'string')
result = {
code: result,
map: undefined
};
// strict null check allows 'null' maps to not be pushed to the chain, while 'undefined' gets the missing map warning
if (result.map !== null) {
const map = decodedSourcemap(result.map);
sourcemapChain.push(map || { missing: true, plugin: plugin.name });
}
return result.code;
}
);
const {
compact,
dir,
file,
sourcemap,
sourcemapExcludeSources,
sourcemapFile,
sourcemapPathTransform
} = options;
if (!compact && code[code.length - 1] !== '\n') code += '\n';
//是否生成sourcemap
if (sourcemap) {
timeStart('sourcemaps', 3);
let resultingFile: string;
if (file) resultingFile = resolve(sourcemapFile || file);
else if (dir) resultingFile = resolve(dir, fileName);
else resultingFile = resolve(fileName);
const decodedMap = magicString.generateDecodedMap({});
map = collapseSourcemaps(
resultingFile,
decodedMap,
usedModules,
sourcemapChain,
sourcemapExcludeSources,
onwarn
);
map.sources = map.sources
.map(sourcePath => {
if (sourcemapPathTransform) {
const newSourcePath = sourcemapPathTransform(sourcePath, `${resultingFile}.map`);
if (typeof newSourcePath !== 'string') {
error(errorFailedValidation(`sourcemapPathTransform function must return a string.`));
}
return newSourcePath;
}
return sourcePath;
})
.map(normalize);
timeEnd('sourcemaps', 3);
}
return {
code,
map
};
}
generateFinalHashes
generateFinalHashes 用于生成 bundle.fileName 的 hash 值。例如: { !{001} => bf6b1c54 }
generateFinalHashes 定义:
function generateFinalHashes(
renderedChunksByPlaceholder: Map<string, RenderedChunkWithPlaceholders>,
hashDependenciesByPlaceholder: Map<string, HashResult>,
bundle: OutputBundleWithPlaceholders
) {
const hashesByPlaceholder = new Map<string, string>();
for (const [placeholder, { fileName }] of renderedChunksByPlaceholder) {
let hash = createHash();
const hashDependencyPlaceholders = new Set<string>([placeholder]);
for (const dependencyPlaceholder of hashDependencyPlaceholders) {
const { containedPlaceholders, contentHash } =
hashDependenciesByPlaceholder.get(dependencyPlaceholder)!;
hash.update(contentHash);
for (const containedPlaceholder of containedPlaceholders) {
// When looping over a map, setting an entry only causes a new iteration if the key is new
hashDependencyPlaceholders.add(containedPlaceholder);
}
}
let finalFileName: string | undefined;
let finalHash: string | undefined;
do {
// In case of a hash collision, create a hash of the hash
if (finalHash) {
hash = createHash();
hash.update(finalHash);
}
finalHash = hash.digest('hex').slice(0, placeholder.length);
finalFileName = replaceSinglePlaceholder(fileName, placeholder, finalHash);
} while (bundle[lowercaseBundleKeys].has(finalFileName.toLowerCase()));
bundle[finalFileName] = FILE_PLACEHOLDER;
hashesByPlaceholder.set(placeholder, finalHash);
}
return hashesByPlaceholder;
}
addChunksToBundle
addChunksToBundle 内部实际上执行了 bundle[finalFileName] = chunk.generateOutputChunk() 来更新 bundle 。
function addChunksToBundle(
renderedChunksByPlaceholder: Map<string, RenderedChunkWithPlaceholders>,
hashesByPlaceholder: Map<string, string>,
bundle: OutputBundleWithPlaceholders,
nonHashedChunksWithPlaceholders: RenderedChunkWithPlaceholders[],
pluginDriver: PluginDriver,
options: NormalizedOutputOptions
) {
for (const { chunk, code, fileName, map } of renderedChunksByPlaceholder.values()) {
let updatedCode = replacePlaceholders(code, hashesByPlaceholder);
const finalFileName = replacePlaceholders(fileName, hashesByPlaceholder);
if (map) {
map.file = replacePlaceholders(map.file, hashesByPlaceholder);
updatedCode += emitSourceMapAndGetComment(finalFileName, map, pluginDriver, options);
}
bundle[finalFileName] = chunk.generateOutputChunk(updatedCode, map, hashesByPlaceholder);
}
for (const { chunk, code, fileName, map } of nonHashedChunksWithPlaceholders) {
let updatedCode =
hashesByPlaceholder.size > 0 ? replacePlaceholders(code, hashesByPlaceholder) : code;
if (map) {
updatedCode += emitSourceMapAndGetComment(fileName, map, pluginDriver, options);
}
bundle[fileName] = chunk.generateOutputChunk(updatedCode, map, hashesByPlaceholder);
}
}
rollup 揭秘相关文章
- rollup 技术揭秘系列一 准备篇(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列二 源码目录结构及打包入口分析(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列三 rollup 函数(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列四 graph.build(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列五 构建依赖图谱(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列六 模块排序(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列七 includeStatements(可能是全网最系统性的 rollup 源码分析文章
- rollup 技术揭秘系列八 node.hasEffects(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列九 module.include()(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十 includeStatements 总结(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十一 rollup 打包配置选项整理(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十二 handleGenerateWrite(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十三 bundle.generate(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十四 renderChunks(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十五 renderModules(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十六 Rollup 插件开发指南(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十七 rollup-cli 的开发(可能是全网最系统性的 rollup 源码分析文章)
- rollup 技术揭秘系列十八 Rollup 打包流程示意图(可能是全网最系统性的 rollup 源码分析文章)