webpack 过程

版本为webpack@4.44.1
一个简单的示例代码

// webpack.config配置文件
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin'); 
const webpack = require('webpack'); 

module.exports = {
    entry: './src/webpack/index.js',
    output: {
        path: path.resolve(__dirname +'/src/webpack/', 'dist'),
        filename: 'webpack-bundle.js'
    },
    mode: 'development',
    plugins: [
        new HtmlWebpackPlugin({template: './src/webpack/index.html'})
    ]
}
 // 入口文件index
 import {getA} from './a.js';
    let name = 'haha';
    function init() {
        getA();
    }
init();

// a.js
import B from './b.js';
let a = 'haha'
export function getA() {
    B();
    console.log(a);
    return a;
}

// b.js
function B () {
    console.log('i am a B')
}

export default B;

webpack下debug断点调试命令node --inspect --inspect-brk ./node_modules/webpack-cli/bin/cli.js --config webpack.config1.js
在webpack-cli下bin/cli.js 文件中找到const webpack = require("webpack");

webpack编译的流程图大体如下


compile_white.jpg

查看入口文件可知, 在进行webpack调用时会new一个 Complier作为返回的对象; 调用run方法时,实现编译打包的过程。
在run的过程中创建Compilation对象进行文件的分析、依赖的收集、chunks以及优化等工作。

Complier

Complier的主要方法

  1. run方法, 执行一些hooks方法,然后进入complie编译阶段, 设置编译完成后的回调函数onCompiled;
  2. this.compile方法, 执行一些hooks方法,
    a. 创建Compilation对象,执行hooks.make.callAsync方法进入到buildModule过程.
    b. 执行compilation.seal方法进入的模块的封装过程。
    c. 执行compile完成后的回调, 执行onCompiled方法。
  3. onCompiled方法中主要是执行emitAssets, emitRecords以及hooks方法。
  4. 获取source内容输出最终的打包文件。
new webpack
  1. new一个compiler对象
  2. 通过NodeEnvironmentPlugin.apply方法在compiler对象上绑定用于文件输入输出的对象。
  3. 循环执行绑定的plugn的初始化方法。
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") {
        options = new WebpackOptionsDefaulter().process(options);

        compiler = new Compiler(options.context);
        compiler.options = options;
        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();
        compiler.options = new WebpackOptionsApply().process(options, compiler);
    } else {
        throw new Error("Invalid argument: options");
    }
    if (callback) {
        ...
    }
    return compiler;
};

NodeEnvironmentPlugin主要是用于创建文件输入输出对象, 其结构流程如下


nodeEventSystem.png
  1. 创建一个cacheInputFiles的对象, 可以对文件进行缓存的处理。
  2. 创建一个outputFiles对象, 将原生的fs上的 方法进行绑定
run

通过此方法执行hooks钩子函数, 调用compile方法进入编译阶段。 同时该方法上设置onCompiled 编译完成后的回调函数。
编译完成后调用this.emitAssets实现文件的输出。

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);

            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;
            }

            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);
                    });
                });
            });
        };

        this.hooks.beforeRun.callAsync(this, err => {
            if (err) return finalCallback(err);

            this.hooks.run.callAsync(this, err => {
                if (err) return finalCallback(err);

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

                    this.compile(onCompiled);
                });
            });
        });
    }
this.emitAssets

由代码可知,该方法的主要功能是将source内容写入到输出文件中, 最终实现打包输出。

emitAssets(compilation, callback) {
        let outputPath;
        const emitFiles = err => {
            if (err) return callback(err);

            asyncLib.forEachLimit(
                compilation.getAssets(),
                15,
                ({ name: file, source }, callback) => {
                    let targetFile = file;
                    const queryStringIdx = targetFile.indexOf("?");
                    if (queryStringIdx >= 0) {
                        targetFile = targetFile.substr(0, queryStringIdx);
                    }

                    const writeOut = err => {
                        if (err) return callback(err);
                        const targetPath = this.outputFileSystem.join(
                            outputPath,
                            targetFile
                        );
                        // TODO webpack 5 remove futureEmitAssets option and make it on by default
                        if (this.options.output.futureEmitAssets) {
                            ...
                        } else {
                            if (source.existsAt === targetPath) {
                                source.emitted = false;
                                return callback();
                            }
                            let content = source.source();

                            if (!Buffer.isBuffer(content)) {
                                content = Buffer.from(content, "utf8");
                            }

                            source.existsAt = targetPath;
                            source.emitted = true;
                            this.outputFileSystem.writeFile(targetPath, content, err => {
                                if (err) return callback(err);
                                this.hooks.assetEmitted.callAsync(file, content, callback);
                            });
                        }
                    };

                    if (targetFile.match(/\/|\\/)) {
                        const dir = path.dirname(targetFile);
                        this.outputFileSystem.mkdirp(
                            this.outputFileSystem.join(outputPath, dir),
                            writeOut
                        );
                    } else {
                        writeOut();
                    }
                },
                err => {
                    ...
                }
            );
        };

        this.hooks.emit.callAsync(compilation, err => {
            if (err) return callback(err);
            outputPath = compilation.getPath(this.outputPath);
            this.outputFileSystem.mkdirp(outputPath, emitFiles);
        });
    }

