H1介绍
为什么需要打包工具?
开发时,我们会使用框架(React、Vue),ES6 模块化语法,Less/SASS 等 CSS 预处理器等语法进行开发。
这样的代码要想在浏览器运行必须经过编译成浏览器能识别的 JS、CSS 等语法,才能运行。
所以我们需要打包工具帮我们做完这些事。
除此之外,打包工具还能压缩代码、做兼容性处理、提升代码性能等。
H1入口 Entry
指示 Webpack 从哪个文件开始打包
js
module.exports = {
// 相对路径和绝对路径都行
entry: "./src/main.js",
};
H1输出 Output
指示 Webpack 打包完的文件输出到哪里去,如何命名等
js
output: {
// path: 所有文件输出目录,必须是绝对路径
// path.resolve() 方法返回一个绝对路径
// __dirname 当前文件的文件夹绝对路径
path: path.resolve(__dirname, "dist"),
// filename: 入口文件打包输出文件名
// 将 js 文件输出到 static/js 目录中
filename: "static/js/main.js",
// 自动清空上次打包内容
clean: true,
},
H1加载器 Loader
Webpack 本身是不能识别样式资源的,所以我们需要借助 Loader 来帮助 Webpack 解析样式资源。
H2CSS
css-loader
:负责将 CSS 文件编译成 Webpack 能识别的模块style-loader
:会动态创建一个 Style 标签,里面放置 Webpack 中 CSS 模块内容
配置:
js
module: {
rules: [
{
// 用来匹配 .css 结尾的文件
test: /\.css$/,
// use 数组里面 Loader 执行顺序是 从右到左
use: ["style-loader", "css-loader"],
},
],
},
H2Babel
将 ES6 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
js
{
test: /\.js$/,
exclude: /node_modules/, // 排除node_modules代码不编译
loader: "babel-loader",
option: {
presets: ["@babel/preset-env"],
}
},
H1资源
H2对图片资源进行优化
将小于某个大小的图片转化成 data URI 形式(Base64 格式)
- 优点:减少请求数量
- 缺点:体积变得更大
javascript
{
test: /\.(png|jpe?g|gif|webp)$/,
type: "asset",
parser: {
dataUrlCondition: {
maxSize: 10 * 1024 // 小于10kb的图片会被base64处
}
}
},
H2修改输出资源的路径和名称
js
{
test: /\.(png|jpe?g|gif|webp)$/,
type: "asset",
generator: {
// 将图片文件输出到 static/imgs 目录中
// 将图片文件命名 [hash:8][ext][query]
// [hash:8]: hash值取8位
// [ext]: 使用之前的文件扩展名
// [query]: 添加之前的query参数
filename: "static/imgs/[hash:8][ext][query]",
},
},
H2字体
js
{
test: /\.(ttf|woff2?)$/,
type: "asset/resource",
generator: {
filename: "static/media/[hash:8][ext][query]",
},
},
H1插件 Plugin
H2ESLint
它是用来检测 js 和 jsx 语法的工具
webpack.config.js
js
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
module.exports = {
plugins: [
new ESLintWebpackPlugin({
// 指定检查文件的根目录
context: path.resolve(__dirname, "src"),
}),
],
};
H2HTML
自动在 html 文件里引入打包文件
js
const HTMLWebpackPlugin = require("html-webpack-plugin");
module.exports = {
plugins: [
new HTMLWebpackPlugin({
// 以 public/index.html 为模板创建文件
// 新的html文件有两个特点:1. 内容和源文件一致 2. 自动引入打包生成的js等资源
template: path.resolve(__dirname, "public/index.html"),
}),
],
};
H1开发服务器 Dev Server
自动编译代码,代码更改后立马呈现效果
js
module.exports = {
// 开发服务器
devServer: {
host: "localhost", // 启动服务器域名
port: "3000", // 启动服务器端口号
open: true, // 是否自动打开浏览器
},
};
H1模式 Mode
H2开发模式
-
编译代码。使浏览器能识别运行
开发时我们有样式资源、字体图标、图片资源、html 资源等,webpack 默认都不能处理这些资源,所以我们要加载配置来编译这些资源
-
代码质量检查。树立代码规范
提前检查代码规范和格式,统一团队编码风格,让代码更优雅美观。
H2生产模式
这个模式下我们主要对代码进行优化,让其运行性能更好。主要优化代码运行性能和打包速度。
H3CSS 处理
H4提取 CSS 成单独文件
此前 CSS 文件被打包到 JS 文件中,当 JS 文件加载时,style-loader
才会创建一个 <style>
标签来生成样式。现在用 mini-css-extract-plugin
插件来替换 style-loader
。
webpack.prod.js
js
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
module: {
rules: [
{
// 用来匹配 .css 结尾的文件
test: /\.css$/,
// use 数组里面 Loader 执行顺序是从右到左
use: [MiniCSSExtractPlugin.loader, "css-loader"],
},
{
test: /\.less$/,
use: [MiniCSSExtractPlugin.loader, "css-loader", "less-loader"],
},
{
test: /\.s[ac]ss$/,
use: [MiniCSSExtractPlugin.loader, "css-loader", "sass-loader"],
},
{
test: /\.styl$/,
use: [MiniCSSExtractPlugin.loader, "css-loader", "stylus-loader"],
},
],
},
};
H4CSS 兼容性处理
使用 postcss
加载器加强样式代码在旧版浏览器的兼容性。
npm i postcss-loader postcss postcss-preset-env -D
webpack.prod.js
javascript
module: {
rules: [
{
test: /\.less$/,
use: [
MiniCSSExtractPlugin.loader,
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
"postcss-preset-env", // 能解决大多数样式兼容性问题
],
},
},
},
"less-loader",
],
},
];
}
注意:写在 css 和 less 之间
实际开发中一般不考虑旧版本浏览器,可在 package.json
设置:
json
{
// ...
"browserslist": ["last 2 version", "> 1%", "not dead"]
}
H4CSS 压缩
使用 css-minimizer-webpack-plugin
插件
webpack.prod.js
js
const CSSMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
plugins: [
// css压缩
new CSSMinimizerPlugin(),
],
};
H3HTML & JS 压缩
默认生产模式开启了 HTML, JS 压缩。
H2开发 & 生产
webpack.dev.js
js
module.exports = {
output: {
path: undefined, // 开发模式没有输出,不需要指定输出目录
filename: "static/js/main.js", // 开启开发服务器后依然要写,其决定虚拟内存中的文件名
// clean: true, // 开发模式没有输出,不需要清空输出结果
},
devServer: {
host: "localhost", // 启动服务器域名
port: "3000", // 启动服务器端口号
open: true, // 是否自动打开浏览器
},
};
运行开发模式的指令(地址可变):
bash
npx webpack serve --config ./config/webpack.dev.js
webpack.prod.js
js
module.exports = {
output: {
path: path.resolve(__dirname, "../dist"), // 生产模式需要输出
filename: "static/js/main.js", // 将 js 文件输出到 static/js 目录中
clean: true,
},
// 开发服务器可移除
};
为了方便运行不同 mode 指令,可将 package.json
中 script
进行改变:
js
"scripts": {
"start": "npm run dev",
"dev": "npx webpack serve --config ./config/webpack.dev.js",
"build": "npx webpack --config ./config/webpack.prod.js"
}
H1高级优化
H2提升开发体验
H3SourceMap
开发报错时,提示代码错误位置会不精准,因为代码经过压缩、混淆和合并后,生成的代码与原始源代码差异较大,使得调试非常困难。
SourceMap(源代码映射)是一个用来生成源代码与构建后代码一一映射的文件的方案。
开发模式:cheap-module-source-map
- 优点:打包编译速度快,只包含行映射
- 缺点:没有列映射
javascript
module.exports = {
mode: "development",
devtool: "cheap-module-source-map",
};
生产模式:source-map
- 优点:包含行/列映射
- 缺点:打包编译速度更慢
js
module.exports = {
mode: "production",
devtool: "source-map",
};
H2提升打包构建速度
H3热模块替换 HMR
开发时修改一处代码,Webpack 默认会将所有模块全部重新打包编译,很慢。
HMR (Hot Module Replacement) 能做到只修改某处模块代码,仅它能重新打包编译,其他模块不变,这样打包速度更快。
js
devServer: {
...
hot: true, // 开启HMR功能(只能用于开发环境,生产环境不需要了)
},
实际开发我们使用其他 loader 来解决。 比如:vue-loader, react-hot-loader。
H3OneOf
打包时每个文件都会经过所有 loader 处理,尽管由于 test 正则 最终只被一个 loader 处理,但都要遍历一遍,比较慢。
OneOf
能让你匹配上 loader 后就停止。
js
module: {
rules: [
{
oneOf: [
{
// 用来匹配 .css 结尾的文件
test: /\.css$/,
// use 数组里面 Loader 执行顺序是从右到左
use: ["style-loader", "css-loader"],
},
],
},
];
}
H3Include/Exclude
node_modules 不需要编译直接可用,所以在对 JS 文件处理时,要排除 node_modules 下的文件。
js
{
test: /\.js$/,
// exclude: /node_modules/, // 排除node_modules代码不编译
include: path.resolve(__dirname, "../src"), // 也可以用包含
loader: "babel-loader",
},
H3Cache
每次打包 JS 文件都要经过 ESLint 检查 和 Babel 编译,速度较慢。我们可以缓存之前的 ESLint 检查 和 Babel 编译结果,这样第二次打包时速度就会更快了。
js
module: {
rules: [
{
oneOf: [
{
test: /\.js$/,
// exclude: /node_modules/, // 排除node_modules代码不编译
include: path.resolve(__dirname, "../src"), // 也可以用包含
loader: "babel-loader",
options: {
cacheDirectory: true, // 开启babel编译缓存
cacheCompression: false, // 缓存文件不要压缩
},
},
],
},
];
}
plugins: [
new ESLintWebpackPlugin({
// 指定检查文件的根目录
context: path.resolve(__dirname, "../src"),
exclude: "node_modules", // 默认值
cache: true, // 开启缓存
// 缓存目录
cacheLocation: path.resolve(
__dirname,
"../node_modules/.cache/.eslintcache"
),
}),
];
H3Thread
项目越庞大,打包速度越慢。想继续提升打包速度,主要提升 JS 打包速度,主要就是 ESLint 、Babel、Terser(自带压缩 JS 工具) 三个工具。
我们可以开启多进程同时处理 JS 文件,这样速度比单进程打包更快。(适用于大项目)
先下载 thread-loader
js
const os = require("os");
const TerserPlugin = require("terser-webpack-plugin");
// cpu核数
const threads = os.cpus().length;
{
test: /\.js$/,
// exclude: /node_modules/, // 排除node_modules代码不编译
include: path.resolve(__dirname, "../src"), // 也可以用包含
use: [
{
loader: "thread-loader", // 开启多进程
options: {
workers: threads, // 数量
},
},
{
loader: "babel-loader",
options: {
cacheDirectory: true, // 开启babel编译缓存
},
},
],
},
new ESLintWebpackPlugin({
// 指定检查文件的根目录
context: path.resolve(__dirname, "../src"),
exclude: "node_modules", // 默认值
cache: true, // 开启缓存
// 缓存目录
cacheLocation: path.resolve(
__dirname,
"../node_modules/.cache/.eslintcache"
),
threads, // 开启多进程
}),
optimization: {
minimize: true,
minimizer: [
// css压缩也可以写到optimization.minimizer里面,效果一样的
new CSSMinimizerPlugin(),
// 当生产模式会默认开启TerserPlugin,但是我们需要进行其他配置,就要重新写了
new TerserPlugin({
parallel: threads // 开启多进程
})
],
},
H2减少代码体积
H3Tree Shaking
开发时我们定义了些工具函数库,或引用第三方工具函数库或组件库。若无特殊处理,打包时会引入整个库,但实际我们可能只用了一部分功能。
Webpack 已经默认开启了这个功能,无需其他配置。
H3Babel
Babel 为每个文件都插入了辅助代码,使代码体积过大。Babel 对一些公共方法使用了很小的辅助代码,如 _extend
。默认情况下会被添加到每个需要它的文件中。可以将这些辅助代码作为一个独立模块,来避免重复引入。
先下载 babel/plugin-transform-runtime
插件
js
{
test: /\.js$/,
// exclude: /node_modules/, // 排除node_modules代码不编译
include: path.resolve(__dirname, "../src"), // 也可以用包含
use: [
{
loader: "thread-loader", // 开启多进程
options: {
workers: threads, // 数量
},
},
{
loader: "babel-loader",
options: {
cacheDirectory: true, // 开启babel编译缓存
cacheCompression: false, // 缓存文件不要压缩
plugins: ["@babel/plugin-transform-runtime"], // 减少代码体积
},
},
],
},
H3Image Minimizer
用 image-minimizer-webpack-plugin
压缩图片插件
无损压缩 npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D
有损压缩 npm install imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo -D
js
optimization: {
minimizer: [
// 压缩图片
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.imageminGenerate,
options: {
plugins: [
["gifsicle", { interlaced: true }],
["jpegtran", { progressive: true }],
["optipng", { optimizationLevel: 5 }],
[
"svgo",
{
plugins: [
"preset-default",
"prefixIds",
{
name: "sortAttrs",
params: {
xmlnsOrder: "alphabetical",
},
},
],
},
],
],
},
},
}),
],
},
H2优化代码运行性能
H3Code Split
打包时会将所有 JS 文件打包到一个文件中,体积太大。若只渲染首页,就该只加载首页的 JS 文件。所以需要将打包生成的文件进行代码分割,这样加载的资源少,速度快。
代码分割:
- 分割文件:将打包生成的文件进行分割,生成多个 js 文件。
- 按需加载:需要哪个文件就加载哪个文件。
H4多入口
js
entry: {
main: "./src/main.js",
app: "./src/app.js",
},
output: {
path: path.resolve(__dirname, "./dist"),
// [name]是webpack命名规则,使用chunk的name作为输出的文件名。
// 什么是chunk?打包的资源就是chunk,输出出去叫bundle。
// chunk的name是啥呢? 比如: entry中xxx: "./src/xxx.js", name就是xxx。注意是前面的xxx,和文件名无关。
// 为什么需要这样命名呢?如果还是之前写法main.js,那么打包生成两个js文件都会叫做main.js会发生覆盖。(实际上会直接报错的)
filename: "js/[name].js",
clear: true,
},
H4提取公共模块
js
optimization: {
// 代码分割配置
splitChunks: {
chunks: "all", // 对所有模块都进行分割
// 以下是默认值
// minSize: 20000, // 分割代码最小的大小
// minRemainingSize: 0, // 类似于minSize,最后确保提取的文件大小不能为0
// minChunks: 1, // 至少被引用的次数,满足条件才会代码分割
// maxAsyncRequests: 30, // 按需加载时并行加载的文件的最大数量
// maxInitialRequests: 30, // 入口js文件最大并行请求数量
// enforceSizeThreshold: 50000, // 超过50kb一定会单独打包(此时会忽略minRemainingSize、maxAsyncRequests、maxInitialRequests)
// cacheGroups: { // 组,哪些模块要打包到一个组
// defaultVendors: { // 组名
// test: /[\\/]node_modules[\\/]/, // 需要打包到一起的模块
// priority: -10, // 权重(越大越高)
// reuseExistingChunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
// },
// default: { // 其他没有写的配置会使用上面的默认值
// minChunks: 2, // 这里的minChunks权重更大
// priority: -20,
// reuseExistingChunk: true,
// },
// },
// 修改配置
cacheGroups: {
// 组,哪些模块要打包到一个组
// defaultVendors: { // 组名
// test: /[\\/]node_modules[\\/]/, // 需要打包到一起的模块
// priority: -10, // 权重(越大越高)
// reuseExistingChunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
// },
default: {
// 其他没有写的配置会使用上面的默认值
minSize: 2, // 我们定义的文件体积太小了,所以要改打包的最小文件体积
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
}
H4按需加载,动态导入
js
document.getElementById("btn").onclick = function () {
// 动态导入 --> 实现按需加载
// 即使只被引用了一次,也会代码分割
import("./math.js").then(({ sum }) => {
alert(sum(1, 2, 3, 4, 5));
});
};
H4单入口
开发时我们可能是单页面应用(SPA)
js
module.exports = {
// 单入口
entry: "./src/main.js",
output: {
path: path.resolve(__dirname, "./dist"),
filename: "js/[name].js",
clean: true,
},
plugins: [
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
],
mode: "production",
optimization: {
// 代码分割配置
splitChunks: {
chunks: "all", // 对所有模块都进行分割
},
};
H4给动态导入文件取名
js
document.getElementById("btn").onClick = function () {
// eslint会对动态导入语法报错,需要修改eslint配置文件
// webpackChunkName: "math":这是webpack动态导入模块命名的方式
// "math"将来就会作为[name]的值显示。
import(/* webpackChunkName: "math" */ "./js/math.js").then(({ count }) => {
console.log(count(2, 1));
});
};
webpack.prod.js
js
output: {
path: path.resolve(__dirname, "../dist"), // 生产模式需要输出
filename: "static/js/[name].js", // 入口文件打包输出资源命名方式
chunkFilename: "static/js/[name].chunk.js", // 动态导入输出资源命名方式
assetModuleFilename: "static/media/[name].[hash][ext]", // 图片、字体等资源命名方式(注意用hash)
clean: true,
},
H3Preload / Prefetch
即便已做代码分割,同时会使用 import 动态导入语法来进行代码按需加载(懒加载)。但加载速度还不够好,比如:用户点击按钮时才加载这个资源,若资源体积大,那么会有明显卡顿效果。
我们想在浏览器空闲时间,加载后续需要使用的资源。我们就需要用上 Preload
或 Prefetch
技术。
Preload | Prefetch | |
---|---|---|
加载时间 | 告诉浏览器立即加载资源 | 在浏览器空闲时才开始加载资源。 也可以加载下个页面的所需资源。 |
优先级 | 高 | 低 |
兼容性 | 较低 | 低 |
webpack.prod.js
js
const PreloadWebpackPlugin = require("@vue/preload-webpack-plugin");
plugins: [
new PreloadWebpackPlugin({
rel: "preload", // preload兼容性更好
as: "script",
// rel: 'prefetch' // prefetch兼容性更差
}),
];
H3Network Cache
开发时都会对静态资源使用缓存来优化,这样浏览器二次请求能直接读取缓存,但这样会有个问题,因为前后输出的文件名是一样的,都叫 main.js
,一旦将来发布新版本,因为文件名没有变化导致浏览器会直接读取缓存,不会加载新资源,项目也就没法更新了。
所以我们通过给文件名加上哈希值,确保更新前后文件名不一样,这样浏览器就会加载新资源而不是使用旧缓存。
- fullhash(webpack4 是 hash):每次修改任何一个文件,所有文件名的 hash 至都将改变。所以一旦修改了任何一个文件,整个项目的文件缓存都将失效。
- chunkhash:根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值。我们 js 和 css 是同一个引入,会共享一个 hash 值。
- contenthash:根据文件内容生成 hash 值,只有文件内容变化了,hash 值才会变化。所有文件 hash 值是独享且不同的。
js
output: {
path: path.resolve(__dirname, "../dist"), // 生产模式需要输出
// [contenthash:8]使用contenthash,取8位长度
filename: "static/js/[name].[contenthash:8].js", // 入口文件打包输出资源命名方式
chunkFilename: "static/js/[name].[contenthash:8].chunk.js", // 动态导入输出资源命名方式
assetModuleFilename: "static/media/[name].[hash][ext]", // 图片、字体等资源命名方式(注意用hash)
clean: true,
},
H3Core-js
过去咋使用 Babel 对 JS 代码进行了兼容性处理。它能将 ES6 的一些语法进行编译转换,比如箭头函数、扩展运算符等。但 async 函数、promise 对象、数组的一些方法(includes)等,它没法处理。
babel.config.js
(自动按需引入)
js
module.exports = {
// 智能预设:能够编译ES6语法
presets: [
[
"@babel/preset-env",
// 按需加载core-js的polyfill
{ useBuiltIns: "usage", corejs: { version: "3", proposals: true } },
],
],
};
H3PWA
渐进式网络应用程序 (progressive web application - PWA):是一种可以提供类似于 native app(原生应用程序)离线体验的 Web App 的技术。
H1参考
文档:
视频: