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

738 阅读9分钟

bundle.generate

Bundle 类定义在 src/Bundle.ts:

class Bundle {
  private readonly facadeChunkByModule = new Map<Module, Chunk>();
  private readonly includedNamespaces = new Set<Module>();

  constructor(
    private readonly outputOptions: NormalizedOutputOptions,
    private readonly unsetOptions: ReadonlySet<string>,
    private readonly inputOptions: NormalizedInputOptions,
    private readonly pluginDriver: PluginDriver,
    private readonly graph: Graph
  ) {}

  async generate(isWrite: boolean): Promise<OutputBundle> {
    const outputBundleBase: OutputBundle = Object.create(null);
    // outputBundle 是一个 proxy
    const outputBundle = getOutputBundle(outputBundleBase);
    //...
    try {
      //...
      const getHashPlaceholder = getHashPlaceholderGenerator();
      // chunks => [chunk]
      const chunks = await this.generateChunks(outputBundle, getHashPlaceholder);
      if (chunks.length > 1) {
        //校验 outputOptions 选项是否合法
        validateOptionsForMultiChunkOutput(this.outputOptions, this.inputOptions.onwarn);
      }
      //...
      for (const chunk of chunks) {
        chunk.generateExports();
      }
      /**
       * renderChunks 主要作用是生成最终的 outputBundle。
       * 例如:{index.js: {…}, acorn-bf6b1c54.js: {…}}
       *
       */
      await renderChunks(
        chunks,
        outputBundle,
        this.pluginDriver,
        this.outputOptions,
        this.inputOptions.onwarn
      );
    } catch (error_: any) {
      await this.pluginDriver.hookParallel('renderError', [error_]);
      throw error_;
    }
    //...
    return outputBundleBase;
  }

  private async addManualChunks(
    manualChunks: Record<string, readonly string[]>
  ): Promise<Map<Module, string>> {
    const manualChunkAliasByEntry = new Map<Module, string>();
    /**
		 * alias 就是我们定义manualChunks 的 key,例如 acorn
		 * await this.graph.moduleLoader.addAdditionalModules(files) 会得到一个 module[]
		 * chunkEntries
		    [{…}]
				0: {alias: 'acorn', entries: Array(1)}
				length: 1
				[[Prototype]]: Array(0)
				[[Prototype]]: Object
		 */
    const chunkEntries = await Promise.all(
      Object.entries(manualChunks).map(async ([alias, files]) => ({
        alias,
        entries: await this.graph.moduleLoader.addAdditionalModules(files)
      }))
    );
    for (const { alias, entries } of chunkEntries) {
      for (const entry of entries) {
        //内部调用 manualChunkAliasByEntry.set(module, alias)
        addModuleToManualChunk(alias, entry, manualChunkAliasByEntry);
      }
    }
    return manualChunkAliasByEntry;
  }

  private assignManualChunks(getManualChunk: GetManualChunk): Map<Module, string> {
    // eslint-disable-next-line unicorn/prefer-module
    const manualChunkAliasesWithEntry: [alias: string, module: Module][] = [];
    const manualChunksApi = {
      getModuleIds: () => this.graph.modulesById.keys(),
      getModuleInfo: this.graph.getModuleInfo
    };
    for (const module of this.graph.modulesById.values()) {
      if (module instanceof Module) {
        const manualChunkAlias = getManualChunk(module.id, manualChunksApi);
        if (typeof manualChunkAlias === 'string') {
          manualChunkAliasesWithEntry.push([manualChunkAlias, module]);
        }
      }
    }
    manualChunkAliasesWithEntry.sort(([aliasA], [aliasB]) =>
      aliasA > aliasB ? 1 : aliasA < aliasB ? -1 : 0
    );
    const manualChunkAliasByEntry = new Map<Module, string>();
    for (const [alias, module] of manualChunkAliasesWithEntry) {
      addModuleToManualChunk(alias, module, manualChunkAliasByEntry);
    }
    return manualChunkAliasByEntry;
  }

