你們好,我是小雨小雨,致力於分享有趣的、實用的技術文章。
內容分爲翻譯和原創,若是有問題,歡迎隨時評論或私信,但願和你們一塊兒進步。
分享不易,但願可以獲得你們的支持和關注。javascript
rollup系列打算一章一章的放出,內容更精簡更專注更易於理解vue
目前打算分爲一下幾章:java
在進入枯燥的代碼解析以前,先大白話說下整個過程,rollup.rollup()主要分爲如下幾步:webpack
按照這個思路來看其實很簡單,可是具體的細節倒是百般複雜的。
不過咱們也沒必要糾結於具體的某些實現,畢竟條條大路通羅馬,咱們能夠吸納並改進或學習一些沒見過的代碼技巧或優化方法,在我看來,這纔是良好的閱讀源碼的方式。:)git
!!!提示 => 標有TODO爲具體實現細節,會視狀況分析。github
!!!注意 => 每個子標題都是父標題(函數)內部實現web
!!!強調 => rollup中模塊(文件)的id就是文件地址,因此相似resolveID這種就是解析文件地址的意思,咱們能夠返回咱們想返回的文件id(也就是地址,相對路徑、決定路徑)來讓rollup加載api
rollup是一個核心,只作最基礎的事情,好比提供默認模塊(文件)加載機制, 好比打包成不一樣風格的內容,咱們的插件中提供了加載文件路徑,解析文件內容(處理ts,sass等)等操做,是一種插拔式的設計,和webpack相似
插拔式是一種很是靈活且可長期迭代更新的設計,這也是一箇中大型框架的核心,人多力量大嘛~數組
1.調用getInputOptions標準化input配置參數promise
const inputOptions = getInputOptions(rawInputOptions);
1.1. 調用mergeOptions,設置默認的input和output配置,並返回input配置 和 使用非法配置屬性的錯誤信息
let { inputOptions, optionError } = mergeOptions({ config: rawInputOptions });
1.2. 調用options鉤子函數,以在input配合徹底標準化以前進行自定義修改
inputOptions = inputOptions.plugins!.reduce(applyOptionHook, inputOptions);
1.3. 標準化插件操做:爲返回對象中沒有name屬性的插件設置默認的插件名 => at position 當前插件在全部插件中索引值
inputOptions.plugins = normalizePlugins(inputOptions.plugins!, ANONYMOUS_PLUGIN_PREFIX);
1.4. 對不兼容內嵌動態引入模塊
或保留模塊兩種狀況的配置,進行警告報錯
// 將動態導入的依賴(import | require.ensure() | other)內嵌到一個chunk而不建立獨立的包,相關的代碼邏輯以下 if (inputOptions.inlineDynamicImports) { // preserveModules: 儘量的保留模塊,而不是混合起來,建立更少的chunks,默認爲false,不開啓 if (inputOptions.preserveModules) // 若是開啓了,就與內嵌衝突了 return error({ code: 'INVALID_OPTION', message: `"preserveModules" does not support the "inlineDynamicImports" option.` }); // 其餘判斷,具體參考代碼倉庫:index.ts } else if (inputOptions.preserveModules) { // 又對 以原始文件命名,不綜合打包 的功能進行排異處理 if (inputOptions.manualChunks) return error({ code: 'INVALID_OPTION', message: '"preserveModules" does not support the "manualChunks" option.' }); // 其餘判斷,具體參考代碼倉庫:index.ts }
1.5. 返回處理後的input配置
return inputOptions;
javascript initialiseTimers(inputOptions);
javascript const graph = new Graph(inputOptions, curWatcher);
3.1. 初始化警告函數,對已經提示過得警告進行緩存
this.onwarn = (options.onwarn as WarningHandler) || makeOnwarn();
3.2. 給當前圖掛載路徑追蹤系統,無構造函數,只有屬性和更改屬性的方法
this.deoptimizationTracker = new PathTracker();
3.3. 初始化當前圖的惟一模塊緩存容器,能夠將上個打包結果的cache屬性賦給下一次打包,提高打包速度 =>
this.cachedModules = new Map();
3.4. 讀取傳遞的上次build結果中的模塊和插件。插件緩存參考 =>,下文中解釋。
if (options.cache) { if (options.cache.modules) for (const module of options.cache.modules) this.cachedModules.set(module.id, module); } if (options.cache !== false) { this.pluginCache = (options.cache && options.cache.plugins) || Object.create(null); for (const name in this.pluginCache) { const cache = this.pluginCache[name]; for (const key of Object.keys(cache)) cache[key][0]++; } }
3.5. treeshake信息掛載。
if (options.treeshake !== false) { this.treeshakingOptions = options.treeshake && options.treeshake !== true ? { annotations: options.treeshake.annotations !== false, moduleSideEffects: options.treeshake.moduleSideEffects, propertyReadSideEffects: options.treeshake.propertyReadSideEffects !== false, pureExternalModules: options.treeshake.pureExternalModules, tryCatchDeoptimization: options.treeshake.tryCatchDeoptimization !== false, unknownGlobalSideEffects: options.treeshake.unknownGlobalSideEffects !== false } : { annotations: true, moduleSideEffects: true, propertyReadSideEffects: true, tryCatchDeoptimization: true, unknownGlobalSideEffects: true }; if (typeof this.treeshakingOptions.pureExternalModules !== 'undefined') { this.warnDeprecation( `The "treeshake.pureExternalModules" option is deprecated. The "treeshake.moduleSideEffects" option should be used instead. "treeshake.pureExternalModules: true" is equivalent to "treeshake.moduleSideEffects: 'no-external'"`, false ); } }
3.6. 初始化代碼解析器,具體參數和插件參考Graph.ts
this.contextParse = (code: string, options: acorn.Options = {}) => this.acornParser.parse(code, { ...defaultAcornOptions, ...options, ...this.acornOptions }) as any;
3.7. 插件驅動器
this.pluginDriver = new PluginDriver( this, options.plugins!, this.pluginCache, // 處理軟連文件的時候,是否覺得軟連所在地址做爲上下文,false爲是,true爲不是。 options.preserveSymlinks === true, watcher );
3.7.2. 實例化FileEmitter而且將實例所攜帶方法設置到插件驅動器上
// basePluginDriver爲PluginDriver的第六個參數,表明graph的'根'插件驅動器 this.fileEmitter = new FileEmitter(graph, basePluginDriver && basePluginDriver.fileEmitter); this.emitFile = this.fileEmitter.emitFile; this.getFileName = this.fileEmitter.getFileName; this.finaliseAssets = this.fileEmitter.assertAssetsFinalized; this.setOutputBundle = this.fileEmitter.setOutputBundle;
3.7.3. 插件拼接
this.plugins = userPlugins.concat( basePluginDriver ? basePluginDriver.plugins : [getRollupDefaultPlugin(preserveSymlinks)] );
3.7.4. 緩存插件們的上下文環境,以後執行插件的的時候會經過index獲取並注入到插件內
// 利用map給每一個插件注入plugin特有的context,並緩存 this.pluginContexts = this.plugins.map( getPluginContexts(pluginCache, graph, this.fileEmitter, watcher) );
3.7.5. input和output設置的插件衝突的時候,報錯
if (basePluginDriver) { for (const plugin of userPlugins) { for (const hook of basePluginDriver.previousHooks) { if (hook in plugin) { graph.warn(errInputHookInOutputPlugin(plugin.name, hook)); } } } }
3.8. 監聽模式的設定
if (watcher) { const handleChange = (id: string) => this.pluginDriver.hookSeqSync('watchChange', [id]); watcher.on('change', handleChange); watcher.once('restart', () => { watcher.removeListener('change', handleChange); }); }
3.9. 全局上下文
this.scope = new GlobalScope();
3.10. 設置模塊的全局上下文,默認爲false
this.context = String(options.context); // 用戶是否自定義了上下文環境 const optionsModuleContext = options.moduleContext; if (typeof optionsModuleContext === 'function') { this.getModuleContext = id => optionsModuleContext(id) || this.context; } else if (typeof optionsModuleContext === 'object') { const moduleContext = new Map(); for (const key in optionsModuleContext) { moduleContext.set(resolve(key), optionsModuleContext[key]); } this.getModuleContext = id => moduleContext.get(id) || this.context; } else { this.getModuleContext = () => this.context; }
3.11. 初始化moduleLoader,用於模塊(文件)的解析和加載
// 模塊(文件)解析加載,內部調用的resolveID和load等鉤子,讓使用者擁有更多的操做能力 this.moduleLoader = new ModuleLoader( this, this.moduleById, this.pluginDriver, options.external!, (typeof options.manualChunks === 'function' && options.manualChunks) as GetManualChunk | null, (this.treeshakingOptions ? this.treeshakingOptions.moduleSideEffects : null)!, (this.treeshakingOptions ? this.treeshakingOptions.pureExternalModules : false)! );
4.執行buildStart鉤子函數,打包獲取chunks,以供後續生成和寫入使用
try { // buildStart鉤子函數觸發 await graph.pluginDriver.hookParallel('buildStart', [inputOptions]); // 這一步經過id,深度分析拓撲關係,去除無用塊,進而生成咱們的chunks // build的邏輯詳見下文 chunks = await graph.build( // 這個chunks是閉包,因此generate和write能夠用到 inputOptions.input as string | string[] | Record<string, string>, inputOptions.manualChunks, inputOptions.inlineDynamicImports! ); } catch (err) { const watchFiles = Object.keys(graph.watchFiles); if (watchFiles.length > 0) { err.watchFiles = watchFiles; } await graph.pluginDriver.hookParallel('buildEnd', [err]); throw err; }
5.返回一個對象,包括緩存,監聽文件和generate、write兩個方法
return { cache, watchFiles, generate, write }
build方法經過id,深度分析拓撲關係,去除無用塊,進而生成咱們的chunks
接受三個參數:入口、提取公共塊規則(manualChunks)、是否內嵌動態導入模塊
build是很單一的方法,就是產出咱們的chunks。他返回一個promise對象供以後的使用。
return Promise.all([ 入口模塊, // 代碼爲: this.moduleLoader.addEntryModules(normalizeEntryModules(entryModules), true) 用戶定義公共模塊 // 這塊沒有返回值,只是將公共模塊緩存到模塊加載器上,處理結果由入口模塊代理返回。巧妙的處理方式,一箭雙鵰 ]).then((入口模塊的返回) => { // 模塊的依賴關係處理 return chunks; });
normalizeEntryModules對入口進行標準化處理,返回統一的格式:
UnresolvedModule { fileName: string | null; id: string; name: string | null; }
addEntryModules對模塊進行加載、去重,再排序操做,最後返回模塊,公共chunks。其中,在加載過程當中會將處理過的模塊緩存到ModuleLoaders的modulesById(Map對象)上。部分代碼以下:
// 模塊加載部分 private fetchModule( id: string, importer: string, moduleSideEffects: boolean, syntheticNamedExports: boolean, isEntry: boolean ): Promise<Module> { // 主流程以下: // 獲取緩存,提高效率: const existingModule = this.modulesById.get(id); if (existingModule instanceof Module) { existingModule.isEntryPoint = existingModule.isEntryPoint || isEntry; return Promise.resolve(existingModule); } // 新建模塊: const module: Module = new Module( this.graph, id, moduleSideEffects, syntheticNamedExports, isEntry ); // 緩存,以備優化 this.modulesById.set(id, module); // 爲每個入庫模塊設置已監聽 this.graph.watchFiles[id] = true; // 調用用戶定義的manualChunk方法,獲取公共chunks別名,好比: // 好比 manualChunkAlias(id){ // if (xxx) { // return 'vendor'; // } // } const manualChunkAlias = this.getManualChunk(id); // 緩存到 manualChunkModules if (typeof manualChunkAlias === 'string') { this.addModuleToManualChunk(manualChunkAlias, module); } // 調用load鉤子函數並返回處理結果,其中第二個數組參數爲傳到鉤子函數的的參數 return Promise.resolve(this.pluginDriver.hookFirst('load', [id])) .cache() .then(source => { // 統一格式: sourceDescription return { code: souce, // ... } }) .then(sourceDescription => { // 返回鉤子函數transform處理後的代碼,好比jsx解析結果,ts解析結果 // 參考: https://github.com/rollup/plugins/blob/e7a9e4a516d398cbbd1fa2b605610517d9161525/packages/wasm/src/index.js return transform(this.graph, sourceDescription, module); }) .then(source => { // 代碼編譯結果掛在到當前解析的入口模塊上 module.setSource(source); // 模塊id與模塊綁定 this.modulesById.set(id, module); // 處理模塊的依賴們,將導出的模塊也掛載到module上 // !!! 注意: fetchAllDependencies中建立的模塊是經過ExternalModule類建立的,有別的入口模塊的 return this.fetchAllDependencies(module).then(() => { for (const name in module.exports) { if (name !== 'default') { module.exportsAll[name] = module.id; } } for (const source of module.exportAllSources) { const id = module.resolvedIds[source].id; const exportAllModule = this.modulesById.get(id); if (exportAllModule instanceof ExternalModule) continue; for (const name in exportAllModule!.exportsAll) { if (name in module.exportsAll) { this.graph.warn(errNamespaceConflict(name, module, exportAllModule!)); } else { module.exportsAll[name] = exportAllModule!.exportsAll[name]; } } } // 返回這些處理後的module對象,從id(文件路徑) 轉換到 一個近乎具備文件完整信息的對象。 return module; }) }
// 去重 let moduleIndex = firstEntryModuleIndex; for (const entryModule of entryModules) { // 是否爲用戶定義,默認是 entryModule.isUserDefinedEntryPoint = entryModule.isUserDefinedEntryPoint || isUserDefined; const existingIndexModule = this.indexedEntryModules.find( indexedModule => indexedModule.module.id === entryModule.id ); // 根據moduleIndex進行入口去重 if (!existingIndexModule) { this.indexedEntryModules.push({ module: entryModule, index: moduleIndex }); } else { existingIndexModule.index = Math.min(existingIndexModule.index, moduleIndex); } moduleIndex++; } // 排序 this.indexedEntryModules.sort(({ index: indexA }, { index: indexB }) => indexA > indexB ? 1 : -1 );
已經加載處理過的模塊會緩存到moduleById上,因此直接遍歷之,再根據所屬模塊類進行分類
// moduleById是 id => module 的存儲, 是全部合法的入口模塊 for (const module of this.moduleById.values()) { if (module instanceof Module) { this.modules.push(module); } else { this.externalModules.push(module); } }
獲取全部入口,找到正確的、移除無用的依賴,並過濾出真正做爲入口的模塊
// this.link(entryModules)方法的內部 // 找到全部的依賴 for (const module of this.modules) { module.linkDependencies(); } // 返回全部的入口啓動模塊(也就是非外部模塊),和那些依賴了一圈結果成死循環的模塊相對路徑 const { orderedModules, cyclePaths } = analyseModuleExecution(entryModules); // 對那些死循環路徑進行警告 for (const cyclePath of cyclePaths) { this.warn({ code: 'CIRCULAR_DEPENDENCY', cycle: cyclePath, importer: cyclePath[0], message: `Circular dependency: ${cyclePath.join(' -> ')}` }); } // 過濾出真正的入口啓動模塊,賦值給modules this.modules = orderedModules; // ast語法的進一步解析 // TODO: 視狀況詳細補充 for (const module of this.modules) { module.bindReferences(); }
剩餘部分
// 引入全部的導出,設定相關關係 // TODO: 視狀況詳細補充 for (const module of entryModules) { module.includeAllExports(); } // 根據用戶的treeshaking配置,給引入的環境設置上下文環境 this.includeMarked(this.modules); // 檢查全部沒使用的模塊,進行提示警告 for (const externalModule of this.externalModules) externalModule.warnUnusedImports(); // 給每一個入口模塊添加hash,以備後續整合到一個chunk裏 if (!this.preserveModules && !inlineDynamicImports) { assignChunkColouringHashes(entryModules, manualChunkModulesByAlias); } let chunks: Chunk[] = []; // 爲每一個模塊都建立chunk if (this.preserveModules) { // 遍歷入口模塊 for (const module of this.modules) { // 新建chunk實例對象 const chunk = new Chunk(this, [module]); // 是入口模塊,而且非空 if (module.isEntryPoint || !chunk.isEmpty) { chunk.entryModules = [module]; } chunks.push(chunk); } } else { // 建立儘量少的chunk const chunkModules: { [entryHashSum: string]: Module[] } = {}; for (const module of this.modules) { // 將以前設置的hash值轉換爲string const entryPointsHashStr = Uint8ArrayToHexString(module.entryPointsHash); const curChunk = chunkModules[entryPointsHashStr]; // 有的話,添加module,沒有的話建立並添加,相同的hash值會添加到一塊兒 if (curChunk) { curChunk.push(module); } else { chunkModules[entryPointsHashStr] = [module]; } } // 將同一hash值的chunks們排序後,添加到chunks中 for (const entryHashSum in chunkModules) { const chunkModulesOrdered = chunkModules[entryHashSum]; // 根據以前的設定的index排序,這個應該表明引入的順序,或者執行的前後順序 sortByExecutionOrder(chunkModulesOrdered); // 用排序後的chunkModulesOrdered新建chunk const chunk = new Chunk(this, chunkModulesOrdered); chunks.push(chunk); } } // 將依賴掛載到每一個chunk上 for (const chunk of chunks) { chunk.link(); }
以上就是rollup.rollup的主流程分析,具體細節參考代碼庫註釋
function createPluginCache(cache: SerializablePluginCache): PluginCache { // 利用閉包將cache緩存 return { has(id: string) { const item = cache[id]; if (!item) return false; item[0] = 0; // 若是訪問了,那麼重置訪問過時次數,猜想:就是說明用戶有意向主動去使用 return true; }, get(id: string) { const item = cache[id]; if (!item) return undefined; item[0] = 0; // 若是訪問了,那麼重置訪問過時次數 return item[1]; }, set(id: string, value: any) { cache[id] = [0, value]; }, delete(id: string) { return delete cache[id]; } }; }
能夠看到rollup利用對象加數組的結構來爲插件提供緩存能力,即:
{ test: [0, '內容'] }
數組的第一項是當前訪問的計數器,和緩存的過時次數掛鉤,再加上js的閉包能力簡單實用的提供了插件上的緩存能力
到目前爲止,再一次加深了職能單一和依賴注入重要性,好比模塊加載器,插件驅動器,還有Graph。還有rollup的(數據)模塊化,webpack也相似,vue也相似,都是將具象的內容轉換爲抽象的數據,再不斷掛載相關的依賴的其餘抽象數據,固然這其中須要符合某些規範,好比estree規範。
鄙人一直對構建很感興趣,個人github有接近一半都是和構建有關的,因此此次從rollup入口,開始揭開構建世界的那一層層霧霾,還咱們一個清晰地世界。:)
rollup系列不會參考別人的分享(目前也沒找到有人分析rollup。。),徹底自食其力一行一行的閱讀,因此不免會有些地方不是很正確。
沒辦法,閱讀別人的代碼,有些地方就像猜女人的心思,太tm難了,因此有不對的地方但願大佬們多多指點,互相學習。
仍是那句話,創做不已,但願獲得你們的支持,與君共勉,我們下期見!