手把手教你写一个Webpack的文件监视系统

427 阅读3分钟

前言

众所周知 对开发者来说webpack-dev-server最常用的功能就是热更新

而想要实现热更新,首先就需要一个文件监视系统,监视src文件夹下文件的变化。

今天来扒一扒webpack的热更新代码,手写一个文件监视模块。

Watch-pack

webpack中大多数的工具都是官方团队自己实现的,并没有使用第三方库或者是node自带的API,正所谓力量一定要掌握在自己手里

PS: node自带的fs.watch() fs.watchFile()是真的不好用!

今天我们来手写一下watch-pack,理解原理,获得力量。

基本逻辑

  1. 遍历文件树,监视需要的文件。
  2. 定义文件变化后的事件(onChang,onCreate,onRemove)
  3. 启动轮询扫描器,setInterval 持续扫描文件
  4. 文件发生变化,执行对应的事件

image.png

架构说明

image.png

结构很简单 一个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) 主要功能

  1. 保存所有的File Watcher
  2. 递归文件树,给每个文件创建File Watcher
  3. 遍历扫描所有的File Watcher
  4. 提供文件变化后的事件(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

  1. 从DirectoryWatcher可以看出, 启动watch后会执行setTimeout,轮询scan
  2. 以下为单次scan的流程

image.png

上代码

  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)