webpack模块加载机制

mattuy 2020年04月26日 318次浏览

前端项目的规模越来越庞大,模块化开发已经是普遍需求。早期的打包工具将所有模块化的代码打包到一个bundle文件中,在一个简单的html文件中引入脚本。webpack允许输出为多个bundle文件,从而实现按需加载,更好的利用浏览器缓存,提升用户体验。

这里不讨论如何配置webpack,只说webpack如何加载模块。考虑一个多页面程序,有page1和page2两个页面,都用到一个工具模块util,page1直接引用util,而page2在页面加载后满足一定条件时动态加载util。

//util.js
console.log('util')
export function log(arg) {
    console.log(arg)
}
//page1.js
import { log } from './util'
//...
log('page1')
//page2.js
//...
//条件满足时动态加载util
import('./util').then((util) => {
    util.log('page2')
})

首先明确webpack中的两个概念:

  • module:一般是源代码中的一个文件,打包后被包裹在一个函数中,webpack模拟了模块环境
  • chunk:一个chunk即一个输出bundle文件,不考虑其它分割规则,每个入口将产出一个chunk。一个chunk包含若干module

假设我们已经配置好webpack,让它输出三个chunk:

0.js		//util.js
page1.js	//页面1
page2.js	//页面2

下面从webpack的打包输出来分析webpack模块加载机制。

静态依赖

webpack中,一个js源文件作为一个模块(也可以是其他文件)。webpack将模块代码包裹在一个函数中,返回模块的导出内容。注意,每个模块最多仅被执行一次,第二次请求时会直接从缓存返回。

(打包后的)page1.js:

//立即执行函数初始化webpack运行时,然后加载入口模块
(function (modules) {
    //modules是被打包到page1.js这个文件(chunk)中的模块列表,此处有page1和util两个模块
    //已加载的模块的缓存
    var installedModules = {};
    // webpack require函数,用来加载模块
    function __webpack_require__(moduleId) {
        //检查模块是否已装载,是则返回缓存的模块
        if (installedModules[moduleId]) {
            return installedModules[moduleId].exports;
        }
        //将模块添加到缓存
        var module = installedModules[moduleId] = {
            i: moduleId,    //模块ID
            l: false,       //模块是否已装载
            exports: {}     //模块的导出内容
        };
        // 执行模块初始化
        modules[moduleId].call(
            module.exports,     //将模块代码的this指向其自己的exports
            module,             //相当于commonjs中的module,对模块自身的引用
            module.exports,     //相当于commonjs中的exports,模块的导出内容
            __webpack_require__ //相当于commonjs中的require,提供加载同一chunk中其他模块的方法
        );
        // 将模块标识为已加载
        module.l = true;
        // 返回模块的导出内容
        return module.exports;
    }
    
    //加载入口模块,即我们的page1.js
    return __webpack_require__(__webpack_require__.s = "./src/page1.js");
})({
    //page1模块
    "./src/page1.js":
        function (module, __webpack_exports__, __webpack_require__) {
            let util = __webpack_require__('./src/util.js')
            util.log('page1')
        },

    //util模块
    "./src/util.js":
        function (module, __webpack_exports__, __webpack_require__) {
            function log (arg) {
                console.log(arg)
            }
            //util模块导出log函数
            __webpack_exports__['log'] = log
        }
});

代码被我简化了一下,webpack有很多兼容性和安全性方面的考虑,我将那部分修改了让代码看起来清晰。

可以看到,util和page1被函数包围,模拟模块环境,然后作为参数传递给立即执行函数。注意,模块只有被加载时才会执行内部代码。

在立即执行函数中,先进行了webpack初始化,即提供核心的模块加载函数__webpack_require__函数,并在函数上配置一些静态字段记录必要的信息(如__webpack_require__.s为入口模块ID),然后装入入口模块,开始执行我们自己的逻辑代码。

按需加载

在page2中我们使用了动态import()方法来加载util模块。webpack足够智能,无需配置它也聪明的自动将util独立到单独的文件(chunk),在需要时再进行请求。

webpack通过jsonp机制加载不同chunk中的模块。

page2.js:

