浅析webpack源码之NodeEnvironmentPlugin(一)

1,475 阅读3分钟

1、Compiler

Compiler 模块是 webpack 的支柱引擎,它通过 CLI 或 Node API 传递的所有选项,创建出一个 compilation 实例。它扩展(extend)自 Tapable 类,以便注册和调用插件。大多数面向用户的插件首会先在 Compiler 上注册。

其中NodeEnvironmentPlugin是Compiler中比较重要的一个模块,主要负责文件I/O还有监听文件内容改变
以下是一张简易的编译流程图,概括了NodeEnvironmentPlugin的关键作用

1.2、NodeEnvironmentPlugin

NodeEnvironmentPlugin由四个基本的文件系统组成,分别是inputFileSystem与outputFileSystem处理文件i/o,watchFileSystem监听文件改动,intermediateFileSystem则处理所有不被看做是输入或输出文件系统操作,比如写记录,缓存或者分析输出。 outputFileSystem与intermediateFileSystem就是普通的fs,inputFileSystem与watchFileSystem是经过优化后的fs。

1.2、inputFileSystem

compiler.inputFileSystem = new CachedInputFileSystem(fs, 60000);

CachedInputFileSystem对node的文件(信息)内容读取方法做了一层封装,添加了缓存机制,缓存主要原理就是将文件路径与文件信息内容以kv的存储方式出入Map,把过期时间以500ms为一个单元拆成n个Set单元,再用定时器每间隔500ms使Set中的所有文件路径对应的内容删除实现内容过期。

CachedInputFileSystem源码地址

1.3、watchFileSystem

watchFileSystem内容比较多,有兴趣的朋友可以看一下watchpack

watchpack中实现文件变动监听的方式分为两种polledWatching与fs.watch,可以理解为polledWatching为文件扫描(主动),fs.watch即是系统回调(被动)。

1.2.1、polledWatching

可以通过配置options.poll(number)来选择使用轮询扫描的方式,轮询扫描会开启一个定时器然后在固定周期(即options.poll参数数值)扫描文件,使用fs.lstat获取文件的状态,如下:

Stats {
  dev: 2114,
  ino: 48064969,
  mode: 33188,
  nlink: 1,
  uid: 85,
  gid: 100,
  rdev: 0,
  size: 527,
  blksize: 4096,
  blocks: 8,
  atimeMs: 1318289051000.1,
  mtimeMs: 1318289051000.1,
  ctimeMs: 1318289051000.1,
  birthtimeMs: 1318289051000.1,
  atime: Mon, 10 Oct 2011 23:24:11 GMT,
  mtime: Mon, 10 Oct 2011 23:24:11 GMT,
  ctime: Mon, 10 Oct 2011 23:24:11 GMT,
  birthtime: Mon, 10 Oct 2011 23:24:11 GMT }
  • actime - active time 访问时间
  • mtime - modify time 修改时间
  • ctime - change time 文件的元数据发生变化。比如权限,所有者等

原理也比较简单,通过当前文件状态与上次的文件状态的mtime,如果不相同则代表文件发生了修改。

1.2.2、fs.watch

watchpack抽象出两种类型的watcher,分别是DirectWatcher与RecursiveWatcher,本质都是fs.watch,两者的区别是后者的recursive为true。

出于电脑性能问题,watcher的数量是有限的,watchpack中默认mac为2000、windows为10000。

const watcherLimit = +process.env.WATCHPACK_WATCHER_LIMIT || (IS_OSX ? 2000 : 10000);

当watcher超出限制的时候,watchpack只会创建DirectWatcher。

// Fast case when we are not reaching the limit
if (!SUPPORTS_RECURSIVE_WATCHING || watcherLimit - watcherCount >= map.size) {
    // Create watchers for all entries in the map
    for (const [filePath, entry] of map) {
        const w = createDirectWatcher(filePath);
        if (Array.isArray(entry)) {
            for (const item of entry) w.add(item);
        } else {
            w.add(entry);
        }
    }
    return;
}

未超出限制的情况下,watchpack会判断当前目录是否有多个文件被监听或者子目录有文件被监听,判断是的话优先使用RecursiveWatcher,毕竟DirectWatcher与RecursiveWatcher构建都只算一个watcher,而RecursiveWatcher可以监听的文件可以比DirectWatcher多很多。

// Merge map entries to keep watcher limit
// Create a 10% buffer to be able to enter fast case more often
const plan = reducePlan(map, watcherLimit * 0.9);

// Update watchers for all entries in the map
for (const [filePath, entry] of plan) {
    if (entry.size === 1) {
        for (const [watcher, filePath] of entry) {
            const w = createDirectWatcher(filePath);
            const old = underlyingWatcher.get(watcher);
            if (old === w) continue;
            w.add(watcher);
            if (old !== undefined) old.remove(watcher);
        }
    } else {
        const filePaths = new Set(entry.values());
        if (filePaths.size > 1) {
            const w = createRecursiveWatcher(filePath);
            for (const [watcher, watcherPath] of entry) {
                const old = underlyingWatcher.get(watcher);
                if (old === w) continue;
                w.add(watcherPath, watcher);
                if (old !== undefined) old.remove(watcher);
            }
        } else {
            for (const filePath of filePaths) {
                const w = createDirectWatcher(filePath);
                for (const watcher of entry.keys()) {
                    const old = underlyingWatcher.get(watcher);
                    if (old === w) continue;
                    w.add(watcher);
                    if (old !== undefined) old.remove(watcher);
                }
            }
        }
    }
}