creeperyang / blog

前端博客,关注基础知识和性能优化。
MIT License
2.63k stars 211 forks source link

webpack runtime 源码分析 #59

Open creeperyang opened 1 year ago

creeperyang commented 1 year ago

项目基本配置可参考基于 https://github.com/taniarascia/webpack-boilerplate.git

(一)从打包产物——html 开始

<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <meta http-equiv="x-ua-compatible" content="ie=edge" />
    <title>webpack-demo</title>
    <link rel="icon" href="/favicon.ico">
    <script defer="defer" src="/js/runtime.dacd45666f2c5eb35b8d.bundle.js"></script>
    <script defer="defer" src="/js/main.7086fc1df0c0b6a0d989.bundle.js"></script>
    <link href="/styles/main.64e10d654e2697258d08.css" rel="stylesheet">
</head>

<body>
    <div id="root"></div>
</body>

</html>

从浏览器角度,html 是应用入口和运行起点。可以看到,编译出的JS通过<script>标签插入了 html。

这里需额外注意到:

defer 异步加载,不阻塞 html 下载和解析,且保证这些 script 按序执行(保证了runtime先执行);按序执行非常重要,runtime 脚本提供了webpack的全局变量/方法,提供了模块加载执行的能力等等。

(二)打包产物——模块/chunk 是怎么包装的?

简单来说,我们写的js文件(模块/module)被webpack处理后变成了什么?

有最简单的 src/js/info.js

export const text = '2021/02/01'

编译后就是dist/js/info.be139a6f62da8ba74d4c.chunk.js

"use strict";
(self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || []).push([[996],{

/***/ 67:
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "text": () => (/* binding */ text)
/* harmony export */ });
var text = '2021/02/01';

/***/ })

}]);

为了让编译后的代码保持可读性,webpack的配置去除了压缩。我们可以看到:

  1. 代码被包装在 (__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 我们的真正代码 }

    1. 这个并不是一定的,也可能是 (module) => { 我们的真正代码 },这取决于代码是否 ES Module 形式的等等。
    2. webpack 一定帮我们提供了 module|exports|require 的环境,保证了我们代码能正常运行。当然这三个变量webpack会编译并匹配包装函数。
    3. __webpack_require__ 上提供了一些工具函数,具体是什么下个章节讨论。
  2. webpack的编译产出是 chunk,是一个或多个module产出一个chunk。所以我们看到 {moduleId: wrappedModuleCode} 的形式:

    1. 就是上面的 {67: (__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 代码 })}
  3. 最终chunk的形式是 (self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || []).push(参数)

    1. 参数是数组,self["webpackChunkwebpack_demo"].push 保证了数组里面如果有代码需要运行是可以运行的,而不是静态的数组。
    2. 参数的形式是 [[chunkId], moreModules, runtime]runtime 就是在 push 时可以运行的代码。

看看entry point编译出的 initial chunk 会是什么样?

