Webpack 高级配置
所谓高级配置其实就是进行 Webpack 优化,让代码在编译/运行时性能更好~
会从以下角度来进行优化:
- 提升开发体验: sourceMap
- 提升打包构建速度: thread、oneOf、Cache(js)、HMR、Include\Exclude(js)
- 减少代码体积: tree shaking、@babel/plugin-transform-runtime、Image Minimizer
- 优化代码运行性能:code split、preload和prefetch、network cache、core-js
提升开发体验
SourMap
为什么要使用SourMap
经过打包后,所有 css 和 js 合并成了一个文件,并且多了其他代码。此时如果代码运行出错那么提示代码错误位置我们是看不懂的。一旦将来开发代码文件很多,那么很难去发现错误出现在哪里。所以我们需要更加准确的错误提示,来帮助我们更好的开发代码。
SourMap是什么?
SourceMap(源代码映射)是一个用来生成源代码与构建后代码一一映射的文件的方案。
它会生成一个 xxx.map 文件,里面包含源代码和构建后代码每一行、每一列的映射关系。当构建后代码出错了,会通过 xxx.map 文件,从构建后代码出错位置找到映射后源代码出错位置,从而让浏览器提示源代码文件出错位置,帮助我们更快的找到错误根源。
SourMap怎么使用?
SourceMap 的值有很多种情况,但实际开发时我们只需要关注两种情况即可:
1 2 3 4 5
| module.exports = { mode: "development", devtool: "cheap-module-source-map", };
|
1 2 3 4 5
| module.exports = { mode: "production", devtool: "source-map", };
|
提升打包构建速度
HotModuleReplacement(开发环境)
为什么要使用hotmodulereplacement?
开发时我们修改了其中一个模块代码,Webpack 默认会将所有模块全部重新打包编译,速度很慢。我们需要做到修改某个模块代码,就只有这个模块代码需要重新打包编译,其他模块不变,这样打包速度就能很快。
hotmodulereplacement是什么?
hotmodulereplacement(HMR/热模块替换):在程序运行中,替换、添加或删除模块,而无需重新加载整个页面
怎么使用hotmodulereplace
- 基本配置
1 2 3 4 5 6 7 8 9
| module.exports = { devServer: { host: "localhost", port: "3000", open: true, hot: true, }, };
|
此时 css 样式经过 style-loader 处理,已经具备 HMR 功能了。
但是 js 还不行。
- JS配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import count from "./js/count"; import sum from "./js/sum";
import "./css/iconfont.css"; import "./css/index.css"; import "./less/index.less"; import "./sass/index.sass"; import "./sass/index.scss"; import "./styl/index.styl";
const result1 = count(2, 1); console.log(result1); const result2 = sum(1, 2, 3, 4); console.log(result2);
if (module.hot) { module.hot.accept("./js/count.js", function (count) { const result1 = count(2, 1); console.log(result1); });
module.hot.accept("./js/sum.js", function (sum) { const result2 = sum(1, 2, 3, 4); console.log(result2); }); }
|
上面这样写会很麻烦,所以实际开发我们会使用其他 loader 来解决。
比如:vue-loader, react-hot-loader。
OneOf(开发、生产)
为什么要使用OneOf?
打包时每个文件都会经过所有 loader 处理,虽然因为 test
正则原因实际没有处理上,但是都要过一遍,比较慢。我们希望文件在遇到能处理它的loader之后就不再继续向后比较了。
OneOf是什么?
顾名思义就是只能匹配上一个 loader, 剩下的就不匹配了。
使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| const path = require("path"); const ESLintWebpackPlugin = require("eslint-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = { entry: "./src/main.js", output: { path: undefined, filename: "static/js/main.js", }, module: { rules: [ { oneOf: [ { test: /\.css$/, use: ["style-loader", "css-loader"], }, { test: /\.less$/, use: ["style-loader", "css-loader", "less-loader"], }, { test: /\.s[ac]ss$/, use: ["style-loader", "css-loader", "sass-loader"], }, { test: /\.styl$/, use: ["style-loader", "css-loader", "stylus-loader"], }, { test: /\.(png|jpe?g|gif|webp)$/, type: "asset", parser: { dataUrlCondition: { maxSize: 10 * 1024, }, }, generator: { filename: "static/imgs/[hash:8][ext][query]", }, }, { test: /\.(ttf|woff2?)$/, type: "asset/resource", generator: { filename: "static/media/[hash:8][ext][query]", }, }, { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader", }, ], }, ], }, plugins: [ new ESLintWebpackPlugin({ context: path.resolve(__dirname, "../src"), }), new HtmlWebpackPlugin({ template: path.resolve(__dirname, "../public/index.html"), }), ], devServer: { host: "localhost", port: "3000", open: true, hot: true, }, mode: "development", devtool: "cheap-module-source-map", };
|
生产模式相同配置
Include/Exclude
为什么要使用Include/Exclude?
开发时我们需要使用第三方的库或插件,所有文件都下载到 node_modules 中了。而这些文件是不需要编译可以直接使用的。所以我们在对 js 文件处理时,要排除 node_modules 下面的文件。
Include/Exclude是什么?
包含,只处理 xxx 文件
排除,除了 xxx 文件以外其他文件都处理
怎么使用Include/Exclude?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| const path = require("path"); const ESLintWebpackPlugin = require("eslint-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = { entry: "./src/main.js", output: { path: undefined, filename: "static/js/main.js", }, module: { rules: [ { oneOf: [ { test: /\.css$/, use: ["style-loader", "css-loader"], }, { test: /\.less$/, use: ["style-loader", "css-loader", "less-loader"], }, { test: /\.s[ac]ss$/, use: ["style-loader", "css-loader", "sass-loader"], }, { test: /\.styl$/, use: ["style-loader", "css-loader", "stylus-loader"], }, { test: /\.(png|jpe?g|gif|webp)$/, type: "asset", parser: { dataUrlCondition: { maxSize: 10 * 1024, }, }, generator: { filename: "static/imgs/[hash:8][ext][query]", }, }, { test: /\.(ttf|woff2?)$/, type: "asset/resource", generator: { filename: "static/media/[hash:8][ext][query]", }, }, { test: /\.js$/, include: path.resolve(__dirname, "../src"), loader: "babel-loader", }, ], }, ], }, plugins: [ new ESLintWebpackPlugin({ context: path.resolve(__dirname, "../src"), exclude: "node_modules", }), new HtmlWebpackPlugin({ template: path.resolve(__dirname, "../public/index.html"), }), ], devServer: { host: "localhost", port: "3000", open: true, hot: true, }, mode: "development", devtool: "cheap-module-source-map", };
|
Cache
为什么要使用Cache?
每次打包时 js 文件都要经过 Eslint 检查 和 Babel 编译,速度比较慢。我们可以缓存之前的 Eslint 检查 和 Babel 编译结果,这样第二次打包时速度就会更快了。
Cache是什么?
对 Eslint 检查 和 Babel 编译结果进行缓存。
怎么使用?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
| const path = require("path"); const ESLintWebpackPlugin = require("eslint-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = { entry: "./src/main.js", output: { path: undefined, filename: "static/js/main.js", }, module: { rules: [ { oneOf: [ { test: /\.css$/, use: ["style-loader", "css-loader"], }, { test: /\.less$/, use: ["style-loader", "css-loader", "less-loader"], }, { test: /\.s[ac]ss$/, use: ["style-loader", "css-loader", "sass-loader"], }, { test: /\.styl$/, use: ["style-loader", "css-loader", "stylus-loader"], }, { test: /\.(png|jpe?g|gif|webp)$/, type: "asset", parser: { dataUrlCondition: { maxSize: 10 * 1024, }, }, generator: { filename: "static/imgs/[hash:8][ext][query]", }, }, { test: /\.(ttf|woff2?)$/, type: "asset/resource", generator: { filename: "static/media/[hash:8][ext][query]", }, }, { test: /\.js$/, include: path.resolve(__dirname, "../src"), loader: "babel-loader", options: { cacheDirectory: true, cacheCompression: false, }, }, ], }, ], }, plugins: [ new ESLintWebpackPlugin({ context: path.resolve(__dirname, "../src"), exclude: "node_modules", cache: true, cacheLocation: path.resolve( __dirname, "../node_modules/.cache/.eslintcache" ), }), new HtmlWebpackPlugin({ template: path.resolve(__dirname, "../public/index.html"), }), ], devServer: { host: "localhost", port: "3000", open: true, hot: true, }, mode: "development", devtool: "cheap-module-source-map", };
|
Thread
为什么要使用Thread?
当项目越来越庞大时,打包速度越来越慢,甚至于需要一个下午才能打包出来代码。这个速度是比较慢的。我们想要继续提升打包速度,其实就是要提升 js 的打包速度,因为其他文件都比较少。而对 js 文件处理主要就是 eslint 、babel、Terser 三个工具,所以我们要提升它们的运行速度。我们可以开启多进程同时处理 js 文件,这样速度就比之前的单进程打包更快了。
Thread是什么?
多进程打包:开启电脑的多个进程同时干一件事,速度更快。
需要注意:请仅在特别耗时的操作中使用,因为每个进程启动就有大约为 600ms 左右开销。
怎么使用?
- 获取CPU的核数
1 2 3 4
| const os = require("os");
const threads = os.cpus().length;
|
- 下载包
- 使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
| const os = require("os"); const path = require("path");
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: { path: path.resolve(__dirname, "../dist"), filename: "static/js/main.js", clean: true, }, module: { rules: [ { oneOf: [ { test: /\.js$/, include: path.resolve(__dirname, "../src"), use: [ { loader: "thread-loader", options: { workers: threads, }, }, { loader: "babel-loader", options: { cacheDirectory: true, }, }, ], }, ], }, ], }, plugins: [ 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: [ new CssMinimizerPlugin(), new TerserPlugin({ parallel: threads }) ], }, mode: "production", devtool: "source-map", };
|
减少代码体积
Tree Shaking
为什么要使用Tree Shaking?
开发时我们定义了一些工具函数库,或者引用第三方工具函数库或组件库。如果没有特殊处理的话我们打包时会引入整个库,但是实际上可能我们可能只用上极小部分的功能。这样将整个库都打包进来,体积就太大了。
Tree Shaking是什么?
Tree Shaking
是一个术语,通常用于描述移除 JavaScript 中的没有使用上的代码。
注意:它依赖 ES Module
。如果项目使用commonjs就无法使用
怎么使用Tree Shaking?
Webpack 已经默认开启了这个功能,无需其他配置。
Tree Shaking的实现原理
CommonJS、AMD、CMD等JS模块化方案中,导入导出行为是动态的,难以预测。而ES模块则要求所有的导入导出语句只能出现在模块顶层,且导入导出的模块名必须为字符串常量。ES模块之间的依赖关系是高度确定的,与运行状态无关,编译工具只需要对ES模块做静态分析就可以从代码中推断出哪些模块未曾被其他模块使用。
ES6模块中引入了静态分析,在编译的时候可以正确判断加载了哪些模块。静态分析程序流可以判断哪些模块和变量未被使用或者引用,进而删除对应的代码。
tree-shaking的实现首先是先标记出模块导出值中哪些没有被用过,然后使用Terser删除掉没有用到的导出语句。
为什么要使用这个插件?
Babel 为编译的每个文件都插入了辅助代码,使代码体积过大!Babel 对一些公共方法使用了非常小的辅助代码,比如 _extend
。默认情况下会被添加到每一个需要它的文件中。你可以将这些辅助代码作为一个独立模块,来避免重复引入。就好像微信中的同一份文件,给不同的人传就会生成不同的文件,但这些文件的内容是一样的,只是文件名的哈希值不一样。
这个插件是什么?
@babel/plugin-transform-runtime
: 禁用了 Babel 自动对每个文件的 runtime 注入,而是引入 @babel/plugin-transform-runtime
并且使所有辅助代码从这里引用。
怎么用?
- 下载包
1
| npm i @babel/plugin-transform-runtime -D
|
- 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| const os = require("os"); const path = require("path");
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: { path: path.resolve(__dirname, "../dist"), filename: "static/js/main.js", clean: true, }, module: { rules: [ { oneOf: [ { test: /\.js$/, include: path.resolve(__dirname, "../src"), use: [ { loader: "thread-loader", options: { workers: threads, }, }, { loader: "babel-loader", options: { cacheDirectory: true, cacheCompression: false, plugins: ["@babel/plugin-transform-runtime"], }, }, ], }, ], }, ], }, mode: "production", devtool: "source-map", };
|
Image Minimizer
为什么要使用Image Minimizer?
开发如果项目中引用了较多图片,那么图片体积会比较大,将来请求速度比较慢。我们可以对图片进行压缩,减少图片体积。
注意:如果项目中图片都是在线链接,那么就不需要了。本地项目静态图片才需要进行压缩。
是什么?
image-minimizer-webpack-plugin
: 用来压缩图片的插件
使用
- 下载包
1
| npm i image-minimizer-webpack-plugin imagemin -D
|
下载包时,有两种模式:
1
| npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D
|
1
| npm install imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo -D
|
- 配置(无损压缩)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
module.exports = { entry: "./src/main.js", output: { path: path.resolve(__dirname, "../dist"), filename: "static/js/main.js", clean: true, }, module: { rules: [ { oneOf: [ ], }, ], }, plugins: [ ], 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", }, }, ], }, ], ], }, }, }), ], }, mode: "production", devtool: "source-map", };
|
优化代码运行性能
Code Split
为什么要使用Code Split?
打包代码时会将所有 js 文件打包到一个文件中,体积太大了。我们如果只要渲染首页,就应该只加载首页的 js 文件,其他文件不应该加载。所以我们需要将打包生成的文件进行代码分割,生成多个 js 文件,渲染哪个页面就只加载某个 js 文件,这样加载的资源就少,速度就更快。
Code Split是什么?
代码分割(Code Split)主要做了两件事:
- 分割文件:将打包生成的文件进行分割,生成多个 js 文件。
- 按需加载:需要哪个文件就加载哪个文件。
使用
代码分割实现方式有不同的方式,以下分别从多入口、单入口(SPA)进行说明。
多入口
- 文件目录
1 2 3 4 5 6
| ├── public ├── src | ├── app.js | └── main.js ├── package.json └── webpack.config.js
|
- 下载包
1
| npm i webpack webpack-cli html-webpack-plugin -D
|
- 新建文件
1
| console.log("hello app");
|
1
| console.log("hello main");
|
- 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = { entry: { main: "./src/main.js", app: "./src/app.js", }, output: { path: path.resolve(__dirname, "./dist"), filename: "js/[name].js", clear: true, }, plugins: [ new HtmlWebpackPlugin({ template: "./public/index.html", }), ], mode: "production", };
|
- 运行指令
此时在 dist 目录我们能看到输出了两个 js 文件。
总结:配置了几个入口,至少输出几个 js 文件。
如果多入口文件中都引用了同一份代码,我们不希望这份代码被打包到两个文件中,导致代码重复,体积更大。我们需要提取多入口的重复代码,只打包生成一个 js 文件,其他文件引用它就好。
- 修改文件
1 2 3 4
| import { sum } from "./math";
console.log("hello app"); console.log(sum(1, 2, 3, 4));
|
1 2 3 4
| import { sum } from "./math";
console.log("hello main"); console.log(sum(1, 2, 3, 4, 5));
|
1 2 3
| export const sum = (...args) => { return args.reduce((p, c) => p + c, 0); };
|
- 修改配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = { entry: { main: "./src/main.js", app: "./src/app.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", cacheGroups: { default: { minSize: 0, minChunks: 2, priority: -20, reuseExistingChunk: true, }, }, }, }, };
|
- 运行指令
此时我们会发现生成 3 个 js 文件,其中有一个就是提取的公共模块。
想要实现按需加载,动态导入模块。还需要额外配置:
- 修改文件
1 2 3 4 5 6 7 8
| console.log("hello main"); document.getElementById("btn").onclick = function () { import("./math.js").then(({ sum }) => { alert(sum(1, 2, 3, 4, 5)); }); };
|
1
| console.log("hello app");
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| <!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>Code Split</title> </head> <body> <h1>hello webpack</h1> <button id="btn">计算</button> </body> </html>
|
- 运行指令
我们可以发现,一旦通过 import 动态导入语法导入模块,模块就被代码分割,同时也能按需加载了。
单入口
- 基本配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin");
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", }, };
|
- 使用单入口+代码分割+动态导入方式来进行配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
| const os = require("os"); const path = require("path"); const ESLintWebpackPlugin = 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 ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
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: { path: path.resolve(__dirname, "../dist"), filename: "static/js/main.js", clean: true, }, module: { rules: [ { oneOf: [ { test: /\.css$/, use: getStyleLoaders(), }, { test: /\.less$/, use: getStyleLoaders("less-loader"), }, { test: /\.s[ac]ss$/, use: getStyleLoaders("sass-loader"), }, { test: /\.styl$/, use: getStyleLoaders("stylus-loader"), }, { test: /\.(png|jpe?g|gif|svg)$/, type: "asset", parser: { dataUrlCondition: { maxSize: 10 * 1024, }, }, generator: { filename: "static/imgs/[hash:8][ext][query]", }, }, { test: /\.(ttf|woff2?)$/, type: "asset/resource", generator: { filename: "static/media/[hash:8][ext][query]", }, }, { test: /\.js$/, include: path.resolve(__dirname, "../src"), use: [ { loader: "thread-loader", options: { workers: threads, }, }, { loader: "babel-loader", options: { cacheDirectory: true, cacheCompression: false, plugins: ["@babel/plugin-transform-runtime"], }, }, ], }, ], }, ], }, plugins: [ new ESLintWebpackPlugin({ context: path.resolve(__dirname, "../src"), exclude: "node_modules", cache: true, cacheLocation: path.resolve( __dirname, "../node_modules/.cache/.eslintcache" ), threads, }), new HtmlWebpackPlugin({ template: path.resolve(__dirname, "../public/index.html"), }), new MiniCssExtractPlugin({ filename: "static/css/main.css", }), ], optimization: { minimizer: [ new CssMinimizerPlugin(), new TerserPlugin({ parallel: threads, }), 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", }, }, ], }, ], ], }, }, }), ], splitChunks: { chunks: "all", }, }, mode: "production", devtool: "source-map", };
|
- 给动态导入文件取名称
此时经过分割的文件的名称是随意的,我们需要给这个文件规范命名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import sum from "./js/sum";
import "./css/iconfont.css"; import "./css/index.css"; import "./less/index.less"; import "./sass/index.sass"; import "./sass/index.scss"; import "./styl/index.styl";
const result2 = sum(1, 2, 3, 4); console.log(result2);
if (module.hot) { module.hot.accept("./js/sum.js", function (sum) { const result2 = sum(1, 2, 3, 4); console.log(result2); }); }
document.getElementById("btn").onClick = function () { import( "./js/math.js").then(({ count }) => { console.log(count(2, 1)); }); };
|
1
| npm i eslint-plugin-import -D
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| module.exports = { extends: ["eslint:recommended"], env: { node: true, browser: true, }, plugins: ["import"], parserOptions: { ecmaVersion: 6, sourceType: "module", }, rules: { "no-var": 2, }, };
|
统一命名配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
| const os = require("os"); const path = require("path"); const ESLintWebpackPlugin = 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 ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
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: { path: path.resolve(__dirname, "../dist"), filename: "static/js/[name].js", chunkFilename: "static/js/[name].chunk.js", assetModuleFilename: "static/media/[name].[hash][ext]", clean: true, }, module: { rules: [ { oneOf: [ { test: /\.css$/, use: getStyleLoaders(), }, { test: /\.less$/, use: getStyleLoaders("less-loader"), }, { test: /\.s[ac]ss$/, use: getStyleLoaders("sass-loader"), }, { test: /\.styl$/, use: getStyleLoaders("stylus-loader"), }, { test: /\.(png|jpe?g|gif|svg)$/, type: "asset", parser: { dataUrlCondition: { maxSize: 10 * 1024, }, }, }, { test: /\.(ttf|woff2?)$/, type: "asset/resource", }, { test: /\.js$/, include: path.resolve(__dirname, "../src"), use: [ { loader: "thread-loader", options: { workers: threads, }, }, { loader: "babel-loader", options: { cacheDirectory: true, cacheCompression: false, plugins: ["@babel/plugin-transform-runtime"], }, }, ], }, ], }, ], }, plugins: [ new ESLintWebpackPlugin({ context: path.resolve(__dirname, "../src"), exclude: "node_modules", cache: true, cacheLocation: path.resolve( __dirname, "../node_modules/.cache/.eslintcache" ), threads, }), new HtmlWebpackPlugin({ template: path.resolve(__dirname, "../public/index.html"), }), new MiniCssExtractPlugin({ filename: "static/css/[name].css", chunkFilename: "static/css/[name].chunk.css", }), ], optimization: { minimizer: [ new CssMinimizerPlugin(), new TerserPlugin({ parallel: threads, }), 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", }, }, ], }, ], ], }, }, }), ], splitChunks: { chunks: "all", }, }, mode: "production", devtool: "source-map", };
|
- 运行指令
观察打包输出 js 文件名称。
Preload / Prefetch
为什么使用Preload / Prefetch?
我们前面已经做了代码分割,同时会使用 import 动态导入语法来进行代码按需加载(我们也叫懒加载,比如路由懒加载就是这样实现的)。
但是加载速度还不够好,比如:是用户点击按钮时才加载这个资源的,如果资源体积很大,那么用户会感觉到明显卡顿效果。
我们想在浏览器空闲时间,加载后续需要使用的资源。我们就需要用上 Preload
或 Prefetch
技术。
Preload / Prefetch是什么?
它们共同点:
它们区别:
Preload
加载优先级高,Prefetch
加载优先级低。
Preload
只能加载当前页面需要使用的资源,Prefetch
可以加载当前页面资源,也可以加载下一个页面需要使用的资源。
总结:
- 当前页面优先级高的资源用
Preload
加载。
- 下一个页面需要使用的资源用
Prefetch
加载。
它们的问题:兼容性较差。
- 我们可以去 Can I Use 网站查询 API 的兼容性问题。
Preload
相对于 Prefetch
兼容性好一点。
使用
- 下包
1
| npm i @vue/preload-webpack-plugin -D
|
- 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| const PreloadWebpackPlugin = require("@vue/preload-webpack-plugin");
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][ext]", clean: true, }, module: { rules: [ { }, ], }, plugins: [ new PreloadWebpackPlugin({ rel: "preload", as: "script", }), ], mode: "production", devtool: "source-map", };
|
Network Cache
为什么要使用Network Cache?
将来开发时我们对静态资源会使用缓存来优化,这样浏览器第二次请求资源就能读取缓存了,速度很快。
但是这样的话就会有一个问题, 因为前后输出的文件名是一样的,都叫 main.js,一旦将来发布新版本,因为文件名没有变化导致浏览器会直接读取缓存,不会加载新资源,项目也就没法更新了。
所以我们从文件名入手,确保更新前后文件名不一样,这样就可以做缓存了。
Network Cache是什么?
Network Cache将会为文件生成一个唯一的 hash 值。
- fullhash(webpack4 是 hash)
每次修改任何一个文件,所有文件名的 hash 值都将改变。所以一旦修改了任何一个文件,整个项目的文件缓存都将失效。
根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值。我们 js 和 css 是同一个引入,会共享一个 hash 值。
根据文件内容生成 hash 值,只有文件内容变化了,hash 值才会变化。所有文件 hash 值是独享且不同的。
使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| module.exports = { entry: "./src/main.js", output: { path: path.resolve(__dirname, "../dist"), filename: "static/js/[name].[contenthash:8].js", chunkFilename: "static/js/[name].[contenthash:8].chunk.js", assetModuleFilename: "static/media/[name].[hash][ext]", clean: true, }, module: { rules: [ { }, ], }, plugins: [ ], optimization: { minimizer: [ ], splitChunks: { chunks: "all", }, }, mode: "production", devtool: "source-map", };
|
当我们修改 math.js 文件再重新打包的时候,因为 contenthash 原因,math.js 文件 hash 值发生了变化(这是正常的)。
但是 main.js 文件的 hash 值也发生了变化,这会导致 main.js 的缓存失效。明明我们只修改 math.js, 为什么 main.js 也会变身变化呢?
原因:
- 更新前:math.xxx.js, main.js 引用的 math.xxx.js
- 更新后:math.yyy.js, main.js 引用的 math.yyy.js, 文件名发生了变化,间接导致 main.js 也发生了变化
解决:
将 hash 值单独保管在一个 runtime 文件中。
我们最终输出三个文件:main、math、runtime。当 math 文件发生变化,变化的是 math 和 runtime 文件,main 不变。
runtime 文件只保存文件的 hash 值和它们与文件关系,整个文件体积就比较小,所以变化重新请求的代价也小。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| module.exports = { entry: "./src/main.js", output: { path: path.resolve(__dirname, "../dist"), filename: "static/js/[name].[contenthash:8].js", chunkFilename: "static/js/[name].[contenthash:8].chunk.js", assetModuleFilename: "static/media/[name].[hash][ext]", clean: true, }, module: { rules: [ { }, ], }, plugins: [ ], optimization: { minimizer: [ ], splitChunks: { chunks: "all", }, runtimeChunk: { name: (entrypoint) => `runtime~${entrypoint.name}`, }, }, mode: "production", devtool: "source-map", };
|
Core-js
为什么要使用Core-js?
过去我们使用 babel 对 js 代码进行了兼容性处理,其中使用@babel/preset-env 智能预设来处理兼容性问题。
它能将 ES6 的一些语法进行编译转换,比如箭头函数、点点点运算符等。但是如果是 async 函数、promise 对象、数组的一些方法(includes)等,它没办法处理。
所以此时我们 js 代码仍然存在兼容性问题,一旦遇到低版本浏览器会直接报错。所以我们想要将 js 兼容性问题彻底解决。
Core-js是什么?
core-js
是专门用来做 ES6 以及以上 API 的 polyfill
。
polyfill
翻译过来叫做垫片/补丁。就是用社区上提供的一段代码,让我们在不兼容某些新特性的浏览器上,使用该新特性。
使用
- 修改main.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import count from "./js/count"; import sum from "./js/sum";
import "./css/iconfont.css"; import "./css/index.css"; import "./less/index.less"; import "./sass/index.sass"; import "./sass/index.scss"; import "./styl/index.styl";
const result1 = count(2, 1); console.log(result1); const result2 = sum(1, 2, 3, 4); console.log(result2);
const promise = Promise.resolve(); promise.then(() => { console.log("hello promise"); });
|
此时Eslint会对Promise报错!!!
- 修改配置文件
1
| npm i @babel/eslint-parser -D
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| module.exports = { extends: ["eslint:recommended"], parser: "@babel/eslint-parser", env: { node: true, browser: true, }, plugins: ["import"], parserOptions: { ecmaVersion: 6, sourceType: "module", }, rules: { "no-var": 2, }, };
|
- 运行指令
此时观察打包输出的 js 文件,我们发现 Promise 语法并没有编译转换,所以我们需要使用 core-js
来进行 polyfill
。
- 使用
core-js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import "core-js"; import count from "./js/count"; import sum from "./js/sum";
import "./css/iconfont.css"; import "./css/index.css"; import "./less/index.less"; import "./sass/index.sass"; import "./sass/index.scss"; import "./styl/index.styl";
const result1 = count(2, 1); console.log(result1); const result2 = sum(1, 2, 3, 4); console.log(result2);
const promise = Promise.resolve(); promise.then(() => { console.log("hello promise"); });
|
这样引入会将所有兼容性代码全部引入,体积太大了。我们只想引入 promise 的 polyfill
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import "core-js/es/promise"; import count from "./js/count"; import sum from "./js/sum";
import "./css/iconfont.css"; import "./css/index.css"; import "./less/index.less"; import "./sass/index.sass"; import "./sass/index.scss"; import "./styl/index.styl";
const result1 = count(2, 1); console.log(result1); const result2 = sum(1, 2, 3, 4); console.log(result2);
const promise = Promise.resolve(); promise.then(() => { console.log("hello promise"); });
|
只引入打包 promise 的 polyfill
,打包体积更小。但是将来如果还想使用其他语法,我需要手动引入库很麻烦。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import count from "./js/count"; import sum from "./js/sum";
import "./css/iconfont.css"; import "./css/index.css"; import "./less/index.less"; import "./sass/index.sass"; import "./sass/index.scss"; import "./styl/index.styl";
const result1 = count(2, 1); console.log(result1); const result2 = sum(1, 2, 3, 4); console.log(result2);
const promise = Promise.resolve(); promise.then(() => { console.log("hello promise"); });
|
1 2 3 4 5 6 7 8 9 10
| module.exports = { presets: [ [ "@babel/preset-env", { useBuiltIns: "usage", corejs: { version: "3", proposals: true } }, ], ], };
|
此时就会自动根据我们代码中使用的语法,来按需加载相应的 polyfill
了。