你們好,我是小雨小雨,致力於分享有趣的、實用的技術文章。 內容分爲翻譯和原創,若是有問題,歡迎隨時評論或私信,但願和你們一塊兒進步。 分享不易,但願可以獲得你們的支持和關注。javascript
rollup系列打算一章一章的放出,內容更精簡更專注更易於理解java
目前打算分爲如下幾章:node
書接上文,咱們知道rollup.rollup對配置中的入口進行了解析、依賴掛載、數據化這些操做,最終返回了一個chunks,而後返回了一些方法:webpack
rollup() { const chunks = await graph.build(); return { generate, // ... } } 這其中利用了閉包的原理,以便後續方法能夠訪問到rollup結果 複製代碼
這期咱們就深刻generate方法,來看看它的心裏世界
git
仍是老套路,在看代碼前,先大白話說下整個過程,rollup.generate()主要分爲如下幾步:github
最近看到這麼一句話:web
'將者,智、信、仁、勇、嚴也'數組
指的是將者的素養,順序表明着每一個能力的重要性:promise
智: 智略、謀略 信:信義、信用 仁:仁義、聲譽 勇:勇武、果斷 嚴:鐵律、公證緩存
時至今日,仍然奏效,哪怕是放到it領域。雖然不能直接拿過來,但內涵都是同樣的。
想要作好it這一行,先要自身硬(智),而後是產出質量(信),同事間的默契合做(仁),對事情的判斷(勇)和對團隊的要求以及獎懲制度(嚴)。
全部的註釋都在這裏,可自行閱讀
!!!版本 => 筆者閱讀的rollup版本爲: 1.32.0
!!!提示 => 標有TODO爲具體實現細節,會視狀況分析。
!!!注意 => 每個子標題都是父標題(函數)內部實現
!!!強調 => rollup中模塊(文件)的id就是文件地址,因此相似resolveID這種就是解析文件地址的意思,咱們能夠返回咱們想返回的文件id(也就是地址,相對路徑、決定路徑)來讓rollup加載
rollup是一個核心,只作最基礎的事情,好比提供默認模塊(文件)加載機制, 好比打包成不一樣風格的內容,咱們的插件中提供了加載文件路徑,解析文件內容(處理ts,sass等)等操做,是一種插拔式的設計,和webpack相似 插拔式是一種很是靈活且可長期迭代更新的設計,這也是一箇中大型框架的核心,人多力量大嘛~
調用封裝好的內置私有方法,返回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; }) 複製代碼
該方法經過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
方法獲取到標準化以後的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; 複製代碼
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)[]] }; } 複製代碼
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點個贊吧。