寻根webpack

250 阅读3分钟

概览

为最新的webpack5代码体量过于庞大,本文将结合1.0版本的代码阐释webppack构建的基本流程

基本概念

image.png

从make钩子讲起:

compiler.plugin("make", function(compilation, callback) {
    compilation.addEntry(this.context, new SingleEntryDependency(this.entry), this.name, callback);
}.bind(this));
sequenceDiagram
Compilation->>Compilation: _addModuleChain
Compilation->>NormalModuleFactory: create
NormalModuleFactory->>Compilation: module
Compilation->>Compilation: addModule(添加module到modules数组且设置_modules对象)
Compilation->>Compilation: onModule callback(addentry就是添加到entries数组中)
Compilation->>Compilation: buildModule(回调执行moduleReady,即processModuleDependencies)
Compilation->>NormalModule: build
NormalModule->>NormalModuleMixin: doBuild(应用一系列的loader后读取文件返回normalModule继续执行)
NormalModule->>NormalModule.parser: parse(此时的模块文件已经为应用loader之后的代码,parse方法拿到这个字符串形式的代码)
Compilation->>Compilation: processModuleDependencies(buildModule结束后处理模块引入的依赖,module也是一个block)
Compilation->>Compilation: addModuleDependencies(对于每个dependency,找到对应的工厂函数生成module)
Compilation->>NormalModuleFactory: create

SEAL

结束make钩子,进入seal方法,做chunks处理

this.applyPluginsParallel("make", compilation, function(err) {
    if (err) return callback(err);

    compilation.seal(function(err) {
        if (err) return callback(err);

        this.applyPluginsAsync("after-compile", compilation, function(err) {
            if (err) return callback(err);

            return callback(null, compilation);
        });
    }.bind(this));
}.bind(this));
function seal(callback) {
    this.applyPlugins("seal");
    this.preparedChunks.forEach(function(preparedChunk) {
        var module = preparedChunk.module;
        var chunk = this.addChunk(preparedChunk.name, module);
        chunk.initial = chunk.entry = true;
        chunk.addModule(module);
        module.addChunk(chunk);
        this.processDependenciesBlockForChunk(module, chunk);
    }, this);
}
  • compilation.addChunks
  • 一个compilation有许多chunks
  • 一个chunk包含多个modules
  • chunkmodule相互引用
  • 一个module有许多blocks(继承自DependenciesBlock)
  • processDependenciesBlockForChunkmoduledependencies module都加入到这一个chunk中
Compilation.prototype.processDependenciesBlockForChunk = function processDependenciesBlockForChunk(block, chunk) {
    block.blocks.forEach(function(b) {
        var c;
        if (!b.chunk) {
            c = this.addChunk(b.chunkName, b.module, b.loc);
            b.chunk = c;
            c.addBlock(b);
        } else {
            c = b.chunk;
        }
        chunk.addChunk(c);
        c.addParent(chunk);
        this.processDependenciesBlockForChunk(b, c);
    }, this);

    function iteratorDependency(d) {
        if (!d.module) return;
        if (d.module.error) {
            d.module = null;
            return;
        }
        if (chunk.addModule(d.module)) {
            d.module.addChunk(chunk);
            this.processDependenciesBlockForChunk(d.module, chunk);
        }
    }
    block.dependencies.forEach(iteratorDependency, this);
    block.variables.forEach(function(v) {
        v.dependencies.forEach(iteratorDependency, this);
    }, this);
};

添加module到chunk完之后,进入seal的下一阶段:

this.applyPlugins("optimize");

this.applyPlugins("optimize-modules", this.modules);
this.applyPlugins("after-optimize-modules", this.modules);

// RemoveParentModulesPlugin ->  RemoveEmptyChunksPlugin -> MergeDuplicateChunksPlugin
this.applyPlugins("optimize-chunks", this.chunks);
this.applyPlugins("after-optimize-chunks", this.chunks);

进入下一阶段

