Appearance
优化代码运行性能
Code Split
打包代码时会将所有 js 文件打包到一个文件中,体积太大。所以我们需要将打包生成的文件进行代码分割,生成多个 js 文件,渲染哪个页面就只加载某个 js 文件,这样加载的资源变少,速度就更快
- 分割文件:将打包生成的文件进行分割,生成多个 js 文件
- 按需加载:需要哪个文件就加载哪个文件
一、 多入口
1. 文件目录
text
├── public
├── src
| ├── app.js
| └── main.js
├── package.json
└── webpack.config.js
2. 配置
js
// 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,输出出去叫bundle。
// 此处chunk的 name 为多入口定义的 app 和 main,但与文件名无关
filename: "js/[name].js",
clean: true,
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "./public/index.html"),
}),
],
//...
};
3. 输出
dist/js
目录下会输出两个 js 文件;
配置了几个入口,至少输出几个 js 文件
二、提取重复代码
如果多入口文件都引用了同一份代码,会分别打包到各自输出文件中,致使包体积变大。所以可以提取出多入口中重复的代码,打包成单独的 js 文件,然后直接引用
- math.js
js
export const sum = (a, b) => {
return a + b;
};
- main.js
js
import { sum } from "./math.js";
sum(1, 2);
- app.js
js
import { sum } from "./math.js";
sum(3, 4);
1. 修改配置文件
- splitChunks 的默认值:
js
splitChunks: {
chunks: 'async',
minSize: 20000, //生成 chunk 的最小体积(以 bytes 为单位)
minRemainingSize: 0, // 类似minSize,最后确保提取的文件大小不能为0
minChunks: 1, //至少被引用的次数,满足条件才会代码分割
maxAsyncRequests: 30, // 按需加载时的最大并行请求数
maxInitialRequests: 30, //入口点的最大并行请求数
enforceSizeThreshold: 50000, //强制执行拆分的体积阈值(minRemainingSize,maxAsyncRequests,maxInitialRequests)将被忽略
// 组,哪些模块要打包到一个组
cacheGroups: {
// 组名
defaultVendors: {
test: /[\\/]node_modules[\\/]/, // 需要打包到一起的模块
priority: -10, // 权重(越大越高)
// 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
reuseExistingChunk: true,
},
// 此处定义的属性会替换上面相应的默认属性
default: {
minChunks: 2,
priority: -20, // 权重(越大优先执行)
reuseExistingChunk: true,
},
},
}
- 项目中配置:
js
const path = require("path");
module.exports = {
//...
optimization: {
splitChunks: {
chunks: "all",
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};
三、按需加载,动态导入
1. 基础配置
js
// main.js
// 动态导入 --> 实现按需加载
// 即使只被引用了一次,也会代码分割
document.getElementById("btn").onclick = () => {
import("./math.js").then(({ sum }) => {
sum(1, 2);
});
};
2. 动态导入命名
eslint 会对动态导入语法报错,需要修改 eslint 配置文件
- main.js
js
document.getElementById("btn").onclick = () => {
// webpackChunkName: "math":这是webpack动态导入模块命名的方式
// "math"将来就会作为[name]的值显示。
import(/* webpackChunkName: "math" */ "./js/math.js").then(({ sum }) => {
sum(1, 2);
});
};
- eslint 配置
shell
npm i eslint-plugin-import -D
js
// .eslintrc.js
module.exports = {
extends: ["eslint:recommended"],
env: {
node: true, // 启用node中全局变量
browser: true, // 启用浏览器中全局变量
},
// 解决动态导入import语法报错问题 --> 实际使用eslint-plugin-import的规则解决的
plugins: ["import"],
parserOptions: {
ecmaVersion: 6,
sourceType: "module",
},
rules: {
"no-var": 2, // 不能使用 var 定义变量
},
};
Preload/Prefetch
预获取/预加载后续需要用到的资源并缓存,提升浏览体验
1. 定义
prefetch(预获取):将来某些导航下可能需要的资源
preload(预加载):当前导航下可能需要资源
共同点:
- 只加载资源,并不执行
- 都会有缓存
- 兼容性差,
prefetch
相对更差些
区别:
preload
优先级更高,与父chunk
并行加载,prefetch
在父chunk
加载结束后加载preload
立即加载,prefetch
在浏览器空闲时加载
2. 使用
shell
npm i @vue/preload-webpack-plugin -D
js
// webpack.config.js
const PreloadWebpackPlugin = require("@vue/preload-webpack-plugin");
module.exports = {
//...
plugins: [
new PreloadWebpackPlugin({
// rel: "preload",
rel: "prefetch",
as: "script",
}),
],
//...
};
打包后输出:
html
<!DOCTYPE html>
<html lang="en">
<head>
<!-- 其他略 -->
<link rel="preload" as="script" href="static/js/math.chunk.js" />
</head>
<body>
<div>hello webpack</div>
</body>
</html>
Network Cache
浏览器会缓存静态资源在本地,当第二次加载时可以读取缓存资源,减少请求提升运行速度
当更改一个文件内代码时,所输出的文件名是不会改变的,这样会导致因文件名相同,浏览器缓存之前的资源,而没有加载最新的资源文件。所以,需要对文件名进行 hash 值操作
- fullhash(webpack4 为 hash)
所有输出文件 hash 值都相同,修改任何一个文件,所有文件 hash 值都将改变,导致所有文件缓存失效要重新加载 - chunkhash
根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值。 - contenthash
在 js 文件中导入 CSS 文件,因是同一个入口文件,会共享一个hash
值。只改动 JS 代码,CSS 文件的 hash 也会跟着变,这个时候就需要用contenthash
1. 使用
js
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
entry: "./src/main.js",
output: {
path: path.resolve(__dirname, "dist"),
// 入口文件打包命名
filename: "./static/js/[name].[chunkhash:8].js",
// 动态导入输出文件
chunkFilename: "./static/js/[name].[chunkhash:8].chunk.js",
clean: true,
},
//...
plugins: [
// 提取css成单独文件
new MiniCssExtractPlugin({
filename: "static/css/[name].[contenthash:8].css",
chunkFilename: "static/css/[name].[contenthash:8].chunk.css",
}),
],
//...
};
2. runtimeChunk
将包含 chunks 映射关系的 list 单独从 main.js
里提取出来,单独生成一个 runtime~xxx.js 的文件。避免每次修改文件,main.js
的 hash 值都会改变,导致缓存失效
js
// webpack.config.js
module.exports = {
//...
optimization: {
splitChunks: {
chunks: all,
},
// 提取runtime文件
// runtimeChunk: true,
runtimeChunk: {
// 自定义runtime文件命名规则
name: (entrypoint) => `runtime~${entrypoint.name}`,
},
},
//...
};
Core-js
core-js
是专门用来做 ES6 以及以上 API 的 polyfill
一般用 babel 对 js 代码进行兼容性处理,其中使用
@babel/preset-env
智能预设来处理兼容性问题。它能将 ES6 的一些语法进行编译转换,比如箭头函数、...运算符等。但是如果是 async 函数、promise 对象、数组的一些方法(includes)等,它没办法处理。
1. 配置 eslint
js
// main.js
//...
// 添加promise代码
const promise = Promise.resolve();
promise.then(() => {
console.log("hello promise");
});
此时eslint
会对promise
报错
- 配置 eslint
shell
npm i @babel/eslint-parser -D
js
// .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 定义变量
},
};
打包输出 js 文件, Promise 语法并没有编译转换,需要使用 core-js
来进行 polyfill
2. 手动引入
shell
npm i core-js
js
// main.js
// import "core-js"; //引入全部
import "core-js/es/promise"; //按需引入
// 添加promise代码
const promise = Promise.resolve();
promise.then(() => {
console.log("hello promise");
});
3. 自动按需引入
- babel.config.js
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 技术实现的。
使用
shell
npm i workbox-webpack-plugin -D
- webpack.config.js
js
const WorkboxPlugin = require("workbox-webpack-plugin");
//...
plugins: [
new WorkboxPlugin.GenerateSW({
// 这些选项帮助快速启用 ServiceWorkers
// 不允许遗留任何“旧的” ServiceWorkers
clientsClaim: true,
skipWaiting: true,
}),
];
//...
- main.js
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);
});
});
}
运行
此时如果直接通过 VSCode 访问打包后页面,在浏览器控制台会发现 SW registration failed
因为打开的访问路径是:http://127.0.0.1:5500/dist/index.html
。此时页面会去请求 service-worker.js
文件,请求路径是:http://127.0.0.1:5500/service-worker.js
,这样找不到会 404。
实际 service-worker.js
文件路径是:http://127.0.0.1:5500/dist/service-worker.js
- 解决路径问题
shell
npm i serve -g
运行指令:
shell
serve dist
此时通过 serve 启动的服务器, service-worker 就能注册成功