(self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || []).push([[179],{

/***/ 573:
/***/ ((__unused_webpack_module, __unused_webpack___webpack_exports__, __webpack_require__) => {

"use strict";

;// CONCATENATED MODULE: ./src/js/title.js
var getTitle = function getTitle() {
  return '柴门闻犬吠,风雪夜归人。';
};
// EXTERNAL MODULE: ./src/js/log.js
var log = __webpack_require__(967);
var log_default = /*#__PURE__*/__webpack_require__.n(log);
;// CONCATENATED MODULE: ./src/images/logo.svg
const logo_namespaceObject = "data:image/svg+xml;base64,...........";
;// CONCATENATED MODULE: ./src/index.js
// Test import of a JavaScript function

// Test import of an asset

// Test import of styles

var logo = document.createElement('img');
logo.src = logo_namespaceObject;
var heading = document.createElement('h1');
heading.textContent = getTitle();
var app = document.querySelector('#root');
app.append(logo, heading);
setTimeout(function () {
  __webpack_require__.e(/* import() | info */ 996).then(__webpack_require__.bind(__webpack_require__, 67)).then(function (v) {
    var footer = document.createElement('footer');
    footer.textContent = v.text;
    app.append(footer);
    log_default()();
  });
}, 100);
__webpack_require__.e(/* import() */ 232).then(__webpack_require__.bind(__webpack_require__, 232)).then(function (v) {
  var div = document.createElement('div');
  div.textContent = v.author;
  app.append(div);
});

/***/ }),

/***/ 967:
/***/ ((module) => {

module.exports = function () {
  console.log('xxxxxxxxx');
};

/***/ })

},
/******/ __webpack_require__ => { // webpackRuntimeModules
/******/ /* webpack/runtime/startup prefetch */
/******/ (() => {
/******/    __webpack_require__.O(0, [179], () => {
/******/        __webpack_require__.E(996);
/******/    }, 5);
/******/ })();
/******/ 
/******/ var __webpack_exec__ = (moduleId) => (__webpack_require__(__webpack_require__.s = moduleId))
/******/ var __webpack_exports__ = (__webpack_exec__(573));
/******/ }
]);

(三)打包产物——webpack runtime 有哪些功能

一切魔法都在 webpack runtime 脚本和模块代码的包装中,从 webpack runtime 开始分析编译出的代码,看看一个简单的项目是怎么跑起来的。而我们写的 import/export 这些模块化的代码,是怎么成功跑在浏览器里的。

这里首先明确一个概念:

一定程度上,中文“模块”一词可能存在混用以上概念,注意分辨。

然后我们正式来分析 webpack runtime。webpack runtime 代码根据配置/是否有异步模块等不同,稍有不同,但总共不超过几百行代码。

webpack runtime 结构

整个webpack runtime是个IIFE(Immediately Invoked Function Expression),整体结构如下:

(() => { // webpackBootstrap
    "use strict";
    var __webpack_modules__ = ({});
/************************************************************************/
    // 缓存,缓存了所有模块的 exports
    var __webpack_module_cache__ = {};

    // The require function
    function __webpack_require__(moduleId) {
        // 检查是不是在缓存里?在直接返回即可。
        var cachedModule = __webpack_module_cache__[moduleId];
        if (cachedModule !== undefined) {
            return cachedModule.exports;
        }
        // 否则创建 module,并放到缓存。
        var module = __webpack_module_cache__[moduleId] = {
            // no module.id needed
            // no module.loaded needed
            exports: {}
        };

        // 执行对应的模块,执行完成后 epxorts 就有值了
        __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

        // Return the exports of the module
        return module.exports;
    }

    // expose the modules object (__webpack_modules__)
    __webpack_require__.m = __webpack_modules__;

    // 下面是一堆自执行函数(IIFE):
    // 1. 为__webpack_require__添加各种方法;
    // 2. 最终往window上添加 webpackChunkwebpack_demo 数组(最终是否叫webpackChunkwebpack_demo取决于config)。
})()
  1. 定义了 __webpack_modules__ 来存储所有模块,定义了 __webpack_require__ 来作为模块中用到的 require 方法。

  2. 通过一堆 IIFE 在 __webpack_require__ 上定义了一堆工具函数,这些函数可以被编译的模块去使用。

  3. 最后一个IIFE中定义了一个唯一全局变量 'webpackChunkwebpack_demo',该变量为数组,提供 push 方法来加载 chunk。

    • 变量名由 output.chunkLoadingGlobal 等配置确定(默认值是'webpackChunkwebpack');
    • 所有的模块最终通过 webpackChunkwebpack_demo.push 来执行,或者说所有的模块被它包装: (self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || []).push([[id],{id:code},runtime])

最终我们的入口 chunk 执行时,通过webpackChunkwebpack_demo.push来最终执行业务代码。

webpack runtime 中的工具函数解析

