rollup技术揭秘系列十四 renderChunks(可能是全网最系统性的rollup源码分析文章)

517 阅读7分钟

renderChunks

await renderChunks(chunks, outputBundle, this.pluginDriver, this.outputOptions, this.inputOptions.onwarn); 函数内部主要逻辑可以分为以下六步:

  1. 设置入口 chunk 的 preliminaryFileName
  2. 执行chunk.render() 生成 magicStringBundle
  3. 生成 chunkGraph
  4. 执行 transformChunksAndGenerateContentHashes (它内部执行了 'renderChunk' 钩子函数转换 chunk 并生成 code 和 sourcemap)
  5. 执行 generateFinalHashes 用于生成 bundle.fileName 的 hash 值
  6. 执行 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() 内部主要逻辑:

  1. 设置入口chunk 的 dependencies (设置this.dependencies)
  2. 执行 this.renderModules 生成 magicStringBundle
  3. 执行 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 揭秘相关文章