webpack是如何实现模块化加载?

webpack支持的模块规范有 AMDCommonJSES2015 import 等规范。不管何种规范大致可以分为同步加载异步加载两种情况。本文将介绍webpack是如何实现模块管理和加载。

同步加载如下:

import a from './a';
console.log(a);

异步加载如下:

import('./a').then(a => console.log(a));

webpacks实现的启动函数,直接将入口程序module传入启动函数内,并缓存在闭包内,如下:

(function(modules){
    ......
    // 加载入口模块并导出(实现启动程序)
    return __webpack_require__(__webpack_require__.s = 0);
})({
    0: (function(module, __webpack_exports__, __webpack_require__) {
        module.exports = __webpack_require__(/*! ./src/app.js */"./src/app.js");
    })
})

webpack在实现模块管理上不管服务端还是客户端大致是一样,主要由installedChunks记录已经加载的chunkinstalledModules记录已经执行过的模块,具体如下:

/**
 * module 缓存器
 * key 为 moduleId (一般为文件路径)
 * value 为 module 对象 {i: moduleId, l: false, exports: {}}
 */
var installedModules = {};
/**
 * chunks加载状态记录器
 * key 一般为 chunk 索引
 * value undefined:未加载 0:已经加载 (客户端特有 null: 准备加载 [resolve, reject]: 加载中)
 */
var installedChunks = {
    "app": 0
}

不管是服务端还是客户端同步加载的方法都一样,主要是检测installedModules中是否已经缓存有要加载的module,有则直接返回,否则就创建一个新的module,并执行返回module.exports,具体实现如下:

// 编译后的同步加载
__webpack_require__(/*! ./src/app.js */"./src/app.js");

// 加载模块的方法,即require方法
function __webpack_require__(moduleId) {
    // 检查当前的module是否已经存在缓存中
    if(installedModules[moduleId]) {
        return installedModules[moduleId].exports; // 直接返回已缓存的 module.exports
    }
    // 创建一个新的 module, 并添加到缓存中
    var module = installedModules[moduleId] = {
        i: moduleId,
        l: false, // 是否已经加载
        exports: {} // 暴露的对象
    };
    // 执行当前 module 的方法
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    // 标记 module 加载完成状态
    module.l = true;
    // 返回 module 暴露的 exports 对象
    return module.exports;
}

服务端的异步加载是通过node的require方法加载chunk并返回一个promises对象。所有chunk都是暴露出idsmodules对象,具体实现如下:

// 编译后的异步加载方法
__webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./c.js */ "./src/c.js"))

// chunk 0 代码如下(即0.js的代码)
exports.ids = [0];
exports.modules = {
    "./src/c.js": (function(module, __webpack_exports__, __webpack_require__) {
        "use strict";
        __webpack_require__.r(__webpack_exports__);
        __webpack_exports__["default"] = (function () {
            console.log('c');
        })
    })
}

// 异步加载模块方法
__webpack_require__.e = function requireEnsure(chunkId) {
    var promises = [];
    if(installedChunks[chunkId] !== 0) {
        var chunk = require("./" + ({}[chunkId]||chunkId) + ".js");
        var moreModules = chunk.modules, chunkIds = chunk.ids;
        for(var moduleId in moreModules) {
            modules[moduleId] = moreModules[moduleId];
        }
        for(var i = 0; i < chunkIds.length; i++)
            installedChunks[chunkIds[i]] = 0;
    }
    return Promise.all(promises);
}

客户端的异步加载是通过JSONP原理进行加载资源,将chunk内容([chunkIds, modules])存到全局的webpackJsonp数组中,并改造webpackJsonppush方法实现监听chunk加载完成事件。具体实现如下:

// 编译后的异步加载方法
__webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./c.js */ "./src/c.js"))

// chunk 0 代码如下(即0.js的代码)
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
    "./src/c.js": (function(module, __webpack_exports__, __webpack_require__) {
        "use strict";
        __webpack_require__.r(__webpack_exports__);
        __webpack_exports__["default"] = (function () {
            console.log('c');
        });
    })
}]);

// 加载成功的回调函数
function webpackJsonpCallback(data) {
    var chunkIds = data[0];
    var moreModules = data[1];
    
    // 将本次加载回来的 chunk 标记为加载完成状态,并执行回调
    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; // 将chunk标记为加载完成
    }
    // 将本次加载回来的 module 添加到全局的 modules 对象
    for(moduleId in moreModules) {
        if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
            modules[moduleId] = moreModules[moduleId];
        }
    }
    // 判断 webpackJsonp 数组原始的push方法是否存在,存在则将数据追加到webpackJsonp中
    if(parentJsonpFunction) parentJsonpFunction(data);
    // 执行所有 chunk 回调
    while(resolves.length) {
        resolves.shift()();
    }
};

