前述
本文内容都是关于webpack优化的部分知识点总结,比较零碎,如需自取:
一、webpack优化之“资源管理”
~ style-loader css-loader 解析css文件
loader的解析顺序是从下到上,从右往左,编译顺序应该是先用css-loader编译css代码,再用style-loader放入到网页的style标签里面去。所以css-loader在右,style-loader在左,按照指定顺序排列。
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
+ module: {
+ rules: [
+ {
+ test: /.css$/i,
+ use: ['style-loader', 'css-loader'],
+ },
+ ],
+ },
};
处理图片的话不需要使用额外的loader配置因为wp5内置了Asset Modules
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /.css$/i,
use: ['style-loader', 'css-loader'],
},
+ {
+ test: /.(png|svg|jpg|jpeg|gif)$/i,
+ type: 'asset/resource',
+ },
],
},
};
那么,像字体这样的其他资源如何处理呢?
使用 Asset Modules 可以接收并加载任何文件,然后将其输出到构建目录。这就是说,我们可以将它们用于任何类型的文件,也包括字体。通过更新 webpack.config.js 来处理字体文件
webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
+ {
+ test: /.(woff|woff2|eot|ttf|otf)$/i,
+ type: 'asset/resource',
+ },
],
},
};
json也不需要配置loader,import或是require最后都会被baber转译为webpack--require
二、webpack优化之“输出管理”
HtmlWebpackPlugin(更新index.html)
假如我们自己在dist文件夹中已经mock了一个index.html,并且在其中引入了打包文件,如果输出的打包文件的文件名称发生变化或者说输出多个打包文件,但是index.html中引入的打包文件还是旧的,那我们再去执行这个indexhtml的时候显然就得不到我们想要的结果,那么怎样解决这个问题呢,别慌,webpack给我们提供了HtmlWebpackPlugin,即即使在 dist/ 文件夹如果我们已经有了 index.html 这个文件, HtmlWebpackPlugin 还是会默认生成它自己的 index.html 文件。也就是说,它会用新生成的 index.html 文件,替换我们的原有文件,我们看下如何配置这个插件:
首先安装插件,并且调整 webpack.config.js 文件:
npm install --save-dev html-webpack-plugin
webpack.config.js中配置
const path = require('path');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
index: './src/index.js',
print: './src/print.js',
},
+ plugins: [
+ new HtmlWebpackPlugin({
+ title: '输出管理',
+ }),
+ ],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
重新打包后,你会看到 HtmlWebpackPlugin 创建了一个全新的文件,所有的 bundle 会自动添加到 html 中去。
“/dist”文件夹清理
如果dist文件夹放的有一些无用的代码就会使得dist显得杂乱,但是我们只想要有用的代码,而webpack是不会追踪哪些文件是实际在项目中用到的。这个时候我们可以通过output.clean来清理dist文件夹,如下:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
index: './src/index.js',
print: './src/print.js',
},
plugins: [
new HtmlWebpackPlugin({
title: 'Output Management',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
+ clean: true,
},
};
manifest runtime(对于理解wbp打包很重要,下文有再提到)
在使用 webpack 构建的典型应用程序或站点中,有三种主要的代码类型:
1、你和你的团队编写的源码。
2、第三方代码你的源码会依赖的任何第三方的 library(包) 或 "vendor" 代码。
3、webpack 的 runtime 和 manifest,管理所有模块的交互。
runtime
runtime,以及伴随的 manifest 数据,主要是指:在浏览器运行过程中,webpack 用来连接模块化应用程序所需的所有代码。它包含:在模块交互时,连接模块所需的加载和解析逻辑。包括:已经加载到浏览器中的连接模块逻辑,以及尚未加载模块的延迟加载逻辑。
manifest
一旦你的应用在浏览器中以 index.html 文件的形式被打开,一些 bundle 和应用需要的各种资源都需要用某种方式被加载与链接起来。在经过打包、压缩、为延迟加载而拆分为细小的 chunk 这些 webpack 优化 之后,你精心安排的 /src 目录的文件结构都已经不再存在。所以 webpack 如何管理所有所需模块之间的交互呢?这就是 manifest 数据用途的由来……
当 compiler 开始执行、解析和映射应用程序时,它会保留所有模块的详细要点。这个数据集合称为 "manifest",当完成打包并发送到浏览器时,runtime 会通过 manifest 来解析和加载模块。无论你选择哪种 [模块语法],那些 import 或 require 语句现在都已经转换为 __webpack_require__ 方法,此方法指向模块标识符(module identifier)。通过使用 manifest 中的数据,runtime 将能够检索这些标识符,找出每个标识符背后对应的模块。
但是runtime和manifest,这对我有什么影响呢?
你可能会问。答案是大多数情况下没有。runtime 做完成这些工作:一旦你的应用程序加载到浏览器中,使用 manifest,然后所有内容将展现出魔幻般运行结果。然而,如果你决定通过使用浏览器缓存来改善项目的性能,理解这一过程将突然变得极为重要。
通过使用内容散列(content hash)作为 bundle 文件的名称,这样在文件内容修改时,会计算出新的 hash,浏览器会使用新的名称加载文件,从而使缓存无效。一旦你开始这样做,你会立即注意到一些有趣的行为。即使某些内容明显没有修改,某些 hash 还是会改变。这是因为,注入的 runtime 和 manifest(统称为引导模板,下文有提及) 在每次构建后都会发生变化。
source map(报错定位)
source map 如果bundle报错可以帮助定位映射到具体源码 eval-source-map(编译比较快) hidden-source-map
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
print: './src/print.js',
},
+ devtool: 'inline-source-map',
plugins: [
new HtmlWebpackPlugin({
title: 'Development',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
三、webpack优化之“自动编译”
每次修改完代码进行编译时都手动去执行npm build就会比较麻烦,那webpack中有没有配置能够自动编译呢?
1、watch mode(观察模式)
自动编译代码,可以在package.json中配置webpack watch mode 的 npm scripts
{
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
+ "watch": "webpack --watch",
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"html-webpack-plugin": "^4.5.0",
"webpack": "^5.4.0",
"webpack-cli": "^4.2.0"
},
"dependencies": {
"lodash": "^4.17.20"
}
}
命令行中运行 npm run watch,然后就会看到 webpack 是如何编译代码。 然而,你会发现并没有退出命令行。这是因为此 script 当前还在 watch 你的文件,但是watch mode只能监控文件的改变,不能完成自动编译后再去自动刷新浏览器页面的展示,webpack-dev-server的出现就解决了这个问题。
2、webpack-dev-server(这个最常用)
webpack-dev-server为你提供了一个基本的 web server,并且具有 live reloading(实时重新加载) 功能。设置如下:
首先需要安装(watch mode不需要安装,直接配置到package.json文件中)
npm install --save-dev webpack-dev-server
修改webpack.config.js配置文件,告知dev server从什么位置查找文件
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
print: './src/print.js',
},
devtool: 'inline-source-map',
+ devServer: {
+ static: './dist',
+ },
plugins: [
new HtmlWebpackPlugin({
title: 'Development',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
以上配置告知 webpack-dev-server,将 dist 目录下的文件 serve 到 localhost:8080 下。(注释:serve,将资源作为 server 的可访问文件)
webpack-dev-server 会从 output.path 中定义的目录为服务提供 bundle 文件,即,文件将可以通过 http://[devServer.host]:[devServer.port]/[output.publicPath]/[output.filename] 进行访问。
我们添加一个可以直接运行 dev server 的 script:
{
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"watch": "webpack --watch",
+ "start": "webpack serve --open",
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"html-webpack-plugin": "^4.5.0",
"webpack": "^5.4.0",
"webpack-cli": "^4.2.0",
"webpack-dev-server": "^3.11.0"
},
"dependencies": {
"lodash": "^4.17.20"
}
}
运行 npm start,我们会看到浏览器自动加载页面。如果你更改任何源文件并保存它们,web server 将在编译代码后自动重新加载。
3、webpack-dev-middleware(模块热更新)
webpack-dev-middleware 是一个封装器(wrapper),它可以把 webpack 处理过的文件发送到一个 server。webpack-dev-server 在内部使用了它,然而它也可以作为一个单独的 package 来使用,以便根据需求进行更多自定义设置。下面是一个 webpack-dev-middleware 配合 express server 的示例。
首先,安装 express 和 webpack-dev-middleware:
npm install --save-dev express webpack-dev-middleware
现在,我们需要调整 webpack 配置文件,以确保 middleware(中间件) 功能能够正确启用:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
print: './src/print.js',
},
devtool: 'inline-source-map',
devServer: {
static: './dist',
},
plugins: [
new HtmlWebpackPlugin({
title: 'Development',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
+ publicPath: '/',
},
};
我们将会在 server 脚本使用 publicPath,以确保文件资源能够正确地 serve 在 http://localhost:3000 下,稍后我们会指定 port number(端口号)。接下来是设置自定义 express server:
project
webpack-demo
|- package.json
|- webpack.config.js
+ |- server.js
|- /dist
|- /src
|- index.js
|- print.js
|- /node_modules
server.js
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);
// 告知 express 使用 webpack-dev-middleware,
// 以及将 webpack.config.js 配置文件作为基础配置。
app.use(
webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath,
})
);
// 将文件 serve 到 port 3000。
app.listen(3000, function () {
console.log('Example app listening on port 3000!\n');
});
现在,添加一个 npm script,以使我们更方便地运行 server:
{
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"watch": "webpack --watch",
"start": "webpack serve --open",
+ "server": "node server.js",
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"express": "^4.17.1",
"html-webpack-plugin": "^4.5.0",
"webpack": "^5.4.0",
"webpack-cli": "^4.2.0",
"webpack-dev-middleware": "^4.0.2",
"webpack-dev-server": "^3.11.0"
},
"dependencies": {
"lodash": "^4.17.20"
}
}
在 terminal(终端) 中执行 npm run server,将会有类似如下信息输出:
Example app listening on port 3000!
...
<i> [webpack-dev-middleware] asset index.bundle.js 1.38 MiB [emitted] (name: index)
<i> asset print.bundle.js 6.25 KiB [emitted] (name: print)
<i> asset index.html 274 bytes [emitted]
<i> runtime modules 1.9 KiB 9 modules
<i> cacheable modules 530 KiB
<i> ./src/index.js 406 bytes [built] [code generated]
<i> ./src/print.js 83 bytes [built] [code generated]
<i> ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
<i> webpack 5.4.0 compiled successfully in 709 ms
<i> [webpack-dev-middleware] Compiled successfully.
<i> [webpack-dev-middleware] Compiling...
<i> [webpack-dev-middleware] assets by status 1.38 MiB [cached] 2 assets
<i> cached modules 530 KiB (javascript) 1.9 KiB (runtime) [cached] 12 modules
<i> webpack 5.4.0 compiled successfully in 19 ms
<i> [webpack-dev-middleware] Compiled successfully.
打开浏览器,访问 http://localhost:3000。可以看到 webpack 应用程序已经运行
四、webpack优化之“代码分离”
代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。 代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。
常用的代码分离方法有三种:
- 入口起点:使用
entry配置手动地分离代码。 - 防止重复:使用 Entry dependencies 或者
SplitChunksPlugin去重和分离 chunk。 - 动态导入:通过模块的内联函数调用来分离代码。
1、入口起点
这个是最简单直观的代码分离方式,由于手动配置有点多所以会有一些隐患。下面操作从 main bundle中分离another module
project
webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
+ |- another-module.js
|- /node_modules
another-module.js
import _ from 'lodash';
console.log(_.join(['Another', 'module', 'loaded!'], ' '));
webpack.config.js
const path = require('path');
module.exports = {
- entry: './src/index.js',
+ mode: 'development',
+ entry: {
+ index: './src/index.js',
+ another: './src/another-module.js',
+ },
output: {
- filename: 'main.js',
+ filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
构建结果如下:
...
[webpack-cli] Compilation finished
asset index.bundle.js 553 KiB [emitted] (name: index)
asset another.bundle.js 553 KiB [emitted] (name: another)
runtime modules 2.49 KiB 12 modules
cacheable modules 530 KiB
./src/index.js 257 bytes [built] [code generated]
./src/another-module.js 84 bytes [built] [code generated]
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 245 ms
这种方式存在的不足就是:
如果入口chunk之间包含一些重复的模块,那些重复的模块都会被引入到各个bundle中,显然这种方法不够灵活,不能动态地将核心应用程序逻辑中的代码拆分出来。例如我们再./src/index.js中引入过lodash,在another.js中也引入过,这样就在生成的两个bundle中造成了重复引用。
2、防止重复(prevent duplication)
入口依赖
在webpack.config.js中配置 dependOn option 选项,这样可以在多个 chunk 之间共享模块:
webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: {
- index: './src/index.js',
- another: './src/another-module.js',
+ index: {
+ import: './src/index.js',
+ dependOn: 'shared',
+ },
+ another: {
+ import: './src/another-module.js',
+ dependOn: 'shared',
+ },
+ shared: 'lodash',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
如果我们要在一个 HTML 页面上使用多个入口时,还需设置 optimization.runtimeChunk: 'single'。
const path = require('path');
module.exports = {
mode: 'development',
entry: {
index: {
import: './src/index.js',
dependOn: 'shared',
},
another: {
import: './src/another-module.js',
dependOn: 'shared',
},
shared: 'lodash',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
+ optimization: {
+ runtimeChunk: 'single',
+ },
};
构建结果如下:
...
[webpack-cli] Compilation finished
asset shared.bundle.js 549 KiB [compared for emit] (name: shared)
asset runtime.bundle.js 7.79 KiB [compared for emit] (name: runtime)
asset index.bundle.js 1.77 KiB [compared for emit] (name: index)
asset another.bundle.js 1.65 KiB [compared for emit] (name: another)
Entrypoint index 1.77 KiB = index.bundle.js
Entrypoint another 1.65 KiB = another.bundle.js
Entrypoint shared 557 KiB = runtime.bundle.js 7.79 KiB shared.bundle.js 549 KiB
runtime modules 3.76 KiB 7 modules
cacheable modules 530 KiB
./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
./src/another-module.js 84 bytes [built] [code generated]
./src/index.js 257 bytes [built] [code generated]
webpack 5.4.0 compiled successfully in 249 ms
可以看到,除了生成 shared.bundle.js,index.bundle.js 和 another.bundle.js 之外,还生成了一个 runtime.bundle.js 文件。
SplitChunksPlugin
SplitChunksPlugin 插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。我们可以使用这个插件,将之前的示例中重复的 lodash 模块去除:
webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
another: './src/another-module.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
+ optimization: {
+ splitChunks: {
+ chunks: 'all',
+ },
+ },
};
其他分离代码的方法大家可以自己去webpack官网搜一下这里不做赘述。
五、webpack优化之“缓存策略”
我们使用 webpack 来打包我们的模块化后的应用程序,webpack 会生成一个可部署的 /dist 目录,然后把打包后的内容放置在此目录中。只要 /dist 目录中的内容部署到 server 上,client(通常是浏览器)就能够访问此 server 的网站及其资源。而最后一步获取资源是比较耗费时间的,这就是为什么浏览器使用一种名为 缓存 的技术。可以通过命中缓存,以降低网络流量,使网站加载速度更快,然而,如果我们在部署新版本时不更改资源的文件名,浏览器可能会认为它没有被更新,就会使用它的缓存版本。由于缓存的存在,当你需要获取新的代码时,就会显得很棘手。
下面讲下通过必要的配置,以确保 webpack 编译生成的文件能够被客户端缓存,而在文件内容变化后,能够请求到新的文件。
输出文件的文件名
我们可以通过替换 output.filename 中的 substitutions 设置,来定义输出文件的名称。webpack 提供了一种使用称为 substitution(可替换模板字符串) 的方式,通过带括号字符串来模板化文件名。其中,[contenthash] substitution 将根据资源内容创建出唯一 hash。当资源内容发生变化时,[contenthash] 也会发生变化。
project
webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
|- /node_modules
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
- title: 'Output Management',
+ title: 'Caching',
}),
],
output: {
- filename: 'bundle.js',
+ filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
然后运行我们的 build script 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 在入口 chunk 中,包含了某些 boilerplate(引导模板),特别是 runtime 和 manifest。(译注:boilerplate 指 webpack 运行时的引导代码)
提取引导模板(runtime和manifest)
SplitChunksPlugin 可以用于将模块分离到单独的 bundle 中。webpack 还提供了一个优化功能,可使用 optimization.runtimeChunk 选项将 runtime 代码拆分为一个单独的 chunk。将其设置为 single 来为所有 chunk 创建一个 runtime bundle:
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
title: 'Caching',
}),
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
+ optimization: {
+ runtimeChunk: 'single',
+ },
};
再次构建,然后查看提取出来的 runtime 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
将第三方库(library)(例如 lodash 或 react vue)提取到单独的 vendor chunk 文件中,是比较推荐的做法,因为它们很少像本地的源代码那样频繁修改。因此通过实现以上步骤,利用 client 的长效缓存机制,命中缓存来消除请求,并减少向 server 获取资源,同时还能保证 client 代码和 server 代码版本一致。 这可以通过使用 webpack官网中的SplitChunksPlugin 示例 2 中演示的 SplitChunksPlugin 插件的 cacheGroups 选项来实现。我们在 optimization.splitChunks 添加如下 cacheGroups 参数并构建:
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
title: 'Caching',
}),
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
optimization: {
runtimeChunk: 'single',
+ splitChunks: {
+ cacheGroups: {
+ vendor: {
+ test: /[\/]node_modules[\/]/,
+ name: 'vendors',
+ chunks: 'all',
+ },
+ },
+ },
},
};
再次构建,然后查看新的 vendor 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]
...
可以看到nodemodules中的vendors模块已经从main模块中分离了出来,并且main的体积减少到了240bytes
模块标识符
在项目中添加一个print.js文件
webpack-demo
|- package.json
|- package-lock.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 是由当前 script 脚本 import 进来的
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ element.onclick = Print.bind(null, 'Hello webpack!');
return element;
}
document.body.appendChild(component());
再次运行构建,然后我们期望的是,只有 main bundle 的 hash 发生变化,然而……
...
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]
...
……我们可以看到这三个文件的 hash 都变化了。这是因为每个 module.id 会默认地基于解析顺序(resolve order)进行增量。也就是说,当解析顺序发生变化,ID 也会随之改变。简要概括:
- main bundle 会随着自身的新增内容的修改,而发生变化。
- vendor bundle 会随着自身的
module.id的变化,而发生变化。 - manifest runtime 会因为现在包含一个新模块的引用,而发生变化。
容易知道,第一个和最后一个是可以理解的行为,但是第二个 vendor 的hash的变化是我们要修复的内容,此时我们需要将 optimization.moduleIds 设置为'deterministic':
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
title: 'Caching',
}),
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
optimization: {
+ moduleIds: 'deterministic',
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
vendor: {
test: /[\/]node_modules[\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
};
现在,不论是否添加任何新的本地依赖,对于前后两次构建,vendor hash 都应该保持一致:
...
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,临时移除额外的依赖:
import _ from 'lodash';
- import Print from './print';
+ // import Print from './print';
function component() {
const element = document.createElement('div');
// lodash 是由当前 script 脚本 import 进来的
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());
最后,再次运行我们的构建:
...
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创建library(库)
除了打包应用程序,webpack还可以用于打包js library
我们来编写一个名为 webpack-numbers 的小的 library,可以将数字 1 到 5 转换为文本表示,反之亦然,例如将 2 转换为 'two'。
基本的项目结构可能如下所示:
project
|- webpack.config.js
+ |- package.json
+ |- /src
+ |- index.js
+ |- ref.json
使用 npm 初始化项目,然后安装 webpack,webpack-cli 和 lodash:
npm init -y
npm install --save-dev webpack webpack-cli lodash
我们将 lodash 安装为 devDependencies 而不是 dependencies,因为我们不需要将其打包到我们的库中,否则我们的库体积很容易变大。
src/ref.json
[ { "num": 1, "word": "One" }, { "num": 2, "word": "Two" }, { "num": 3, "word": "Three" }, { "num": 4, "word": "Four" }, { "num": 5, "word": "Five" }, { "num": 0, "word": "Zero" }]
src/index.js
import _ from 'lodash';
import numRef from './ref.json';
export function numToWord(num) {
return _.reduce(
numRef,
(accum, ref) => {
return ref.num === num ? ref.word : accum;
},
''
);
}
export function wordToNum(word) {
return _.reduce(
numRef,
(accum, ref) => {
return ref.word === word && word.toLowerCase() ? ref.num : accum;
},
-1
);
}
我们可以从如下 webpack 基本配置开始: webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'webpack-numbers.js',
},
};
在上面的例子中,我们将通知 webpack 将 src/index.js 打包到 dist/webpack-numbers.js 中。
到目前为止,一切都应该与打包应到目前为止,一切都应该与打包应用程序一样,这里是不同的部分 - 我们需要通过 output.library 配置项暴露从入口导出的内容。
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'webpack-numbers.js',
+ library: "webpackNumbers",
},
};
我们将入口点命名为为WebPackageNumber,因此用户可以通过脚本引用使用它
<script src="https://example.org/webpack-numbers.js"></script>
<script>
window.webpackNumbers.wordToNum('Five');
</script>
然而它只能通过被 script 标签引用而发挥作用,它不能运行在 CommonJS、AMD、Node.js 等环境中。
作为一个库作者,我们希望它能够兼容不同的环境,也就是说,用户应该能够通过以下方式使用打包后的库:
CommonJS module require:
const webpackNumbers = require('webpack-numbers');
// ...
webpackNumbers.wordToNum('Two');
AMD module require:
require(['webpackNumbers'], function (webpackNumbers) {
// ...
webpackNumbers.wordToNum('Two');
});
script tag:
<!DOCTYPE html>
<html>
...
<script src="https://example.org/webpack-numbers.js"></script>
<script>
// ...
// Global variable
webpackNumbers.wordToNum('Five');
// Property in the window object
window.webpackNumbers.wordToNum('Five');
// ...
</script>
</html>
我们更新 output.library 配置项,将其 type 设置为 'umd'(umd就是通用的意思):
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'webpack-numbers.js',
- library: 'webpackNumbers',
+ library: {
+ name: 'webpackNumbers',
+ type: 'umd',
+ },
},
};
现在 webpack 将打包一个库,其可以与 CommonJS、AMD 以及 script 标签使用。
外部化 lodash
现在,如果执行 webpack,你会发现创建了一个体积相当大的文件。如果你查看这个文件,会看到 lodash 也被打包到代码中。在这种场景中,我们更倾向于把 lodash 当作 peerDependency。也就是说,consumer(使用者) 应该已经安装过 lodash 。因此,你就可以放弃控制此外部 library ,而是将控制权让给使用 library 的 consumer。
这可以使用 externals 配置来完成:
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'webpack-numbers.js',
library: {
name: "webpackNumbers",
type: "umd"
},
},
+ externals: {
+ lodash: {
+ commonjs: 'lodash',
+ commonjs2: 'lodash',
+ amd: 'lodash',
+ root: '_',
+ },
+ },
};
这意味着你的 library 需要一个名为 lodash 的依赖,这个依赖在 consumer 环境中必须存在且可用
对于想要实现从一个依赖中调用多个文件的那些 library:
import A from 'library/one';
import B from 'library/two';
// ...
无法通过在 externals 中指定整个 library 的方式,将它们从 bundle 中排除。而是需要逐个或者使用一个正则表达式,来排除它们。
module.exports = {
//...
externals: [
'library/one',
'library/two',
// 匹配以 "library/" 开始的所有依赖
/^library/.+$/,
],
};
发布
遵循 生产环境 指南中提到的步骤,来优化生产环境下的输出结果。那么,我们还需要将生成 bundle 的文件路径,添加到 package.json 中的 main 字段中。
package.json
{
...
"main": "dist/webpack-numbers.js",
...
}
或者,按照这个 指南,将其添加为标准模块:
{
...
"module": "src/index.js",
...
}
这里的 key(键) main 是参照 package.json 标准,而 module 是参照 一个提案,此提案允许 JavaScript 生态系统升级使用 ES2015 模块,而不会破坏向后兼容性。
现在,你可以 将其发布为一个 npm package,并且在 unpkg.com 找到它,并分发给你的用户。
注:为了暴露和 library 关联着的样式表,你应该使用 MiniCssExtractPlugin。然后,用户可以像使用其他样式表一样使用和加载这些样式表。
最后 构建项目性能
使用最新的 webpack 版本,将 Node.js 更新到最新版本,也有助于提高性能。除此之外,将你的 package 管理工具(例如 npm 或者 yarn)更新到最新版本,也有助于提高性能。较新的版本能够建立更高效的模块树以及提高解析速度。
loader
通过include将loader应用于最少数量的必要模块,每个额外的 loader/plugin 都有其启动时,否则将浪费计算机资源,如下:
const path = require('path');
module.exports = {
//...
module: {
rules: [
{
test: /.js$/,
include: path.resolve(__dirname, 'src'),
loader: 'babel-loader',
options: { // options选项
presets: ['@babel/preset-env'], // presets设置的就是当前js的版本
plugins: [require('@babel/plugin-transform-object-rest-spread')] // plugin是需要的插件
}
},
],
},
};
其中babel-loader是用于将es6转换成计算机可以编译的js语法。可以使用options来给loader传递选项:
cacheDirectory:主要是给webpack的bable-loader指定解析后的缓存路径,默认值是false. 当设置了这个值时,指定的目录将会用来缓冲loader的执行结果。之后的webpack 构建,将会尝试读取缓冲,来避免每次都执行时,产生高性能消耗的编译过程。如果提供的时空值或者传入true,那么loader使用默认的缓冲目录node_modules/.cache/babel-loader
优化bable-loader的编译速度可以使用:
- 比如排除
node_modules,参考文档中的 loaders 配置的exclude选项。 - 或者通过使用
cacheDirectory选项,将 babel-loader 提速至少两倍。 这会将转译的结果缓存到文件系统中。
课外小知识:
某度上经常会有人搞不清nodejs npm webpack三者之间的关系,在看下面的文章之前首先我们来理一下nodejs npm webpack之间的关系:
~ 首先nodejs是基于chrome V8 engine的js的运行环境,而npm是nodejs内置的一个包管理工具,webpack则是npm仓库中的众多“包”之一。
~ npm的m是Management,npx的x可以理解为eXecute,当执行npx xxx的时候,npx先看xxxz在$PATH里有没有,如果没有,找当前目录的node_modules里有没有,如果还是没有,就安装这个xxx 来执行。由以上可知npx也可以理解为少些package.json里一个script而诞生的。
什么是chunk ?
webpack打包生成的js文件就叫chunk,每个chunk就是一个代码块,name指的是打包前文件的名字。
module/chunk/bundle 区别(容易混淆)
- module : js模块
- chunk:webpack编译过程中,由多个jsmodule组成的文件
- bundle: 是chunk文件的最终状态,是webpack编译后的结果
webpack的hash策略(缓存相关):
hash chunkhash contenthash(name/区别)
webpack的hash策略,及三种hash的区别
下面是三种hash的原意表述:
- hash: unique hash generated for every build
- chunkhash: hashes based on each chunks' content
- contenthash: hashes generated for extracted content