核心的 'webpackChunkwebpack_demo'(即webpackJsonpCallback

webpackChunkwebpack_demo.push 是什么?webpackChunkwebpack_demo.push 最终调用 webpackJsonpCallback

// self["webpackChunkwebpack_demo"]一般为undefined,初始化为空数组;如果不为空则保留。
var chunkLoadingGlobal = self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || [];

// 假如 self["webpackChunkwebpack_demo"] 之前非空,那么对其每项调用 webpackJsonpCallback,
// 把相关 chunks & modules 存入缓存(module未执行,后面的 startup 真正执行)。
// 这里 0 作为参数传入,即 parentChunkLoadingFunction 是0,防止了 webpackJsonpCallback 执行时元素重复压入chunkLoadingGlobal。
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));

// 设置 chunkLoadingGlobal数组的push方法(这是其它模块的wrapper function);
// webpackJsonpCallback 绑定 parentChunkLoadingFunction 为 chunkLoadingGlobal.push, 元素被压入chunkLoadingGlobal
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));

来具体看看 webpackJsonpCallback 作用是什么。请结合上面的 initial chunk 代码来看(看调用chunkLoadingGlobal.push的传参)。

var __webpack_modules__ = ({});
// expose the modules object (__webpack_modules__)
__webpack_require__.m = __webpack_modules__;

// 缓存所有加载完成/加载中的模块,0代表加载完成
// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
var installedChunks = {
    666: 0
};

/**
 * install a JSONP callback for chunk loading
 * @params {function|0} parentChunkLoadingFunction,可能是数组的push方法,也可能是0
 * @params {Array} data 格式是 [[moduleId], {moduleId: moduleCode}, runtime]
 * 
 * moduleId: 数字;moduleCode:wrapper function包裹的模块代码;runtime: (__webpack_require__)=>any
 **/
var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
    // "moreModules" 就是当前加载的chunk自带的模块,包括自身代码的模块、CONCATENATED过来的模块、其它依赖模块。
    var [chunkIds, moreModules, runtime] = data;
    // add "moreModules" to the modules object,
    // then flag all "chunkIds" as loaded and fire callback
    var moduleId, chunkId, i = 0;
    // 当前加载的chunk一般不会是0,所以执行这个if内逻辑,把moreModules添加到__webpack_modules__
    if(chunkIds.some((id) => (installedChunks[id] !== 0))) {
        for(moduleId in moreModules) {
            // moreModules 形式为 {id: wrappedCode},我们直接把它们挂载到 __webpack_modules__
            // 方便之后__webpack_require__(id) 可以正确执行对应模块。
            if(__webpack_require__.o(moreModules, moduleId)) {
                __webpack_require__.m[moduleId] = moreModules[moduleId];
            }
        }
        // 然后看有需要执行的代码就直接执行
        if(runtime) var result = runtime(__webpack_require__);
    }
    // push 到 chunkLoadingGlobal 数组
    if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
    for(;i < chunkIds.length; i++) {
        chunkId = chunkIds[i];
        // 如果这个chunk是loading状态,则resolve掉它,方便通知之前等待该chunk的代码继续执行。
        if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
            installedChunks[chunkId][0]();
        }
        // 标记这个chunk加载完成
        installedChunks[chunkId] = 0;
    }
    // 那么__webpack_require__.O在干嘛?下一小节看
    return __webpack_require__.O(result);
}
var chunkLoadingGlobal = self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || [];
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
// chunkLoadingGlobal.push.bind(chunkLoadingGlobal) 作为 parentChunkLoadingFunction,此时是 chunkLoadingGlobal 的数组push方法
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));

webpackJsonpCallback 作为真正的业务模块调用入口,主要就是把 push 进来的 chunk 指定的模块加载到 __webpack_modules__,并执行 chunk 指定的业务代码(模块)。

__webpack_require__.O 在干什么?

webpackJsonpCallback 中只剩__webpack_require__.O没搞明白作用,这一小节来解释下。

