原来rollup这么简单之 rollup.watch篇
- 2020 年 3 月 26 日
- 笔记
大家好,我是小雨小雨,致力于分享有趣的、实用的技术文章。
内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步。
大家的支持是我创作的动力。
计划
rollup系列打算一章一章的放出,内容更精简更专一更易于理解
目前打算分为一下几章:
- rollup.rollup
- rollup.generate + rollup.write
- rollup.watch <==== 当前文章
- 具体实现或思想的分析,比如tree shaking、插件的实现等
TL;DR
一图胜千言啊!
注意点
所有的注释都在这里,可自行阅读
!!!提示 => 标有TODO为具体实现细节,会视情况分析。
!!!注意 => 每一个子标题都是父标题(函数)内部实现
!!!强调 => rollup中模块(文件)的id就是文件地址,所以类似resolveID这种就是解析文件地址的意思,我们可以返回我们想返回的文件id(也就是地址,相对路径、决定路径)来让rollup加载
rollup是一个核心,只做最基础的事情,比如提供默认模块(文件)加载机制, 比如打包成不同风格的内容,我们的插件中提供了加载文件路径,解析文件内容(处理ts,sass等)等操作,是一种插拔式的设计,和webpack类似
插拔式是一种非常灵活且可长期迭代更新的设计,这也是一个中大型框架的核心,人多力量大嘛~
主要通用模块以及含义
- Graph: 全局唯一的图,包含入口以及各种依赖的相互关系,操作方法,缓存等。是rollup的核心
- PathTracker: 无副作用模块依赖路径追踪
- PluginDriver: 插件驱动器,调用插件和提供插件环境上下文等
- FileEmitter: 资源操作器
- GlobalScope: 全局作用局,相对的还有局部的
- ModuleLoader: 模块加载器
- 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篇,看到这里的朋友有什么想法可以跟我说下哈。
这期差不多就到这了,说点题外话。
时间飞快,’被寒假’估计就要结束了,之前一直想要是能在家里办公可太棒了,现在也是体验了一把,怎么硕呢..
效率嗷嗷的啊,一周的活,两天就干完了,也有时间干自己的事情了,那感觉不要太爽,哈哈哈
估计有这种想法的人数应该也有一部分,搞不好以后就有云办公了,人人都是外包公司
(狗头保命
又想到一句话:
夫钝兵挫锐,屈力殚货,则诸侯乘其弊而起,虽有智者,不能善其后矣。故兵闻拙速,未睹巧之久也。
其中的拙速
,曾国藩理解为准备要慢,动手要快。
说的很对,我们对待每个需求都应该这样,准备要充分,干活要麻利,然而在公司的时候,或许并不都是这样的。
如果这篇文章对大家有一点点帮助,希望得到大家的支持,这是我最大的动力,拜了个拜~