原來rollup這麼簡單之 rollup.generate + rollup.write篇

你們好,我是小雨小雨,致力於分享有趣的、實用的技術文章。 內容分爲翻譯和原創,若是有問題,歡迎隨時評論或私信,但願和你們一塊兒進步。 分享不易,但願可以獲得你們的支持和關注。javascript

計劃

rollup系列打算一章一章的放出,內容更精簡更專注更易於理解java

目前打算分爲如下幾章:node

TL;DR

書接上文,咱們知道rollup.rollup對配置中的入口進行了解析、依賴掛載、數據化這些操做,最終返回了一個chunks,而後返回了一些方法:webpack

rollup() {
    const chunks = await graph.build();
    return {
        generate,
        // ...
    }
}
這其中利用了閉包的原理,以便後續方法能夠訪問到rollup結果
複製代碼

這期咱們就深刻generate方法,來看看它的心裏世界git

仍是老套路,在看代碼前,先大白話說下整個過程,rollup.generate()主要分爲如下幾步:github

  1. 配置標準化、建立插件驅動器
  2. chunks、assets收集
  3. preserveModules模式處理
  4. 預渲染
  5. chunk優化
  6. 源碼render
  7. 產出過濾、排序

最近看到這麼一句話:web

'將者,智、信、仁、勇、嚴也'數組

指的是將者的素養,順序表明着每一個能力的重要性:promise

智: 智略、謀略 信:信義、信用 仁:仁義、聲譽 勇:勇武、果斷 嚴:鐵律、公證緩存

時至今日,仍然奏效,哪怕是放到it領域。雖然不能直接拿過來,但內涵都是同樣的。

想要作好it這一行,先要自身硬(智),而後是產出質量(信),同事間的默契合做(仁),對事情的判斷(勇)和對團隊的要求以及獎懲制度(嚴)。

注意點

全部的註釋都在這裏,可自行閱讀

!!!版本 => 筆者閱讀的rollup版本爲: 1.32.0

!!!提示 => 標有TODO爲具體實現細節,會視狀況分析。

!!!注意 => 每個子標題都是父標題(函數)內部實現

!!!強調 => rollup中模塊(文件)的id就是文件地址,因此相似resolveID這種就是解析文件地址的意思,咱們能夠返回咱們想返回的文件id(也就是地址,相對路徑、決定路徑)來讓rollup加載

rollup是一個核心,只作最基礎的事情,好比提供默認模塊(文件)加載機制, 好比打包成不一樣風格的內容,咱們的插件中提供了加載文件路徑,解析文件內容(處理ts,sass等)等操做,是一種插拔式的設計,和webpack相似 插拔式是一種很是靈活且可長期迭代更新的設計,這也是一箇中大型框架的核心,人多力量大嘛~

主要通用模塊以及含義

  1. Graph: 全局惟一的圖,包含入口以及各類依賴的相互關係,操做方法,緩存等。是rollup的核心
  2. PathTracker: 無反作用模塊依賴路徑追蹤
  3. PluginDriver: 插件驅動器,調用插件和提供插件環境上下文等
  4. FileEmitter: 資源操做器
  5. GlobalScope: 全局做用局,相對的還有局部的
  6. ModuleLoader: 模塊加載器
  7. NodeBase: ast各語法(ArrayExpression、AwaitExpression等)的構造基類

主流程解析

  • generate方法:

調用封裝好的內置私有方法,返回promise,一個一個的來,先來看getOutputOptionsAndPluginDriver

generate: ((rawOutputOptions: GenericConfigObject) => {
    // 過濾output配置選項,並建立output的插件驅動器
    const { outputOptions, outputPluginDriver } = getOutputOptionsAndPluginDriver(
        rawOutputOptions
    );
    const promise = generate(outputOptions, false, outputPluginDriver).then(result =>
        createOutput(result)
    );
    // 丟棄老版本字段
    Object.defineProperty(promise, 'code', throwAsyncGenerateError);
    Object.defineProperty(promise, 'map', throwAsyncGenerateError);
    return promise;
})
複製代碼
  • getOutputOptionsAndPluginDriver:

