一、什么是Webpack?
作为其核心,webpack是一个静态模块捆绑器。在特定项目中,webpack将所有文件和资产视为模块。在后台,它依赖于依赖图。依赖图描述了模块之间如何使用文件之间的引用(require和import语句)相互关联。这样,webpack会静态遍历所有模块以构建图,并使用它生成单个捆绑包(或多个捆绑包)-一个JavaScript文件,其中包含所有模块以正确顺序组合而成的代码。“静态地”表示,当webpack构建其依赖关系图时,它不执行源代码,而是将模块及其依赖关系缝合在一起。然后可以将其包含在HTML文件中。
二、概念介绍
Entry point(入口)
Webpack的entry point是收集前端项目的所有依赖项的起点。 实际上,这是一个简单的JavaScript文件。
这些依赖关系形成依赖关系图。
Webpack的默认entry point(从版本4开始)是src / index.js,它是可配置的。 webpack可以有多个entry point。
Output(输出)
output是在构建过程中收集生成的JavaScript和静态文件的位置。
Webpack的默认output文件夹(自版本4起)为dist /,也可以配置。
生成的JavaScript文件是所谓的bundle的一部分。
Loaders(加载程序)
loader是第三方扩展程序,可帮助webpack处理各种文件扩展名。 例如,有用于CSS,图像或txt文件的loader。
loaders的目标是在module中转换文件(JavaScript以外的文件)。 文件成为module后,webpack可以将其用作项目中的依赖项。
Plugins(插件)
Plugins是第三方扩展,可以更改webpack的工作方式。 例如,有一些用于提取HTML,CSS或设置环境变量的Plugin。
Mode(模式)
webpack有两种操作模式:开发和生产。 它们之间的主要区别是生产模式自动将最小化和其他优化应用于JavaScript代码。
Code splitting(代码拆分)
代码拆分或延迟加载是一种避免较大bundle产生的优化技术。
通过代码拆分,开发人员可以决定仅加载响应某些用户交互(例如单击或路由更改(或其他条件))的整个JavaScript代码块。
被拆分的一段代码变成了一个代码块(chunk)。
三、动手实践
要开始使用webpack,请创建一个新文件夹并在命令行中进入那个文件夹,初始化NPM项目:
mkdir webpack-tutorial && cd $_
npm init -y
接着安装webpack, webpack-cli, 和 webpack-dev-server
npm i webpack webpack-cli webpack-dev-server --save-dev
如果要用NPM脚本运行webpack,打开package.json并配置一个“ dev”脚本:
"scripts": {
"dev": "webpack --mode development"
},
上述脚本,我们指定webpack在开发模式下工作,便于本地调试。
运行webpack的步骤
让webpack在开发模式下运行:
npm run dev
可能会有以下错误:
ERROR in Entry module not found: Error: Can't resolve './src'
webpack在这里寻找默认 entry point src / index.js 创建src文件夹,并在src内创建一个简单的JavaScript文件:
mkdir src
echo 'console.log("Hello webpack!")' > src/index.js
现在再次运行npm run dev,应该不会再看到错误。 运行输出结果在一个叫dist/的新文件夹,其中包含一个名为main.js的JavaScript文件:
dist
└── main.js
这是我们生成的第一个Webpack bundle,也称为output。
配置webpack
对于简单的任务,webpack无需配置就可以工作,但是很快就会遇到一些限制。 通过文件配置webpack,需在项目文件夹中创建webpack.config.js:
touch webpack.config.js
Webpack用JavaScript编写,并在无头JavaScript环境(例如Node.js)上运行。 在这个文件中,至少需要定义module.exports,这是Node.js的Common JS导出:
module.exports = {
//
};
在webpack.config.js中,我们可以更改webpack的行为:
- entry point
- output
- loaders
- plugins
- code splitting
例如,要改变entry point的路径:
const path = require("path");
module.exports = {
entry: { index: path.resolve(__dirname, "source", "index.js") }
};
现在,webpack将在source / index.js中查找要加载的第一个文件。 更改包的输出路径:
const path = require("path");
module.exports = {
output: {
path: path.resolve(__dirname, "build")
}
};
现在webpack会将生成的bundle输出到build文件夹而不是dist中。 (为简单起见,我们还是用默认输出路径)。
webpack 与 HTML
没有HTML页面的Web应用程序几乎没有用。 要在webpack中使用HTML,我们需要安装一个插件html-webpack-plugin:
npm i html-webpack-plugin --save-dev
插件装好后,我们可以在配置中使用他:
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "src", "index.html")
})
]
};
这里是让webpack 从src / index.html加载HTML。
html-webpack-plugin的最终目标有两个:
- 它加载我们的HTML文件
- 它将bundle注入到包含这些bundle文件的html文件中
现在,我们在src / index.html中创建一个简单的HTML页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Webpack tutorial</title>
</head>
<body>
</body>
</html>
然后,这个应用程序就会被webpack的开发服务器运行。
webpack的开发服务器
在前面我们已经安装了webpack-dev-server。如果没有安装,先安装:
npm i webpack-dev-server --save-dev
webpack-dev-server是用于开发的软件包。 配置完成后,我们可以启动本地服务器来提供文件。
要配置webpack-dev-server,打开package.json并添加一个“start”脚本:
"scripts": {
"dev": "webpack --mode development",
"start": "webpack-dev-server --mode development --open",
},
有了这个配置,我们就可以启动服务器了:
npm start
您的默认浏览器应打开。 在浏览器的console中,您还应该看到一个script标签,其中插入了我们的main.js bundle:
webpack 与 loaders
loaders是第三方扩展程序,可帮助webpack处理各种文件扩展名。 例如,有用于CSS,图像或txt文件的loader。
在配置方面,webpack loader的配置如下:
module.exports = {
module: {
rules: [
{
test: /\.filename$/,
use: ["loader-b", "loader-a"]
}
]
},
//
};
相关配置以module开头。 在module内,我们在rules内配置一个或者多个loader。
对于每个我们要视为Module的文件,我们使用test 和use 进行配置:
{
test: /\.filename$/,
use: ["loader-b", "loader-a"]
}
test 告诉webpack: 请把这个文件当成一个module,use 则说明那些Loader 将被运用到这个文件
webpack 与CSS
要在webpack中处理CSS,我们需要安装至少两个loader。loaders 帮助webpack知道如何处理CSS
测试webpack中的CSS, 先定义一个简单的css文件src/style.css:
h1 {
color: orange;
}
然后定义一个html文件src/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Webpack tutorial</title>
</head>
<body>
<h1>Hello webpack!</h1>
</body>
</html>
最后在在src/index.js中加载这个CSS:
import "./style.css";
console.log("Hello webpack!");
在测试之前,我们先安装处理css的loader:
- css-loader, 处理运用import来加载css文件
- style-loader,处理在DOM中加载css
npm i css-loader style-loader --save-dev
然后在webpack.config.js中配置这些loader:
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ["style-loader", "css-loader"]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "src", "index.html")
})
]
};
相关的配置在module里面
现在跑npm start就可以在浏览器里面看到那些css在html的head里面加载了:
如果css被minified, 可以通过MiniCssExtractPlugin展开css
Webpack中loaders的顺序很重要
在webpack中,加载程序在配置中出现的顺序非常重要。 以下配置无效:
//
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ["css-loader", "style-loader"]
}
]
},
//
};
此处,“ style-loader”出现在“ css-loader”之前。 但是style-loader用于在页面中注入样式,而不是用于加载实际的CSS文件。
相反,以下配置有效:
//
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ["style-loader", "css-loader"]
}
]
},
//
};
webpack加载程序从右到左加载(或从上到下考虑)。
Webpack与SASS
要在Webpack中使用SASS,我们至少需要安装适当的loader。
此处的loader对于帮助webpack了解如何处理.scss文件是必需的。
要在webpack中测试SASS,请在src / style.scss中创建一个简单的样式表:
@import url("https://fonts.googleapis.com/css?family=Karla:weight@400;700&display=swap");
$font: "Karla", sans-serif;
$primary-color: #3e6f9e;
body {
font-family: $font;
color: $primary-color;
}
另外,在src / index.html中的HTML模板中添加更多HTML元素:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Webpack tutorial</title>
</head>
<body>
<h1>Hello webpack!</h1>
<p>Hello sass!</p>
</body>
</html>
最后,将SASS文件加载到src / index.js中:
import "./style.scss";
console.log("Hello webpack!");
在测试页面之前,我们需要安装loader(以及Node.js的sass软件包):
sass-loader 处理import加载 SASS 文件
css-loader 处理加载CSS文件作为模块
style-loader 用于在DOM中加载样式表
安装 loaders:
npm i css-loader style-loader sass-loader sass --save-dev
然后在webpack.config.js中配置他们:
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
module.exports = {
module: {
rules: [
{
test: /\.scss$/,
use: ["style-loader", "css-loader", "sass-loader"]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "src", "index.html")
})
]
};
相关的配置仍然是以module开头:
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
module.exports = {
module: {
rules: [
{
test: /\.scss$/,
use: ["style-loader", "css-loader", "sass-loader"]
}
]
},
//
};
注意loader的顺序(从右往左), 第一个是sass-loader,第二个是css-loader,最后是style-loader.
现在运行npm-start,可以在html的head里面看到记载的样式:
一旦安装了SASS和CSS loader,就可以使用MiniCssExtractPlugin展开CSS文件。
Webpack与JavaScript
webpack本身并不知道如何转换JavaScript代码。 该任务已外包给带有babel的第三方loader,特别是babel-loader. babel是一个JavaScript编译器和“转译器”。 给定现代JavaScript语法作为输入,babel可以将其转换为可以在(几乎)任何浏览器中运行的兼容代码。
在继续之前,我们需要安装一堆软件包:
- babel core,实际引擎
- babel preset env,用于将现代Javascript编译为ES5
- babel loader
安装依赖项:
npm i @babel/core babel-loader @babel/preset-env --save-dev
然后通过创建一个新文件babel.config.json配置babel。 在这里,我们将babel配置 use preset-env:
{
"presets": [
"@babel/preset-env"
]
}
最后,配置webpack使用loader来转换JavaScript文件:
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
module.exports = {
module: {
rules: [
{
test: /\.scss$/,
use: ["style-loader", "css-loader", "sass-loader"]
},
{
test: /\.js$/,
exclude: /node_modules/,
use: ["babel-loader"]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "src", "index.html")
})
]
};
要测试转换,请在src / index.js中编写一些现代语法:
import "./style.scss";
console.log("Hello webpack!");
const fancyFunc = () => {
return [1, 2];
};
const [a, b] = fancyFunc();
现在运行npm run dev来查看dist中转换后的代码。 打开dist / main.js并搜索“ fancyFunc”:
\n\nvar fancyFunc = function fancyFunc() {\n return [1, 2];\n};\n\nvar _fancyFunc = fancyFunc(),\n _fancyFunc2 = _slicedToArray(_fancyFunc, 2),\n a = _fancyFunc2[0],\n b = _fancyFunc2[1];\n\n//# sourceURL=webpack:///./src/index.js?"
没有babel,代码将不会被转译:
\n\nconsole.log(\"Hello webpack!\");\n\nconst fancyFunc = () => {\n return [1, 2];\n};\n\nconst [a, b] = fancyFunc();\n\n\n//# sourceURL=webpack:///./src/index.js?");
注意:即使没有babel,webpack也可以正常工作。 仅在运输ES5时才需要进行代码转换过程。
Webpack 中JavaScript模块
webpack将一些文件视为一个模块。 但是,我们不要忘记它的主要目的:加载ES模块。
直到2015年,JavaScript仍没有标准的代码重用机制。 已经进行了很多尝试来标准化这方面,这导致多年来混乱的碎片化。
您可能听说过AMD模块,UMD或Common JS。 没有明确的获胜者。 最后,在ECMAScript 2015中,ES模块以该语言发布。 现在,我们有了一个“官方”模块系统。
webpack中使用ES模块和模块化代码还是很方便的。
要试用webpack中的ES模块,使用以下代码在src / common / usersAPI.js的新文件中创建一个模块:
const ENDPOINT = "https://jsonplaceholder.typicode.com/users/";
export function getUsers() {
return fetch(ENDPOINT)
.then(response => {
if (!response.ok) throw Error(response.statusText);
return response.json();
})
.then(json => json);
}
现在,在src / index.js中,可以加载模块并使用以下功能:
import { getUsers } from "./common/usersAPI";
import "./style.scss";
console.log("Hello webpack!");
getUsers().then(json => console.log(json));
Production 模式
如上所述,webpack具有两种操作模式:开发和生产。 到目前为止,我们仅在开发模式下工作。
在开发模式下,webpack接受我们编写的几乎所有原始JavaScript代码,并将其加载到浏览器中。
没有minify过。 这样可以更快地重新加载开发中的应用程序。
相反,在生产模式下,webpack进行了许多优化:
- 使用TerserWebpackPlugin进行minify以减小bundle包的大小
- 使用ModuleConcatenationPlugin提升范围
将process.env.NODE_ENV设置为“production”, 此环境变量对于在development或production模式中执行操作很有用。
要在生产模式下配置webpack,打开package.json并添加一个“ build”脚本:
"scripts": {
"dev": "webpack --mode development",
"start": "webpack-dev-server --mode development --open",
"build": "webpack --mode production"
},
现在,在执行 "npm run build" 时, webpack将产生一个缩小的包。
使用webpack进行代码拆分
代码拆分是指针对以下方面的优化技术:
- 避免产生很大的bundle 文件
- 避免依赖项重复
webpack社区决定应用程序的初始bundle包的最大大小的限制为:200KB。 要了解为什么让bundle包的size小很重要,Google "The Cost of JavaScript"。
激活webpack中的代码拆分的主要方法有以下三种:
- 定义多个entry point
- 使用 optimization.splitChunks
- 动态import
第一个基于多个entry point的技术适用于较小的项目,但从长远来看却无法扩展。 在这里,我们将仅关注optimization.splitChunks和动态import。
使用Optimization.splitChunks进行代码拆分
假设一个使用Moment.js的JavaScript应用程序,Moment.js是当前流行的一个关于日期和时间的JS库。
将这个library安装在项目文件夹中:
npm i moment
现在清除src / index.js的内容,并在那里导入库:
import moment from "moment";
运行npm run build,查看输出:
main.js 350 KiB 0 [emitted] [big] main
整个库都bundle在我们应用程序的main entry point 中, 这样不好。 通过optimization.splitChunks,我们可以从main bundle中移出moment.js。
要配置代码拆分,打开webpack.config.js并将optimization添加到配置中,配置如下:
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
module.exports = {
module: {
// omitted for brevity
},
optimization: {
splitChunks: { chunks: "all" }
},
// omitted for brevity
};
运行** npm run build**,查看输出:
main.js 5.05 KiB 0 [emitted] main
vendors~main.js 346 KiB 1 [emitted] [big] vendors~main
现在,我们有了一个带有moment.js的vendors〜main.js,而main entry point 的大小更合理。
注意:即使进行代码拆分,moment.js仍然是一个巨大的库。 还有更好的选择,如luxon或date-fns。
使用动态import进行代码拆分
一种更强大的代码拆分技术使用动态import有条件地加载代码。 在ECMAScript 2020中提供此功能之前,webpack就提供了动态导入。
这种方法在Vue和React之类的现代前端库中得到了广泛使用(React有其自己的方式,但是概念是相同的)。
代码拆分可能用在:
- 在module级别
- 在route级别
例如,可以有条件地加载一些JavaScript模块,以响应用户的交互(例如单击或鼠标移动)。 或者,您可以在响应路由更改时加载代码的相关部分。
要开始动态导入,清除src / index.html的内容,然后将以下html的内容放入其中:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Dynamic imports</title>
</head>
<body>
<button id="btn">Load!</button>
</body>
</html>
在src/common/usersAPI.js加入以下fetch模块代码:
const ENDPOINT = "https://jsonplaceholder.typicode.com/users/";
export function getUsers() {
return fetch(ENDPOINT)
.then(response => {
if (!response.ok) throw Error(response.statusText);
return response.json();
})
.then(json => json);
}
然后在src/index.html中加入以下代码:
const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
//
});
如果运行npm run start,然后单击界面中的按钮,则没有任何反应。
现在想象一下,我们想在某人单击按钮后加载用户列表。 “幼稚”的方法可以使用静态导入从src / common / usersAPI.js中加载函数:
import { getUsers } from "./common/usersAPI";
const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
getUsers().then(json => console.log(json));
});
问题在于ES模块是静态的,这意味着我们无法在运行时更改导入。
通过动态导入,我们可以选择何时加载代码:
const getUserModule = () => import("./common/usersAPI");
const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
getUserModule().then(({ getUsers }) => {
getUsers().then(json => console.log(json));
});
});
在这里,我们创建一个函数来动态加载模块:
const getUserModule = () => import("./common/usersAPI");
然后在event listener中,我们将then()链接到动态导入:
btn.addEventListener("click", () => {
getUserModule().then(/**/);
});
这样就可以通过对象解构来提取getUsers函数:
btn.addEventListener("click", () => {
getUserModule().then(({ getUsers }) => {
//
});
});
最后,我们照常使用函数:
//
btn.addEventListener("click", () => {
getUserModule().then(({ getUsers }) => {
getUsers().then(json => console.log(json));
});
});
现在,当第一次使用npm run start加载页面时,会看到控制台中已加载的main bundle:
现在,仅在单击按钮时才加载“ ./common/usersAPI”:
懒加载的chunk是0.js
通过在导入路径前面加上/ * webpackChunkName:“ name_here” * /,我们还可以控制这个chunk的名称:
const getUserModule = () =>
import(/* webpackChunkName: "usersAPI" */ "./common/usersAPI");
const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
getUserModule().then(({ getUsers }) => {
getUsers().then(json => console.log(json));
});
});
0.js 现在将具有如下名称, usersAPI.js:
动态import 与prefetch和preload
webpack 4.6.0+增加了对prefetch和preload的支持。
在声明您的导入时使用这些内联指令可以使webpack输出“ Resource Hint”,它告诉浏览器:
- prefetch:将来可能需要一些导航资源
- preload:当前导航期间可能需要资源
一个简单的prefetch示例: 有一个HomePage组件,该组件呈现一个LoginButton组件,然后按需在单击后加载LoginModal组件。
LoginButton.js
//...
import(/* webpackPrefetch: true */ 'LoginModal');
这将使<link rel =“ prefetch” href =“ login-modal-chunk.js”>附加在页面顶部,指示浏览器在空闲时间预取login-modal-chunk.js 文件。
一旦父块被加载,webpack将添加prefetch提示。
与prefetch相比,Preload指令有很多区别:
- preload的块开始并行于父块加载。 父块完成加载后,prefetch 才开始。
- preload的块具有中等优先级,可以立即下载。 浏览器空闲时,才会下载prefetch的块。
- 父块应立即请求preload的块。 prefetch的块可以在将来的任何时候使用。
- 浏览器支持不同。
一个简单的预加载示例: 有一个组件,该组件始终依赖于应放在单独块中的大库。
让我们想象一个需要巨大ChartingLibrary的组件ChartComponent。 他将显示一个LoadingIndicator,并即时按需导入ChartingLibrary:
ChartComponent.js
//...
import(/* webpackPreload: true */ 'ChartingLibrary');
当请求使用ChartComponent的页面时,也会通过<link rel =“ preload”>请求charting-library-chunk。 假设页面块较小并且完成得比较快,该页面将显示一个LoadingIndicator,直到已经请求的制图库块完成为止。 这将增加一点加载时间,因为它只需要一个往返而不是两个。 特别是在高延迟环境中。
错误地使用webpackPreload实际上会影响性能,因此使用时务必小心。
缓存
我们使用webpack bundle了模块化应用程序,从而产生了可部署的/ dist目录。 将/ dist的内容部署到服务器后,客户端(通常是浏览器)将访问该服务器以抢占该站点及其资产。 最后一步可能很耗时,这就是为什么浏览器使用缓存技术的原因。 这使站点能够以更少的不必要的网络流量更快地加载。 但是,当需要下载新代码时,它也会引起问题。
下面重点介绍确保除非打包文件的内容已更改,否则确保由webpack编译生成的文件可以保持缓存的配置。
输出文件名
我们可以使用output.filename替换设置来定义输出文件的名称。 webpack提供了一种使用方括号括起来的字符串来替代文件名的模板方法。 [contenthash]替换将基于resource的内容添加唯一的哈希。 当resource的内容更改时,[contenthash]也将更改。
让我们使用示例从输出管理插件开始设置项目,因此我们不必手动维护index.html文件:
project
webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
|- /node_modules
webpack.config.js
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
plugins: [
// new CleanWebpackPlugin(['dist/*']) for < v2 versions of CleanWebpackPlugin
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Output Management',
title: 'Caching',
}),
],
output: {
filename: 'bundle.js',
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
},
};
使用此配置运行我们的构建脚本npm run build,将产生以下输出:
...
Asset Size Chunks Chunk Names
main.7e2c49a622975ebd9b7e.js 544 kB 0 [emitted] [big] main
index.html 197 bytes [emitted]
...
如图所见,bundle包的名称现在反映了其内容(通过hash)。 如果我们在不进行任何更改的情况下运行另一个构建,我们希望该文件名保持不变。 但是,如果我们再次运行它,我们可能会发现情况并非如此:
...
Asset Size Chunks Chunk Names
main.205199ab45963f6a62ec.js 544 kB 0 [emitted] [big] main
index.html 197 bytes [emitted]
...
这是因为webpack在条目块中包含某些样板,特别是runtime和manifest。
输出可能会有所不同,具体取决于您当前的Webpack版本。 较新的版本可能没有与某些较旧的版本相同的哈希问题,但是为了安全起见,仍然建议执行以下步骤。
提取样板
正如我们在代码拆分中所了解的那样,SplitChunksPlugin可用于将模块拆分为单独的包。 webpack提供了优化功能,可以使用optimization.runtimeChunk选项将运行时代码拆分为单独的块。 将其设置为single可以为所有块创建单个运行时bundle包:
webpack.config.js
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
plugins: [
// new CleanWebpackPlugin(['dist/*']) for < v2 versions of CleanWebpackPlugin
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Caching',
}),
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
},
optimization: {
runtimeChunk: 'single',
},
};
让我们运行另一个构建,以查看提取的运行时bundle包:
Hash: 82c9c385607b2150fab2
Version: webpack 4.12.0
Time: 3027ms
Asset Size Chunks Chunk Names
runtime.cc17ae2a94ec771e9221.js 1.42 KiB 0 [emitted] runtime
main.e81de2cf758ada72f306.js 69.5 KiB 1 [emitted] main
index.html 275 bytes [emitted]
[1] (webpack)/buildin/module.js 497 bytes {1} [built]
[2] (webpack)/buildin/global.js 489 bytes {1} [built]
[3] ./src/index.js 309 bytes {1} [built]
+ 1 hidden module
将第三方库(例如lodash或react)提取到单独的供应商块中也是一个好习惯,因为与我们的本地源代码相比,第三方供应商块的更改可能性较小。 此步骤将允许客户端向服务器请求更少的请求以保持最新。 这可以通过使用SplitChunksPlugin的如下示例中演示的SplitChunksPlugin的cacheGroups选项来完成。
module.exports = {
//...
optimization: {
splitChunks: {
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
};
这可能会导致包含所有外部程序包的较大块。 建议仅包括您的核心框架和实用程序,并动态加载其余依赖项。
让我们使用带有下一个参数的cacheGroups添加optimization.splitChunks并构建:
webpack.config.js
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
plugins: [
// new CleanWebpackPlugin(['dist/*']) for < v2 versions of CleanWebpackPlugin
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Caching',
}),
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
},
optimization: {
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
};
运行另一个build,查看我们的新供应商bundle包:
...
Asset Size Chunks Chunk Names
runtime.cc17ae2a94ec771e9221.js 1.42 KiB 0 [emitted] runtime
vendors.a42c3ca0d742766d7a28.js 69.4 KiB 1 [emitted] vendors
main.abf44fedb7d11d4312d7.js 240 bytes 2 [emitted] main
index.html 353 bytes [emitted]
...
现在我们可以看到我们的 main bundle包不包含来自node_modules目录的供应商代码,并且大小减小到240个字节!
模块标识符
让我们向项目添加另一个模块print.js:
project
webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
|- print.js
|- /node_modules
print.js
export default function print(text) {
console.log(text);
};
src/index.js
import _ from 'lodash';
import Print from './print';
function component() {
const element = document.createElement('div');
// Lodash, now imported by this script
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.onclick = Print.bind(null, 'Hello webpack!');
return element;
}
document.body.appendChild(component());
运行build,我们希望仅更改main bundle的哈希,但是...
...
Asset Size Chunks Chunk Names
runtime.1400d5af64fc1b7b3a45.js 5.85 kB 0 [emitted] runtime
vendor.a7561fb0e9a071baadb9.js 541 kB 1 [emitted] [big] vendor
main.b746e3eb72875af2caa9.js 1.22 kB 2 [emitted] main
index.html 352 bytes [emitted]
...
...我们可以看到这三个都有改变。 这是因为默认情况下,每个module.id都会根据解析顺序递增。 意思是当resolve顺序改变时,ID也将改变。 因此,回顾一下:
- main bunle包由于其新内容而发生了变化。
- vendor bundle 软件已更改,因为其module.id已更改。
- 而且,runtme bundle发生了变化,因为它现在包含了对新模块的引用。
第一个和最后一个改变是预料的,我们要修复vendor的hash。 让我们将optimization.moduleIds的值改成“ hashed”:
webpack.config.js
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
plugins: [
// new CleanWebpackPlugin(['dist/*']) for < v2 versions of CleanWebpackPlugin
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Caching',
}),
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
},
optimization: {
moduleIds: 'hashed',
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
};
现在,尽管有任何新的本地依赖关系,我们的供应商哈希值仍应在各个版本之间保持一致:
...
Asset Size Chunks Chunk Names
main.216e852f60c8829c2289.js 340 bytes 0 [emitted] main
vendors.55e79e5927a639d21a1b.js 69.5 KiB 1 [emitted] vendors
runtime.725a1a51ede5ae0cfde0.js 1.42 KiB 2 [emitted] runtime
index.html 353 bytes [emitted]
Entrypoint main = runtime.725a1a51ede5ae0cfde0.js vendors.55e79e5927a639d21a1b.js main.216e852f60c8829c2289.js
...
让我们修改src / index.js来临时删除额外的依赖项:
src/index.js
import _ from 'lodash';
- import Print from './print';
+ // import Print from './print';
function component() {
const element = document.createElement('div');
// Lodash, now imported by this script
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
- element.onclick = Print.bind(null, 'Hello webpack!');
+ // element.onclick = Print.bind(null, 'Hello webpack!');
return element;
}
document.body.appendChild(component());
再build一下:
...
Asset Size Chunks Chunk Names
main.ad717f2466ce655fff5c.js 274 bytes 0 [emitted] main
vendors.55e79e5927a639d21a1b.js 69.5 KiB 1 [emitted] vendors
runtime.725a1a51ede5ae0cfde0.js 1.42 KiB 2 [emitted] runtime
index.html 353 bytes [emitted]
Entrypoint main = runtime.725a1a51ede5ae0cfde0.js vendors.55e79e5927a639d21a1b.js main.ad717f2466ce655fff5c.js
...
我们可以看到,两个版本在vendor bundle包的文件名中都产生了55e79e5927a639d21a1b。
缓存可能很复杂,但是对应用程序或站点用户的好处使其值得付出努力
总结-资源
在这篇文章中,我们介绍了webpack的基础知识:代码拆分,配置,加载程序,插件,prefetch preload, 缓存。 当然还有更多。
参考文献
初学者入门
webpack 介绍
其他重要资
webpack文档
Survive JS-Webpack