首页加载不须要的模块,常常经过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" })])