首頁加載不須要的模塊,常常經過webpack的分包機制,將其獨立出單獨的文件。在須要的時候再加載。這樣使首頁加載的文件體積大大縮小,加快了加載時間。本篇探討webpack是加載異步文件的原理以及webpack如何實現其原理的,最後在手動實現一個很是簡單的demo。javascript
webpack異步加載的原理:html
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
1.初始化(執行加載文件代碼以前)數組
根據當前執行js文件的地址,截取公共地址,並賦值帶全局變量中。promise
scriptUrl = document.currentScript.src scriptUrl = scriptUrl.replace(/#.*$/, "").replace(/\?.*$/, "").replace(/\/[^\/]+$/, "/"); // 1. 過濾hash 2.過濾參數 3. 過濾當前文件名 __webpack_require__.p = scriptUrl;
self["webpackChunkimport"].push = webpackJsonpCallback
2.執行中瀏覽器
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
變量裏獲取模塊的。
var url = __webpack_require__.p + __webpack_require__.u(chunkId);
__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" })])