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

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

計劃

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

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

TL;DR

一圖勝千言啊!webpack

注意點

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

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

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

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

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

主要通用模塊以及含義

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

代碼解析

  • 兩個方法 三個類

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

首先是主類: 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篇,看到這裏的朋友有什麼想法能夠跟我說下哈。

這期差很少就到這了,說點題外話。

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

效率嗷嗷的啊,一週的活,兩天就幹完了,也有時間幹本身的事情了,那感受不要太爽,哈哈哈

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

又想到一句話:

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

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

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


若是這篇文章對你們有一點點幫助,但願獲得你們的支持,這是我最大的動力,拜了個拜~

相關文章
相關標籤/搜索