关于javascript:从Webpack编译后的代码探讨Webpack异步加载机制

34次阅读

共计 6561 个字符,预计需要花费 17 分钟才能阅读完成。

从 Webpack 编译后的代码,探讨 Webpack 异步加载机制

首页加载不须要的模块,常常通过 webpack 的分包机制,将其独立出独自的文件。在须要的时候再加载。这样使首页加载的文件体积大大放大,放慢了加载工夫。本篇探讨 webpack 是加载异步文件的原理以及 webpack 如何实现其原理的,最初在手动实现一个非常简单的 demo。

原理

webpack 异步加载的原理:

  1. 首先异步加载的模块,webpack 在打包的时候会将独立打包成一个 js 文件(webpack 如何将异步加载的模块独立打包成一个文件)
  2. 而后须要加载异步模块的时候:
    2.1 创立 script 标签,src 为申请该异步模块的 url,并增加到 document.head 里,由浏览器发动申请。

    2.2 申请胜利后,将异步模块增加到全局的 __webpack_require__ 变量 (该对象是用来治理全副模块) 后

    2.3 申请异步加载文件的 import() 编译后的办法会从全局的 __webpack_require__ 变量中找到对应的模块

    2.4 执行相应的业务代码并删除之前创立的 script 标签

异步加载文件里的 import() 里的回调办法的执行机会,通过利用 promise 的机制来实现的,有些文章是说通过回调函数来实现的可能不太精确。

筹备工作

环境:webpack 版本:”5.7.0″

按一下目录构造创立文件

├── src
│   │── index.js
│   │── info.js
├── index.html
├── webpack.config.json
├── package.json
// src/index.js
function button () {const button = document.createElement('button')
  const text = document.createTextNode('click me')
  button.appendChild(text)
  button.onclick = e => import('./info.js').then(res => {console.log(res.log)
  })
  return button
}

document.body.appendChild(button())
// src/info.js
export const log = "log info"
// webpack.config.json

const path = require('path');

module.exports = {
  entry: './src/index.js',
  mode: 'development',
  output: {path: path.resolve(__dirname, './dist'),
    filename: 'main.js'
  }
}
// package.json
{
  "name": "import",
  "version": "1.0.0",
  "description": "","main":"webpack.config.js","dependencies": {"webpack":"^5.7.0","webpack-cli":"^4.2.0"},"devDependencies": {},"scripts": {"build":"webpack --config webpack.config.js","test":"echo \"Error: no test specified\" && exit 1"},"keywords": [],"author":"",
  "license": "ISC"
}

// index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script src="./dist/main.js"></script>
</body>
</html>

执行 npm run build
失去/dist/main.js`/dist/src_info_js.man.js` 文件。这两个文件就是咱们要剖析 webpack 是如何实现异步加载的入口。

webpack 如何实现的?

1. 初始化(执行加载文件代码之前)

  • 依据以后 script 获取以后地址

依据以后执行 js 文件的地址,截取公共地址,并赋值带全局变量中。

  scriptUrl = document.currentScript.src
  scriptUrl = scriptUrl.replace(/#.*$/, "").replace(/\?.*$/,"").replace(/\/[^\/]+$/, "/"); // 1. 过滤 hash 2. 过滤参数 3. 过滤以后文件名
  __webpack_require__.p = scriptUrl;
  • 重写 webpackChunkimport 数组的 push 办法(webpackJsonpCallback)
  self["webpackChunkimport"].push = webpackJsonpCallback

2. 执行中

  • 创立加载模块的 promise 对象,缓存要加载模块的 promise.resolve, promise.reject 以及 promise 本身。

import()编译成 __webpack_require__.e 办法

__webpack_require__.e = (chunkId) => {return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {__webpack_require__.f[key](chunkId, promises);
    return promises;
  }, []));
};

__webpack_required__f.j = (chunkId, promises) => {var promise = new Promise((resolve, reject) => {installedChunkData = installedChunks[chunkId] = [resolve, reject];
  });
  promises.push(installedChunkData[2] = promise);
  var url = __webpack_require__.p + __webpack_require__.u(chunkId);
  loadingEnded = (event) => {// ...}
    __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId);
}

