webpack 是现代 JavaScript 应用程序的静态模块捆绑器(modulebundler)。 当 webpack 处理一个应用程序时,它会递归地创建一个包含应用程序所需的每个模块的依赖图(dependency graph),然后将所有这些模块打包到一个或多个包中。
后端工程已经发展到明天,webpack 做出了巨大的贡献。 项目工程带来了很多便利。 我们不再需要自动处理依赖关系,我们可以更方便地使用更有用的框架。 我们可以更加关注业务本身,专心塑造我们的产品。
在 webpack 中,使用延迟加载或按需加载是优化网页或应用程序的好方法。 这些形式实际上是在一些逻辑断点处将你的代码分开,然后在完成某些代码块中的单独操作后,立即引用或正式引用其他新的代码块。 这提高了应用程序的初始加载率并减少了其整体大小,因为单个代码块可能永远不会被加载。
那么,我们来看看webpack对于延迟加载模块做了什么~
实施背景
我们首先假设我们正在完成一个真实的项目,该项目具有上传和下载功能。
下载功能通常是打开链接,所以我们直接在主包中实现。 上传功能可能会使用第三方sdk,我们采用延迟加载的方式进行加载。 只有当用户点击上传时,我们才会加载带有上传功能的包进行上传。
上传下载功能可能会用到一些第三方的sdk,而这个第三方sdk的体积往往很大,这个功能进行懒加载也是情理之中的。
为了演示区别,这里我们区分“下载”和“上传”两个功能。
项目基本配置
我们先构建一个基本的webpack配置来支持延迟加载配置,然后我们就可以直接使用打包后的代码来看看延迟加载的效果了。 我们需要一个基本的目录配置。 项目Demo目录结构如下:
文件/目录说明
源代码
入口文件、下载模块、上传模块
索引.html
html 模板文件
webpack.config.js
webpack 配置文件
包.json
项目文件
功能代码实现
我们看一下我们的功能代码实现,分别是download.js、upload.js、index.js。
// ./src/download.js
const download = () => {
console.log("download start");
console.log("schedule download sdk");
console.log("download");
}
export default download;
// ./src/upload.js
const upload = () => {
console.log("upload start");
console.log("schedule upload sdk");
console.log("upload");
}
export default upload;
// ./src/index.js
import download from "./download";
console.log("initial page");
async function handlerUploadClick() {
// 动态加载 upload 模块,该模块的 default 属性就是 upload 方法
const { default: upload } = await import("./upload");
// 调用 upload 方法
upload();
}
async function handlerDownloadClick() {
download();
}
// 点击 upload 按钮时,调用上传方法
document.querySelector("#upload").addEventListener("click", handlerUploadClick, false)
// 点击 download 按钮时,调用下载方法
document.querySelector("#download").addEventListener("click", handlerDownloadClick, false)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Webpack LazyLoad</title>
</head>
<body>
<section>
<h1>Home</h1>
<button id="upload">Upload</button>
<button id="download">Download</button>
</section>
</body>
</html>
在我们的功能代码实现中,我们实现了一个html网页,该网页有两个按钮,一个是上传按钮,另一个是下载按钮。
配置实现
功能实现后,我们需要配置webpack,然后打包生成一个可以直接运行的项目。
我们新建文件webpack.config.js进行配置,代码实现如下:
// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const WebpackChain = require("webpack-chain");
// 使用 webpack chain 组装配置
const chain = new WebpackChain();
// 设置入口文件为 src/index.js
chain.entry("main").add("./src/index.js").end();
// 将构建生成的文件输出到 dist 目录
chain.output.path(path.resolve(__dirname, "./dist")).end();
// 添加 html-webpack-plugin 插件,设置 HTML 模板文件
chain.plugin("html-webpack-plugin").use(HtmlWebpackPlugin, [{
template: path.resolve(__dirname, "./index.html")
}]);
// 设置 source map 生成规则
chain.devtool("cheap-source-map").end();
// 将配置转成 webpack 可识别的配置对象
const config = chain.toConfig();
module.exports = config;
从代码实现可以看出,我们的webpack配置只是配置了入口、出口、html模板文件。
这样我们就需要配置package.json,并在脚本中添加启动命令。 代码实现如下:
"scripts": {
"build": "NODE_ENV=development webpack --config webpack.config.js && cd dist && anywhere"
}
Anywhere是一个快速启动http服务的插件,可以使用npmianywhere-g全局安装。
配置完成后,我们需要运行以下命令来安装一些依赖项
npm i webpack webpack-cli webpack-chain html-webpack-plugin anywhere -D
安装完依赖项后,我们就可以计划启动项目了!
运行项目
我们运行npmbuild开始项目编译webpack拷贝模块,命令行输出如下(如右图)
我们的项目通过webpack打包后,输出到dist目录下,但是在8000端口的任意位置运行一个服务。
当我们打开浏览器时,我们可以看到下面的页面。 (如右图)
当我们打开控制台时,我们会听到我们设置的对应输出(如右图)
页面操作
我们来做一些页面操作,首先点击Download按钮,你会发现控制台的输出如下(如右图)
从上图可以看出,调用了download方法,此时进行了一次下载操作(mock操作)。
然后我们此时再次点击Upload按钮,然后观察控制台输出(如右图)
从控制台我们可以看到我们的上传操作已经被调用了,虽然和下载操作没有什么区别。
此时webpack拷贝模块,我们需要切换到网络控制面板来查看网络请求。 (如右图)
从上图我们可以看到,当调用upload方法时,会加载upload方法对应的文件,从而实现延迟加载。
这样做的好处是可以根据需求有效减小主包体积,提升首屏渲染速率,减轻服务器带宽压力。 同时也减少了JS解析时间,提高了页面渲染速度。
延迟加载的实现是后端性能优化的一门专门选修课。 可以说,项目越复杂,这种延迟加载的好处就越大。
实施分析
接下来我们可以看一下webpack编译后的代码,看看webpack是如何实现延迟加载的。
首先,我们看一下主包文件,即dist/main.js。 在此文件中找到我们在 src/index.js 中实现的初始化页面操作。 (如右图)
从上图可以看出,这个操作是直接打包到生成的bundle文件中的。
下载分析
然后我们看一下src/download.js中实现的download方法调用(如右图)
从上图可以看出,handlerDownloadClick最终调用了_download__WEBPACK_IMPORTED_MODULE_0__.default方法。
这个 _download__WEBPACK_IMPORTED_MODULE_0__.default 是什么?
在建立后的代码中,发现了对该对象的形参操作(如右图)
虽然__webpack_require__函数是加载__webpack_modules__中对应的模块,但这里加载的对应模块是“./src/download.js”模块。
最后,我们在dist/main.js中找到了这个模块的定义。 (如右图)
从上图可以看出,虽然这个模块的定义是src/download.js的实现,但它被打包到dist/main.js中,成为__webpack_modules__对象的一部分。
上传分析
看完了download方法的封装实现,我们再来看看惰性上传是如何实现的?
我们首先找到upload对应的函数调用(如右图)
从上图可以看出,当点击上传按钮时,首先使用__webpack_require__.e方法进行加载操作。 我们来看看这个方法做了什么。
该方法首先拼接本模块对应的绝对路径(如右图)
该路径下的文件可能是我们打包后在dist目录下生成的文件(如右图)
然后采用动态插入脚本标签的形式,将相应的脚本文件插入到文档中。 (如右图)
当插入上传对应的文件时,会手动执行。 webpackJsonpCallback 方法将在 src_upload_js.js 脚本文件上执行。 执行完毕后,刚刚延迟加载的模块会被插入到窗口的webpackChunklazyload链表中。 (如右图)
执行该函数后,upload模块会被注册到__webpack_modules__中,之前的调用过程和调用download是一样的~(如右图)
概括
从一个简单的案例,我们了解了webpack的延迟加载实现。
webpack的延迟加载实现在打包的时候会切掉延迟加载的代码单独打包,然后在主包中按需加载,最后执行调用。
最后我们用一张图来梳理一下延迟加载的加载执行过程。 (如右图)
最后一件事
如果你已经听过这个,希望你在离开之前仍然喜欢它~
您的点赞是对作者最大的鼓励,也让更多的人看到这篇文章!
如果您觉得这篇文章对您有帮助,请帮忙在github上点star鼓励一下!