HMR运行原理 和 webpack的流程

461 阅读2分钟

懒加载流程(webpack5)

f.j (生成promise) --->

f.l (script 动态加载文件) -->

f.j 的promise调用 --->

加载的文件执行 ( self["webpackChunk_02_webpack_config_start"].push ) , push 被改造 --->

webpackJsonpCallback (内容被放入内存) --->

f.t (返回ns, 包含文件内容)

  1. 用import动态导入的文件,webpack会单独打出chunk文件

  2. webpack会用__webpack_require__.e方法引入文件

__webpack_require__.e(/* import() */ 1).then(__webpack_require__.t.bind(__webpack_require__, 1, 23)).then(function (login) {
    console.log(login);
  });

    3. e方法内部会掉哟哦那个 f.j 方法,最后调转到 f.l方法中使用jsonp

        __webpack_require__.e = function(chunkId) {
             debugger
             return Promise.all(Object.keys(__webpack_require__.f).reduce(function(promises, key) {
                 debugger
                 __webpack_require__.f[key](chunkId, promises); // key === j,这里实际调用了 j方法
                 return promises;
             }, []));
         };
        __webpack_require__.f.j = function(chunkId, promises) {
                 var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;
                 if(installedChunkData !== 0) { // 0 means "already installed".
                     if(installedChunkData) {
                         promises.push(installedChunkData[2]);
                     } else {
                         if(true) {
                             var promise = new Promise(function(resolve, reject) { installedChunkData = installedChunks[chunkId] = [resolve, reject]; });
                             promises.push(installedChunkData[2] = promise);

                             var url = __webpack_require__.p + __webpack_require__.u(chunkId);
                             var error = new Error();
                             var loadingEnded = function(event) {
                             };
                             __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
                         } else installedChunks[chunkId] = 0;
                     }
                 }
         };

这里j 控制了模块是否安装了,安装过了直接用installedChunkData数据(所以后面加载的数据肯定存在了installedChunkData中)。

如果没有安装过,则拼接url,用script动态去加载数据

        __webpack_require__.l = function(url, done, key, chunkId) {
             if(inProgress[url]) { inProgress[url].push(done); return; }
             var script, needAttach;
             if(!script) {
                 needAttach = true;
                 script = document.createElement('script');

                 script.charset = 'utf-8';
                 script.timeout = 120;
                 script.setAttribute("data-webpack", dataWebpackPrefix + key);
                 script.src = url;
             }
             inProgress[url] = [done];
             var onScriptComplete = function(prev, event) {
             }

            // 超时处理
             var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
             script.onerror = onScriptComplete.bind(null, script.onerror);
             script.onload = onScriptComplete.bind(null, script.onload);

            // 这一步会去加载文件,就会执行文件内容
             needAttach && document.head.appendChild(script);
         };

文件加载来了以后,会去执行:

    (self["webpackChunk_02_webpack_config_start"] = self["webpackChunk_02_webpack_config_start"] || []).push()

然后 push方法被全局覆盖成了:

chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));

所以 webpackJsonpCallback 函数会被执行:

        var webpackJsonpCallback = function(parentChunkLoadingFunction, data) {
             var chunkIds = data[0];
             var moreModules = data[1];
             var runtime = data[2];
             var moduleId, chunkId, i = 0;
             for(moduleId in moreModules) {
                 if(__webpack_require__.o(moreModules, moduleId)) {
                     __webpack_require__.m[moduleId] = moreModules[moduleId];
                 }
             }
             if(runtime) var result = runtime(__webpack_require__);
             if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
             for(;i < chunkIds.length; i++) {
                 chunkId = chunkIds[i];
                 if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
                     installedChunks[chunkId][0]();
                 }
                 installedChunks[chunkIds[i]] = 0;
             }

         }

其中 installedChunks[chunkId][0] 就是 chunkId 的promise 的resovle方法被执行。

所以到这里问题都解决了:

  1.    为什么要使用promise介入?

    因为我们不知道什么时候文件被加载完成。只有当文件加载完成以后执行了里面的内容我们再调用resolve方法。

    promise来精确控制文件加载完成和加载失败的。

   2. 文件是怎么加载的?

        按需加载的文件是动态创建script标签加载的。

总结流程:当我们点击按钮去加载新的文件的时候,webpack 会根据chunkId调用e方法,e方法最后去调用f.j方法,为每一个chunkId 生成一个promise,并把状态改变的resovle,reject方法唯一存起来:installedChunks[chunkId] = [resolve, reject]等待触发。然后拼接绝地地址,调用l方法,l具体就是动态创建script标签并且放入头部,这样就会去加载文件。加载完的文件就会执行,执行的时候会调用被串改的push的方法,就是webpackJsonpCallback,webpackJsonpCallback把内容存起来以后就调用该chunkId的resolve方法,resolve被触发就会调用t方法,t方法就是正常的用__webpack_require__去加载文件内容,然后执行逻辑代码。

webpack流程的钩子

    钩子汇总:

  1. environment 读取环境

  2. afterEnvironment 读取环境后触发

  3. initiallize 初始化

  4. beforeRun 运行前,启动文件读取功能

  5. run 机器跑起来了,在编译,有缓存则启用缓存

        const run = () => {
            //  看这里,看这里beforeRun钩子
            this.hooks.beforeRun.callAsync(this, err => {
                if (err) return finalCallback(err);
                // 看这里,看这里run钩子
                this.hooks.run.callAsync(this, err => {
                    if (err) return finalCallback(err);

                    this.readRecords(err => {
                        if (err) return finalCallback(err);
                        // 看这里看这里,这是又一个核心方法。
                        this.compile(onCompiled); // 开启编译阶段
                    });
                });
            });
        };
  1. beforCompile 开始编译前的准备,创建的ModuleFactory,创建Compilation,并绑定ModuleFactory到Compilation,同时处理一些不需要编译的模块。
  2. compile 进行编译
  3. make 编译的核心流程
  4. finishMake
  5. finish
  6. seal 对每个 chunk 进行整理、优化、封装__webpack_require__来模拟模块化操作.
  7. afterCompile 编译结束
compile(callback) {
        const params = this.newCompilationParams();
        this.hooks.beforeCompile.callAsync(params, err => {
            ...
            this.hooks.make.callAsync(compilation, err => {
                ...
                this.hooks.finishMake.callAsync(compilation, err => {
                    ...
                    process.nextTick(() => {
                        ...
                        compilation.finish(err => {
                            ...
                            compilation.seal(err => {
                                ...
                                this.hooks.afterCompile.callAsync(compilation, err => {
                                    logger.timeEnd("afterCompile hook");
                                    if (err) return callback(err);

                                    return callback(null, compilation);
                                });
                            });
                        });
                    });
                });
            });
        });
  1. shouldEmit 是否可以输出

  2. emit 输出文件

  3. afterEmit 输出完成

  4. done 所有流程结束

const onCompiled = (err, compilation) => {
    ... 
    if (this.hooks.shouldEmit.call(compilation) === false) {   // 看这里,看这里,shouldEmit是否可以输出
        ...
    }
    process.nextTick(() => {
        ...
        this.emitAssets(compilation, err => {    // emitAssets进行输出
            ...
        });
    });
};

流程: