webpack笔记(8)-原理分析二

251 阅读6分钟

Tapable插件结构和Hooks设计

compiler和compilation都继承自Tapable类,Tapable是一个类似于Node的EventEmitter库,主要是控制狗子函数的发布与订阅。
webpack可以理解为一种基于事件流的编程范例,一些列插件的运行。内部插件监听compile或者compilation上面定义的关键事件节点。

const {
    SyncHook,  //同步钩子
    SyncBailHook,  //同步熔断钩子
    SyncWaterfallHook,  //同步流水钩子
    SyncLoopHook,  //同步循环钩子
    AsyncParallelHook,  //异步并发钩子
    AsyncParallelBailHook, //异步并发熔断钩子
    AsyncSeriesHook,  //异步串行钩子
    AsyncSeriesBailHook,  //异步串行熔断钩子
    AsyncSeriesWaterfallHook //异步串行流水钩子
} = require("tapable");

Tapable hooks类型

typefunction
Hook所有钩子的后缀
Waterfall流水钩子,执行的结果可以传递给下一个插件
Bail熔断钩子,遇到return直接返回
Loop监听函数返回true表示继续循环,返回undefined表示结束循环
Sync同步方法
AsyncSeries异步串行钩子
AsyncParallel异步并行钩子

Tabpack 提供了同步&异步绑定钩子的方法,并且他们都有绑定(监听)事件和执行(触发)事件对应的方法。

AsyncSync
监听:tapAsync/tapPromise/tap监听:tap
触发:callAsync/promise触发:call
// demo
const {
    SyncHook,
    AsyncSeriesHook
} = require('tapable');

class Car {
    constructor() {
        this.hooks = {
            accelerate: new SyncHook(['newspeed']),
            brake: new SyncHook(),
            calculateRoutes: new AsyncSeriesHook(["source", "target", "routesList"])
        }
    }
}


const myCar = new Car();
 
//绑定同步钩子
myCar.hooks.brake.tap("WarningLampPlugin", () => console.log('WarningLampPlugin'));
 
//绑定同步钩子 并传参
myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
 
//绑定一个异步Promise钩子
myCar.hooks.calculateRoutes.tapPromise("calculateRoutes tapPromise", (source, target, routesList, callback) => {
    // return a promise
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            console.log(`tapPromise to ${source} ${target} ${routesList}`)
            resolve();
        },1000)
    })
});


myCar.hooks.brake.call();
myCar.hooks.accelerate.call(10);
 
console.time('cost');
 
//执行异步钩子
myCar.hooks.calculateRoutes.promise('Async', 'hook', 'demo').then(() => {
    console.timeEnd('cost');
}, err => {
    console.error(err);
    console.timeEnd('cost');
});

compiler和compilation

compiler文件中可以看到,定义了很多很多hooks
有流程类的:
· (before-)run
· (before-after-)compile
· make
· (after-)emit
· done
监听类的:
· watch-run
· watch-close

class Compiler extends Tapable {
    constructor(context) {
        super();
        this.hooks = {
            ...
            // 构建完成会走到done钩子
            done: new AsyncSeriesHook(["stats"]),
            
            // 构建前
            beforeRun: new AsyncSeriesHook(["compiler"]),
            // 开始编译
            run: new AsyncSeriesHook(["compiler"]),
            
            // compilation和this.compilation,像webpack一些插件,比如htmlwebpackplugin内部也有独立的构建流程
            thisCompilation: new SyncHook(["compilation", "params"]),
            compilation: new SyncHook(["compilation", "params"]),
            
            // 还有初始化的钩子
            entryOption: new SyncBailHook(["context", "entry"])
        };
        ...
}
module.exports = Compiler;

compilation相关hooks,主要负责模块的编译打包优化:
entry相关,(un-)seal是否成功构建,(before-after-)chunks,optimize优化相关,hash相关。

webpack流程的三个阶段:

  • 1.准备阶段
  • 2.模块打包构建
  • 3.模块优化,代码生成,输出到磁盘

webpack流程之准备阶段

