原來rollup這麼簡單之 rollup.watch篇

  • 2020 年 3 月 26 日
  • 筆記

大家好,我是小雨小雨,致力於分享有趣的、實用的技術文章。
內容分為翻譯和原創,如果有問題,歡迎隨時評論或私信,希望和大家一起進步。
大家的支援是我創作的動力。

計劃

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

目前打算分為一下幾章:

TL;DR

一圖勝千言啊!

注意點

所有的注釋都在這裡,可自行閱讀

!!!提示 => 標有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等)的構造基類

程式碼解析

  • 兩個方法 三個類

沒錯,主要就五個點,每個點各司其職,條修葉貫,妙啊~

首先是主類: Watcher,獲取用戶傳遞的配置,然後創建task實例,然後再下一次事件輪詢的時候調用watcher實例的run方法啟動rollup構建。
Watcher返回emitter對象,除了供用戶添加鉤子函數外,還提供關閉watcher的功能。

class Watcher {  	constructor(configs: GenericConfigObject[] | GenericConfigObject) {  		this.emitter = new (class extends EventEmitter {  			close: () => void;  			constructor(close: () => void) {  				super();                  // 供用戶關閉使  				this.close = close;  				// 不警告  				// Allows more than 10 bundles to be watched without  				// showing the `MaxListenersExceededWarning` to the user.  				this.setMaxListeners(Infinity);  			}  		})(this.close.bind(this)) as RollupWatcher;    		this.tasks = (Array.isArray(configs) ? configs : configs ? [configs] : []).map(  			config => new Task(this, config) // 一個配置入口一個任務,串列執行  		);  		this.running = true;  		process.nextTick(() => this.run());  	}        private run() {  		this.running = true;    		// 當emit 'event' 事件的時候,統一是傳遞給cli使用,通過code區別不同的執行環節,相當於鉤子函數,我們也可以使用增加監聽event事件來做我們想做的事  		this.emit('event', {  			code: 'START'  		});            // 初始化promise  		let taskPromise = Promise.resolve();          // 串列執行task  		for (const task of this.tasks) taskPromise = taskPromise.then(() => task.run());    		return taskPromise  			.then(() => {  				this.running = false;    				this.emit('event', {  					code: 'END'  				});  			})  			.catch(error => {  				this.running = false;  				this.emit('event', {  					code: 'ERROR',  					error  				});  			})  			.then(() => {  				if (this.rerun) {  					this.rerun = false;  					this.invalidate();  				}  			});  	}  }  

然後是Task,任務類,用來執行rollup構建任務,功能單一。當我們上面new Task的時候,會通過Task的構造函數初始化配置,以供rollup構建使用,其中有input配置、output配置、chokidar配置和用戶過濾的文件。
當執行task.run()的時候會進行rollup構建,並通過構建結果快取每一個task,供文件變動時重新構建或監聽關閉時刪除任務。

class Task {  	constructor(watcher: Watcher, config: GenericConfigObject) {  		// 獲取Watch實例  		this.watcher = watcher;    		this.closed = false;  		this.watched = new Set();    		const { inputOptions, outputOptions } = mergeOptions({  			config  		});  		this.inputOptions = inputOptions;    		this.outputs = outputOptions;  		this.outputFiles = this.outputs.map(output => {  			if (output.file || output.dir) return path.resolve(output.file || output.dir!);  			return undefined as any;  		});    		const watchOptions: WatcherOptions = inputOptions.watch || {};  		if ('useChokidar' in watchOptions)  			(watchOptions as any).chokidar = (watchOptions as any).useChokidar;    		let chokidarOptions = 'chokidar' in watchOptions ? watchOptions.chokidar : !!chokidar;    		if (chokidarOptions) {  			chokidarOptions = {  				...(chokidarOptions === true ? {} : chokidarOptions),  				disableGlobbing: true,  				ignoreInitial: true  			};  		}    		if (chokidarOptions && !chokidar) {  			throw new Error(  				`watch.chokidar was provided, but chokidar could not be found. Have you installed it?`  			);  		}    		this.chokidarOptions = chokidarOptions as WatchOptions;  		this.chokidarOptionsHash = JSON.stringify(chokidarOptions);    		this.filter = createFilter(watchOptions.include, watchOptions.exclude);  	}        // 關閉:清理task  	close() {  		this.closed = true;  		for (const id of this.watched) {  			deleteTask(id, this, this.chokidarOptionsHash);  		}  	}    	invalidate(id: string, isTransformDependency: boolean) {  		this.invalidated = true;  		if (isTransformDependency) {  			for (const module of this.cache.modules) {  				if (module.transformDependencies.indexOf(id) === -1) continue;  				// effective invalidation  				module.originalCode = null as any;  			}  		}  		// 再調用watcher上的invalidate  		this.watcher.invalidate(id);  	}    	run() {          // 節流  		if (!this.invalidated) return;  		this.invalidated = false;    		const options = {  			...this.inputOptions,  			cache: this.cache  		};    		const start = Date.now();            // 鉤子  		this.watcher.emit('event', {  			code: 'BUNDLE_START',  			input: this.inputOptions.input,  			output: this.outputFiles  		});            // 傳遞watcher實例,供rollup方法監聽change和restart的觸發,進而觸發watchChange鉤子  		setWatcher(this.watcher.emitter);  		return rollup(options)  			.then(result => {  				if (this.closed) return undefined as any;  				this.updateWatchedFiles(result);  				return Promise.all(this.outputs.map(output => result.write(output))).then(() => result);  			})  			.then((result: RollupBuild) => {  				this.watcher.emit('event', {  					code: 'BUNDLE_END',  					duration: Date.now() - start,  					input: this.inputOptions.input,  					output: this.outputFiles,  					result  				});  			})  			.catch((error: RollupError) => {  				if (this.closed) return;    				if (Array.isArray(error.watchFiles)) {  					for (const id of error.watchFiles) {  						this.watchFile(id);  					}  				}  				if (error.id) {  					this.cache.modules = this.cache.modules.filter(module => module.id !== error.id);  				}  				throw error;  			});  	}    	private updateWatchedFiles(result: RollupBuild) {  		// 上一次的監聽set  		const previouslyWatched = this.watched;  		// 新建監聽set  		this.watched = new Set();  		// 構建的時候獲取的監聽文件,賦給watchFiles  		this.watchFiles = result.watchFiles;  		this.cache = result.cache;  		// 將監聽的文件添加到監聽set中  		for (const id of this.watchFiles) {  			this.watchFile(id);  		}  		for (const module of this.cache.modules) {  			for (const depId of module.transformDependencies) {  				this.watchFile(depId, true);  			}  		}  		// 上次監聽的文件,這次沒有的話,刪除任務  		for (const id of previouslyWatched) {  			if (!this.watched.has(id)) deleteTask(id, this, this.chokidarOptionsHash);  		}  	}    	private watchFile(id: string, isTransformDependency = false) {  		if (!this.filter(id)) return;  		this.watched.add(id);    		if (this.outputFiles.some(file => file === id)) {  			throw new Error('Cannot import the generated bundle');  		}    		// 增加任務  		// this is necessary to ensure that any 'renamed' files  		// continue to be watched following an error  		addTask(id, this, this.chokidarOptions, this.chokidarOptionsHash, isTransformDependency);  	}  }  

到目前為止,我們知道了執行rollup.watch的時候執行了什麼,但是當我們修改文件的時候,rollup又是如何監聽變化進行rebuild的呢?

這就涉及標題中說的兩個方法,一個是addTask,一個是deleteTask,兩個方法很簡單,就是進行任務的增刪操作,這裡不做解釋,自行翻閱。add新建一個task,新建的時候回調用最後一個未提及的類: FileWatcher,沒錯,這就是用來監聽變化的。

FileWatcher初始化監聽任務,使用chokidar或node內置的fs.watch容錯進行文件監聽,使用哪個取決於有沒有傳遞chokidarOptions。

// addTask的時候  const watcher = group.get(id) || new FileWatcher(id, chokidarOptions, group);  

當有文件變化的時候,會觸發invalidate方法

invalidate(id: string, isTransformDependency: boolean) {      this.invalidated = true;      if (isTransformDependency) {          for (const module of this.cache.modules) {              if (module.transformDependencies.indexOf(id) === -1) continue;              // effective invalidation              module.originalCode = null as any;          }      }      // 再調用watcher上的invalidate      this.watcher.invalidate(id);  }  

watcher上的invalidate方法

invalidate(id?: string) {      if (id) {          this.invalidatedIds.add(id);      }  	// 防止刷刷刷      if (this.running) {          this.rerun = true;          return;      }    	// clear pre      if (this.buildTimeout) clearTimeout(this.buildTimeout);        this.buildTimeout = setTimeout(() => {          this.buildTimeout = null;          for (const id of this.invalidatedIds) {              // 觸發rollup.rollup中監聽的事件              this.emit('change', id);          }          this.invalidatedIds.clear();          // 觸發rollup.rollup中監聽的事件          this.emit('restart');          // 又走了一遍構建          this.run();      }, DELAY);  }  

FileWatcher類如下,可自行閱讀

  class FileWatcher {    	constructor(id: string, chokidarOptions: WatchOptions, group: Map<string, FileWatcher>) {  		this.id = id;  		this.tasks = new Set();  		this.transformDependencyTasks = new Set();    		let modifiedTime: number;    		// 文件狀態  		try {  			const stats = fs.statSync(id);  			modifiedTime = +stats.mtime;  		} catch (err) {  			if (err.code === 'ENOENT') {  				// can't watch files that don't exist (e.g. injected  				// by plugins somehow)  				return;  			}  			throw err;  		}    		// 處理文件不同的更新狀態  		const handleWatchEvent = (event: string) => {  			if (event === 'rename' || event === 'unlink') {  				// 重命名 link時觸發  				this.close();  				group.delete(id);  				this.trigger(id);  				return;  			} else {  				let stats: fs.Stats;  				try {  					stats = fs.statSync(id);  				} catch (err) {  					// 文件找不到的時候  					if (err.code === 'ENOENT') {  						modifiedTime = -1;  						this.trigger(id);  						return;  					}  					throw err;  				}  				// 重新觸發構建,且避免多次重複操作  				// debounce  				if (+stats.mtime - modifiedTime > 15) this.trigger(id);  			}  		};    		// 通過handleWatchEvent處理所有文件更新狀態  		this.fsWatcher = chokidarOptions  			? chokidar.watch(id, chokidarOptions).on('all', handleWatchEvent)  			: fs.watch(id, opts, handleWatchEvent);    		group.set(id, this);  	}    	addTask(task: Task, isTransformDependency: boolean) {  		if (isTransformDependency) this.transformDependencyTasks.add(task);  		else this.tasks.add(task);  	}    	close() {  		// 關閉文件監聽  		if (this.fsWatcher) this.fsWatcher.close();  	}    	deleteTask(task: Task, group: Map<string, FileWatcher>) {  		let deleted = this.tasks.delete(task);  		deleted = this.transformDependencyTasks.delete(task) || deleted;    		if (deleted && this.tasks.size === 0 && this.transformDependencyTasks.size === 0) {  			group.delete(this.id);  			this.close();  		}  	}    	trigger(id: string) {  		for (const task of this.tasks) {  			task.invalidate(id, false);  		}  		for (const task of this.transformDependencyTasks) {  			task.invalidate(id, true);  		}  	}  }  

總結

rollup的watch功能還是很清晰的,值得我們借鑒學習,但是他並沒有把內容打進記憶體中,而是直接生成,相比來說速度會略遜一籌,不過這個或許已有插件支援,這裡不做討論,我們懂得他是怎麼運動的,想加東西信手拈來的,干就完了,小夥伴們。

下一期在猶豫出什麼,是插件篇還是tree shaking篇,看到這裡的朋友有什麼想法可以跟我說下哈。

這期差不多就到這了,說點題外話。

時間飛快,’被寒假’估計就要結束了,之前一直想要是能在家裡辦公可太棒了,現在也是體驗了一把,怎麼碩呢..

效率嗷嗷的啊,一周的活,兩天就幹完了,也有時間干自己的事情了,那感覺不要太爽,哈哈哈

估計有這種想法的人數應該也有一部分,搞不好以後就有雲辦公了,人人都是外包公司 (狗頭保命

又想到一句話:

夫鈍兵挫銳,屈力殫貨,則諸侯乘其弊而起,雖有智者,不能善其後矣。故兵聞拙速,未睹巧之久也。

其中的拙速,曾國藩理解為準備要慢,動手要快。

說的很對,我們對待每個需求都應該這樣,準備要充分,幹活要麻利,然而在公司的時候,或許並不都是這樣的。


如果這篇文章對大家有一點點幫助,希望得到大家的支援,這是我最大的動力,拜了個拜~