目录
一、Code Split(代码分割)
代码分割(Code Split)主要做了两件事:
- 分割文件:将打包生成的文件进行分割,生成多个 js 文件
- 按需加载:需要哪个文件就加载哪个文件
1.1 多入口
新创建项目演示
(1)准备
#初始化项目
npm init -y
#下载包
npm i webpack webpack-cli html-webpack-plugin -D
创建app.js 以及 main.js ,这两个js文件只是起到演示多入口演示作用。
(2)webpack.config.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"),
filename: "js/[name].js", // webpack命名方式,[name]以文件名自己命名
clean: true,
},
plugins: [
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
],
mode: "production",
};
(3)打包
npx webpack
可以看到打包后app.js 和 main.js都成功输出
1.2 多入口提取公共模块
(1)count.js
export default function count(x, y) {
return x - y;
}
(2)app.js
import count from "./count.js";
console.log("app");
console.log(count(5,2));
(3)main.js
import count from "./count.js";
console.log("main");
console.log(count(1,1));
(4)webpack.config.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"),
filename: "js/[name].js", // webpack命名方式,[name]以文件名自己命名
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,
},
},
},
},
};
(5)打包
npx webpack
可以看到公共模块也被单独打包出来
1.3 按需加载,动态导入
index.html 设置一个按钮以及点击事件来触发
(1)index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Webpack</title>
</head>
<body>
<button id="btn">计算</button>
</body>
</html>
(2)app.js
console.log("app");
(3)main.js
console.log("main");
document.getElementById("btn").onclick = function () {
// 动态导入 --> 实现按需加载
// 即使只被引用了一次,也会代码分割
import("./count")
.then((res) => {
console.log("模块加载成功", res.default(2, 1));
})
.catch((err) => {
console.log("模块加载失败", err);
});
};
(4)打包
npx webpack
打包后打开dist下html文件,可以看到只有点击按钮时,才会加载计算模块
1.4 单入口
webpack.prod.js
module.exports = {
// 省略...
optimization: {
// 代码分割配置
splitChunks: {
chunks: "all", // 对所有模块都进行分割
},
},
...
};
1.5 统一命名
module.exports = {
entry: "./src/main.js",
output: {
path: path.resolve(__dirname, "../dist"),
filename: "static/js/[name].js", // 入口文件打包输出资源命名方式
chunkFilename: "static/js/[name].chunk.js", // 动态导入输出资源命名方式
assetModuleFilename: "static/media/[name].[hash:10][ext]", // 图片、字体等资源命名方式(注意用hash)
clean: true,
},
// 省略....
// 插件
plugins: [
// plugins的配置
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../public/index.html"),
}),
new MiniCssExtractPlugin({
// 定义输出文件名和目录
filename: "static/css/[name].css",
chunkFilename: "static/css/[name].chunk.css",
}),
],
// 省略....
};
(1)index.html添加计算事件按钮
<button id="btn">计算</button>
(2)main.js修改
import sum from "./js/sum";
// 引入css文件
import "./css/iconfont.css";
import "./css/index.css";
import "./less/index.less";
import "./sass/index.sass";
import "./sass/index.scss";
import "./stylus/index.styl";
document.getElementById("btn").onclick = function () {
// webpackChunkName: "count":这是webpack动态导入模块命名的方式
// "math"将来就会作为[name]的值显示。
import(/* webpackChunkName: "count" */ "./js/count")
.then((res) => {
console.log("模块加载成功", res.default(2, 1));
})
.catch((err) => {
console.log("模块加载失败", err);
});
};
console.log(sum(1, 2, 3, 4, 5, 6));
(3)打包后可看到对应文件的命名,以及动态导入的js命名
二、Preload / Prefetch
2.1 简介
在浏览器空闲时间,加载后续需要使用的资源。我需要用上 Preload 或 Prefetch 技术。
- Preload:告诉浏览器立即加载资源
- Prefetch:告诉浏览器在空闲时才开始加载资源
共同点:
- 都只会加载资源,并不执行
- 都有缓存
区别:
- Preload加载优先级高,Prefetch加载优先级低
- Preload只能加载当前页面需要使用的资源,Prefetch可以加载当前页面资源,也可以加载下一个页面需要使用的资源
因此,当前页面优先级高的资源用 Preload 加载;下一个页面需要使用的资源用 Prefetch 加载
存在的问题:兼容性较差,Preload 相对于 Prefetch 兼容性好一点
Preload兼容性:"Preload" | Can I use... Support tables for HTML5, CSS3, etc
Prefetch兼容性:"Prefetch" | Can I use... Support tables for HTML5, CSS3, etc
2.2 下载包
npm i @vue/preload-webpack-plugin -D
2.3 配置
webpack.prod.js配置文件
// 省略...
const PreloadWebpackPlugin = require("@vue/preload-webpack-plugin");
// 省略...
module.exports = {
// 省略...
// 插件
plugins: [
// plugins的配置
// 省略...
new PreloadWebpackPlugin({
rel: "preload", // preload兼容性更好
as: "script",
// rel: 'prefetch' // prefetch兼容性更差
}),
],
// 省略...
};
打包后,查看dist目录下index.html文件
三、Network Cache
3.1 文件名生成hash值
在开发时,会对静态资源会使用缓存来优化,这样浏览器第二次请求资源就能读取缓存了。
但由于前后输出的文件名都是一样的 —— 都叫main.js。这样的话,一旦发布新版本,由于文件名没有发生变化,这样浏览器会直接读取缓存,而不会加载新资源,从而导致项目无法更新。
因此需要确保更新前后文件名不一样,这样才能做缓存。
一般将文件名生成hash值
- fullhash(webpack4 是 hash)
每次修改任何一个文件,所有文件名的 hash 至都将改变。所以一旦修改了任何一个文件,整个项目的文件缓存都将失效。
- chunkhash
根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值。我们 js 和 css 是同一个引入,会共享一个 hash 值。
- contenthash
根据文件内容生成 hash 值,只有文件内容变化了,hash 值才会变化。所有文件 hash 值是独享且不同的。
以webpack.prod.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:10][ext]", // 图片、字体等资源命名方式(注意用hash)
clean: true,
},
module: {
rules: [
// loader的配置
// 省略...
],
},
// 插件
plugins: [
// plugins的配置
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../public/index.html"),
}),
new MiniCssExtractPlugin({
// 定义输出文件名和目录
// [contenthash:8]使用contenthash,取8位长度
filename: "static/css/[name].[contenthash:8].css",
chunkFilename: "static/css/[name].[contenthash:8].chunk.css",
}),
// new CssMinimizerPlugin(),
new PreloadWebpackPlugin({
rel: "preload",
as: "script",
}),
],
optimization: {
splitChunks: {
chunks: "all",
},
},
// 模式
mode: "production",
devtool: "source-map",
};
3.2 使用hash值命名出现的问题
当修改 count.js 文件再重新打包的时候,因为 contenthash 原因,count.js 文件 hash 值发生了变化。间接地导致main.js的hash值也发生了变化,导致缓存失效。
3.3 原因
- 更新前:count.xxx.js, main.js 引用的 count.xxx.js
- 更新后:count.yyy.js, main.js 引用的 count.yyy.js,文件名发生了变化,间接导致 main.js 也发生了变化
3.4 解决
将 hash 值单独保管在一个 runtime 文件中。
runtime 文件只保存文件的 hash 值和它们与文件关系,整个文件体积就比较小,所以变化重新请求的代价也小。
webpack.prod.js配置文件中
// 省略...
module.exports = {
// 省略...
optimization: {
splitChunks: {
chunks: "all",
},
// 提取runtime文件
runtimeChunk: {
name: (entrypoint) => `runtime~${entrypoint.name}`, // runtime文件命名规则
},
},
// 省略...
};
打包后,可以看到dist下生成了对应的文件,且当修改count.js文件后重新打包,只有runtime和count的文件命名中的hash值会发生变化
四、解决js兼容性问题CoreJS
@babel/preset-env 智能预设处理兼容性问题,能将 ES6 的一些语法进行编译转换,比如箭头函数、点点点运算符等。但是如果是 async 函数、promise 对象、数组的一些方法(includes)等,是没办法处理的。
core-js 是专门用来做 ES6 以及以上 API 的 polyfill。
polyfill翻译过来叫做垫片/补丁。就是用社区上提供的一段代码,让我们在不兼容某些新特性的浏览器上,使用该新特性。
在main.js中使用promise语法
import sum from "./js/sum";
// 引入css文件
import "./css/iconfont.css";
import "./css/index.css";
import "./less/index.less";
import "./sass/index.sass";
import "./sass/index.scss";
import "./stylus/index.styl";
document.getElementById("btn").onclick = function () {
// webpackChunkName: "count":这是webpack动态导入模块命名的方式
// "math"将来就会作为[name]的值显示。
import(/* webpackChunkName: "count" */ "./js/count")
.then((res) => {
console.log("模块加载成功", res.default(2, 1));
})
.catch((err) => {
console.log("模块加载失败", err);
});
};
console.log(sum(1, 2, 3, 4, 5, 6));
// 添加promise代码
const promise = Promise.resolve();
promise.then(() => {
console.log("hello promise");
});
4.1 下载包
npm i core-js
4.2 手动全部引入
- main.js
import "core-js";
4.3 手动按需引入
- main.js
import "core-js/es/promise";
4.4 自动按需引入
- babel.config.js
module.exports = {
// 预设
presets: [
[
"@babel/preset-env",
// 按需加载core-js的polyfill
{
useBuiltIns: "usage", // 按需加载自动引入
corejs: { version: "3", proposals: true },
},
],
],
};
打包后,可以看到将corejs单独打包了出来
五、PWA
渐进式网络应用程序(progressive web application - PWA):是一种可以提供类似于 native app(原生应用程序) 体验的 Web App 的技术。
其中最重要的是,在 离线(offline) 时应用程序能够继续运行功能。
内部通过 Service Workers 技术实现的。
5.1 下载包
npm i workbox-webpack-plugin -D
5.2 配置
- webpack.prod.js中添加代码
// 省略...
// 添加 workbox-webpack-plugin 插件
const WorkboxPlugin = require("workbox-webpack-plugin");
// 省略...
module.exports = {
// 省略...
// 插件
plugins: [
// plugins的配置
// 省略...
new WorkboxPlugin.GenerateSW({
// 这些选项帮助快速启用 ServiceWorkers
// 不允许遗留任何“旧的” ServiceWorkers
clientsClaim: true,
skipWaiting: true,
}),
],
// 省略...
};
- main.js 添加代码,注册 Service Worker
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);
});
});
}
打包后,可以看见生成了service-worker.js文件