該方法經過output配置生成標準化配置和output插件驅動器

PluginDriver類暴露了createOutputPluginDriver方法

class PluginDriver {
    // ...
    public createOutputPluginDriver(plugins: Plugin[]): PluginDriver {
        return new PluginDriver(
            this.graph,
            plugins,
            this.pluginCache,
            this.preserveSymlinks,
            this.watcher,
            this
        );
    }
	// ...
}
複製代碼

引用該方法,建立output的插件驅動器: graph.pluginDriver.createOutputPluginDriver

const outputPluginDriver = graph.pluginDriver.createOutputPluginDriver(
    // 統一化插件
    normalizePlugins(rawOutputOptions.plugins, ANONYMOUS_OUTPUT_PLUGIN_PREFIX)
);
複製代碼

生成標準output配置更簡單了,調用以前在rollup.rollup方法中用到的,用來提取input配置的mergeOptions(參考mergeOptions.ts)方法,獲取處理後的配置,調用outputOptions鉤子函數,該鉤子能夠讀取到即將傳遞給generate/write的配置,進行更改,可是rollup更推薦在renderStart中進行更改等操做。以後進行一些列校驗判斷最終返回ourputOptions

function normalizeOutputOptions( inputOptions: GenericConfigObject, rawOutputOptions: GenericConfigObject, hasMultipleChunks: boolean, outputPluginDriver: PluginDriver ): OutputOptions {
	const mergedOptions = mergeOptions({
		config: {
			output: {
				...rawOutputOptions,
				// 能夠用output裏的覆蓋
				...(rawOutputOptions.output as object),
				// 不過input裏的output優先級最高,可是不是每一個地方都返回,有的不會使用
				...(inputOptions.output as object)
			}
		}
	});

	// 若是merge過程當中出錯了
	if (mergedOptions.optionError) throw new Error(mergedOptions.optionError);

	// 返回的是數組,可是rollup不支持數組,因此獲取第一項,目前也只會有一項
	const mergedOutputOptions = mergedOptions.outputOptions[0];

	const outputOptionsReducer = (outputOptions: OutputOptions, result: OutputOptions) =>
		result || outputOptions;

	// 觸發鉤子函數
	const outputOptions = outputPluginDriver.hookReduceArg0Sync(
		'outputOptions',
		[mergedOutputOptions],
		outputOptionsReducer,
		pluginContext => {
			const emitError = () => pluginContext.error(errCannotEmitFromOptionsHook());
			return {
				...pluginContext,
				emitFile: emitError,
				setAssetSource: emitError
			};
		}
	);

	// 檢查通過插件處理過的output配置
	checkOutputOptions(outputOptions);

	// output.file 和 output.dir是互斥的
	if (typeof outputOptions.file === 'string') {
		if (typeof outputOptions.dir === 'string')
			return error({
				code: 'INVALID_OPTION',
				message:
					'You must set either "output.file" for a single-file build or "output.dir" when generating multiple chunks.'
			});
		if (inputOptions.preserveModules) {
			return error({
				code: 'INVALID_OPTION',
				message:
					'You must set "output.dir" instead of "output.file" when using the "preserveModules" option.'
			});
		}
		if (typeof inputOptions.input === 'object' && !Array.isArray(inputOptions.input))
			return error({
				code: 'INVALID_OPTION',
				message: 'You must set "output.dir" instead of "output.file" when providing named inputs.'
			});
	}

	if (hasMultipleChunks) {
		if (outputOptions.format === 'umd' || outputOptions.format === 'iife')
			return error({
				code: 'INVALID_OPTION',
				message: 'UMD and IIFE output formats are not supported for code-splitting builds.'
			});
		if (typeof outputOptions.file === 'string')
			return error({
				code: 'INVALID_OPTION',
				message:
					'You must set "output.dir" instead of "output.file" when generating multiple chunks.'
			});
	}

	return outputOptions;
}
複製代碼
  • generate內部的generate方法