/* webpack/runtime/chunk loaded */
(() => {
    var deferred = [];
    __webpack_require__.O = (result, chunkIds, fn, priority) => {
        if(chunkIds) {
            priority = priority || 0;
            for(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];
            deferred[i] = [chunkIds, fn, priority];
            return;
        }
        // 以我们下面的传参为例,这里deferred是 [[179], () => {
        //  __webpack_require__.E(996);
        // }, 5]
        var notFulfilled = Infinity;
        for (var i = 0; i < deferred.length; i++) {
            var [chunkIds, fn, priority] = deferred[i];
            var fulfilled = true;
            for (var j = 0; j < chunkIds.length; j++) {
                // 5 & 1 是1,但是 Infinity >= 5 成立,
                // __webpack_require__.O 上只有 j,是检查chunkId对应的chunk是否已加载,这里第一次不成立而第二次成立
                if ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every((key) => (__webpack_require__.O[key](chunkIds[j])))) {
                    chunkIds.splice(j--, 1);
                } else {
                    fulfilled = false;
                    if(priority < notFulfilled) notFulfilled = priority;
                }
            }
            // 第一次不成立,而第二次可以执行
            if(fulfilled) {
                deferred.splice(i--, 1)
                var r = fn(); // 第二次其实是执行 __webpack_require__.E(996),即prefetch 996
                if (r !== undefined) result = r;
            }
        }
        return result;
    };

    __webpack_require__.O.j = (chunkId) => (installedChunks[chunkId] === 0);
})();

多数时候 __webpack_require__.O 接收的参数全部是 undefined,即什么都不干。所以略过它多数时候也不影响理解webpack流程。但我们的入口 chunk 其实会传入不一样的参数,我们来看下:

__webpack_require__ => { // webpackRuntimeModules
/* webpack/runtime/startup prefetch */
(() => {
    __webpack_require__.O(0, [179], () => {
        __webpack_require__.E(996);
    }, 5);
})();

var __webpack_exec__ = (moduleId) => (__webpack_require__(__webpack_require__.s = moduleId))
var __webpack_exports__ = (__webpack_exec__(573));
}

这是initial chunk的runtime函数执行时,会调用__webpack_require__.O,并且是在业务代码执行前。然后等webpackJsonpCallback执行时,__webpack_require__.O会执行第二次,此时会开始 prefetch id 为 996 的 chunk。

这符合预期:prefetch 在父 chunk 加载完成后开始。

__webpack_require__.F__webpack_require__.E 怎么配合完成 prefetch/preload

/* webpack/runtime/chunk prefetch function */
(() => {
    __webpack_require__.F = {};
    __webpack_require__.E = (chunkId) => {
        Object.keys(__webpack_require__.F).map((key) => {
            __webpack_require__.F[key](chunkId);
        });
    }
})();
__webpack_require__.F.j = (chunkId) => {
    // 如果 chunkId 不是 666(runtime chunk),也没有加载过
    if((!__webpack_require__.o(installedChunks, chunkId) || installedChunks[chunkId] === undefined) && 666 != chunkId) {
        // null 代表 chunk preloaded/prefetched
        installedChunks[chunkId] = null;
        var link = document.createElement('link');

        if (__webpack_require__.nc) {
            link.setAttribute("nonce", __webpack_require__.nc);
        }
        link.rel = "prefetch";
        link.as = "script";
        link.href = __webpack_require__.p + __webpack_require__.u(chunkId);
        document.head.appendChild(link);
    }
};

代码一目了然。

重要的jsonp加载chunk相关 __webpack_require__.l & __webpack_require__.f.j & __webpack_require__.e

这里讲讲 webpack 最基本的怎么加载 chunk 的(jsonp)。

