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

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

计划

rollup系列打算一章一章的放出,内容更精简更专一更易于理解

目前打算分为以下几章:

TL;DR

一图胜千言啊!

image

注意点

所有的注释都在这里,可自行阅读

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

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

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

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

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

又想到一句话:

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

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

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


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

image
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,376评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,126评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,966评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,432评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,519评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,792评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,933评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,701评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,143评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,488评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,626评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,292评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,896评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,494评论 2 348

推荐阅读更多精彩内容

  • 大家好,我是小雨小雨,致力于分享有趣的、实用的技术文章。内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和...
    小雨小雨丶阅读 1,422评论 0 0
  • 对网站资源进行优化,并使用不同浏览器测试并不是网站设计过程中最有意思的部分,但是这个过程中的很多重复的任务能够使用...
    懵逼js阅读 1,061评论 0 8
  • 编辑于2015年 转载自某作者的译文 作者要是看到请联系我注明出处 对网站资源进行优化,并使用不同浏览器测试并不是...
    krock01阅读 447评论 0 2
  • 模块化 模块化是指把一个复杂的系统分解到多个模块以方便编码。 缺点 命名空间冲突,两个库可能会使用同一个名称,例如...
    Upcccz阅读 588评论 0 3
  • 作者:Midaoi原文地址:http://www.cnblogs.com/Midaoi/p/4969812.htm...
    IT程序狮阅读 845评论 0 5