(function(modules) {
    //模块加载函数,同page1。注意,此函数仅用于加载已被
    function __webpack_require__(moduleId) {
        //......
    }

    //可用的模块列表,包含本chunk中的模块和已经请求完毕的,来自其他chunk的模块
    __webpack_require__.m = modules;
    //基路径,用于拼接其他chunk的url
    __webpack_require__.p = "";
    //已装载的模块缓存,注意区分__webpack_require__.m,前者应该是后者的子集
    var installedModules = {};
    //这是相对page1中多出来的东西,用来记录其他chunk的加载情况
    //值可能有3种:
    //    undefined, 即该chunk还没有被请求过
    //    0, 该chunk中的模块已经正确的添加到__webpack_require__.m中,可以通过__webpack_require__加载了
    //    [resolve, reject, promise], chunk正在请求中。promise在chunk请求结束后才被resolve或reject
    var installedChunks = {
        "page2": 0  //page2即页面2的chunk自身,当然已经加载完成
    };
    
    //jsonp函数,chunk请求成功时由被请求的chunk调用。它的任务为将chunk中的模块加入可用模块列表
    //data参数由被请求的chunk传过来,包含模块信息。这也是jsonp的核心部分
    function webpackJsonpCallback(data) {
        var chunkIds = data[0];
        var moreModules = data[1];
        var moduleId, chunkId, i = 0, resolves = [];
        for(;i < chunkIds.length; i++) {
            chunkId = chunkIds[i];
            if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
                resolves.push(installedChunks[chunkId][0]);
            }
            //表示chunk已经加载好啦,下次不用去请求了
            installedChunks[chunkId] = 0;
        }
        for(moduleId in moreModules) {
            if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
                //把chunk中的模块们加入可用列表(__webpack_require__.m == modules √)
                modules[moduleId] = moreModules[moduleId];
            }
        }
        //原数组的push函数,详见后文
        if(parentJsonpFunction) parentJsonpFunction(data);
        //resolve请求时创建的promise,执行回调
        while(resolves.length) {
            resolves.shift()();
        }
    };
    
    //异步请求chunk
    __webpack_require__.e = function requireEnsure(chunkId) {
        var promises = [];
        var installedChunkData = installedChunks[chunkId];
        //检查chunk是否已经加载。0表示已加载
        if(installedChunkData !== 0) {
            //正在加载...有点不清楚为啥要重复记录一次promise?
            if(installedChunkData) {
                //installedChunkData[2]是之前请求的promise
                promises.push(installedChunkData[2]);
            } else {
                //没有请求过,开始请求chunk
                //标识chunkId这个chunk正在请求,并记录promise相关信息
                var promise = new Promise(function(resolve, reject) {
                    installedChunkData = installedChunks[chunkId] = [resolve, reject];
                });
                promises.push(installedChunkData[2] = promise);
                //现在installedChunks[chunkId] = [resolve, reject, promise]了
                //用script标签加载chunk
                var script = document.createElement('script');
                var onScriptComplete;
                script.charset = 'utf-8';
                script.timeout = 120;
                script.src = jsonpScriptSrc(chunkId);
                //...请求错误处理...省略
                //...超时处理
                var timeout = setTimeout(function(){
                }, 120000);
                document.head.appendChild(script);
            }
        }
        return Promise.all(promises);
    };
    
    //拼接被请求chunk的url
    function jsonpScriptSrc(chunkId) {
        return __webpack_require__.p + "" + ({}[chunkId]||chunkId) + ".js"
    }
    //通过window.webpackJsonp数组记录加载成功的chunk数据。其他chunk被加载后会将自己的chunk数据push到这个数组
    var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
    //注意,webpack覆盖了window.webpackJsonp数组的push函数。将原生的push函数替换成了webpackJsonpCallback
    //在webpackJsonpCallback函数中我们看到有调用parentJsonpFunction,其实那个函数才是原来的数组的push函数
    var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
    jsonpArray.push = webpackJsonpCallback;
    //理论上说这时jsonpArray应该是空的。。但如果不是空的就手动调用下webpackJsonpCallback装载chunk
    //window["webpackJsonp"]已经被webpack污染啦,所以它复制了一下?
    jsonpArray = jsonpArray.slice();
    for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
    var parentJsonpFunction = oldJsonpFunction;
    
    //加载入口模块
    return __webpack_require__(__webpack_require__.s = "./src/page2.js");
})
({
    //页面2
    "./src/page2.js":
    (function(module, exports, __webpack_require__) {
        //我们的动态import()被webpack转化后大致代码
        //先请求包含util的chunk
        __webpack_require__.e(0)
            //请求成功之后通过__webpack_require__加载util模块
            .then(__webpack_require__.bind(null, 0))
            //执行我们自己的逻辑代码
            .then((util) => {
                util.log('page2')
            })
    })
});

代码仍然有点长,不过已经删除和修改了很多边缘代码,并且调整了一下顺序,结构比较清晰了。代码中的注释解释了整个模块加载过程。先通过jsonp请求得到chunk数据,然后缓存chunk中的模块到可用列表(未装载),这时同步的模块加载已经可用,直接调用__webpack_require__加载相关模块,步骤就和page1一样了。

被加载的chunk结构就很简单了,只是简单的调用jsonp函数传入模块数据。

//0.js

//注意,前面说了,window["webpackJsonp"]数组的push函数被webpack重写了
//所以实际上这里调用了主模块那边定义的webpackJsonpCallback函数
//一个chunk可能包含多个模块,所以参数为 [moduleIds],moreModules
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0], {
    "./src/util.js":
        (function (module, __webpack_exports__, __webpack_require__) {
            "use strict";
            __webpack_exports__['log'] = function (arg) {
                console.log(arg)
            }
        })
}]);

总结

当项目规模增长到一定程度,模块化已是刚需。在ESModule标准制定之前广泛使用的模块化标准有commonjs,AMD,以及综合两者的UMD等。在ES6中模块化终于得到标准的支持,然而各平台对其的支持有限,暂时还难以替代传统解决方案。万幸,webpack提供了综合各种模块化标准的机制,让我们能够平稳的过渡。