Webpack5 教程优化代码运行性能
本文目录
代码分割
打包代码时会将所有 js 文件打包到一个文件中,体积太大了。
所以我们需要将打包生成的文件进行代码分割,生成多个 js 文件,渲染哪个页面就只加载某个 js 文件,这样加载的资源就少,速度就更快。
多入口文件
多个文件入口
1.文件目录
├── public
├── src
| ├── app.js
| └── main.js
├── package.json
└── webpack.config.js
2.js 文件
app.js
import {sum} from './math'
console.log('app.js')
console.log(sum(1,2,3,4))
main.js
import {sum} from './math'
console.log('main.js');
console.log(sum(4,5,6,7))
math.js
export function sum(...args){
return args.reduce((p,c)=>p+c,0);
}
3. 配置文件
// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
// 单入口
// entry: './src/main.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",
clean: true,
},
plugins: [
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
],
mode: "production",
};
4.下载依赖
npm i webpack webpack-cli html-webpack-plugin -D
5. 打包
npx webpack
此时在 dist 目录我们能看到输出了两个 js 文件
此时发现打包好的文件,引入了文件,都会独立写入2个入口文件里面,等于重复了引用,造成代码赘余。
6. 提取重复代码
修改配置文件
// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
// 单入口
// entry: './src/main.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",
clean: true,
},
plugins: [
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
],
mode: "production",
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: 0, // 我们定义的文件体积太小了,所以要改打包的最小文件体积
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};
打包运行
npx webpack
此时我们会发现生成 3 个 js 文件,其中有一个就是提取的公共模块。
按需加载
想要实现按需加载,动态导入模块。
main.js
console.log("hello main");
document.getElementById("btn").onclick = function () {
// 动态导入 --> 实现按需加载
// 即使只被引用了一次,也会代码分割
import("./math.js").then(({ sum }) => {
alert(sum(1, 2, 3, 4, 5));
});
};
app.js
console.log('app.js')
public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button id="btn">加载计算</button>
</body>
</html>
打包
npx webpack
可以发现,一旦通过 import 动态导入语法导入模块,模块就被代码分割,同时也能按需加载了
单入口
单入口+代码分割+动态导入
const os = require("os");
const path = require("path");
const ESLintPlugin = require('eslint-webpack-plugin');
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");
const PreloadWebpackPlugin = require("@vue/preload-webpack-plugin");
// cpu核数
const threads = os.cpus().length;
const getStyleLoaders = (preProcessor) => {
return [
MiniCssExtractPlugin.loader,
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
"postcss-preset-env", // 能解决大多数样式兼容性问题
],
},
},
},
preProcessor,
].filter(Boolean);
};
module.exports = {
//入口
entry: "./src/main.js",
//输出
output: {
//输出路径
//__dirname nodejs的变量,代表当前文件所在的文件夹目录
path: path.resolve(__dirname, "./dist"), //绝对路径
//入口文件输出文件名
filename: "static/js/[name].js", // 入口文件打包输出资源命名方式
clean: true, //自动清空上次打包的内容,原理,将path目录清空,再进行打包
},
//加载器
module: {
rules: [{
oneOf: [
//loader配置
{
test: /\.css$/i,
use: getStyleLoaders(),
},
{
test: /\.less$/i,
//loader:'xx' 只能使用1个loader
use: getStyleLoaders('less-loader'),
},
{
test: /\.s[ac]ss$/i,
use: getStyleLoaders('sass-loader'),
},
{
test: /\.styl$/,
loader: "stylus-loader", // 将 Stylus 文件编译为 CSS
},
{
test: /\.(png|jp?g|gif|webp|svg)$/,
type: "asset",
parser: {
dataUrlCondition: {
//小于10kb的图片转base64
//有点,减少请求数量,缺点,体积会变大
maxSize: 10 * 1024
}
},
generator: {
//hash:10 表示取hash前10位
filename: 'static/img/[hash:10][ext][query]'
}
},
//图标设置
{
test: /\.(ttf|woff2?)$/,
type: "asset/resource",
generator: {
//hash:10 表示取hash前10位
filename: 'static/font/[hash:10][ext][query]'
}
},
//视频文件
{
test: /\.(mp3|avi|mp4)$/,
type: "asset/resource",
generator: {
//hash:10 表示取hash前10位
filename: 'static/video/[hash:10][ext][query]'
}
},
//处理转换es6
{
test: /\.js$/,
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"], // 减少代码体积
},
}
],
},
]
}]
},
//插件
plugins: [
//
new ESLintPlugin({
//检查目录下的文件
context: path.resolve(__dirname, "./src"),
exclude: "node_modules", // 默认值
cache: true, // 开启缓存
// 缓存目录
cacheLocation: path.resolve(
__dirname,
"./node_modules/.cache/.eslintcache"
),
threads, // 开启多进程
}),
new HtmlWebpackPlugin({
// 以 public/index.html 为模板创建文件
// 新的html文件有两个特点:1. 内容和源文件一致 2. 自动引入打包生成的js等资源
template: path.resolve(__dirname, "./public/index.html"),
}),
// 提取css成单独文件
new MiniCssExtractPlugin({
// 定义输出文件名和目录
filename: "static/css/[name].css",
}),
],
optimization: {
minimize: true,
minimizer: [
// css压缩也可以写到optimization.minimizer里面,效果一样的
new CssMinimizerPlugin(),
// 当生产模式会默认开启TerserPlugin,但是我们需要进行其他配置,就要重新写了
new TerserPlugin({
parallel: threads // 开启多进程
}),
],
// 代码分割配置
splitChunks: {
chunks: "all", // 对所有模块都进行分割
// 其他内容用默认配置即可
},
},
//模式
mode: "production",
devtool: "source-map",
}
给动态导入文件取名称
例如我们导入的文件想要取个名字
import(/* webpackChunkName: "fun" */ "./js/math.js").then(({ count }) => {
console.log(count(2, 1));
});
那么这个math.js后面生成之后,会保存为fun.js
如果 eslint 存在无法导入问题,需要配置下
npm i eslint-plugin-import -D
// .eslintrc.js
module.exports = {
// 继承 Eslint 规则
extends: ["eslint:recommended"],
env: {
node: true, // 启用node中全局变量
browser: true, // 启用浏览器中全局变量
},
plugins: ["import"], // 解决动态导入import语法报错问题 --> 实际使用eslint-plugin-import的规则解决的
parserOptions: {
ecmaVersion: 6,
sourceType: "module",
},
rules: {
"no-var": 2, // 不能使用 var 定义变量
},
};
预加载 Preload / Prefetch
前面已经做了代码分割,同时会使用 import 动态导入语法来进行代码按需加载(我们也叫懒加载,比如路由懒加载就是这样实现的)。
但是加载速度还不够好,比如:是用户点击按钮时才加载这个资源的,如果资源体积很大,那么用户会感觉到明显卡顿效果。
我们想在浏览器空闲时间,加载后续需要使用的资源。我们就需要用上 Preload 或 Prefetch 技术。
- Preload:告诉浏览器立即加载资源
- Prefetch:告诉浏览器在空闲时才开始加载资源。
相同点和区别
- 都只会加载资源,并不执行。都有缓存.
- Preload加载优先级高,Prefetch加载优先级低。
- Preload只能加载当前页面需要使用的资源,Prefetch可以加载当前页面资源,也可以加载下一个页面需要使用的资源
那么我们如何使用?
- 当前页面优先级高的资源用 Preload 加载
- 下一个页面需要使用的资源用 Prefetch 加载
- Preload 相对于 Prefetch 兼容性好一点
1.安装
npm i @vue/preload-webpack-plugin -D
2.配置 webpack.prod.js
...
const PreloadWebpackPlugin = require("@vue/preload-webpack-plugin");
plugins: [
...
// new CssMinimizerPlugin(),
new PreloadWebpackPlugin({
rel: "preload", // preload兼容性更好
as: "script",
// rel: 'prefetch' // prefetch兼容性更差
}),
]
将来开发时我们对静态资源会使用缓存来优化,这样浏览器第二次请求资源就能读取缓存了,速度很快,但是将来发布新版本,因为文件名没有变化导致浏览器会直接读取缓存,不会加载新资源,项目也就没法更新了。
网络缓存
上面就存在了缓存导致更新的无法完成浏览器更新。所以我们从文件名入手,确保更新前后文件名不一样,这样就可以做缓存了。
- fullhash(webpack4 是 hash)
每次修改任何一个文件,所有文件名的 hash 至都将改变。所以一旦修改了任何一个文件,整个项目的文件缓存都将失效。
- chunkhash
根据不同的入口文件 (Entry) 进行依赖文件解析、构建对应的 chunk,生成对应的哈希值。我们 js 和 css 是同一个引入,会共享一个 hash 值。
- contenthash
根据文件内容生成 hash 值,只有文件内容变化了,hash 值才会变化。所有文件 hash 值是独享且不同的。
1.修改webpack.config.js
...
module.exports = {
entry: "./src/main.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,
},
plugins: [
...
new MiniCssExtractPlugin({
// 定义输出文件名和目录
filename: "static/css/[name].[contenthash:8].css",
chunkFilename: "static/css/[name].[contenthash:8].chunk.css",
}),
// new CssMinimizerPlugin(),
new PreloadWebpackPlugin({
rel: "preload", // preload兼容性更好
as: "script",
// rel: 'prefetch' // prefetch兼容性更差
}),
]
...
}
上面虽然我们配置了,但是我们在修改 math.js 文件再重新打包的时候,因为 contenthash 原因,math.js 文件 hash 值发生了变化。所以为了解决,我们需要将 hash 值单独保管在一个 runtime 文件中。
2.runtime配置
修改webpack.config.js
...
optimization:{
...
// 提取runtime文件
runtimeChunk: {
name: (entrypoint) => `runtime~${entrypoint.name}`, // runtime文件命名规则
},
}
兼容性问题 core-js
过去我们使用 babel 对 js 代码进行了兼容性处理,其中使用@babel/preset-env 智能预设来处理兼容性问题。
core-js 是专门用来做 ES6 以及以上 API 的 polyfill
polyfill翻译过来叫做垫片/补丁。就是用社区上提供的一段代码,让我们在不兼容某些新特性的浏览器上,使用该新特性。
npm i @babel/eslint-parser -D
.eslintrc.js
module.exports = {
// 继承 Eslint 规则
extends: ["eslint:recommended"],
parser: "@babel/eslint-parser", // 支持最新的最终 ECMAScript 标准
env: {
node: true, // 启用node中全局变量
browser: true, // 启用浏览器中全局变量
},
plugins: ["import"], // 解决动态导入import语法报错问题 --> 实际使用eslint-plugin-import的规则解决的
parserOptions: {
ecmaVersion: 6, // es6
sourceType: "module", // es module
},
rules: {
"no-var": 2, // 不能使用 var 定义变量
},
};
babel.config.js
module.exports = {
// 智能预设:能够编译ES6语法
presets: [
[
"@babel/preset-env",
// 按需加载core-js的polyfill
{ useBuiltIns: "usage", corejs: { version: "3", proposals: true } },
],
],
};
离线访问 PWA
渐进式网络应用程序(progressive web application - PWA):是一种可以提供类似于 native app(原生应用程序) 体验的 Web App 的技术。
其中最重要的是,在 离线(offline) 时应用程序能够继续运行功能。
内部通过 Service Workers 技术实现的。
1.安装插件
npm i workbox-webpack-plugin -D
2.修改配置 webpack.config.js
...
const WorkboxPlugin = require("workbox-webpack-plugin");
plugins: [
...
new WorkboxPlugin.GenerateSW({
// 这些选项帮助快速启用 ServiceWorkers
// 不允许遗留任何“旧的” ServiceWorkers
clientsClaim: true,
skipWaiting: true,
}),
]
3.修改 main.js
在后尾增加
...
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker
.register("/service-worker.js")
.then((registration) => {
console.log("SW registered:", registration);
})
.catch((registrationError) => {
console.log("SW registration failed:", registrationError);
});
});
}
我们执行打包
npx webpack
然后我们运行,发现http://127.0.0.1:5500/service-worker.js
,这样找不到会 404
解决路径问题
npm i serve -g
运行
serve dist
总结
上面所有的配置如下
const os = require("os");
const path = require("path");
const ESLintPlugin = require('eslint-webpack-plugin');
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");
const PreloadWebpackPlugin = require("@vue/preload-webpack-plugin");
const WorkboxPlugin = require("workbox-webpack-plugin");
// cpu核数
const threads = os.cpus().length;
const getStyleLoaders = (preProcessor) => {
return [
MiniCssExtractPlugin.loader,
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
"postcss-preset-env", // 能解决大多数样式兼容性问题
],
},
},
},
preProcessor,
].filter(Boolean);
};
module.exports = {
//入口
entry: "./src/main.js",
//输出
output: {
//输出路径
//__dirname nodejs的变量,代表当前文件所在的文件夹目录
path: path.resolve(__dirname, "./dist"), //绝对路径
//入口文件输出文件名
filename: "static/js/[name].[contenthash:8].js", // 入口文件打包输出资源命名方式
clean: true, //自动清空上次打包的内容,原理,将path目录清空,再进行打包
chunkFilename: "static/js/[name].[contenthash:8].chunk.js", // 动态导入输出资源命名方式
//assetModuleFilename: "static/media/[name].[hash][ext]", // 图片、字体等资源命名方式(注意用hash)
},
//加载器
module: {
rules: [{
oneOf: [
//loader配置
{
test: /\.css$/i,
use: getStyleLoaders(),
},
{
test: /\.less$/i,
//loader:'xx' 只能使用1个loader
use: getStyleLoaders('less-loader'),
},
{
test: /\.s[ac]ss$/i,
use: getStyleLoaders('sass-loader'),
},
{
test: /\.styl$/,
loader: "stylus-loader", // 将 Stylus 文件编译为 CSS
},
{
test: /\.(png|jp?g|gif|webp|svg)$/,
type: "asset",
parser: {
dataUrlCondition: {
//小于10kb的图片转base64
//有点,减少请求数量,缺点,体积会变大
maxSize: 10 * 1024
}
},
generator: {
//hash:10 表示取hash前10位
filename: 'static/img/[hash:10][ext][query]'
}
},
//图标设置
{
test: /\.(ttf|woff2?)$/,
type: "asset/resource",
generator: {
//hash:10 表示取hash前10位
filename: 'static/font/[hash:10][ext][query]'
}
},
//视频文件
{
test: /\.(mp3|avi|mp4)$/,
type: "asset/resource",
generator: {
//hash:10 表示取hash前10位
filename: 'static/video/[hash:10][ext][query]'
}
},
//处理转换es6
{
test: /\.js$/,
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"], // 减少代码体积
},
}
],
},
]
}]
},
//插件
plugins: [
//
new ESLintPlugin({
//检查目录下的文件
context: path.resolve(__dirname, "./src"),
exclude: "node_modules", // 默认值
cache: true, // 开启缓存
// 缓存目录
cacheLocation: path.resolve(
__dirname,
"./node_modules/.cache/.eslintcache"
),
threads, // 开启多进程
}),
new HtmlWebpackPlugin({
// 以 public/index.html 为模板创建文件
// 新的html文件有两个特点:1. 内容和源文件一致 2. 自动引入打包生成的js等资源
template: path.resolve(__dirname, "./public/index.html"),
}),
// 提取css成单独文件
new MiniCssExtractPlugin({
// 定义输出文件名和目录
filename: "static/css/[name].[contenthash:8].css",
chunkFilename: "static/css/[name].[contenthash:8].chunk.css",
}),
// new CssMinimizerPlugin(),
new PreloadWebpackPlugin({
rel: "preload", // preload兼容性更好
as: "script",
// rel: 'prefetch' // prefetch兼容性更差
}),
new WorkboxPlugin.GenerateSW({
// 这些选项帮助快速启用 ServiceWorkers
// 不允许遗留任何“旧的” ServiceWorkers
clientsClaim: true,
skipWaiting: true,
}),
],
optimization: {
minimize: true,
minimizer: [
// css压缩也可以写到optimization.minimizer里面,效果一样的
new CssMinimizerPlugin(),
// 当生产模式会默认开启TerserPlugin,但是我们需要进行其他配置,就要重新写了
new TerserPlugin({
parallel: threads // 开启多进程
}),
],
// 代码分割配置
splitChunks: {
chunks: "all", // 对所有模块都进行分割
// 其他内容用默认配置即可
},
// 提取runtime文件
runtimeChunk: {
name: (entrypoint) => `runtime~${entrypoint.name}`, // runtime文件命名规则
},
},
//模式
mode: "production",
devtool: "source-map",
}
我们从头到尾,我们所需要安装的插件如下:
npm i webpack webpack-cli
css-loader style-loader less-loader sass-loader sass
eslint-webpack-plugin eslint babel-loader @babel/core @babel/preset-env @babel/plugin-transform-runtime
html-webpack-plugin webpack-dev-server
mini-css-extract-plugin postcss-loader postcss postcss-preset-env css-minimizer-webpack-plugin
@vue/preload-webpack-plugin
@babel/eslint-parser @babel/eslint-parser
core-js
workbox-webpack-plugin
serve
-D
版权提示
1.除了标识原创之外,其他可能来源于网友的分享,仅供学习使用2.如您发现侵犯了您的权利,请联系我们删除
3.转载必须带本文链接,否则你将侵权
4.关于会员或其发布的相关内容均由会员自行提供,会员依法应对其提供的任何信息承担全部责任,本站不对此承担任何法律责任