  private finaliseAssets(bundle: OutputBundleWithPlaceholders): void {
    if (this.outputOptions.validate) {
      for (const file of Object.values(bundle)) {
        if ('code' in file) {
          try {
            this.graph.contextParse(file.code, {
              allowHashBang: true,
              ecmaVersion: 'latest'
            });
          } catch (error_: any) {
            this.inputOptions.onwarn(errorChunkInvalid(file, error_));
          }
        }
      }
    }
    this.pluginDriver.finaliseAssets();
  }

  private async generateChunks(
    bundle: OutputBundleWithPlaceholders,
    getHashPlaceholder: HashPlaceholderGenerator
  ): Promise<Chunk[]> {
    /**
     * inlineDynamicImports: 该选项用于内联动态引入,而不是用于创建包含新 Chunk 的独立 bundle。它只在单一输入源时产生作用。
     * manualChunks: 该选项允许你创建自定义的公共模块。
     * preserveModules: 该选项将使用原始模块名作为文件名,为所有模块创建单独的 chunk,而不是创建尽可能少的 chunk。
     *
     */
    const { inlineDynamicImports, manualChunks, preserveModules } = this.outputOptions;
    // outputOption.manualChunks 既可以是对象,也可以是函数
    /**
     * this.addManualChunks(manualChunks) 或者 this.assignManualChunks(manualChunks) 都会返回一个 Map<Module, string> 结构的对象。
     * manualChunkAliasByEntry => [Module, 'acorn']
     */
    const manualChunkAliasByEntry =
      typeof manualChunks === 'object'
        ? await this.addManualChunks(manualChunks)
        : this.assignManualChunks(manualChunks);
    const snippets = getGenerateCodeSnippets(this.outputOptions);
    const includedModules = getIncludedModules(this.graph.modulesById);
    /**
     * getAbsoluteEntryModulePaths(includedModules, false) 返回入口模块并且为绝对路径的一个字符串((module.info.isEntry || preserveModules) && isAbsolute(module.id))
     * commondir 会返回路径中代表文件夹的部分
     * inputBase 就是根路径的意思
     */
    const inputBase = commondir(getAbsoluteEntryModulePaths(includedModules, preserveModules));
    const externalChunkByModule = getExternalChunkByModule(
      this.graph.modulesById,
      this.outputOptions,
      inputBase
    );
    const chunks: Chunk[] = [];
    const chunkByModule = new Map<Module, Chunk>();
    /**
     * 如果没有在配置文件中手动设置 preserveModules 和 inlineDynamicImports 则他们都默认为 false 。
     * 因此执行的是 getChunkAssignments(this.graph.entryModules, manualChunkAliasByEntry)
     */
    for (const { alias, modules } of inlineDynamicImports
      ? [{ alias: null, modules: includedModules }]
      : preserveModules
      ? includedModules.map(module => ({ alias: null, modules: [module] }))
      : getChunkAssignments(this.graph.entryModules, manualChunkAliasByEntry)) {
      sortByExecutionOrder(modules);
      const chunk = new Chunk(
        modules,
        this.inputOptions,
        this.outputOptions,
        this.unsetOptions,
        this.pluginDriver,
        this.graph.modulesById,
        chunkByModule,
        externalChunkByModule,
        this.facadeChunkByModule,
        this.includedNamespaces,
        alias,
        getHashPlaceholder,
        bundle,
        inputBase,
        snippets
      );
      chunks.push(chunk);
    }
    for (const chunk of chunks) {
      chunk.link();
    }
    const facades: Chunk[] = [];
    for (const chunk of chunks) {
      facades.push(...chunk.generateFacades());
    }
    return [...chunks, ...facades];
  }
}

bundle.generate 方法内部主要是执行 "code generate" 阶段的逻辑。

它首先定义了一个 outputBundleBase 对象用于记录打包输出的 fileName 和 chunk。如下是 OutputBundle 的定义:

interface OutputBundle {
  [fileName: string]: OutputAsset | OutputChunk;
}

接着执行 const outputBundle = getOutputBundle(outputBundleBase); 这个 outputBundle 就是一个 proxy 实例。const chunks = await this.generateChunks(outputBundle, getHashPlaceholder); 就是调用 bundle.generateChunks 方法。

bundle.generateChunks