  1. 引用WebpackOptionsDefaulter,设置一些默认参数
  2. 引用NodeEnvironmentPlugin,清理构建缓存
  3. 调用了WebpackOptionsApply对象,将options转化插件
// lib下的webpack,即main入口文件
const webpack = (options, callback) => {
    const webpackOptionsValidationErrors = validateSchema(
        webpackOptionsSchema,
        options
    );
    if (webpackOptionsValidationErrors.length) {
        throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
    }
    let compiler;
    if (Array.isArray(options)) {
        compiler = new MultiCompiler(
            Array.from(options).map(options => webpack(options))
        );
    } else if (typeof options === "object") {
        // 1.引用WebpackOptionsDefaulter,设置一些默认参数
        options = new WebpackOptionsDefaulter().process(options);

        compiler = new Compiler(options.context);
        compiler.options = options;
        //2. 引用NodeEnvironmentPlugin,清理构建缓存
        new NodeEnvironmentPlugin({
            infrastructureLogging: options.infrastructureLogging
        }).apply(compiler);
        if (options.plugins && Array.isArray(options.plugins)) {
            for (const plugin of options.plugins) {
                if (typeof plugin === "function") {
                    plugin.call(compiler, compiler);
                } else {
                    plugin.apply(compiler);
                }
            }
        }
        compiler.hooks.environment.call();
        compiler.hooks.afterEnvironment.call();
        // 3.调用了WebpackOptionsApply对象
        compiler.options = new WebpackOptionsApply().process(options, compiler);
    } else {
        throw new Error("Invalid argument: options");
    }
    if (callback) {
        if (typeof callback !== "function") {
            throw new Error("Invalid argument: callback");
        }
        if (
            options.watch === true ||
            (Array.isArray(options) && options.some(o => o.watch))
        ) {
            const watchOptions = Array.isArray(options)
                ? options.map(o => o.watchOptions || {})
                : options.watchOptions || {};
            return compiler.watch(watchOptions, callback);
        }
        compiler.run(callback);
    }
    return compiler;
};

exports = module.exports = webpack;
exports.version = version;

WebpackOptionsDefaulter文件:

class WebpackOptionsDefaulter extends OptionsDefaulter {
    constructor() {
        super();

        this.set("entry", "./src");

        this.set("devtool", "make", options =>
            options.mode === "development" ? "eval" : false
        );

        this.set("output.filename", "[name].js");
        this.set("output.chunkFilename", "make", options => {
            const filename = options.output.filename;
            if (typeof filename !== "function") {
                const hasName = filename.includes("[name]");
                const hasId = filename.includes("[id]");
                const hasChunkHash = filename.includes("[chunkhash]");
                // Anything changing depending on chunk is fine
                if (hasChunkHash || hasName || hasId) return filename;
                // Elsewise prefix "[id]." in front of the basename to make it changing
                return filename.replace(/(^|\/)([^/]*(?:\?|$))/, "$1[id].$2");
            }
            return "[id].js";
        });
        this.set("output.library", "");
        this.set("output.hotUpdateFunction", "make", options => {
            return Template.toIdentifier(
                "webpackHotUpdate" + Template.toIdentifier(options.output.library)
            );
        });
        this.set("output.jsonpFunction", "make", options => {
            return Template.toIdentifier(
                "webpackJsonp" + Template.toIdentifier(options.output.library)
            );
        });
        ...
        

NodeEnvironmentPlugin文件:

// run之前的清构建缓存的一个处理
compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => {
    if (compiler.inputFileSystem === inputFileSystem) inputFileSystem.purge();
});

WebpackOptionsApply

作用:将所有的配置options参数,转换成webpack内部插件:
output.library -> LibraryTemplatePlugin
externals -> ExternalsPlugin
devtool -> EvalDevtoolModulePlugin,SourceMapDevToolPlugin

触发后哪些地方监听

class WebpackOptionsApply extends OptionsApply {
    constructor() {
        super();
    }
    process(options, compiler) {
        compiler.dependencies = options.dependencies;
        // 设置options.target为web后,WebpackOptionsApply会依次加载需要的插件
        if (typeof options.target === "string") {
            ...
            switch (options.target) {
                case "web":
                    JsonpTemplatePlugin = require("./web/JsonpTemplatePlugin");
                    FetchCompileWasmTemplatePlugin = require("./web/FetchCompileWasmTemplatePlugin");
                    NodeSourcePlugin = require("./node/NodeSourcePlugin");
                    // 通过apply方法,插件都会变成compiler上的一个实例
                    new JsonpTemplatePlugin().apply(compiler);
                    new FetchCompileWasmTemplatePlugin({
                        mangleImports: options.optimization.mangleWasmImports
                    }).apply(compiler);
                    new FunctionModulePlugin().apply(compiler);
                    new NodeSourcePlugin(options.node).apply(compiler);
                    new LoaderTargetPlugin(options.target).apply(compiler);
                    break;
                    ...

WebpackOptionsApply的另一个作用:默认加载了EntryOptionPlugin

// WebpackOptionsApply.js
new EntryOptionPlugin().apply(compiler);
// 触发entryOption这个hook
compiler.hooks.entryOption.call(options.context, options.entry);

执行命令:grep "entryOption" -rn ./node_modules/webpack,看下哪些地方监听了
查找结果: image.png 可以看到,是EntryOptionPlugin.js

// EntryOptionPlugin.js
const SingleEntryPlugin = require("webpack/lib/SingleEntryPlugin");
const MultiEntryPlugin = require("webpack/lib/MultiEntryPlugin");
const DynamicEntryPlugin = require("webpack/lib/DynamicEntryPlugin");

const itemToPlugin = (context, item, name) => {
    if (Array.isArray(item)) {
        // 数组会转成多个entry
        return new MultiEntryPlugin(context, item, name);
    }
    // 对象会单个处理
    return new SingleEntryPlugin(context, item, name);
};

module.exports = class EntryOptionPlugin {
    apply(compiler) {
        compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
            // 针对不同的entry写入,有不同的处理
            // 这就是为什么entry可以是数组,可以是对象,可以是个func
            if (typeof entry === "string" || Array.isArray(entry)) {
                itemToPlugin(context, entry, "main").apply(compiler);
            } else if (typeof entry === "object") {
                for (const name of Object.keys(entry)) {
                    itemToPlugin(context, entry[name], name).apply(compiler);
                }
            } else if (typeof entry === "function") {
                new DynamicEntryPlugin(context, entry).apply(compiler);
            }
            return true;
        });
    }
};

完成准备阶段,我们再次回到Compiler中,这里会创造一个Compilation对象和NormalModuleFactory、ContextModuleFactory工厂方法,之后会走到run方法。

// Compiler.js
run(callback) {
    if (this.running) return callback(new ConcurrentCompilationError());

    const finalCallback = (err, stats) => {
        ...
    };

    const startTime = Date.now();

    this.running = true;

    const onCompiled = (err, compilation) => {
        if (err) return finalCallback(err);

        // 4.触发shouldEmit,判断资源是否生成
        if (this.hooks.shouldEmit.call(compilation) === false) {
            const stats = new Stats(compilation);
            stats.startTime = startTime;
            stats.endTime = Date.now();
            this.hooks.done.callAsync(stats, err => {
                if (err) return finalCallback(err);
                return finalCallback(null, stats);
            });
            return;
        }

        // 5.到构建环节
        this.emitAssets(compilation, err => {
            if (err) return finalCallback(err);

            if (compilation.hooks.needAdditionalPass.call()) {
                compilation.needAdditionalPass = true;

                const stats = new Stats(compilation);
                stats.startTime = startTime;
                stats.endTime = Date.now();
                this.hooks.done.callAsync(stats, err => {
                    if (err) return finalCallback(err);

                    this.hooks.additionalPass.callAsync(err => {
                        if (err) return finalCallback(err);
                        this.compile(onCompiled);
                    });
                });
                return;
            }

            this.emitRecords(err => {
                if (err) return finalCallback(err);

                const stats = new Stats(compilation);
                stats.startTime = startTime;
                stats.endTime = Date.now();
                this.hooks.done.callAsync(stats, err => {
                    if (err) return finalCallback(err);
                    return finalCallback(null, stats);
                });
            });
        });
    };

    // 1.在执行run之前,先执行了beforeRun这个钩子
    // 注意:这个钩子就是在上面NodeEnvironmentPlugin文件中绑定的,做了一个清除构建缓存的处理
    this.hooks.beforeRun.callAsync(this, err => {
        if (err) return finalCallback(err);

        // 2.执行完beforeRun,进入到run钩子中
        this.hooks.run.callAsync(this, err => {
            if (err) return finalCallback(err);

            this.readRecords(err => {
                if (err) return finalCallback(err);

                // 3.执行onCompiled回调
                this.compile(onCompiled);
            });
        });
    });
}

seal:构建完成后,将内容输出,资源生成,优化

webpack流程之模块打包构建阶段

make阶段

继续看compiler文件:

newCompilationParams() {
    const params = {
        normalModuleFactory: this.createNormalModuleFactory(),
        contextModuleFactory: this.createContextModuleFactory(),
        compilationDependencies: new Set()
    };
    return params;
}

compile(callback) {
    // 创建了createNormalModuleFactory和createContextModuleFactory两个工厂类
    const params = this.newCompilationParams();
    this.hooks.beforeCompile.callAsync(params, err => {
        if (err) return callback(err);

        this.hooks.compile.call(params);

        const compilation = this.newCompilation(params);

        // 执行make钩子
        this.hooks.make.callAsync(compilation, err => {
            if (err) return callback(err);

            compilation.finish(err => {
                if (err) return callback(err);

                compilation.seal(err => {
                    if (err) return callback(err);

                    this.hooks.afterCompile.callAsync(compilation, err => {
                        if (err) return callback(err);

                        return callback(null, compilation);
                    });
                });
            });
        });
    });
}

执行命令:查找一下哪里监听make:grep "hooks.make" -rn "./node_modules/webpack" image.png 以SingleEntryPlugin.js为例:

// SingleEntryPlugin.js
compiler.hooks.make.tapAsync(
    "SingleEntryPlugin",
    (compilation, callback) => {
        const { entry, name, context } = this;

        const dep = SingleEntryPlugin.createDependency(entry, name);
        // 往配置中添加入口,make构建阶段正式开始
        compilation.addEntry(context, dep, name, callback);
    }
);

build流程

// Compilation.js
buildModule(module, optional, origin, dependencies, thisCallback) {
    ...
    // 1.触发buildModule
    this.hooks.buildModule.call(module);
    // 2.调用normalModule中的buildMoule?buildMoule调用了doBuild
    module.build(
        this.options,
        this,
        this.resolverFactory.get("normal", module.resolveOptions),
        this.inputFileSystem,
        error => {
            ...
            if (error) {
                // 6.2失败触发failedModule
                this.hooks.failedModule.call(module, error);
                return callback(error);
            }
            // 6.1成功触发succeedModule,构建结果都放在了this.modules数组中
            this.hooks.succeedModule.call(module);
            return callback();
        }
    );
}
// normalModule。j s
const { getContext, runLoaders } = require("loader-runner");

build(options, compilation, resolver, fs, callback) {
    ...

    // 3.执行doBuild方法
    return this.doBuild(options, compilation, resolver, fs, err => {
        ...

        try {
            // 5.构建完成后会运行这个parser,parse中用的是acorn
            // 作用是把js中require的依赖添加到依赖列表中
            const result = this.parser.parse(
                this._ast || this._source.source(),
                {
                    current: this,
                    module: this,
                    compilation: compilation,
                    options: options
                },
                (err, result) => {
                    if (err) {
                        handleParseError(err);
                    } else {
                        handleParseResult(result);
                    }
                }
            );
            if (result !== undefined) {
                // parse is sync
                handleParseResult(result);
            }
        } catch (e) {
            handleParseError(e);
        }
    });
}

doBuild(options, compilation, resolver, fs, callback) {
    const loaderContext = this.createLoaderContext(
        resolver,
        options,
        compilation,
        fs
    );
    // 依赖runLoaders包去执行loader
    runLoaders(
        {
            // 传递静态资源的路径
            resource: this.resource,
            loaders: this.loaders,
            context: loaderContext,
            readResource: fs.readFile.bind(fs)
        },
...
}

seal阶段

image.png

class Compilation extends Tapable {
    constructor(compiler) {
        super();
        this.hooks = {

            /** @type {SyncHook} */
            optimize: new SyncHook([]),
            /** @type {SyncBailHook<Module[]>} */
            optimizeModulesBasic: new SyncBailHook(["modules"]),
            /** @type {SyncBailHook<Module[]>} */
            optimizeModules: new SyncBailHook(["modules"]),
            /** @type {SyncBailHook<Module[]>} */
            optimizeModulesAdvanced: new SyncBailHook(["modules"]),
            /** @type {SyncHook<Module[]>} */
            afterOptimizeModules: new SyncHook(["modules"]),

            /** @type {SyncBailHook<Chunk[], ChunkGroup[]>} */
            optimizeChunksBasic: new SyncBailHook(["chunks", "chunkGroups"]),
            /** @type {SyncBailHook<Chunk[], ChunkGroup[]>} */
            optimizeChunks: new SyncBailHook(["chunks", "chunkGroups"]),
            /** @type {SyncBailHook<Chunk[], ChunkGroup[]>} */
            optimizeChunksAdvanced: new SyncBailHook(["chunks", "chunkGroups"]),
            /** @type {SyncHook<Chunk[], ChunkGroup[]>} */
            afterOptimizeChunks: new SyncHook(["chunks", "chunkGroups"]),
            ...

模块构建好后,会触发compilation中的finish方法?

webpack流程之文件生成

入口文件addEntry -> _addModuleChain -> buildModule -> seal -> createHash -> createModuleAssets
_addModuleChain: 分析入口文件依赖的模块,将模块添加到依赖列表中,同时进行模块构建,执行buildModule createModuleAssets:

// compilation.js
createModuleAssets() {
    for (let i = 0; i < this.modules.length; i++) {
        const module = this.modules[i];
        if (module.buildInfo.assets) {
            const assetsInfo = module.buildInfo.assetsInfo;
            for (const assetName of Object.keys(module.buildInfo.assets)) {
                const fileName = this.getPath(assetName);
                // 将构建好的module放到asset,
                this.emitAsset(
                    fileName,
                    module.buildInfo.assets[assetName],
                    assetsInfo ? assetsInfo.get(assetName) : undefined
                );
                this.hooks.moduleAsset.call(module, fileName);
            }
        }
    }
}

最后到emit阶段,是在compile

// compile.js
this.hooks.emit.callAsync(compilation, err => {
    if (err) return callback(err);
    // compilation获取输出的内容
    outputPath = compilation.getPath(this.outputPath);
    // 输出的内容放到磁盘中
    this.outputFileSystem.mkdirp(outputPath, emitFiles);
});