• Webpack--动态 import 原理及源码分析


    前言

    在平时的开发中,我们经常使用 import()实现代码分割和懒加载。在低版本的浏览器中并不支持动态 import(),那 webpack 是如何实现 import() polyfill 的?

    原理分析

    我们先来看看下面的 demo

    1. function component() {
    2. const btn = document.createElement("button");
    3. btn.onclick = () => {
    4. import("./a.js").then((res) => {
    5. console.log("动态加载a.js..", res);
    6. });
    7. };
    8. btn.innerHTML = "Button";
    9. return btn;
    10. }
    11. document.body.appendChild(component());

    点击按钮,动态加载 a.js脚本,查看浏览器网络请求可以发现,a.js请求返回的内容如下:

    图片

    简单看,实际上返回的就是下面这个东西:

    1. (self["webpackChunkwebpack_demo"] =
    2. self["webpackChunkwebpack_demo"] || []).push([
    3. ["src_a_js"],
    4. {
    5. "./src/a.js": () => {},
    6. },
    7. ]);

    从上面可以看出 3 点信息:

    • 1.webpackChunkwebpack_demo 是挂到全局 window 对象上的属性

    • 2.webpackChunkwebpack_demo 是个数组

    • 3.webpackChunkwebpack_demo 有个 push 方法,用于添加动态的模块。当a.js脚本请求成功后,这个方法会自动执行。

    再来看看 main.js 返回的内容

    图片

    仔细观察,动态 import 经过 webpack 编译后,变成了下面的一坨东西:

    1. __webpack_require__.e("src_a_js")
    2. .then(__webpack_require__.bind(__webpack_require__, "./src/a.js"))
    3. .then((res) => {
    4. console.log("动态加载a.js..", res);
    5. });

    上面代码中,__webpack_require__ 用于执行模块,比如上面我们通过webpackChunkwebpack_demo.push添加的模块,里面的./src/a.js函数就是在__webpack_require__里面执行的。

    __webpack_require__.e函数就是用来动态加载远程脚本。因此,从上面的代码中我们可以看出:

    • 首先 webpack 将动态 import 编译成 __webpack_require__.e 函数

    • __webpack_require__.e函数加载远程的脚本,加载完成后调用 __webpack_require__ 函数

    • __webpack_require__函数负责调用远程脚本返回来的模块,获取脚本里面导出的对象并返回

    源码分析及实现

    如何动态加载远程模块

    在开始之前,我们先来看下如何使用 script 标签加载远程模块

    1. var inProgress = {};
    2. // url: "http://localhost:8080/src_a_js.main.js"
    3. // done: 加载完成的回调
    4. const loadScript = (url, done) => {
    5. if (inProgress[url]) {
    6. inProgress[url].push(done);
    7. return;
    8. }
    9. const script = document.createElement("script");
    10. script.charset = "utf-8";
    11. script.src = url;
    12. inProgress[url] = [done];
    13. var onScriptComplete = (prev, event) => {
    14. var doneFns = inProgress[url];
    15. delete inProgress[url];
    16. script.parentNode && script.parentNode.removeChild(script);
    17. doneFns && doneFns.forEach((fn) => fn(event));
    18. if (prev) return prev(event);
    19. };
    20. script.onload = onScriptComplete.bind(null, script.onload);
    21. document.head.appendChild(script);
    22. };

    loadScript(url, done) 函数比较简单,就是通过创建 script 标签加载远程脚本,加载完成后执行 done 回调。inProgress用于避免多次创建 script 标签。比如我们多次调用loadScript('http://localhost:8080/src_a_js.main.js', done)时,应该只创建一次 script 标签,不需要每次都创建。这也是为什么我们调用多次 import('a.js'),浏览器 network 请求只看到家在一次脚本的原因

    实际上,这就是 webpack 用于加载远程模块的极简版本。

    __webpack_require__.e 函数的实现

     首先我们使用installedChunks对象保存动态加载的模块。key 是 chunkId

    1. // 存储已经加载和正在加载的chunks,此对象存储的是动态import的chunk,对象的key是chunkId,值为
    2. // 以下几种:
    3. // undefined: chunk not loaded
    4. // null: chunk preloaded/prefetched
    5. // [resolve, reject, Promise]: chunk loading
    6. // 0: chunk loaded
    7. var installedChunks = {
    8. main: 0,
    9. };

    由于 import() 返回的是一个 promise,然后import()经过 webpack 编译后就是一个__webpack_require__.e函数,因此可以得出__webpack_require__.e返回的也是一个 promise,如下所示:

    1. const scriptUrl = document.currentScript.src
    2. .replace(/#.*$/, "")
    3. .replace(/\?.*$/, "")
    4. .replace(/\/[^\/]+$/, "/");
    5. __webpack_require__.e = (chunkId) => {
    6. return Promise.resolve(ensureChunk(chunkId, promises));
    7. };
    8. const ensureChunk = (chunkId) => {
    9. var installedChunkData = installedChunks[chunkId];
    10. if (installedChunkData === 0) return;
    11. let promise;
    12. // 1.如果多次调用了__webpack_require__.e函数,即多次调用import('a.js')加载相同的模块,只要第一次的加载还没完成,就直接使用第一次的Promise
    13. if (installedChunkData) {
    14. promise = installedChunkData[2];
    15. } else {
    16. promise = new Promise((resolve, reject) => {
    17. // 2.注意,此时的resolve,reject还没执行
    18. installedChunkData = installedChunks[chunkId] = [resolve, reject];
    19. });
    20. installedChunkData[2] = promise; //3. 此时的installedChunkData 为[resolve, reject, promise]
    21. var url = scriptUrl + chunkId;
    22. var error = new Error();
    23. // 4.在script标签加载完成或者加载失败后执行loadingEnded方法
    24. var loadingEnded = (event) => {
    25. if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId)) {
    26. installedChunkData = installedChunks[chunkId];
    27. if (installedChunkData !== 0) installedChunks[chunkId] = undefined;
    28. if (installedChunkData) {
    29. console.log("加载失败.....");
    30. installedChunkData[1](error); // 5.执行上面的reject,那resolve在哪里执行呢?
    31. }
    32. }
    33. };
    34. loadScript(url, loadingEnded, "chunk-" + chunkId, chunkId);
    35. }
    36. return promise;
    37. };

    __webpack_require__.e的主要逻辑在ensureChunk方法中,注意该方法里面的第 1 到第 5 个注释。这个方法创建一个 promise,并调用loadScript方法加载动态模块。需要特别主要的是,返回的 promise 的 resolve 方法并不是在 script 标签加载完成后改变。如果脚本加载错误或者超时,会在 loadingEnded 方法里调用 promise 的 reject 方法。实际上,promise 的 resolve 方法是在脚本请求完成后,在 self["webpackChunkwebpack_demo"].push()执行的时候调用的

    如何执行远程模块?

    远程模块是通过self["webpackChunkwebpack_demo"].push()函数执行的

    前面我们提到,a.js请求返回的内容是一个self["webpackChunkwebpack_demo"].push()函数。当请求完成,会自动执行这个函数。实际上,这就是一个 jsonp 的回调方式。该方法的实现如下:

    1. var webpackJsonpCallback = (data) => {
    2. var [chunkIds, moreModules] = data;
    3. var moduleId,
    4. chunkId,
    5. i = 0;
    6. for (moduleId in moreModules) {
    7. // 1.__webpack_require__.m存储的是所有的模块,包括静态模块和动态模块
    8. __webpack_require__.m[moduleId] = moreModules[moduleId];
    9. }
    10. for (; i < chunkIds.length; i++) {
    11. chunkId = chunkIds[i];
    12. if (installedChunks[chunkId]) {
    13. // 2.调用ensureChunk方法生成的promise的resolve回调
    14. installedChunks[chunkId][0]();
    15. }
    16. // 3.将该模块标记为0,表示已经加载过
    17. installedChunks[chunkId] = 0;
    18. }
    19. };
    20. self["webpackChunkwebpack_demo"] = [];
    21. self["webpackChunkwebpack_demo"].push = webpackJsonpCallback.bind(null);

    所有通过import()加载的模块,经过 webpack 编译后,都会被 self["webpackChunkwebpack_demo"].push()包裹。

    总结

    在 webpack 构建编译阶段,import()会被编译成类似__webpack_require__.e("src_a_js").then(__webpack_require__.bind(__webpack_require__, "./src/a.js"))的调用方式

    1. __webpack_require__
    2. .e("src_a_js")
    3. .then(__webpack_require__.bind(__webpack_require__, "./src/a.js"))
    4. .then((res) => {
    5. console.log("动态加载a.js..", res);
    6. });

    __webpack_require__.e()方法会创建一个 script 标签用于请求脚本,方法执行完返回一个 promise,此时的 promise 状态还没改变。

    script 标签被添加到 document.head 后,触发浏览器网络请求。请求成功后,动态的脚本会自动执行,此时self["webpackChunkwebpack_demo"].push()方法执行,将动态的模块添加到__webpack_require__.m属性中。同时调用 promise 的 resolve 方法改变状态,模块加载完成。

    脚本执行完成后,最后执行 script 标签的 onload 回调。onload 回调主要是用于处理脚本加载失败或者超时的场景,并调用 promise 的 reject 回调,表示脚本加载失败

  • 相关阅读:
    第02章 BeautifulSoup 入门
    DTU配置工具-F2x16工具
    Oracle报错:ORA-02292: 违反完整约束条件 - 已找到子记录问题解决
    PostgreSQL 正则表达式匹配字段
    Day 59 django ROM 多表查询
    基于51单片机的简易交通灯仿真代码讲解
    2022年09月 Python(四级)真题解析#中国电子学会#全国青少年软件编程等级考试
    电容如何能做升压?(电荷泵的工作原理及特性)
    【蓝桥杯】省赛无忧班(Python 组)第 2 期 11.2剪枝
    cocoscreator3.X 强更 游戏内下载APK和安装APK
  • 原文地址:https://blog.csdn.net/caryxp/article/details/134303667