前言
众所周知 对开发者来说webpack-dev-server最常用的功能就是热更新
而想要实现热更新,首先就需要一个文件监视系统,监视src文件夹下文件的变化。
今天来扒一扒webpack的热更新代码,手写一个文件监视模块。
Watch-pack
webpack中大多数的工具都是官方团队自己实现的,并没有使用第三方库或者是node自带的API,正所谓力量一定要掌握在自己手里
PS: node自带的fs.watch() fs.watchFile()是真的不好用!
今天我们来手写一下watch-pack,理解原理,获得力量。
基本逻辑
- 遍历文件树,监视需要的文件。
- 定义文件变化后的事件(onChang,onCreate,onRemove)
- 启动轮询扫描器,setInterval 持续扫描文件
- 文件发生变化,执行对应的事件
架构说明
结构很简单 一个Dirctory Watcher上保存有多个File Watcher 递归遍历文件树,每个需要监视的文件创建一个File Watcher即可
EventEmitter(node的事件系统)
文件变化时,需要执行对应的事件,这里使用了node的事件系统EventEmitter
当类继承了EventEmitter后 ,就可以自定义事件,并触发
const EventEmitter = require('events');
class Watcher extends EventEmitter(){...};
const watcher= new Watcher();
// 给watcher注册一个事件A
watcher.on('事件A',(arg1,arg2...)=>{
console.log('执行事件A')
})
// 触发事件A
watcher.emit('事件A',arg1,arg2)
Dirctory Watcher
首先我们需要一个Dirctory Watcher,(下面简称D Watcher) 主要功能
- 保存所有的File Watcher
- 递归文件树,给每个文件创建File Watcher
- 遍历扫描所有的File Watcher
- 提供文件变化后的事件(EventEmitter)
废话不多说 上代码
class DirectoryWatcher extends EventEmitter {
constructor(option) {
super();
this.fileList = option.fileList || []; // 需要监听的文件夹
this.directoryList = option.directoryList || [];// 需要监听的文件夹
this.watchers = new Map(); // map保存File Watcher
this.pause = false; // 启动/终止扫描
this.poll = (typeof option.poll === "number") ? option.poll : 1000; //扫描间隔(每秒扫描一次)
this.scanTimeout = undefined;
this.scanTime = 0
}
//----递归收集文件夹下所有的文件-----
collectFiles(pathList) {
const files = []
const cycleFn = (pathList) => {
pathList.forEach(p => {
const stat = fs.statSync(p)
if (stat.isFile()) {
files.push(p)
}
if (stat.isDirectory()) {
const childPathList = fs.readdirSync(p).map((childPath) => {
return path.join(p, childPath)
})
cycleFn(childPathList)
}
})
}
cycleFn(pathList)
return files
}
//---- 检查哪些文件需要挂载watcher---- 判断前后两个list是否有变化
checkNeedWatcherFiles(){
const newFileList = this.collectFiles(this.directoryList);
const needWatcherFiles = getArrDifference(this.fileList, newFileList);
this.fileList = newFileList
return needWatcherFiles
}
//todo 更新watchers
updateWatchers() {
const needWatcherFiles = this.checkNeedWatcherFiles()
needWatcherFiles.forEach((path) => {
this.watchers.set(path, new Watcher(this, path))
})
}
//todo 进行单次扫描
doScan() {
if (this.pause) return
this.updateWatchers()
this.forEachWatchers()
this.scanTime++
}
// 遍历检查所有的watchers,是否有文件变化
forEachWatchers() {
this.watchers.forEach((w, key) => {
w.checkEvent()
})
}
//todo 进行监视 开启interval轮询扫描
watch() {
console.log('--------------正在监视--------------------');
this.scanInerval = setInterval(() => {
this.doScan()
}, this.poll)
}
//todo 停止扫描
stopWatch() {
clearInterval(this.scanInerval)
}
}
Watcher
watcher用于监测单个文件前后是否发生变化,执行对应的事件
我们这里使用fs.lstat()方法,获取文件的属性, 主要获取保存的时间ctimeMs,通过判断文件保存时间来判断文件是否变化,更新或者删除
(这种方法只要文件保存后就会执行)
lstat方法属于计算机底层方法,速度很快,我的电脑扫描上千个文件仅用20ms,对于项目来说足够。
const fs = require('fs')
const EventEmitter = require('events')
class Watcher extends EventEmitter {
constructor(directoryWatcher, path) {
super();
this.filePath = path; // 文件绝对路径
this.saveTime = 1; // 文件保存的时间
this.directoryWatcher = directoryWatcher
}
checkEvent() {
fs.lstat(this.filePath, (err, state) => {
if (!this.saveTime && !state) return console.error(`文件${this.filePath}不存在`)
let saveTime = Math.floor(state?.ctimeMs)
//TODO 如果文件被删除 触发remove事件并删除该watcher
if (this.filePath && !state) {
this.directoryWatcher.emit('remove', this.filePath, 'remove')
this.directoryWatcher.watchers.delete(this.filePath)
return
}
//TODO 文件添加
if (this.saveTime === 1 && this.directoryWatcher.scanTime > 1) {
this.directoryWatcher.emit('create', this.filePath, 'create')
}
//TODO 文件修改 触发change事件
if (this.saveTime !== 1 && saveTime !== this.saveTime) {
this.directoryWatcher.emit('change', this.filePath, 'change')
}
this.saveTime = saveTime
})
}
}
Scan
- 从DirectoryWatcher可以看出, 启动watch后会执行setTimeout,轮询scan
- 以下为单次scan的流程
上代码
doScan() {
if (this.pause) return
this.updateWatchers()
this.forEachWatchers()
this.scanTime++
}
forEachWatchers() { // 遍历检查所有的file watcher
this.watchers.forEach((w, key) => {
w.checkEvent()
})
}
启动监听
最后我们执行一下watch方法,监听一个文件夹
function watchDir(path){
// 新建一个watcher
const watcher = new DirectoryWatcher({
directoryList: [srcPath],
poll: 1000 // 轮询时间
})
// 定义文件变化事件
this.watcher.on('change', (path) => {
console.log(`file ${path} change`);
}
this.watcher.on('create', (path) => {
console.log(`file ${path} create`);
}
this.watcher.on('remove', (path) => {
console.log(`file ${path} remove`);
}
})
// 启动监听
this.watcher.watch()
}
结束
创作不易,喜欢的大佬们给个赞就是对小弟的支持。
最近在手写react Query 强推一下本人的react Query源码分析专栏,最后想自己创造一个简易版的react Query进行使用, 全网最细 React-Query源码探秘 - 不月阳九的专栏 - 掘金 (juejin.cn)