Compilation

Compilation主要有两个过程, 一是创建module进行依赖收集,另一个是seal过程。

  1. 首先是从入口文件调用addEntry、_addModuleChain递归的创建module。
  2. 通过processModuleDependencies和addModuleDependencies的相互调用, 从而实现循环buildModule和依赖收集的工作。
  3. module.build类似工厂模式, 不同的module分别调用其自身的build方法。在build的过程中会执行相关的loader插件, 进行parse解析生成AST树。
  4. seal过程中进行asset的创建收集, chunks的创建分组、创建hash以及代码的优化工作。
_addModuleChain

该方法主要功能点如下

  1. 创建一个模块工厂函数, 调用create方法生成module。
  2. 对当前的模块进行build的过程, build完成后会有其依赖的文件的相关信息。
  3. 对于有依赖文件的执行 this.processModuleDependencies的调用。
_addModuleChain(context, dependency, onModule, callback) {
        ...
        const Dep = /** @type {DepConstructor} */ (dependency.constructor);
        const moduleFactory = this.dependencyFactories.get(Dep);
        if (!moduleFactory) {
            throw new Error(
                `No dependency factory available for this dependency type: ${dependency.constructor.name}`
            );
        }

        this.semaphore.acquire(() => {
            moduleFactory.create(
                {
                    contextInfo: {
                        issuer: "",
                        compiler: this.compiler.name
                    },
                    context: context,
                    dependencies: [dependency]
                },
                (err, module) => {
                    ...
                    const addModuleResult = this.addModule(module);
                    module = addModuleResult.module;

                    onModule(module);

                    dependency.module = module;
                    module.addReason(null, dependency);

                    const afterBuild = () => {
                        if (addModuleResult.dependencies) {
                            this.processModuleDependencies(module, err => {
                                if (err) return callback(err);
                                callback(null, module);
                            });
                        } else {
                            return callback(null, module);
                        }
                    };

                    ...

                    if (addModuleResult.build) {
                        this.buildModule(module, false, null, null, err => {
                            if (err) {
                                this.semaphore.release();
                                return errorAndCallback(err);
                            }

                            if (currentProfile) {
                                const afterBuilding = Date.now();
                                currentProfile.building = afterBuilding - afterFactory;
                            }

                            this.semaphore.release();
                            afterBuild();
                        });
                    } else {
                        this.semaphore.release();
                        this.waitForBuildingFinished(module, afterBuild);
                    }
                }
            );
        });
    }
processModuleDependencies

processModuleDependencies 主要是处理依赖信息以及接下来调用this.addModuleDependencies

processModuleDependencies(module, callback) {
        const dependencies = new Map();

        const addDependency = dep => {
            const resourceIdent = dep.getResourceIdentifier();
            if (resourceIdent) {
                const factory = this.dependencyFactories.get(dep.constructor);
                if (factory === undefined) {
                    throw new Error(
                        `No module factory available for dependency type: ${dep.constructor.name}`
                    );
                }
                let innerMap = dependencies.get(factory);
                if (innerMap === undefined) {
                    dependencies.set(factory, (innerMap = new Map()));
                }
                let list = innerMap.get(resourceIdent);
                if (list === undefined) innerMap.set(resourceIdent, (list = []));
                list.push(dep);
            }
        };

        const addDependenciesBlock = block => {
            if (block.dependencies) {
                iterationOfArrayCallback(block.dependencies, addDependency);
            }
            if (block.blocks) {
                iterationOfArrayCallback(block.blocks, addDependenciesBlock);
            }
            if (block.variables) {
                iterationBlockVariable(block.variables, addDependency);
            }
        };

        try {
            addDependenciesBlock(module);
        } catch (e) {
            callback(e);
        }

        const sortedDependencies = [];

        for (const pair1 of dependencies) {
            for (const pair2 of pair1[1]) {
                sortedDependencies.push({
                    factory: pair1[0],
                    dependencies: pair2[1]
                });
            }
        }

        this.addModuleDependencies(
            module,
            sortedDependencies,
            this.bail,
            null,
            true,
            callback
        );
    }
