從Webpack編譯後的代碼,探討Webpack異步加載機制

從Webpack編譯後的代碼,探討Webpack異步加載機制

首頁加載不須要的模塊,常常經過webpack的分包機制,將其獨立出單獨的文件。在須要的時候再加載。這樣使首頁加載的文件體積大大縮小,加快了加載時間。本篇探討webpack是加載異步文件的原理以及webpack如何實現其原理的,最後在手動實現一個很是簡單的demo。javascript

原理

webpack異步加載的原理:html

  1. 首先異步加載的模塊,webpack在打包的時候會將獨立打包成一個js文件(webpack如何將異步加載的模塊獨立打包成一個文件)
  2. 而後須要加載異步模塊的時候:
    2.1 建立script標籤,src爲請求該異步模塊的url,並添加到document.head裏,由瀏覽器發起請求。

    2.2 請求成功後,將異步模塊添加到全局的__webpack_require__變量(該對象是用來管理所有模塊)後java

    2.3 請求異步加載文件的import()編譯後的方法會從全局的__webpack_require__變量中找到對應的模塊webpack

    2.4 執行相應的業務代碼並刪除以前建立的script標籤web

異步加載文件裏的 import()裏的回調方法的執行時機, 經過利用promise的機制來實現的,有些文章是說經過回調函數來實現的可能不太準確。

準備工做

環境:webpack版本:"5.7.0"

按一下目錄結構建立文件npm

├── 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是如何實現異步加載的入口。json

webpack如何實現的?

1.初始化(執行加載文件代碼以前)數組

  • 根據當前script獲取當前地址

根據當前執行js文件的地址,截取公共地址,並賦值帶全局變量中。promise

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 異步加載原理

相關文章
相關標籤/搜索