zoukankan      html  css  js  c++  java
  • 原来rollup这么简单之 rollup.watch篇

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

    计划

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

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

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

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

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

    又想到一句话:

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

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

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


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

  • 相关阅读:
    一.创建型模式 Factory
    Tcp/Ip I/O函数
    Tcp/Ip协议理解_简单实例
    Tcp/Ip协议理解_3
    Tcp/Ip协议理解_2
    Tcp/Ip协议理解_1
    abp+angular+bootstrap-table的使用
    Abp mvc angular 添加视图
    Abp添加菜单
    JS 获取一串路径中的文件名称
  • 原文地址:https://www.cnblogs.com/xiaoyuxy/p/12574632.html
Copyright © 2011-2022 走看看