addModuleDependencies

addModuleDependencies方法循环遍历依赖模块, 对每个模块进行build。 如果build后的模块含有dependencies会继续进入processModuleDependencies方法(注意, 这也是实现依赖模块递归build和收集的关键。)

addModuleDependencies(
        module,
        dependencies,
        bail,
        cacheGroup,
        recursive,
        callback
    ) {
        const start = this.profile && Date.now();
        const currentProfile = this.profile && {};

        asyncLib.forEach(
            dependencies,
            (item, callback) => {
                const dependencies = item.dependencies;

                ...
                const semaphore = this.semaphore;
                semaphore.acquire(() => {
                    const factory = item.factory;
                    factory.create(
                        {
                            ...
                        },
                        (err, dependentModule) => {
                            let afterFactory;

                            ...

                            const iterationDependencies = depend => {
                                for (let index = 0; index < depend.length; index++) {
                                    const dep = depend[index];
                                    dep.module = dependentModule;
                                    dependentModule.addReason(module, dep);
                                }
                            };

                            const addModuleResult = this.addModule(
                                dependentModule,
                                cacheGroup
                            );
                            dependentModule = addModuleResult.module;
                            iterationDependencies(dependencies);

                            const afterBuild = () => {
                                if (recursive && addModuleResult.dependencies) {
                                    this.processModuleDependencies(dependentModule, callback);
                                } else {
                                    return callback();
                                }
                            };

                            ...

                            if (addModuleResult.build) {
                                this.buildModule(
                                    dependentModule,
                                    isOptional(),
                                    module,
                                    dependencies,
                                    err => {
                                        if (err) {
                                            semaphore.release();
                                            return errorOrWarningAndCallback(err);
                                        }

                                        if (currentProfile) {
                                            const afterBuilding = Date.now();
                                            currentProfile.building = afterBuilding - afterFactory;
                                        }

                                        semaphore.release();
                                        afterBuild();
                                    }
                                );
                            } else {
                                semaphore.release();
                                this.waitForBuildingFinished(dependentModule, afterBuild);
                            }
                        }
                    );
                });
            },
            err => {
                // In V8, the Error objects keep a reference to the functions on the stack. These warnings &
                // errors are created inside closures that keep a reference to the Compilation, so errors are
                // leaking the Compilation object.

                if (err) {
                    // eslint-disable-next-line no-self-assign
                    err.stack = err.stack;
                    return callback(err);
                }

                return process.nextTick(callback);
            }
        );
    }
buildModule

以NormalModule为例, 我们可以先看一下其流程大体如下图所示


buildModuleDiagram.png
  • 由图可知, NormalModule继承至module, 而module又继承至DependenciesBlock。 故而NormalModule存在addDependency方法,用来实现依赖的收集。
    *build方法首先会对文件进行loaders插件处理, 执行loader插件实现source的变更。 然后进入parser阶段
  • parser用来进行解析source将其生成ast树, 遍历ast的节点实现依赖收集的工作。注意多个parsePlugin都会涉及到module.addDependency的调用。
createChunkAssets

createChunkAssets阶段对module中代码进行变更替换,进行chunk分组。
其大致流程如下


createChunksAssetsDiagram.png
  • 首先会通过template.getRenderMainfest生成render方法, 然后调用fileManifest.render实现代码的生成。
  • ModuleTemplate中的render的方法,生成module的代码片段组,然后进行拼装生成代码块。 如开始的示例中,a.js文件会生成
import B from './b.js';
let a = 'haha'
export function getA() {
    B();
    console.log(a);
    return a;
}
// 编译后生成的代码结构,为了查看方便注释部分有删减
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getA", function() { return getA; });
/* harmony import */ var _b_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./b.js */ "./src/webpack/b.js");


let a = 'haha'
function getA() {
    Object(_b_js__WEBPACK_IMPORTED_MODULE_0__["default"])();
    console.log(a);
    return a;
}
  • JavascriptGenerator是一个js的代码生成器, 在语法树解析的阶段会对文件的依赖进行收集, 文件中需要插入或者替换的部分也进行收集。然后再generate阶段生成各种代码片段。
  • createChunkAssets 是一个进行代码片段收集以及封装的过程。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,794评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,050评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,587评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,861评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,901评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,898评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,832评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,617评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,077评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,349评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,483评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,199评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,824评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,442评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,632评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,474评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,393评论 2 352