獲取到標準化以後的output配合和插件驅動器後,到了內置的generate方法了,該方法接受三個參數,其中第二個參數標識是否寫入,也就是說該方法同時用於generate和下一篇write中。

首先獲取用戶定義的資源名,沒有的話取默認值

const assetFileNames = outputOptions.assetFileNames || 'assets/[name]-[hash][extname]';
複製代碼

獲取chunks的目錄交集,也就是公共的根目錄

const inputBase = commondir(getAbsoluteEntryModulePaths(chunks));
複製代碼

getAbsoluteEntryModulePaths獲取全部絕對路徑的chunks id,commondir參考的node-commondir模塊,原理是先獲取第一個文件的路徑,進行split轉成數組(設爲a),而後遍歷剩餘全部文件id,進行比對,找到不相等的那個索引,而後從新賦值給a,進行下一次循環,直到結束,就獲得了公共的目錄。

function commondir(files: string[]) {
	if (files.length === 0) return '/';
	if (files.length === 1) return path.dirname(files[0]);
	const commonSegments = files.slice(1).reduce((commonSegments, file) => {
		const pathSegements = file.split(/\/+|\\+/);
		let i;
		for (
			i = 0;
			commonSegments[i] === pathSegements[i] &&
			i < Math.min(commonSegments.length, pathSegements.length);
			i++
		);
		return commonSegments.slice(0, i);
	}, files[0].split(/\/+|\\+/));

	// Windows correctly handles paths with forward-slashes
	return commonSegments.length > 1 ? commonSegments.join('/') : '/';
}
複製代碼

建立一個包含全部chunks和assets信息的對象

const outputBundleWithPlaceholders: OutputBundleWithPlaceholders = Object.create(null);
複製代碼

調用插件驅動器上的setOutputBundle將output設置到上面建立的outputBundleWithPlaceholders上。

outputPluginDriver.setOutputBundle(outputBundleWithPlaceholders, assetFileNames);
複製代碼

setOutputBundle在FileEmitter類上實現,在插件驅動器類(PluginDriver)上實例化,並將公共方法賦給插件驅動器。 reserveFileNameInBundle方法爲outputBundleWithPlaceholders上掛載文件chunks。 finalizeAsset方法只處理資源,將資源格式化後,添加到outputBundleWithPlaceholders上。格式爲:

{
    fileName,
    get isAsset(): true {
        graph.warnDeprecation(
            'Accessing "isAsset" on files in the bundle is deprecated, please use "type === \'asset\'" instead',
            false
        );

        return true;
    },
    source,
    type: 'asset'
};
複製代碼
class FileEmitter {
    // ...
    setOutputBundle = (
        outputBundle: OutputBundleWithPlaceholders,
        assetFileNames: string
    ): void => {
        this.output = {
            // 打包出來的命名
            assetFileNames,
            // 新建的空對象 => Object.create(null)
            bundle: outputBundle
        };
        // filesByReferenceId是經過rollup.rollup中emitChunks的時候設置的,表明已使用的chunks
        // 處理文件
        for (const emittedFile of this.filesByReferenceId.values()) {
            if (emittedFile.fileName) {
                // 文件名掛在到this.output上,做爲key,值爲: FILE_PLACEHOLDER
                reserveFileNameInBundle(emittedFile.fileName, this.output.bundle, this.graph);
            }
        }
        // 遍歷set 處理資源
        for (const [referenceId, consumedFile] of this.filesByReferenceId.entries()) {
            // 插件中定義了source的狀況
            if (consumedFile.type === 'asset' && consumedFile.source !== undefined) {
                // 給this.output上綁定資源
                this.finalizeAsset(consumedFile, consumedFile.source, referenceId, this.output);
            }
        }
    };
	// ...
}
複製代碼

