原来rollup这么简单之 rollup.watch篇
大家好,我是小雨小雨,致力于分享有趣的、实用的技术文章。
内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步。
大家的支持是我创作的动力。
计划
rollup系列打算一章一章的放出,内容更精简更专一更易于理解
目前打算分为以下几章:
- rollup.rollup
- rollup.generate + rollup.write
- rollup.watch <==== 当前文章
- tree shaking
- plugins
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篇,看到这里的朋友有什么想法可以跟我说下哈。
这期差不多就到这了,说点题外话。
时间飞快,'被寒假'估计就要结束了,之前一直想要是能在家里办公可太棒了,现在也是体验了一把,怎么硕呢..
效率嗷嗷的啊,一周的活,两天就干完了,也有时间干自己的事情了,那感觉不要太爽,哈哈哈
估计有这种想法的人数应该也有一部分,搞不好以后就有云办公了,人人都是外包公司 (狗头保命
又想到一句话:
夫钝兵挫锐,屈力殚货,则诸侯乘其弊而起,虽有智者,不能善其后矣。故兵闻拙速,未睹巧之久也。
其中的拙速,曾国藩理解为准备要慢,动手要快。
说的很对,我们对待每个需求都应该这样,准备要充分,干活要麻利,然而在公司的时候,或许并不都是这样的。
如果这篇文章对大家有一点点帮助,希望得到大家的支持,这是我最大的动力,拜了个拜~
原来rollup这么简单之 rollup.watch篇的更多相关文章
- 原来rollup这么简单之 rollup.generate + rollup.write篇
大家好,我是小雨小雨,致力于分享有趣的.实用的技术文章. 内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步. 分享不易,希望能够得到大家的支持和关注. 计划 rollup系列打算 ...
- 原来rollup这么简单之 tree shaking篇
大家好,我是小雨小雨,致力于分享有趣的.实用的技术文章. 内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步. 分享不易,希望能够得到大家的支持和关注. 计划 rollup系列打算 ...
- 原来rollup这么简单之 rollup.rollup篇
大家好,我是小雨小雨,致力于分享有趣的.实用的技术文章. 内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步. 分享不易,希望能够得到大家的支持和关注. 计划 rollup系列打算 ...
- 手把手教你从零写一个简单的 VUE--模板篇
教程目录1.手把手教你从零写一个简单的 VUE2.手把手教你从零写一个简单的 VUE--模板篇 Hello,我又回来了,上一次的文章教会了大家如何书写一个简单 VUE,里面实现了VUE 的数据驱动视图 ...
- 原来rollup这么简单之插件篇
大家好,我是小雨小雨,致力于分享有趣的.实用的技术文章. 内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步. 大家的支持是我创作的动力. 计划 rollup系列打算一章一章的放出 ...
- salesforce零基础学习(七十九)简单排序浅谈 篇一
我们在程序中经常需要对数据列表进行排序,有时候使用SOQL的order by 不一定能完全符合需求,需要对数据进行排序,排序可以有多种方式,不同的方式针对不同的场景.篇一只是简单的描述一下选择排序,插 ...
- 基于ABP做一个简单的系统——实战篇:1.项目准备
现阶段需要做一个小项目,体量很小,业务功能比较简单,就想到用最熟悉的.net来做,更何况现在.net core已经跨平台,也可以在linux服务器上部署.所以决定用.net core 3.1+mysq ...
- 基于ABP做一个简单的系统——实战篇:2.代码生成器
上一篇正说着呢,代码生成器就来了. 1.适用于ABP官网的Startup Template V3.x的包含了登录.用户等页面的MPA应用模板2.当前view仅支持文本框生成,远期规划根据字段类型生成不 ...
- 简单工厂模式-Java篇
简单工厂模式就是考虑如何实例化对象的问题,就是说到底要实例化谁,将来会不会增加实例化对象,比如计算器类中增加开根元素,应该考虑用一个单独的类来创造实例的过程,这就是工厂.下面将利用计算器类举例,解释简 ...
随机推荐
- 那些被刻意“阉割”的名人名言
"天才是百分之一的灵感,百分之九十九的汗水",这句名言大家都知道的吧!不过还有好多人不知道的是这句名言还有后半句:"但百分之一的灵感甚至比百分之九十九的汗水更重要.&qu ...
- 2020 倒计时 1 天,Python 工程师找工作更难了?
Python 是最神奇的编程语言. 无意引战,我说的是"神奇",不是"最好",并不想去"撼动" PHP 的地位. ...
- fiddler 针对单个接口打断点
在命令行输入相关指令: 以慕课网为例: 请求前设置断点:bpu 实例: bpu https://www.imooc.com/index/getstarlist 请求 https://www.imooc ...
- 阿里投资Magic Leap 是美酒还是毒药?
Leap 是美酒还是毒药?" title="阿里投资Magic Leap 是美酒还是毒药?"> 土豪阿里又摊上"大事"了!但这次不是让人头痛的假 ...
- 事务Transaction
目录 为什么写这系列的文章 事务概念 ACID 并发事务导致的问题 脏读(Dirty Read) 非重复读(Nonrepeatable Read) 幻读(Phantom Reads) 丢失修改(Los ...
- 使用QT绘制一个多边形
目录 1. 概述 2. 实现 2.1. 代码 2.2. 解析 3. 结果 1. 概述 可以通过QT的重绘事件和鼠标事件来绘制多边形,最简单的办法就是在继承QWidget的窗体中重写paintEvent ...
- js判断PC端还是移动端的代码小坑
我在写官网的时候做了pc和移动端两个,在通过网上查找了这样的代码,看着完全没问题,等放进去页面中后,PC端页面一直刷新,根本停不下来,找了类似js还是同样的问题.通过不断尝试后才发现,问题就是多了一行 ...
- 前端面试题-<!DOCTYPE>
现在的各种前端开发工具都足够强大,支持插入模板代码,也就导致我们往往会忽略已经自动生成的代码,而代码的第一行 DOCTYPE 声明,就是最容易忽略的部分. 一.DOCTYPE DOCTYPE 是 do ...
- Linux进程操作记录
关于Gunicorn如何终止进程: 1.用进程树显示主进程PID: pstree -ap | grep gunicorn 2.如果有daemon进程无法用kill -9删除(可能是因为daemon屏蔽 ...
- 小白学 Python 数据分析(15):数据可视化概述
人生苦短,我用 Python 前文传送门: 小白学 Python 数据分析(1):数据分析基础 小白学 Python 数据分析(2):Pandas (一)概述 小白学 Python 数据分析(3):P ...