从Webpack编译后的代码,探讨Webpack异步加载机制
从Webpack编译后的代码,探讨Webpack异步加载机制
首页加载不需要的模块,经常通过webpack的分包机制,将其独立出单独的文件。在需要的时候再加载。这样使首页加载的文件体积大大缩小,加快了加载时间。本篇探讨webpack是加载异步文件的原理以及webpack如何实现其原理的,最后在手动实现一个非常简单的demo。
原理
webpack异步加载的原理:
- 首先异步加载的模块,webpack在打包的时候会将独立打包成一个js文件(webpack如何将异步加载的模块独立打包成一个文件)
然后需要加载异步模块的时候: 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"
})])
转载自:https://segmentfault.com/a/1190000038336623