bundle.generateChunks 方法内部首先通过 const { inlineDynamicImports, manualChunks, preserveModules } = this.outputOptions; 获取传入的 output 配置。因为 output.manualChunks 配置既可以是对象也可以是函数。

例如我们在配置文件中像这样设置了 manualChunks 选项:

//rollup.config.js

import nodeResolve from '@rollup/plugin-node-resolve';

export default {
  input: 'example/index.js',
  output: {
    dir: 'example/dest',
    format: 'es',
    manualChunks: {
      acorn: ['acorn']
    }
  },
  plugins: [nodeResolve()]
};

因此在获取 manualChunkAliasByEntry 的时候会首先判断 typeof manualChunks === 'object' ,如果是对象结构的话则调用 this.addManualChunks(manualChunks),否则调用 this.assignManualChunks(manualChunks)方法。 无论如何,最终都会返回 manualChunkAliasByEntry 的一个 map 对象。这个 manualChunkAliasByEntry 的对象结构的示意如下:

manualChunkAliasByEntry: {
  Module, 'acorn';
}

const snippets = getGenerateCodeSnippets(this.outputOptions); 这个 snippets 中定义了一些生成打包代码时所用到的公共方法.

export interface GenerateCodeSnippets {
  _: string;
  cnst: string;
  n: string;
  s: string;
  getDirectReturnFunction(
    parameters: string[],
    options: {
      functionReturn: boolean;
      lineBreakIndent: { base: string; t: string } | null;
      name: string | null;
    }
  ): [left: string, right: string];
  getDirectReturnIifeLeft(
    parameters: string[],
    returned: string,
    options: {
      needsArrowReturnParens: boolean | undefined;
      needsWrappedFunction: boolean | undefined;
    }
  ): string;
  getFunctionIntro(
    parameters: string[],
    options: { isAsync: boolean; name: string | null }
  ): string;
  getNonArrowFunctionIntro(
    parameters: string[],
    options: { isAsync: boolean; name: string | null }
  ): string;
  getObject(
    fields: [key: string | null, value: string][],
    options: { lineBreakIndent: { base: string; t: string } | null }
  ): string;
  getPropertyAccess(name: string): string;
}

举个例子: getNonArrowFunctionIntro 方法可以获取一个非箭头函数的模版(不包含函数体部分)。

snippets.getNonArrowFunctionIntro([1, 2], { isAsync: false, name: 'test' });
// 'function test (1, 2) '

const includedModules = getIncludedModules(this.graph.modulesById) 就是应该被包含到打包输出的 modules。

const inputBase = commondir(getAbsoluteEntryModulePaths(includedModules, preserveModules)); inputBase 表示输出文件的根路径。getAbsoluteEntryModulePaths(includedModules, preserveModules) 会返回一个包含路径字符串的数组。例如:

getAbsoluteEntryModulePaths(includedModules, preserveModules); // ['c:\Users\**\Desktop\study\rollup-master\rollup\example\index.js']

commondir 会返回路径中代表文件夹的那部分。因此 inputBase 大概就是 'c:\Users**\Desktop\study\rollup-master\rollup\example' 这样。

const externalChunkByModule = getExternalChunkByModule( this.graph.modulesById, this.outputOptions, inputBase ) 的目的是为了获取我们配置文件中设置了 external 的模块。

例如下面例子中设置了 'acorn' 为 externalModule :

export default {
  input: 'example/index.js',
  external: ['acorn'],
  output: {
    file: 'dest/bundle.js',
    format: 'es'
  },
  plugins: []
};

接着分别定义了 chunks(代表需要打包的区块)、chunkByModule(module => chunk 组成的 map 对象)。

如果我们没有手动设置配置文件中的 preserveModules 和 inlineDynamicImports 则他们默认都是 false。因此 for 循环的是 getChunkAssignments(this.graph.entryModules, manualChunkAliasByEntry) 执行后的返回值。因此我们看下 getChunkAssignments 函数的定义:

src/utils/chunkAssignment.ts

