原來rollup這麼簡單之 rollup.rollup篇
阿新 • • 發佈:2020-03-17
> 大家好,我是小雨小雨,致力於分享有趣的、實用的技術文章。
> 內容分為翻譯和原創,如果有問題,歡迎隨時評論或私信,希望和大家一起進步。
> 分享不易,希望能夠得到大家的支援和關注。
#### 計劃
rollup系列打算一章一章的放出,內容更精簡更專一更易於理解
目前打算分為一下幾章:
- rollup.rollup <==== 當前文章
- rollup.generate
- rollup.write
- rollup.watch
- 具體實現或思想的分析,比如tree shaking、外掛的實現等
#### TL;DR
在進入枯燥的程式碼解析之前,先大白話說下整個過程,rollup.rollup()主要分為以下幾步:
1. 配置收集、標準化
2. 檔案分析
3. 原始碼編譯,生成ast
4. 模組生成
5. 依賴解析
6. 過濾淨化
7. 產出chunks
按照這個思路來看其實很簡單,但是具體的細節卻是百般複雜的。
不過我們也不必糾結於具體的某些實現,畢竟條條大路通羅馬,我們可以吸納並改進或學習一些沒見過的程式碼技巧或優化方法,在我看來,這才是良好的閱讀原始碼的方式。:)
#### 注意點
> !!!提示 => 標有TODO為具體實現細節,會視情況分析。
> !!!注意 => 每一個子標題都是父標題(函式)內部實現
> !!!強調 => rollup中模組(檔案)的id就是檔案地址,所以類似resolveID這種就是解析檔案地址的意思,我們可以返回我們想返回的檔案id(也就是地址,相對路徑、決定路徑)來讓rollup載入
> rollup是一個核心,只做最基礎的事情,比如提供[預設模組(檔案)載入機制](https://github.com/FoxDaxian/rollup-analysis/blob/master/src/utils/defaultPlugin.ts#L6), 比如打包成不同風格的內容,我們的外掛中提供了載入檔案路徑,解析檔案內容(處理ts,sass等)等操作,是一種插拔式的設計,和webpack類似
> 插拔式是一種非常靈活且可長期迭代更新的設計,這也是一箇中大型框架的核心,人多力量大嘛~
#### 主要通用模組以及含義
1. Graph: 全域性唯一的圖,包含入口以及各種依賴的相互關係,操作方法,快取等。是rollup的核心
2. PathTracker: 無副作用模組依賴路徑追蹤
3. PluginDriver: 外掛驅動器,呼叫外掛和提供外掛環境上下文等
4. FileEmitter: 資源操作器
5. GlobalScope: 全域性作用局,相對的還有區域性的
6. ModuleLoader: 模組載入器
7. NodeBase: ast各語法(ArrayExpression、AwaitExpression等)的構造基類
#### 主流程解析
- 1.呼叫getInputOptions標準化input配置引數
```javascript
const inputOptions = getInputOptions(rawInputOptions);
```
- 1.1. 呼叫mergeOptions,設定預設的input和output配置,並返回input配置 和 使用非法配置屬性的錯誤資訊
```javascript
let { inputOptions, optionError } = mergeOptions({
config: rawInputOptions
});
```
- 1.2. 呼叫options鉤子函式,以在input配合完全標準化之前進行自定義修改
```javascript
inputOptions = inputOptions.plugins!.reduce(applyOptionHook, inputOptions);
```
- 1.3. 標準化外掛操作:為返回物件中沒有name屬性的外掛設定預設的外掛名 => at position 當前外掛在所有外掛中索引值
```javascript
inputOptions.plugins = normalizePlugins(inputOptions.plugins!, ANONYMOUS_PLUGIN_PREFIX);
```
- 1.4. 對不相容內嵌`動態引入模組`或保留模組兩種情況的配置,進行警告報錯
```javascript
// 將動態匯入的依賴(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配置
```javascript
return inputOptions;
```
- 2.是否開啟效能檢測,檢測inputOptions.perf屬性,如果未設定沒那麼檢測函式為空
```javascript
initialiseTimers(inputOptions);
```
- 3.建立圖,引數為input配置和watch,watch當前不考慮
```javascript
const graph = new Graph(inputOptions, curWatcher);
```
- 3.1. 初始化警告函式,對已經提示過得警告進行快取
```javascript
this.onwarn = (options.onwarn as WarningHandler) || makeOnwarn();
```
- 3.2. 給當前圖掛載路徑追蹤系統,無建構函式,只有屬性和更改屬性的方法
```javascript
this.deoptimizationTracker = new PathTracker();
```
- 3.3. 初始化當前圖的唯一模組快取容器,可以將上個打包結果的cache屬性賦給下一次打包,提升打包速度 [=>](https://rollupjs.org/guide/en/#advanced-functionality)
```javascript
this.cachedModules = new Map();
```
- 3.4. 讀取傳遞的上次build結果中的模組和外掛。[外掛快取參考 =>](https://github.com/rollup/rollup/pull/2389),下文中解釋。
```javascript
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資訊掛載。
```javascript
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](https://github.com/FoxDaxian/rollup-analysis/blob/master/src/Graph.ts#L155)
```javascript
this.contextParse = (code: string, options: acorn.Options = {}) =>
this.acornParser.parse(code, {
...defaultAcornOptions,
...options,
...this.acornOptions
}) as any;
```
- 3.7. 外掛驅動器
```javascript
this.pluginDriver = new PluginDriver(
this,
options.plugins!,
this.pluginCache,
// 處理軟連檔案的時候,是否以為軟連所在地址作為上下文,false為是,true為不是。
options.preserveSymlinks === true,
watcher
);
```
- 3.7.1. 棄用api警告,引數掛載
- 3.7.2. 例項化FileEmitter並且將例項所攜帶方法設定到外掛驅動器上
```javascript
// 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. 外掛拼接
```javascript
this.plugins = userPlugins.concat(
basePluginDriver ? basePluginDriver.plugins : [getRollupDefaultPlugin(preserveSymlinks)]
);
```
- 3.7.4. 快取外掛們的上下文環境,之後執行外掛的的時候會通過index獲取並注入到外掛內
```javascript
// 利用map給每個外掛注入plugin特有的context,並快取
this.pluginContexts = this.plugins.map(
getPluginContexts(pluginCache, graph, this.fileEmitter, watcher)
);
```
- 3.7.5. input和output設定的外掛衝突的時候,報錯
```javascript
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. 監聽模式的設定
```javascript
if (watcher) {
const handleChange = (id: string) => this.pluginDriver.hookSeqSync('watchChange', [id]);
watcher.on('change', handleChange);
watcher.once('restart', () => {
watcher.removeListener('change', handleChange);
});
}
```
- 3.9. 全域性上下文
```javascript
this.scope = new GlobalScope();
```
- 3.10. 設定模組的全域性上下文,預設為false
```javascript
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,用於模組(檔案)的解析和載入
```javascript
// 模組(檔案)解析載入,內部呼叫的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,以供後續生成和寫入使用
```javascript
try {
// buildStart鉤子函式觸發
await graph.pluginDriver.hookParallel('buildStart', [inputOptions]);
// 這一步通過id,深度分析拓撲關係,去除無用塊,進而生成我們的chunks
// build的邏輯詳見下文
chunks = await graph.build( // 這個chunks是閉包,所以generate和write可以用到
inputOptions.input as string | string[] | Record,
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兩個方法
```javascript
return {
cache,
watchFiles,
generate,
write
}
```
##### graph.build邏輯解析
> build方法通過id,深度分析拓撲關係,去除無用塊,進而生成我們的chunks
> 接受三個引數:入口、提取公共塊規則(manualChunks)、是否內嵌動態匯入模組
- build是很單一的方法,就是產出我們的chunks。他返回一個promise物件供之後的使用。
```javascript
return Promise.all([
入口模組, // 程式碼為: this.moduleLoader.addEntryModules(normalizeEntryModules(entryModules), true)
使用者定義公共模組 // 這塊沒有返回值,只是將公共模組快取到模組載入器上,處理結果由入口模組代理返回。巧妙的處理方式,一舉兩得
]).then((入口模組的返回) => {
// 模組的依賴關係處理
return chunks;
});
```
- 入口模組: this.moduleLoader.addEntryModules(normalizeEntryModules(entryModules), true)
- normalizeEntryModules對入口進行標準化處理,返回統一的格式:
```javascript
UnresolvedModule {
fileName: string | null;
id: string;
name: string | null;
}
```
- addEntryModules對模組進行載入、去重,再排序操作,最後返回模組,公共chunks。其中,在載入過程中會將處理過的模組快取到ModuleLoaders的modulesById(Map物件)上。部分程式碼如下:
```javascript
// 模組載入部分
private fetchModule(
id: string,
importer: string,
moduleSideEffects: boolean,
syntheticNamedExports: boolean,
isEntry: boolean
): Promise {
// 主流程如下:
// 獲取快取,提升效率:
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;
})
}
```
```javascript
// 去重
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上,所以直接遍歷之,再根據所屬模組類進行分類
```javascript
// moduleById是 id => module 的儲存, 是所有合法的入口模組
for (const module of this.moduleById.values()) {
if (module instanceof Module) {
this.modules.push(module);
} else {
this.externalModules.push(module);
}
}
```
- 獲取所有入口,找到正確的、移除無用的依賴,並過濾出真正作為入口的模組
```javascript
// 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();
}
```
- 剩餘部分
```javascript
// 引入所有的匯出,設定相關關係
// 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的主流程分析,具體細節參考[程式碼庫註釋](https://github.com/FoxDaxian/rollup-analysis)
#### 部分功能的具體解析
- 外掛快取能力解析,為開發者們提供了外掛上的快取能力,利用cacheKey可以共享相同外掛的不同例項間的資料
```javascript
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利用物件加陣列的結構來為外掛提供快取能力,即:
```javascript
{
test: [0, '內容']
}
```
陣列的第一項是當前訪問的計數器,和快取的過期次數掛鉤,再加上js的閉包能力簡單實用的提供了外掛上的快取能力
#### 總結
到目前為止,再一次加深了職能單一和依賴注入重要性,比如模組載入器,外掛驅動器,還有Graph。還有rollup的(資料)模組化,webpack也類似,vue也類似,都是將具象的內容轉換為抽象的資料,再不斷掛載相關的依賴的其他抽象資料,當然這其中需要符合某些規範,比如[estree規範](https://github.com/estree/estree)。
鄙人一直對構建很感興趣,我的github有接近一半都是和構建有關的,所以這次從rollup入口,開始揭開構建世界的那一層層霧霾,還我們一個清晰地世界。:)
rollup系列不會參考別人的分享(目前也沒找到有人分析rollup。。),完全自食其力一行一行的閱讀,所以難免會有些地方不是很正確。
沒辦法,閱讀別人的程式碼,有些地方就像猜女人的心思,太tm難了,所以有不對的地方希望大佬們多多指點,互相學習。
![](https://user-gold-cdn.xitu.io/2020/3/16/170e39420db7f687?w=228&h=212&f=png&s=46389)
還是那句話,創作不已,希望得到大家的支援,與君共勉,咱們下期見!
![](https://user-gold-cdn.xitu.io/2020/3/16/170e3940b2c98a5f?w=240&h=240&f=jpeg&