你們好,我是小雨小雨,致力於分享有趣的、實用的技術文章。 內容分爲翻譯和原創,若是有問題,歡迎隨時評論或私信,但願和你們一塊兒進步。 你們的支持是我創做的動力。javascript
rollup系列打算一章一章的放出,內容更精簡更專注更易於理解java
目前打算分爲如下幾章:node
一圖勝千言啊!webpack
全部的註釋都在這裏,可自行閱讀git
!!!提示 => 標有TODO爲具體實現細節,會視狀況分析。github
!!!注意 => 每個子標題都是父標題(函數)內部實現web
!!!強調 => rollup中模塊(文件)的id就是文件地址,因此相似resolveID這種就是解析文件地址的意思,咱們能夠返回咱們想返回的文件id(也就是地址,相對路徑、決定路徑)來讓rollup加載promise
rollup是一個核心,只作最基礎的事情,好比提供默認模塊(文件)加載機制, 好比打包成不一樣風格的內容,咱們的插件中提供了加載文件路徑,解析文件內容(處理ts,sass等)等操做,是一種插拔式的設計,和webpack相似 插拔式是一種很是靈活且可長期迭代更新的設計,這也是一箇中大型框架的核心,人多力量大嘛~緩存
沒錯,主要就五個點,每一個點各司其職,條修葉貫,妙啊~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篇,看到這裏的朋友有什麼想法能夠跟我說下哈。
這期差很少就到這了,說點題外話。
時間飛快,'被寒假'估計就要結束了,以前一直想要是能在家裏辦公可太棒了,如今也是體驗了一把,怎麼碩呢..
效率嗷嗷的啊,一週的活,兩天就幹完了,也有時間幹本身的事情了,那感受不要太爽,哈哈哈
估計有這種想法的人數應該也有一部分,搞很差之後就有云辦公了,人人都是外包公司
(狗頭保命
又想到一句話:
夫鈍兵挫銳,屈力殫貨,則諸侯乘其弊而起,雖有智者,不能善其後矣。故兵聞拙速,未睹巧之久也。
其中的拙速
,曾國藩理解爲準備要慢,動手要快。
說的很對,咱們對待每一個需求都應該這樣,準備要充分,幹活要麻利,然而在公司的時候,或許並不都是這樣的。
若是這篇文章對你們有一點點幫助,但願獲得你們的支持,這是我最大的動力,拜了個拜~