从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.jsfunction 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.jsexport const log = "log info"// webpack.config.jsonconst 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 异步加载原理