var webpackJsonpCallback = (data) => {var [chunkIds, moreModules, runtime] = data;
  var moduleId, chunkId, i = 0,
    resolves = [];
  for (; i < chunkIds.length; i++) {chunkId = chunkIds[i];
    if (__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {resolves.push(installedChunks[chunkId][0]);
    }
    installedChunks[chunkId] = 0;
  }
  for (moduleId in moreModules) {if (__webpack_require__.o(moreModules, moduleId)) {__webpack_require__.m[moduleId] = moreModules[moduleId];
    }
  }
  parentChunkLoadingFunction(data);
  while (resolves.length) {resolves.shift()();}
}

webpack 是如何执行加载异步模块的?
1. 这里将 webpackJsonpCalback 放在一起,了解起来会跟好。由 webpack 将 import() 编译成的 __webpack_require__.e 办法,实际上是一个由 Promise.all 返回的 Promise 对象,每加载一个异步模块都会新建一个 promise 对象,并将其 resolve、reject 以及本身保留在 installedChunks 变量中。
2.webpackJsonpCallback是在异步加载文件中执行 webpackChunkimport 数组的 push 才会调用的,执行到 webpackJsonpCallback 办法时意味着异步加载的文件曾经加载胜利了。所以在该办法里将异步加载文件里的模块增加到 __webpack_require__.m 变量中(该变量保护着所有模块)。并将之前的创立的 promise 对象的 resolve 办法执行。
3.

// 申请异步加载的代码(编译前的代码)
function button () {const button = document.createElement('button')
  const text = document.createTextNode('click me')
  button.appendChild(text)
  button.onclick = e => import('./info.js').then(res => {console.log(res.log)
  })
  return button
}

document.body.appendChild(button())

// 申请异步加载的代码(编译后的代码)
function button() {const button = document.createElement('button');
const text = document.createTextNode('click me');
button.appendChild(text);
button.onclick = e =>
  __webpack_require__.e(/*! import() */ "src_info_js")
  .then(__webpack_require__.bind(__webpack_require__, "./src/info.js"))
  .then(res => {console.log(res.log)
  })
return button
}
document.body.appendChild(button())

察看申请异步加载的代码编译前后的不同,会发现编译后 import() 办法变成了 __webpack_requre__.e,而且还多了个 then 办法。为什么多了个 then 办法呢? 因为__webpack_require__.e 执行 resolve,没有返回的值,只是阐明该异步文件曾经加载胜利了并将模块增加到了 __webpack_require__.m, 而多的 then 办法里的代码就是从__webpack_require__.m 变量里获取模块的。

  • 生成 url
var url = __webpack_require__.p + __webpack_require__.u(chunkId);
  • 创立 script 标签,并增加加载胜利 script.onload 和失败的函数 script.onerror
__webpack_require__.l = (url, done, key) => {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;
    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];
    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);
};

3. 执行实现后
script.onload 加载机会
当异步加载的文件加载实现并执行完之后,触发 onload 办法,将之前新增的 script 标签删除。

简略实现

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <button class="btn">import something</button>
  <script>
    document.querySelector(".btn").addEventListener("click", () => {ensure("jsonp.js")
        .then(() => {return requireModule("jsonp.js")();})
        .then(res => {console.log(res.log);
        })
    })
    
    let modules = {};
    let handlers;
    window.jsonp = [];
    window.jsonp.push = webpackJsonpCallback;
    function requireModule (id) {return modules[id];
    }

    function webpackJsonpCallback (data) {let [id, moreModule] = data;
      modules[id] = moreModule;
      handlers.shift()();
    }

    function ensure (id, promises) {let promise = new Promise((resolve, reject) => {handlers = [resolve]
      })
      
      script = document.createElement('script');
      script.src = "jsonp.js";
      document.head.appendChild(script)

      return promise;
    }

  </script>
</body>
</html>
window.jsonp.push(["jsonp.js", () => ({"log": "log info"})])

参考:
你的 import 被 webpack 编译成了什么?
webpack 异步加载原理

正文完
 0