export function getChunkAssignments(
  entryModules: readonly Module[],
  manualChunkAliasByEntry: ReadonlyMap<Module, string>
): ChunkDefinitions {
  const chunkDefinitions: ChunkDefinitions = [];
  const modulesInManualChunks = new Set(manualChunkAliasByEntry.keys());
  const manualChunkModulesByAlias: Record<string, Module[]> = Object.create(null);
  /**
   *下面的for循环逻辑中主要作用是将 manualChunkAliasByEntry 的结构({ Module {graph, …} => acorn})
   * 进行 key-value 的反转然后添加到 manualChunkModulesByAlias 对象中
   */
  for (const [entry, alias] of manualChunkAliasByEntry) {
    const chunkModules = (manualChunkModulesByAlias[alias] =
      manualChunkModulesByAlias[alias] || []);
    addStaticDependenciesToManualChunk(entry, chunkModules, modulesInManualChunks);
  }
  for (const [alias, modules] of Object.entries(manualChunkModulesByAlias)) {
    chunkDefinitions.push({ alias, modules });
  }

  const assignedEntryPointsByModule: DependentModuleMap = new Map();
  const { dependentEntryPointsByModule, dynamicEntryModules } = analyzeModuleGraph(entryModules);
  /**
   * getDynamicDependentEntryPoints 的作用是获取动态 import 所对应的 importer
   * 在这个例子中就是为了获取 user 模块对应的 importer (index模块)
   * {size: 1, Module {graph, …} => Set(1) {…}}
   */
  const dynamicallyDependentEntryPointsByDynamicEntry: DependentModuleMap =
    getDynamicDependentEntryPoints(dependentEntryPointsByModule, dynamicEntryModules);
  const staticEntries = new Set(entryModules);

  function assignEntryToStaticDependencies(
    entry: Module,
    dynamicDependentEntryPoints: ReadonlySet<Module> | null
  ) {
    const modulesToHandle = new Set([entry]);
    for (const module of modulesToHandle) {
      const assignedEntryPoints = getOrCreate(assignedEntryPointsByModule, module, () => new Set());
      if (
        dynamicDependentEntryPoints &&
        areEntryPointsContainedOrDynamicallyDependent(
          dynamicDependentEntryPoints,
          dependentEntryPointsByModule.get(module)!
        )
      ) {
        continue;
      } else {
        assignedEntryPoints.add(entry);
      }
      for (const dependency of module.getDependenciesToBeIncluded()) {
        if (!(dependency instanceof ExternalModule || modulesInManualChunks.has(dependency))) {
          modulesToHandle.add(dependency);
        }
      }
    }
  }

  function areEntryPointsContainedOrDynamicallyDependent(
    entryPoints: ReadonlySet<Module>,
    containedIn: ReadonlySet<Module>
  ): boolean {
    const entriesToCheck = new Set(entryPoints);
    for (const entry of entriesToCheck) {
      if (!containedIn.has(entry)) {
        if (staticEntries.has(entry)) return false;
        const dynamicallyDependentEntryPoints =
          dynamicallyDependentEntryPointsByDynamicEntry.get(entry)!;
        for (const dependentEntry of dynamicallyDependentEntryPoints) {
          entriesToCheck.add(dependentEntry);
        }
      }
    }
    return true;
  }

  for (const entry of entryModules) {
    if (!modulesInManualChunks.has(entry)) {
      assignEntryToStaticDependencies(entry, null);
    }
  }

  for (const entry of dynamicEntryModules) {
    if (!modulesInManualChunks.has(entry)) {
      assignEntryToStaticDependencies(
        entry,
        dynamicallyDependentEntryPointsByDynamicEntry.get(entry)!
      );
    }
  }

  chunkDefinitions.push(
    ...createChunks([...entryModules, ...dynamicEntryModules], assignedEntryPointsByModule)
  );
  return chunkDefinitions;
}

getChunkAssignments 函数返回值是一个 ChunkDefinitions,它里面保存了区块的别名和对应的 module 信息。ChunkDefinitions 如下:

type ChunkDefinitions = { alias: string | null; modules: Module[] }[];

所以最终通过 for (const { alias, modules } of getChunkAssignments(this.graph.entryModules, manualChunkAliasByEntry)) 循环执行 chunks.push(new Chunk()) 得到 chunks。再执行 for (const chunk of chunks) { chunk.link(); } 为每个 ckunk 设置 this.dependencies 等。