// 加载完成监听方法的实现
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;

// 异步加载模块方法
__webpack_require__.e = function requireEnsure(chunkId) {
    var promises = [];
    var installedChunkData = installedChunks[chunkId];
    if(installedChunkData !== 0) { // 0 时表示已经安装完成
        if(installedChunkData) { // 加载中
            promises.push(installedChunkData[2]);
        } else {
            // 创建一个回调的Promise,并将Promise缓存到installedChunks中
            var promise = new Promise(function(resolve, reject) {
                installedChunkData = installedChunks[chunkId] = [resolve, reject];
            });
            promises.push(installedChunkData[2] = promise);
            
            var script = document.createElement('script');
            var onScriptComplete;
            script.charset = 'utf-8';
            script.timeout = 120;
            if (__webpack_require__.nc) {
                script.setAttribute("nonce", __webpack_require__.nc);
            }
            script.src = jsonpScriptSrc(chunkId);
            
            var error = new Error();
            onScriptComplete = function (event) { // 加载完成回调
                // 避免IE内存泄漏。
                script.onerror = script.onload = null;
                clearTimeout(timeout); // 关闭超时定时器
                var chunk = installedChunks[chunkId];
                if(chunk !== 0) { // 未加载完成
                    if(chunk) { // 加载中
                        var errorType = event && (event.type === 'load' ? 'missing' : event.type);
                        var realSrc = event && event.target && event.target.src;
                        error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
                        error.name = 'ChunkLoadError';
                        error.type = errorType;
                        error.request = realSrc;
                        chunk[1](error);
                    }
                    installedChunks[chunkId] = undefined;
                }
            };
            var timeout = setTimeout(function(){ // 设置超时定时器
                onScriptComplete({ type: 'timeout', target: script });
            }, 120000);
            script.onerror = script.onload = onScriptComplete; // 设置加载完成回调
            document.head.appendChild(script);
        }
    }
    return Promise.all(promises);
};

更多可以查看编译后的代码 客户端服务端

1 声望
0 粉丝
0 条评论
推荐阅读
单文件组件下的vue,可以擦出怎样的火花
与时俱进吧,看着 vue3 和 vite,虽然不会用,但还是心痒痒,然后就把原先基于 vue@2 的实现做了重构。不周之处,大家见谅!下面关于过期的内容,我就用删除线标记了。

leftstick65阅读 45.2k评论 18

WebPack面试题汇总
现在的前端网页功能丰富,特别是SPA(single page web application 单页应用)技术流行后,JavaScript的复杂度增加和需要一大堆依赖包,还需要解决Scss、Less……新增样式的扩展写法的编译工作。Webpack 最核心的功...

xiangzhihong7阅读 370

想弄懂Babel?你必须得先弄清楚这几个包
相信很多人都知道Babel,知道它是用来编译ES6+的东西。但是再深入一点,大家都清楚我们平时项目中Babel用到的那些包作用是什么吗?为什么要用那几个包?

limingcan1阅读 604

封面图
前端微服务跨域配置解决办法,devServer为例
前言Nginx: 在上一篇我提到的跨域配置是正式上线的时候使用nginx做为配置的参考。Webpack: 而我们更多的时候是在开发阶段就需要通过跨域进行联合开发各个子应用部分功能DevServer配置解决跨域子应用静态资源跨域...

smallStone1阅读 1.1k评论 5

macOS上HBuildX 开发 uni-app项目,运行调试时编辑保存文件不会自动编译更新页面
点击运行到"Chrome",HBuildX能够正常编译并打开浏览器正常显示网页,这个时候如果去修改代码并保存,HBuildX底部控制台并不会出现重新编译的字样(正常是有的),浏览器也不会自动刷新,同时手动刷新变动的内容...

RobinTang阅读 887

alicdn边缘节点不稳定导致页面崩溃问题
某工作日,线上某用户向客服专员反馈没法正常访问“查看报价页面”,页面内容没有呈现。客服专员收到反馈后,将问题转交给SRE处理。很奇怪的是,SRE访问生产环境“查看报价页面”显示正常,为了进一步分析定位问题,S...

记得要微笑阅读 748

Transpile Webpack Plugin:让 Webpack 按照源文件的目录结构输出
作为 Web 开发者,你是否也纠结过如何用 Webpack 做文件转译?就像 Babel CLI 转译文件那样按照源文件的目录结构输出?如果有,那么这篇文章就是为你而写,我们一起瞧一瞧怎么做吧。

乌柏木5阅读 740

1 声望
0 粉丝
宣传栏