this.applyPluginsAsyncSeries("optimize-tree", this.chunks, this.modules, function(err) {
    if (err) return callback(err);

    //RecordIdsPlugin
    this.applyPlugins("revive-modules", this.modules, this.records);
    this.applyPlugins("optimize-module-order", this.modules);
    // 给每个module赋予一个ID,取自nextFreeModuleId++
    this.applyModuleIds();
    this.applyPlugins("optimize-module-ids", this.modules);
    this.applyPlugins("after-optimize-module-ids", this.modules);
    
    // RecordIdsPlugin
    // 给compilation的records赋值records.modules.byIdentifier[identifier] = module.id;
    // 通过request路径取到module id
    this.applyPlugins("record-modules", this.modules, this.records);
    
    // RecordIdsPlugin
    this.applyPlugins("revive-chunks", this.chunks, this.records);
    this.applyPlugins("optimize-chunk-order", this.chunks);
    // 给每个module赋予一个ID,取自nextFreeChunkId++,维护一个ids数组,包含所有的chunkId
    this.applyChunkIds();
    // FlagIncludedChunksPlugin
    this.applyPlugins("optimize-chunk-ids", this.chunks);
    this.applyPlugins("after-optimize-chunk-ids", this.chunks);
    
    // 修改records
    // if (!records.chunks) records.chunks = {};
    // if (!records.chunks.byName) records.chunks.byName = {};
    // if (!records.chunks.byBlocks) records.chunks.byBlocks = {};
    this.applyPlugins("record-chunks", this.chunks, this.records);
    // 根据ID大小对modules和chunks排序,包括module里的chunks,和chunk里面的module,小id在前
    this.sortItems();
    this.applyPlugins("before-hash");
    // 给每个chunk计算一个hash,最后为compilation计算一个fullHash,根据hashDigestLength裁剪前多少位hash值
    this.createHash();
    this.applyPlugins("after-hash");
    this.applyPlugins("before-chunk-assets");
    
    // 用template渲染成字符串保存在this.assets中,键为生成的文件名,值为ConcatSource类型,包含了即将生成代码的source map信息。
    this.createChunkAssets();
    this.applyPlugins("additional-chunk-assets", this.chunks);
    
    // 填充this.fileDependencies
    // 筛除重复的fileDependencies
    /** this.fileDependencies.sort();
	this.fileDependencies = filterDups(this.fileDependencies);
	this.contextDependencies.sort();
	this.contextDependencies = filterDups(this.contextDependencies);**/
    this.summarizeDependencies();
    this.applyPlugins("record", this, this.records);

    this.applyPluginsAsync("optimize-chunk-assets", this.chunks, function(err) {
        if (err) return callback(err);
        this.applyPlugins("after-optimize-chunk-assets", this.chunks);
        this.applyPluginsAsync("optimize-assets", this.assets, function(err) {
            if (err) return callback(err);
            this.applyPlugins("after-optimize-assets", this.assets);
            return callback();
        }.bind(this));
    }.bind(this));
}.bind(this));
Compilation.prototype.applyChunkIds = function applyChunkIds() {
    this.chunks.forEach(function(chunk) {
        if (chunk.id === null) {
            if (chunk.id === null)
                chunk.id = this.nextFreeChunkId++;
        }
        if (!chunk.ids)
            chunk.ids = [chunk.id];
    }, this);
};
Compilation.prototype.sortItems = function sortItems() {
    function byId(a, b) {
        return a.id - b.id;
    }
    this.chunks.sort(byId);
    this.modules.sort(byId);
    this.modules.forEach(function(module) {
        module.chunks.sort(byId);
        module.reasons.sort(function(a, b) {
            return byId(a.module, b.module)
        });
    });
    this.chunks.forEach(function(chunk) {
        chunk.modules.sort(byId);
    });
};

records结构

record-modules之后

image.png

record-chunks之后

image.png

结束seal,emitAssets,emitRecords

this.compile(function(err, compilation) {
    if (err) return callback(err);
    // 输出文件
    this.emitAssets(compilation, function(err) {
        if (err) return callback(err);

        this.emitRecords(function(err) {
            if (err) return callback(err);

            var stats = compilation.getStats();
            stats.startTime = startTime;
            stats.endTime = new Date().getTime();
            this.applyPlugins("done", stats);
            return callback(null, stats);
        }.bind(this));
    }.bind(this));
}.bind(this));

最后返回至起始调用点webpack函数

var compiler = webpack(options, function(err, stats) {
    if (!options.watch) {
        // Do not keep cache anymore
        var ifs = compiler.inputFileSystem;
        if (ifs && ifs.purge) ifs.purge();
    }
    if (err) {
        console.error(err.stack || err);
        if (err.details) console.error(err.details);
        if (!options.watch) {
            process.on("exit", function() {
                process.exit(1);
            });
        }
        return;
    }
    if (outputOptions.json)
        process.stdout.write(JSON.stringify(stats.toJson(outputOptions), null, 2) + "\n");
    else {
    // 终端打印统计信息
        process.stdout.write(stats.toString(outputOptions) + "\n");
    }
});

一些类层次结构

classDiagram
DependenciesBlock <|-- Module
Module <|-- NormalModule
NormalModuleMixin <|-- NormalModule
DependenciesBlock: blocks
DependenciesBlock: dependencies
DependenciesBlock: variables
DependenciesBlock: addDependency()
classDiagram
NullDependency <|-- RequireHeaderDependency
Dependency <|-- NullDependency
Dependency <|-- ModuleDependency
ModuleDependency <|-- SingleEntryDependency
classDiagram
Template <|-- MainTemplate
Template <|-- ChunkTemplate
MainTemplate <|-- JsonpMainTemplate
ChunkTemplate <|-- JsonpChunkTemplate
classDiagram
Source <|-- SourceMapNodeSource
SourceMapNodeSource <|-- ConcatSource

总结

可以看到,webpack依赖tapable实现的插件架构,在构建过程中的hooks绑定许多处理方法,进而影响后续的构建过程。从入口文件开始,解析文件地址,初始化module对象,在这个过程中通过loader对文件内容进行转换,利用loader构建完毕之后,使用js parser解析成ast,然后递归处理依赖,将依赖模块添加到不同的module对象中。

graph TD
webpack --> compiler.run --> compiler.compile --> id1(make hook ) --> addEntry --> addModlueChain --> moduleFactory.create --> id(module对象) --> buildModule --> module.build --> loaders(module.doBuild//应用loaders) --> onModuleBuild(onModuleBuild//模块构建完毕) --> parse[parser.parse//利用ast改变, 影响module.dependencies] --> moduleReady --> processModuleDependencies --> add(addModuleDependencies//递归添加依赖构建模块) --> compilation.seal --> rest(// 添加module到chunk//生成chunk和moduleid//利用template文件结合source-map生成最终的bundle)