/* webpack/runtime/load script */
(() => {
    var inProgress = {};
    var dataWebpackPrefix = "webpack-demo:";
    // loadScript function to load a script via script tag
    __webpack_require__.l = (url, done, key, chunkId) => {
        // 如果已经在加载中了,直接返回
        if(inProgress[url]) { inProgress[url].push(done); return; }
        var script, needAttach;
        if(key !== undefined) {
            var scripts = document.getElementsByTagName("script");
            for(var i = 0; i < scripts.length; i++) {
                var s = scripts[i];
                if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; }
            }
        }
        if(!script) {
            needAttach = true;
            script = document.createElement('script');

            script.charset = 'utf-8';
            script.timeout = 120;
            if (__webpack_require__.nc) {
                script.setAttribute("nonce", __webpack_require__.nc);
            }
            script.setAttribute("data-webpack", dataWebpackPrefix + key);
            script.src = url;
        }
        inProgress[url] = [done];
        var onScriptComplete = (prev, event) => {
            // avoid mem leaks in IE.
            script.onerror = script.onload = null;
            clearTimeout(timeout);
            var doneFns = inProgress[url];
            // 加载完成就删除inProgress[url]
            delete inProgress[url];
            script.parentNode && script.parentNode.removeChild(script);
            doneFns && doneFns.forEach((fn) => (fn(event)));
            if(prev) return 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);
    };
})();

/* webpack/runtime/ensure chunk */
(() => {
    __webpack_require__.f = {};
    // This file contains only the entry chunk.
    // The chunk loading function for additional chunks
    __webpack_require__.e = (chunkId) => {
        return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
            __webpack_require__.f[key](chunkId, promises);
            return promises;
        }, []));
    };
})();
// import()|require.ensure 的核心实现代码
    __webpack_require__.f.j = (chunkId, promises) => {
            // JSONP chunk loading for javascript
            var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;
            if(installedChunkData !== 0) { // 0 means "already installed".

                // a Promise means "currently loading".
                if(installedChunkData) {
                    // 正在加载,那么取出promise放到 promises 即可
                    promises.push(installedChunkData[2]);
                } else {
                    if(666 != chunkId) {
                        // setup Promise in chunk cache
                        var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));
                        promises.push(installedChunkData[2] = promise);

                        // start chunk loading 构造异步模块的正确加载路径(publicPath + filename)
                        var url = __webpack_require__.p + __webpack_require__.u(chunkId);
                        // create error before stack unwound to get useful stacktrace later
                        var error = new Error();
                        // 只需要处理加载失败的问题(因为chunk代码执行会更新installedChunks[chunkId]为0)
                        var loadingEnded = (event) => {
                            if(__webpack_require__.o(installedChunks, chunkId)) {
                                installedChunkData = installedChunks[chunkId];
                                // 加入没有加载成功(chunk代码执行会更新installedChunks[chunkId]为0),
                                // 则置为 undefined(chunk not loaded)
                                if(installedChunkData !== 0) installedChunks[chunkId] = undefined;
                                if(installedChunkData) {
                                    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;
                                    installedChunkData[1](error);
                                }
                            }
                        };
                        __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
                    } else installedChunks[chunkId] = 0;
                }
            }
    };

显而易见的 __webpack_require__.o & __webpack_require__.r & __webpack_require__.d

1. __webpack_require__.ohasOwnProperty

/* webpack/runtime/hasOwnProperty shorthand */
(() => {
    __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
})();

2. __webpack_require__.r

exports 对象加上 __esModule=true 等标记。

/* webpack/runtime/make namespace object */
(() => {
    // define __esModule on exports
    __webpack_require__.r = (exports) => {
        if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
            Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
        }
        Object.defineProperty(exports, '__esModule', { value: true });
    };
})();

3. __webpack_require__.d

exports 对象加上我们导出的那些属性/方法(其它模块通过 import 来使用)。注意,这里是通过 getter 来导出。

/* webpack/runtime/define property getters */
(() => {
    // define getter functions for harmony exports
    __webpack_require__.d = (exports, definition) => {
        for(var key in definition) {
            if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
                Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
            }
        }
    };
})();