再之后就是 for (const chunk of chunks) { facades.push(...chunk.generateFacades()); }。 chunk.generateFacades() 也会生成 chunk 并返回。

class Chunk {
  //...
  generateFacades(): Chunk[] {
		const facades: Chunk[] = [];
		const entryModules = new Set([...this.entryModules, ...this.implicitEntryModules]);
		const exposedVariables = new Set<Variable>(
			this.dynamicEntryModules.map(({ namespace }) => namespace)
		);
		//收集需要导出的变量
		for (const module of entryModules) {
			if (module.preserveSignature) {
				for (const exportedVariable of module.getExportNamesByVariable().keys()) {
					exposedVariables.add(exportedVariable);
				}
			}
		}
		for (const module of entryModules) {
			// eslint-disable-next-line unicorn/prefer-spread
			const requiredFacades: FacadeName[] = Array.from(
				new Set(
					module.chunkNames.filter(({ isUserDefined }) => isUserDefined).map(({ name }) => name)
				),
				// mapping must run after Set 'name' dedupe
				name => ({
					name
				})
			);
			if (requiredFacades.length === 0 && module.isUserDefinedEntryPoint) {
				requiredFacades.push({});
			}
			// eslint-disable-next-line unicorn/prefer-spread
			requiredFacades.push(...Array.from(module.chunkFileNames, fileName => ({ fileName })));
			if (requiredFacades.length === 0) {
				requiredFacades.push({});
			}
			if (!this.facadeModule) {
				const needsStrictFacade =
					module.preserveSignature === 'strict' ||
					(module.preserveSignature === 'exports-only' &&
						module.getExportNamesByVariable().size > 0);
				if (
					!needsStrictFacade ||
					this.outputOptions.preserveModules ||
					this.canModuleBeFacade(module, exposedVariables)
				) {
					this.facadeModule = module;
					this.facadeChunkByModule.set(module, this);
					//module.preserveSignature = 'exports-only'
					if (module.preserveSignature) {
						this.strictFacade = needsStrictFacade;
					}
					//设置this.fileName 或者 this.name
					this.assignFacadeName(
						requiredFacades.shift()!,
						module,
						this.outputOptions.preserveModules
					);
				}
			}

			for (const facadeName of requiredFacades) {
				facades.push(
					Chunk.generateFacade(
						this.inputOptions,
						this.outputOptions,
						this.unsetOptions,
						this.pluginDriver,
						this.modulesById,
						this.chunkByModule,
						this.externalChunkByModule,
						this.facadeChunkByModule,
						this.includedNamespaces,
						module,
						facadeName,
						this.getPlaceholder,
						this.bundle,
						this.inputBase,
						this.snippets
					)
				);
			}
		}
		for (const module of this.dynamicEntryModules) {
			if (module.info.syntheticNamedExports) continue;
			if (!this.facadeModule && this.canModuleBeFacade(module, exposedVariables)) {
				this.facadeModule = module;
				this.facadeChunkByModule.set(module, this);
				this.strictFacade = true;
				this.dynamicName = getChunkNameFromModule(module);
			} else if (
				this.facadeModule === module &&
				!this.strictFacade &&
				this.canModuleBeFacade(module, exposedVariables)
			) {
				this.strictFacade = true;
			} else if (!this.facadeChunkByModule.get(module)?.strictFacade) {
				this.includedNamespaces.add(module);
				this.exports.add(module.namespace);
			}
		}
		if (!this.outputOptions.preserveModules) {
			this.addNecessaryImportsForFacades();
		}
		return facades;
	}
}

最终 generateChunks 方法执行了 return [...chunks, ...facades]; 也就是所有的区块集合。

我们继续回到 Bundle.generate 方法:

async generate(isWrite: boolean): Promise<OutputBundle> {
  //...
  const outputBundleBase: OutputBundle = Object.create(null);
  // outputBundle 是一个 proxy
  const outputBundle = getOutputBundle(outputBundleBase);
  this.pluginDriver.setOutputBundle(outputBundle, this.outputOptions);

  try {
  //...

  const getHashPlaceholder = getHashPlaceholderGenerator();
  // chunks => [chunk]
  const chunks = await this.generateChunks(outputBundle, getHashPlaceholder);
  if (chunks.length > 1) {
    //校验 outputOptions 选项是否合法
    validateOptionsForMultiChunkOutput(this.outputOptions, this.inputOptions.onwarn);
  }
  /**
   * chunk.generateExports 主要做了以下几件事:
   * 1. 设置 chunk.exportNamesByVariable
   * 2. 设置 chunk.exportsByName
   * 3. 压缩变量名称(默认情况下)
   * 4. 设置 chunk.exportMode
   */
  for (const chunk of chunks) {
    chunk.generateExports();
  }
  /**
   * renderChunks 主要作用是生成最终的 outputBundle。
   * 例如:{index.js: {…}, acorn-bf6b1c54.js: {…}}
   * 主要可以分为以下几步:
   * 1. 设置入口 chunk 的 preliminaryFileName
   * 2. 执行chunk.render()
   * 3. 生成 chunkGraph
   * 4. addChunksToBundle
   */
  await renderChunks(
    chunks,
    outputBundle,
    this.pluginDriver,
    this.outputOptions,
    this.inputOptions.onwarn
  );
  } catch (error_: any) {
    await this.pluginDriver.hookParallel('renderError', [error_]);
    throw error_;
  }
  //...
  return outputBundleBase;
}

在执行 const chunks = await this.generateChunks(outputBundle, getHashPlaceholder); 的时候这个 chunks 就是刚才 bundle.generateChunks 返回的 chunks。

chunk.generateExports() 主要做了以下4件事:

  1. 设置 chunk.exportNamesByVariable
  2. 设置 chunk.exportsByName
  3. 压缩变量名称(默认情况下)
  4. 设置 chunk.exportMode

如下是 chunk.generateExports 代码定义:

generateExports(): void {
  this.sortedExportNames = null;
  const remainingExports = new Set(this.exports);
  if (
    this.facadeModule !== null &&
    (this.facadeModule.preserveSignature !== false || this.strictFacade)
  ) {
    const exportNamesByVariable = this.facadeModule.getExportNamesByVariable();
    for (const [variable, exportNames] of exportNamesByVariable) {
      this.exportNamesByVariable.set(variable, [...exportNames]);
      for (const exportName of exportNames) {
        this.exportsByName.set(exportName, variable);
      }
      remainingExports.delete(variable);
    }
  }
  if (this.outputOptions.minifyInternalExports) {
    //压缩变量名称
    assignExportsToMangledNames(remainingExports, this.exportsByName, this.exportNamesByVariable);
  } else {
    assignExportsToNames(remainingExports, this.exportsByName, this.exportNamesByVariable);
  }
  if (this.outputOptions.preserveModules || (this.facadeModule && this.facadeModule.info.isEntry))
    this.exportMode = getExportMode(
      this,
      this.outputOptions,
      this.facadeModule!.id,
      this.inputOptions.onwarn
    );
}

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

  1. 设置入口 chunk 的 preliminaryFileName
  2. 执行chunk.render()
  3. 生成 chunkGraph
  4. addChunksToBundle

renderChunks 主要作用是生成最终的 outputBundle。例如:

{index.js: {…}, acorn-bf6b1c54.js: {…}}

最终 bundle.generate 函数内部将 outputBundleBase 返回。

generate 总结

generate 内部主要分为以下几个步骤:

  1. 定义 outputBundle - const outputBundle = getOutputBundle(outputBundleBase); 。
  2. 获取 chunks - const chunks = await this.generateChunks(outputBundle, getHashPlaceholder);
  3. 校验 outputOptions 选项是否合法 - validateOptionsForMultiChunkOutput(this.outputOptions, this.inputOptions.onwarn);
  4. 设置 chunk.exportMode、chunk.exportsByName、chunk.exportNamesByVariable 等 - chunk.generateExports();
  5. 生成最终的 outputBundle。 - await renderChunks(chunks, outputBundle, this.pluginDriver, this.outputOptions, this.inputOptions.onwarn)
  6. 将 outputBundleBase 返回。

下一节我们继续深入分析 renderChunks 内部逻辑。

rollup 揭秘相关文章