大家好,我是小雨小雨,致力于分享有趣的、实用的技术文章。

内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步。

大家的支持是我创作的动力。

计划

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篇,看到这里的朋友有什么想法可以跟我说下哈。

这期差不多就到这了,说点题外话。

时间飞快,'被寒假'估计就要结束了,之前一直想要是能在家里办公可太棒了,现在也是体验了一把,怎么硕呢..

效率嗷嗷的啊,一周的活,两天就干完了,也有时间干自己的事情了,那感觉不要太爽,哈哈哈

估计有这种想法的人数应该也有一部分,搞不好以后就有云办公了,人人都是外包公司 (狗头保命

又想到一句话:

夫钝兵挫锐,屈力殚货,则诸侯乘其弊而起,虽有智者,不能善其后矣。故兵闻拙速,未睹巧之久也。

其中的拙速,曾国藩理解为准备要慢,动手要快。

说的很对,我们对待每个需求都应该这样,准备要充分,干活要麻利,然而在公司的时候,或许并不都是这样的。


如果这篇文章对大家有一点点帮助,希望得到大家的支持,这是我最大的动力,拜了个拜~

原来rollup这么简单之 rollup.watch篇的更多相关文章

  1. 原来rollup这么简单之 rollup.generate + rollup.write篇

    大家好,我是小雨小雨,致力于分享有趣的.实用的技术文章. 内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步. 分享不易,希望能够得到大家的支持和关注. 计划 rollup系列打算 ...

  2. 原来rollup这么简单之 tree shaking篇

    大家好,我是小雨小雨,致力于分享有趣的.实用的技术文章. 内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步. 分享不易,希望能够得到大家的支持和关注. 计划 rollup系列打算 ...

  3. 原来rollup这么简单之 rollup.rollup篇

    大家好,我是小雨小雨,致力于分享有趣的.实用的技术文章. 内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步. 分享不易,希望能够得到大家的支持和关注. 计划 rollup系列打算 ...

  4. 手把手教你从零写一个简单的 VUE--模板篇

    教程目录1.手把手教你从零写一个简单的 VUE2.手把手教你从零写一个简单的 VUE--模板篇 Hello,我又回来了,上一次的文章教会了大家如何书写一个简单 VUE,里面实现了VUE 的数据驱动视图 ...

  5. 原来rollup这么简单之插件篇

    大家好,我是小雨小雨,致力于分享有趣的.实用的技术文章. 内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步. 大家的支持是我创作的动力. 计划 rollup系列打算一章一章的放出 ...

  6. salesforce零基础学习(七十九)简单排序浅谈 篇一

    我们在程序中经常需要对数据列表进行排序,有时候使用SOQL的order by 不一定能完全符合需求,需要对数据进行排序,排序可以有多种方式,不同的方式针对不同的场景.篇一只是简单的描述一下选择排序,插 ...

  7. 基于ABP做一个简单的系统——实战篇:1.项目准备

    现阶段需要做一个小项目,体量很小,业务功能比较简单,就想到用最熟悉的.net来做,更何况现在.net core已经跨平台,也可以在linux服务器上部署.所以决定用.net core 3.1+mysq ...

  8. 基于ABP做一个简单的系统——实战篇:2.代码生成器

    上一篇正说着呢,代码生成器就来了. 1.适用于ABP官网的Startup Template V3.x的包含了登录.用户等页面的MPA应用模板2.当前view仅支持文本框生成,远期规划根据字段类型生成不 ...

  9. 简单工厂模式-Java篇

    简单工厂模式就是考虑如何实例化对象的问题,就是说到底要实例化谁,将来会不会增加实例化对象,比如计算器类中增加开根元素,应该考虑用一个单独的类来创造实例的过程,这就是工厂.下面将利用计算器类举例,解释简 ...

随机推荐

  1. 那些被刻意“阉割”的名人名言

    "天才是百分之一的灵感,百分之九十九的汗水",这句名言大家都知道的吧!不过还有好多人不知道的是这句名言还有后半句:"但百分之一的灵感甚至比百分之九十九的汗水更重要.&qu ...

  2. 2020 倒计时 1 天,Python 工程师找工作更难了?

    Python 是最神奇的编程语言. 无意引战,我说的是"神奇",不是"最好",并不想去"撼动" PHP 的地位.               ...

  3. fiddler 针对单个接口打断点

    在命令行输入相关指令: 以慕课网为例: 请求前设置断点:bpu 实例: bpu https://www.imooc.com/index/getstarlist 请求 https://www.imooc ...

  4. 阿里投资Magic Leap 是美酒还是毒药?

    Leap 是美酒还是毒药?" title="阿里投资Magic Leap 是美酒还是毒药?"> 土豪阿里又摊上"大事"了!但这次不是让人头痛的假 ...

  5. 事务Transaction

    目录 为什么写这系列的文章 事务概念 ACID 并发事务导致的问题 脏读(Dirty Read) 非重复读(Nonrepeatable Read) 幻读(Phantom Reads) 丢失修改(Los ...

  6. 使用QT绘制一个多边形

    目录 1. 概述 2. 实现 2.1. 代码 2.2. 解析 3. 结果 1. 概述 可以通过QT的重绘事件和鼠标事件来绘制多边形,最简单的办法就是在继承QWidget的窗体中重写paintEvent ...

  7. js判断PC端还是移动端的代码小坑

    我在写官网的时候做了pc和移动端两个,在通过网上查找了这样的代码,看着完全没问题,等放进去页面中后,PC端页面一直刷新,根本停不下来,找了类似js还是同样的问题.通过不断尝试后才发现,问题就是多了一行 ...

  8. 前端面试题-<!DOCTYPE>

    现在的各种前端开发工具都足够强大,支持插入模板代码,也就导致我们往往会忽略已经自动生成的代码,而代码的第一行 DOCTYPE 声明,就是最容易忽略的部分. 一.DOCTYPE DOCTYPE 是 do ...

  9. Linux进程操作记录

    关于Gunicorn如何终止进程: 1.用进程树显示主进程PID: pstree -ap | grep gunicorn 2.如果有daemon进程无法用kill -9删除(可能是因为daemon屏蔽 ...

  10. 小白学 Python 数据分析(15):数据可视化概述

    人生苦短,我用 Python 前文传送门: 小白学 Python 数据分析(1):数据分析基础 小白学 Python 数据分析(2):Pandas (一)概述 小白学 Python 数据分析(3):P ...