对一位合格的前端开发工程师来说,完成业务功能的需求开发只是基本的要求,能够及时准确地发现系统中存在的性能瓶颈,并且给出合适的解决方案,这才是区分初,中级前端工程师和高级前端工程师的重要依据。
对于什么是webpack,这里我就不多做解释,要是你还不知道,那本片文章就不适合现在的你阅读。
webpack5 基本配置
我们先来看一份基本的配置,基于webpack5的webpack.confing.js文件。
/*
* webpack.confing.js
*/
const { resolve } = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
// 模式
mode: "development",
// 入口
entry: "./src/index.js",
// 出口
output: {
filename: "built.js",
path: resolve(__dirname, "build"),
// 自定义输出 静态资源文件名(图片)
assetModuleFilename: "assets/[hash][ext]",
},
// 模块
module: {
rules: [
// loader的配置
{
test: /\.css$/,
// 使用loader对文件进行处理
/**
* use数组中的执行顺序是 从右到左 从下到上 依次执行
* style-loader 创建style标签,将样式文件引入到header中
* css-loader 将css模块变成commonjs模块加载到js中, 文件内容是样式字符串
*/
use: ["style-loader", "css-loader"],
},
{
test: /\.less$/,
use: [
"style-loader",
"css-loader",
// 将less文件编译成css文件
// 需要下载 less-loader和less
"less-loader",
],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
// webpack 5 内置了资源类型,已经废弃了之前的 url-loader 和 file-loader
type: "asset/resource",
},
{
test: /\.html$/,
// 处理html中的图片文件 引入img文件进而让url-loader处理
loader: "html-loader",
},
{
// 处理其他资源
exclude: /\.(html|js|css|less|png|svg|jpg|jpeg|gif)$/i,
type: "asset/resource",
},
],
},
// 插件
plugins: [
// HtmlWebpackPlugin
// 默认会创建一个空的html文件,会自动引入打包完成的所有资源。
// 需要有结构的html文件
new HtmlWebpackPlugin({
template: "./src/index.html",
}),
],
/**
* 下载 yarn add webpack-dev-server --dev
* 运行 npx webpack serve
*/
devServer: {
// 项目构建后的路径
contentBase: resolve(__dirname, "build"),
// 自动打开浏览器
open: true,
// 端口号
port: 5555,
// 开启gzip压缩
compress: true,
},
};
复制代码
上面的代码是我们这篇文章的最基础的代码,后边的打包优化就是基于这个文件来进行配置的。
HMR 热更新
模块热替换(HMR – hot module replacement)功能会在应用程序运行过程中,替换、添加或删除 模块,而无需重新加载整个页面。
优点:
- 保留在完全重新加载页面期间丢失的应用程序状态。
- 只更新变更内容,以节省宝贵的开发时间。
- 在源代码中 CSS/JS 产生修改时,会立刻在浏览器中进行更新,这几乎相当于在浏览器 devtools 直接更改样式。
问题图片
我们可以从上图看到,我们在改变样式的时候,我们的js文件重新执行了一遍,这样对于我们的项目来说是一个问题,我们的优化方向是改变那个文件,只有该文件重新加载。
代码
根据上边的问题,我们只需要改变devServer处的代码。
/*
* webpack.confing.js
*/
...
module.exports = {
...
devServer: {
// 项目构建后的路径
contentBase: resolve(__dirname, "build"),
// 自动打开浏览器
open: true,
// 端口号
port: 5555,
// 开启gzip压缩
compress: true,
// 新增---> 开启热更新
// 模块热替换功能会在程序运行过程中,替换,添加或删除模块,而无需重新加载整个页面。
hot: true,
},
};
复制代码
效果图片
devtool 源码调试方式
选择一种 source map 格式来增强调试过程。不同的值会明显影响到构建(build)和重新构建(rebuild)的速度。
-
对于开发环境的源码调试
eval-source-map – 初始化 source map 时比较慢,但是会在重新构建时提供比较快的速度,并且生成实际的文件。行数能够正确映射,因为会映射到原始代码中。它会生成用于开发环境的最佳品质的 source map。
-
对于生产环境的源码调试
(none)(省略 devtool 选项) – 不生成 source map。这是一个不错的选择。
Rule.oneOf 匹配规则
loader的匹配规则。
/*
* webpack.confing.js
*/
...
module.exports = {
...
// 模块
module: {
rules: [
// loader的配置
{
oneOf: [
// 以下的loader只会执行匹配的文件一次。
{
test: /\.css$/,
// 使用loader对文件进行处理
/**
* use数组中的执行顺序是 从右到左 从下到上 依次执行
* style-loader 创建style标签,将样式文件引入到header中
* css-loader 将css模块变成commonjs模块加载到js中, 文件内容是样式字符串
*/
use: ["style-loader", "css-loader"],
},
{
test: /\.less$/,
use: [
"style-loader",
"css-loader",
// 将less文件编译成css文件
// 需要下载 less-loader和less
"less-loader",
],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
// webpack 5 内置了资源类型,已经废弃了之前的 url-loader 和 file-loader
type: "asset/resource",
},
{
test: /\.html$/,
// 处理html中的图片文件 引入img文件进而让url-loader处理
loader: "html-loader",
},
{
// 处理其他资源
exclude: /\.(html|js|css|less|png|svg|jpg|jpeg|gif)$/i,
type: "asset/resource",
},
],
},
],
},
...
}
复制代码
如此写法,在打包时loader的匹配将提高速度。
文件缓存
在生产环境时,我们可以将我们打包的css和js等资源缓存到浏览器中,以提高我们第二次进入的页面是的速度。所以我们得对webpack.confing.js进行配置,对js文件和css文件和图片文件进行配置缓存。
首先我们得下载如下loader:
yarn add babel-loader @babel/core @babel/preset-env mini-css-extract-plugin --dev
优化步骤
- 将css文件从打包文件中提取为单独的文件。
- 对js文件使用babel缓存。
提取css并文件资源缓存
/*
* webpack.confing.js
*/
...
// 新增插件
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
...
// 模式
mode: "production",
output: {
filename: "built.[contenthash:10].js",
...
},
...
// 模块
module: {
rules: [
// loader的配置
{
oneOf: [
// 以下的loader只会执行匹配的文件一次。
{
test: /\.css$/,
// 将 style-loader 替换
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
{
test: /\.less$/,
use: [
// 将 style-loader 替换
MiniCssExtractPlugin.loader,
"css-loader",
"less-loader",
],
},
...
// 新增js的loader
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
// 开启babel缓存
// 第二次构建时,会读取之前的缓存
// 用来缓存 loader的执行结果。之后的webpack 构建,将会尝试读取缓存,
// 来避免在每次执行时,可能产生的、高性能消耗的 Babel
// 重新编译过程(recompilation process)。
cacheDirectory: true,
},
},
},
],
},
],
},
// 插件
plugins: [
...
//新增插件
new MiniCssExtractPlugin({
filename: "css/built.[contenthash:10].css",
}),
],
};
复制代码
-
hash: 每次wepack构建时会生成一个唯一的hash值。
- 问题: 因为js和css同时使用一个hash值。
- 如果重新打包,会导致所有缓存失效。(可能我却只改动一个文件)
-
chunkhash:根据chunk生成的hash值。如果打包来源于同一个chunk,那么hash值就一样。
- 问题: js和css的hash值还是一样的
- 因为css是在js中被引入的,所以同属于一个chunk
-
contenthash: 根据文件的内容生成hash值。
- 不同文件hash值一定不一样,让代码上线运行缓存更好使用。
新增jsloader并设置缓存
/*
* webpack.confing.js
*/
...
module.exports = {
...
// 模块
module: {
rules: [
// loader的配置
{
oneOf: [
...
// 新增js的loader
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
// 开启babel缓存
// 第二次构建时,会读取之前的缓存
// 用来缓存 loader的执行结果。之后的webpack 构建,将会尝试读取缓存,
// 来避免在每次执行时,可能产生的、高性能消耗的 Babel
// 重新编译过程(recompilation process)。
cacheDirectory: true,
},
},
},
],
},
],
},
...
};
复制代码
接下来我们书写测试代码。新建server.js文件,创建新的服务器进行测试。下载express框架。
yarn add express --dev
/*
* server.js
*/
const express = require("express");
const server = express();
// 缓存一个小时
server.use(express.static("build", { maxAge: 1000 * 3600 }));
server.listen(5555, () => {
console.log("服务器启动成功!", "http://localhost:5555/");
});
复制代码
检查缓存步骤
- 进行webpack打包 webpack
- 启动服务器 node server.js
- http://localhost:5555/
效果图片
tree shaking 去除无用代码
使用前提
- 必须使用ES6模块化
- 开启production环境
优点: 在打包生产环境时,可以将我们未使用的代码进行忽略,从打包文件中删除我们未使用的代码,减小打包文件的体积。
tree shaking 只要我们满足前面的两个前提,webpack在打包时我自动进行无效代码的删除。
新增测试文件
/*
* testTreeShaking.js
*/
export const test1 = () => {
console.log("test1");
};
export const test2 = () => {
console.log("test2");
};
复制代码
修改文件
/*
* index.js
*/
...
import { test1 } from "./testTreeShaking";
test1();
// 未引入test2函数。打包时会忽略。
复制代码
/*
* webpack.confing.js
*/
...
module.exports = {
...
// 修改打包模式
mode: 'production',
...
};
复制代码
package.json中配置
/*
* ackage.json
*/
"sideEffects": false
// 没有副作用(都可以进行tree shaking)
// 可能会把css / @babel/polyfill (副作用)文件干掉
"sideEffects": ["*.css", "*.less"]
复制代码
webpack打包,查看打包文件如下图:
可以看到,test2函数并没有打包进入打包文件。
code Split 代码分割
代码分割分为三种:
- 多入口文件会自动进行代码分割
- optimization.splitChunks 控制代码分割
- optimization.splitChunks + import() 进行代码分割
优点: 进行代码分割可以有效拒绝js文件过于庞大。
多入口文件会自动进行代码分割
/*
* webpack.confing.js
*/
...
module.exports = {
...
entry: {
// 多入口:有一个入口,最终输出就有一个bundle
index: "./src/index.js",
test: "./src/testTreeShaking.js",
},
...
};
复制代码
webpack打包查效果,如下图:
optimization.splitChunks控制代码分割
/*
* webpack.confing.js
*/
...
module.exports = {
...
// 新增代码
optimization: {
/*
1. 可以将node_modules中代码单独打包一个chunk最终输出
2. 自动分析多入口chunk中,有没有公共的文件。如果有会打包成单独一个chunk
*/
splitChunks: {
//这表明将选择哪些 chunk 进行优化。当提供一个字符串,有效值为 all,async 和 initial。
//设置为 all 可能特别强大,因为这意味着 chunk 可以在异步和非异步 chunk 之间共享。
chunks: "all",
},
},
...
};
复制代码
webpack打包查效果,如下图:
optimization.splitChunks + import() 进行代码分割
/*
* webpack.confing.js
*/
...
module.exports = {
...
// 新增代码
optimization: {
/*
1. 可以将node_modules中代码单独打包一个chunk最终输出
2. 自动分析多入口chunk中,有没有公共的文件。如果有会打包成单独一个chunk
*/
splitChunks: {
//这表明将选择哪些 chunk 进行优化。当提供一个字符串,有效值为 all,async 和 initial。
//设置为 all 可能特别强大,因为这意味着 chunk 可以在异步和非异步 chunk 之间共享。
chunks: "all",
},
},
...
};
复制代码
/*
* index.js
*/
/*
通过js代码,让某个文件被单独打包成一个chunk
import动态导入语法:能将某个文件单独打包
*/
import("./testTreeShaking").then(
(res) => {
console.log("res", res);
res.test1();
}
);
复制代码
webpack打包查效果,如下图:
文件懒加载 预加载
区别
- 使用文件的再加载
- 浏览器空闲时先进行加载
修改文件
/*
*index.html
*/
...
<button id="btn">加载testTreeShaking文件</button>
...
复制代码
/*
*index.js
*/
console.log("加载index文件");
复制代码
/*
*testTreeShaking.js
*/
export const test1 = () => {
console.log("test1");
};
export const test2 = () => {
console.log("test2");
};
console.log("加载index文件");
复制代码
文件懒加载
/*
*index.js
*/
console.log("加载index文件");
document.getElementById("btn").onclick = function () {
// 懒加载:当文件需要使用时才加载
import("./testTreeShaking").then(({ test1 }) => {
test1();
});
};
复制代码
懒加载效果图
预加载
/*
*index.js
*/
console.log("加载index文件");
document.getElementById("btn").onclick = function () {
// 预加载 prefetch:会在使用之前,提前加载js文件
// 正常加载可以认为是并行加载(同一时间加载多个文件)
// 预加载 prefetch: webpackPrefetch 等其他资源加载完毕,浏览器空闲了,再偷偷加载资源
import(/* webpackPrefetch: true */ "./testTreeShaking").then(({ test1 }) => {
test1();
});
};
复制代码
预加载效果图
PWA 渐进式网络应用程序
PWA 可以用来做很多事。其中最重要的是,在离线(offline)时应用程序能够继续运行功能。这是通过使用名为 Service Workers 的 web 技术来实现的。
淘宝的PWA效果图
添加 workbox-webpack-plugin 插件,然后调整 webpack.config.js 文件:
yarn add workbox-webpack-plugin --dev
修改webpack.confing.js
/*
* webpack.confing.js
*/
...
// 新增插件
const WorkboxWebpackPlugin = require("workbox-webpack-plugin");
module.exports = {
...
// 插件
plugins: [
...
//新增插件
new WorkboxWebpackPlugin.GenerateSW({
/*
1. 帮助serviceworker快速启动
2. 删除旧的 serviceworker
生成一个 serviceworker 配置文件~
*/
clientsClaim: true,
skipWaiting: true,
}),
],
};
复制代码
注册 Service Worker
/*
* index.js
*/
/*
sw代码必须运行在服务器上
--> nodejs
-->
npm server 启动服务器,将build目录下所有资源作为静态资源暴露出去
*/
// 注册serviceWorker
// 处理兼容性问题
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker
.register("/service-worker.js")
.then(() => {
console.log("sw注册成功了~");
})
.catch(() => {
console.log("sw注册失败了~");
});
});
}
复制代码
接下来我们将之前的server.js文件复制过来直接来使用:
- 先进行打包
- node server.js 启动服务器
- http://localhost:5555/
检查PWA效果图片
多进程打包
yarn add thread-loader --dev
修改webpack.confing.js文件:
/*
* webpack.confing.js
*/
...
module.exports = {
...
// 模块
module: {
rules: [
// loader的配置
{
oneOf: [
...
// 新增js的loader
{
test: /\.js$/,
exclude: /node_modules/,
use: [
/*
新增代码
开启多进程打包。
进程启动大概为600ms,进程通信也有开销。
只有工作消耗时间比较长,才需要多进程打包
*/
{
loader: "thread-loader",
options: {
// 产生的 worker 的数量,默认是 (cpu 核心数 - 1),或者,
// 在 require('os').cpus() 是 undefined 时回退至 1
workers: 2, // 进程2个
},
},
...
],
},
],
},
],
},
...
};
复制代码
运行webpack可以查看效果,因为这个多进程打包启动需要600ms,所以比较适合代码体量大一点的项目。
未开启多进程打包效果图
开启多进程打包效果图
externals
防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。
修改webpack.confing.js文件,添加externals属性
/*
* webpack.confing.js
*/
...
module.exports = {
...
externals: {
jquery: "jQuery",
},
};
复制代码
选择JQ的CDN链接,添加到index.html
/*
* index.html
*/
<!DOCTYPE html>
<html lang="zn">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>webpack打包优化</title>
<!-- 新增代码 -->
<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
</head>
<body>
<h1>webpack 打包优化</h1>
<div class="color_1"></div>
<div class="color_2"></div>
<img src="https://juejin.cn/post/img/zp.jpg" />
</body>
</html>
复制代码
/*
* index.js
*/
import $ from "jquery";
console.log("$", $);
复制代码
webpack打包进行测试。
效果图片
webpack打包优化理论篇至此结束,其实关于打包还有很多点,欢迎大家评论区进行评论,我们共同成长!!
下一篇 打包实践篇。