調用renderStart鉤子函數,用來訪問output和input配置,可能你們看到了不少調用鉤子函數的方法,好比hookParallel、hookSeq等等,這些都是用來觸發插件裏提供的鉤子函數,不過是執行方式不一樣,有的是並行的,有的是串行的,有的只能執行經過一個等等,這會單獨抽出來講。

await outputPluginDriver.hookParallel('renderStart', [outputOptions, inputOptions]);
複製代碼

執行footer banner intro outro鉤子函數,內部就是執行這幾個鉤子函數,默認值爲option[footer|banner|intro|outro],最後返回字符串結果待拼接。

const addons = await createAddons(outputOptions, outputPluginDriver);
複製代碼

處理preserveModules模式,也就是是否儘量少的打包,而不是每一個模塊都是一個chunk 若是是儘量少的打包的話,就將chunks的導出多掛載到chunks的exportNames屬性上,供以後使用 若是每一個模塊都是一個chunk的話,推導出導出模式

for (const chunk of chunks) {
    // 儘量少的打包模塊
    // 設置chunk的exportNames
    if (!inputOptions.preserveModules) chunk.generateInternalExports(outputOptions);

    // 儘量多的打包模塊
    if (inputOptions.preserveModules || (chunk.facadeModule && chunk.facadeModule.isEntryPoint))
        // 根據導出,去推斷chunk的導出模式
        chunk.exportMode = getExportMode(chunk, outputOptions, chunk.facadeModule!.id);
}
複製代碼

預渲染chunks。 使用magic-string模塊進行source管理,初始化render配置,對依賴進行解析,添加到當前chunks的dependencies屬性上,按照執行順序對依賴們進行排序,處理準備動態引入的模塊,設置惟一標誌符(?)

for (const chunk of chunks) {
    chunk.preRender(outputOptions, inputBase);
}
複製代碼

優化chunks

if (!optimized && inputOptions.experimentalOptimizeChunks) {
    optimizeChunks(chunks, outputOptions, inputOptions.chunkGroupingSize!, inputBase);
    optimized = true;
}
複製代碼

將chunkId賦到上文建立的outputBundleWithPlaceholders上

assignChunkIds(
    chunks,
    inputOptions,
    outputOptions,
    inputBase,
    addons,
    outputBundleWithPlaceholders,
    outputPluginDriver
);
複製代碼

設置好chunks的對象,也就是將chunks依照id設置到outputBundleWithPlaceholders上,這時候outputBundleWithPlaceholders上已經有完整的chunk信息了

outputBundle = assignChunksToBundle(chunks, outputBundleWithPlaceholders);
複製代碼

語法樹解析生成code操做,最後返回outputBundle。

await Promise.all(
    chunks.map(chunk => {
        const outputChunk = outputBundleWithPlaceholders[chunk.id!] as OutputChunk;
        return chunk
            .render(outputOptions, addons, outputChunk, outputPluginDriver)
            .then(rendered => {
                // 引用類型,outputBundleWithPlaceholders上的也變化了,因此outputBundle也變化了,最後返回outputBundle
                outputChunk.code = rendered.code;
                outputChunk.map = rendered.map;

                return outputPluginDriver.hookParallel('ongenerate', [
                    { bundle: outputChunk, ...outputOptions },
                    outputChunk
                ]);
            });
    })
);

return outputBundle;
複製代碼
  • generate內部的createOutput方法

createOutput接受generate的返回值,並對生成的OutputBundle進行過濾和排序

function createOutput(outputBundle: Record<string, OutputChunk | OutputAsset | {}>): RollupOutput {
	return {
		output: (Object.keys(outputBundle)
			.map(fileName => outputBundle[fileName])
			.filter(outputFile => Object.keys(outputFile).length > 0) as (
			| OutputChunk
			| OutputAsset
		)[]).sort((outputFileA, outputFileB) => {
			const fileTypeA = getSortingFileType(outputFileA);
			const fileTypeB = getSortingFileType(outputFileB);
			if (fileTypeA === fileTypeB) return 0;
			return fileTypeA < fileTypeB ? -1 : 1;
		}) as [OutputChunk, ...(OutputChunk | OutputAsset)[]]
	};
}
複製代碼
  • rollup.write

write方法和generate方法幾乎一致,只不過是generate方法的第二個參數爲true,供generateBundle鉤子函數中使用,已代表當前是wirte仍是generate階段。 以後是獲取當前的chunks數,多出口的時候會檢測配置的file和sourcemapFile進而拋出錯誤提示

let chunkCount = 0; //計數
for (const fileName of Object.keys(bundle)) {
    const file = bundle[fileName];
    if (file.type === 'asset') continue;
    chunkCount++;
    if (chunkCount > 1) break;
}
if (chunkCount > 1) {
    // sourcemapFile配置
    if (outputOptions.sourcemapFile)
        return error({
            code: 'INVALID_OPTION',
            message: '"output.sourcemapFile" is only supported for single-file builds.'
        });
    // file字段
    if (typeof outputOptions.file === 'string')
        return error({
            code: 'INVALID_OPTION',
            message:
                'When building multiple chunks, the "output.dir" option must be used, not "output.file".' +
                (typeof inputOptions.input !== 'string' ||
                inputOptions.inlineDynamicImports === true
                    ? ''
                    : ' To inline dynamic imports, set the "inlineDynamicImports" option.')
        });
}
複製代碼

以後調用寫入方法: writeOutputFile

await Promise.all(
    Object.keys(bundle).map(chunkId =>
        writeOutputFile(result, bundle[chunkId], outputOptions, outputPluginDriver)
    )
);
複製代碼

writeOutputFile方法就很直觀了,解析路徑

const fileName = resolve(outputOptions.dir || dirname(outputOptions.file!), outputFile.fileName);
複製代碼

根據chunk類型進行不一樣的處理,assets直接獲取代碼便可,chunks的話還需根據sourcemap選項將sourcemp追加到代碼以後。

if (outputFile.type === 'asset') {
    source = outputFile.source;
} else {
    source = outputFile.code;
    if (outputOptions.sourcemap && outputFile.map) {
        let url: string;
        if (outputOptions.sourcemap === 'inline') {
            url = outputFile.map.toUrl();
        } else {
            url = `${basename(outputFile.fileName)}.map`;
            writeSourceMapPromise = writeFile(`${fileName}.map`, outputFile.map.toString());
        }
        if (outputOptions.sourcemap !== 'hidden') {
            source += `//# ${SOURCEMAPPING_URL}=${url}\n`;
        }
    }
}
複製代碼

最後調用fs模塊進行文件建立和內容寫入便可

function writeFile(dest: string, data: string | Buffer) {
	return new Promise<void>((fulfil, reject) => {
		mkdirpath(dest);

		fs.writeFile(dest, data, err => {
			if (err) {
				reject(err);
			} else {
				fulfil();
			}
		});
	});
}
複製代碼

以上就是代碼流程的解析部分,具體細節參考代碼庫註釋

部分功能的具體解析

總結

隨着深刻閱讀發現rollup細節操做不少,很複雜,須要話更多的時間去打磨,暫時先分析了下主流程,具體的實現細節好比優化chunks、prerender等以後視狀況再說吧。

不過也學到了一些東西,rollup將全部的ast類型分紅了一個個的類,一個類專門處理一個ast類型,調用的時候只須要遍歷ast body,獲取每一項的類型,而後動態調用就能夠了,很使用。對於ast沒有畫面感的同窗能夠看這裏 => ast在線解析

rollup從構建到打包,經歷了三個大步驟:

加載、解析 => 分析(依賴分析、引用次數、無用模塊分析、類型分析等) => 生成

看似簡單,實則龐雜。爲rollup點個贊吧。

相關文章
相關標籤/搜索