前言
webpack 基础学习的相关内容
介绍
是一个静态资源打包工具,将 Webpack 输出的文件叫做 bundle。
参考 webpack
功能介绍
Webpack 本身功能是有限的:(其他功能,需要配置才能完成)
Webpack 本身功能比较少,只能处理 js、json 资源,一旦遇到 css 等其他资源就会报错。
- 开发模式:仅能编译 JS 中的 ES Module
- 生产模式:能编译 JS 中的 ES Module,还能压缩 JS 代码
基本使用
1 2 3 4 5
| 需要注意的是 package.json 中 name 字段不能叫做 webpack, 否则安装包会报错 // 开发模式 npx webpack ./src/main.js --mode=development // 生产模式 npx webpack ./src/main.js --mode=production
|
基本配置
5 大核心概念
- entry(入口)
指示 Webpack 从哪个文件开始打包
- output(输出)
指示 Webpack 打包完的文件输出到哪里去,如何命名等
- loader(加载器)
webpack 本身只能处理 js、json 等资源,其他资源需要借助 loader,Webpack 才能解析
- plugins(插件)
扩展 Webpack 的功能
- mode(模式)
主要由两种模式:
- 开发模式:development
- 生产模式:production
注意:使用 loader 不需要引入,plugins 需要引入
Webpack 配置文件
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 path = require("path");
module.exports = { entry: "./src/main.js", output: { path: path.resolve(__dirname, "dist"), filename: "main.js", }, module: { rules: [], }, plugins: [], mode: "development", };
|
处理样式资源
处理 Css 资源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| npm i css-loader style-loader -D
rules: [ { test: /\.css$/, use: ["style-loader", "css-loader"], }, ],
|
处理 less、sass、stylus
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| pnpm add less less-loader sass sass-loader stylus stylus-loader -D
rules: [ { 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"], }, ]
|
处理图片资源
过去在 Webpack4 时,我们处理图片资源通过 file-loader 和 url-loader 进行处理
现在 Webpack5 已经将两个 Loader 功能内置到 Webpack 里了,我们只需要简单配置即可处理图片资源
1 2 3 4 5 6 7 8 9 10 11
| rules: [ { test: /\.(png|jpe?g|gif|webp)$/, type: "asset", parser: { dataUrlCondition: { maxSize: 10 * 1024 } } }, ]
|
修改输出资源的名称和路径
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| output: { filename: "static/js/main.js", }, module: { rules: [ { test: /\.(png|jpe?g|gif|webp)$/, type: "asset", parser: { dataUrlCondition: { maxSize: 10 * 1024, }, }, generator: { filename: "static/imgs/[hash:8][ext][query]", }, }, ] }
|
自动清空上次打包资源
1 2 3
| output: { clean: true, }
|
处理字体图标资源(其他资源同下 test 修改即可)
1 2 3 4 5 6 7 8 9 10 11 12
| module: { rules: [ { test: /\.(ttf|woff2?|map3|map4|avi)$/, type: "asset/resource", generator: { filename: "static/media/[hash:10][ext][query]", }, }, ] }
|
处理 js 资源
Eslint
webpack 配置 eslint 后在打包时候会校验代码,如不配置只装了 vscode 插件只是文件有代码报错提示
vite 可以使用 vite-plugin-eslint 插件
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
| pnpm add eslint-webpack-plugin eslint -D
module.exports = { extends: ["eslint:recommended"], env: { node: true, browser: true, }, parserOptions: { ecmaVersion: 6, sourceType: "module", }, rules: { "no-var": 2, }, };
const ESLintWebpackPlugin = require("eslint-webpack-plugin"); plugins: [ new ESLintWebpackPlugin({ context: path.resolve(__dirname, "src"), }), ],
|
Babel
主要用于将 ES6 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中
具体配置
1 2 3 4 5 6 7 8 9 10 11
| module.exports = { // 预设 presets: [], }; /* presets 预设 简单理解:就是一组 Babel 插件, 扩展 Babel 功能 @babel/preset-env @babel/preset-react @babel/preset-typescript */
|
在 Webpack 中使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| pnpm add babel-loader @babel/core @babel/preset-env -D
module.exports = { module: { rules: [ { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader", }, ] } }
module.exports = { presets: ["@babel/preset-env"], };
|
处理 Html 资源
1 2 3 4 5 6 7 8 9 10 11 12 13
| pnpm add html-webpack-plugin -D
const HtmlWebpackPlugin = require("html-webpack-plugin"); plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, "public/index.html"), }), ],
|
开发服务器
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| pnpm add webpack-dev-server -D
module.exports = { devServer: { host: "localhost", port: "3000", open: true, }, }
|
生产模式配置
css 处理
提取 Css 成单独文件
Css 文件目前被打包到 js 文件中,当 js 文件加载时,会创建一个 style 标签来生成样式
这样对于网站来说,会出现闪屏现象,用户体验不好
我们应该是单独的 Css 文件,通过 link 标签加载性能才好
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
| pnpm add mini-css-extract-plugin -D
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); module.exports = { module: { rules: [ { test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"], }, ] }, plugins: [ new MiniCssExtractPlugin({ filename: "static/css/main.css", }), ] }
|
Css 兼容性处理
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
| pnpm add postcss-loader postcss postcss-preset-env -D
module.exports = { module: { rules: [ { test: /\.css$/, use: [ MiniCssExtractPlugin.loader, "css-loader", { loader: "postcss-loader", options: { postcssOptions: { plugins: [ "postcss-preset-env", ], }, }, }, ], }, ] } }
function getStyleLoader(pre) { return [ MiniCssExtractPlugin.loader, "css-loader", { loader: "postcss-loader", options: { postcssOptions: { plugins: [ "postcss-preset-env", ], }, }, }, pre, ].filter(Boolean); } module.exports = { module: { rules: [ { test: /\.css$/, use: getStyleLoader() }, { test: /\.less$/, use: getStyleLoader("less-loader"), }, ] } }
"browserslist": [ "last 2 version", "> 1%", "not dead" ]
|
Css 压缩
html、js 默认生产打包开启了压缩
1 2 3 4 5 6 7 8 9 10 11 12 13
| pnpm add css-minimizer-webpack-plugin -D
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); module.exports = { optimization: { minimizer: [ new CssMinimizerPlugin(), ], }, }
|
高级优化
提升开发体验
SourceMap
通过查看 Webpack DevTool 文档open in new window 可知,SourceMap 的值有很多种情况。
- 开发模式:cheap-module-source-map
优点:打包编译速度快,只包含行映射
缺点:没有列映射
优点:包含行/列映射
缺点:打包编译速度更慢
1 2 3 4 5 6 7 8 9 10 11
| module.exports = { mode: "development", devtool: "cheap-module-source-map", };
module.exports = { mode: "production", devtool: "source-map", };
|
提升打包构建速度
HotModuleReplacement
在程序运行中,替换、添加或删除模块,而无需重新加载整个页面。
原因
开发时我们修改了其中一个模块代码,Webpack 默认会将所有模块全部重新打包编译,速度很慢。
所以我们需要做到修改某个模块代码,就只有这个模块代码需要重新打包编译,其他模块不变,这样打包速度就能很快。
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
| module.exports = { devServer: { host: "localhost", port: "3000", open: true, hot: true, }, };
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); }); }
|
OneOf
打包时每个文件都会经过所有 loader 处理,虽然因为 test 正则原因实际没有处理上,但是都要过一遍。比较慢。
使用后:就是只能匹配上一个 loader, 剩下的就不匹配了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| module.exports = { module: { rules: [ { oneOf: [ { test: /\.css$/, use: ["style-loader", "css-loader"], }, ] } ] } }
|
Include/Exclude
开发时我们需要使用第三方的库或插件,所有文件都下载到 node_modules 中了。而这些文件是不需要编译可以直接使用的。
所以我们在对 js 文件处理时,要排除 node_modules 下面的文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| module.exports = { module: { rules: [ { oneOf: [ { test: /\.js$/, include: path.resolve(__dirname, "../src"), loader: "babel-loader", }, ] } ] }, plugins: [ new ESLintWebpackPlugin({ context: path.resolve(__dirname, "../src"), exclude: "node_modules", }), ] }
|
Cache
每次打包时 js 文件都要经过 Eslint 检查 和 Babel 编译,速度比较慢。
我们可以缓存之前的 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
| module.exports = { module: { rules: [ { oneOf: [ { 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" ), }), ] }
|
Thead
对 js 文件处理主要就是 eslint 、babel、Terser 三个工具,所以我们要提升它们的运行速度
多进程打包:开启电脑的多个进程同时干一件事,速度更快。
需要注意:请仅在特别耗时的操作中使用,因为每个进程启动就有大约为 600ms 左右开销。
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 os = require("os"); const TerserPlugin = require("terser-webpack-plugin");
const threads = os.cpus().length;
module.exports = { 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({ threads, }) ], optimization: { minimize: true, minimizer: [ new CssMinimizerPlugin(), new TerserPlugin({ parallel: threads }) ], } };
|
减少代码体积
Tree Shaking
开发时我们定义了一些工具函数库,或者引用第三方工具函数库或组件库。
如果没有特殊处理的话我们打包时会引入整个库,但是实际上可能我们可能只用上极小部分的功能。
这样将整个库都打包进来,体积就太大了。
Tree Shaking 是一个术语,通常用于描述移除 JavaScript 中的没有使用上的代码。
注意:它依赖 ES Module。
Webpack 已经默认开启了这个功能,无需其他配置。
Babel
Babel 为编译的每个文件都插入了辅助代码,使代码体积过大!
Babel 对一些公共方法使用了非常小的辅助代码,比如 _extend。默认情况下会被添加到每一个需要它的文件中。
你可以将这些辅助代码作为一个独立模块,来避免重复引入
@babel/plugin-transform-runtime: 禁用了 Babel 自动对每个文件的 runtime 注入,而是引入 @babel/plugin-transform-runtime 并且使所有辅助代码从这里引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| npm i @babel/plugin-transform-runtime -D
{ 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"], }, }, ], },
|
Image Minimizer
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
| npm i image-minimizer-webpack-plugin imagemin -D
npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D
npm install imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo -D
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
optimization: { 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", }, }, ], }, ], ], }, }, }), }
|
优化代码运行性能
Code Split
1.多入口
代码分割(Code Split)主要做了两件事:
- 分割文件:将打包生成的文件进行分割,生成多个 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 29 30
|
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", };
|
2.提取重复代码
如果多入口文件中都引用了同一份代码,我们不希望这份代码被打包到两个文件中,导致代码重复,体积更大。
我们需要提取多入口的重复代码,只打包生成一个 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 29 30 31 32 33 34 35 36 37 38 39 40
| optimization = {
splitChunks: { chunks: "all", cacheGroups: { default: { minSize: 0, minChunks: 2, priority: -20, reuseExistingChunk: true, }, }, },
|
3.按需加载,动态导入
1 2 3 4 5 6 7 8
| document.getElementById("btn").onclick = function () { import("./math.js").then(({ sum }) => { alert(sum(1, 2, 3, 4, 5)); }); };
|
单入口
开发时我们可能是单页面应用(SPA),只有一个入口(单入口)。那么我们需要这样配置
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 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
| document.getElementById("btn").onClick = function () { import( "./js/math.js").then(({ count }) => { console.log(count(2, 1)); }); };
|
eslint 配置
1 2 3 4 5
|
plugins: ["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 43 44 45 46 47
| module.exports = { output: { filename: "static/js/[name].js", chunkFilename: "static/js/[name].chunk.js", assetModuleFilename: "static/media/[name].[hash][ext]", }, module: { rules: [ { oneOf: [ { test: /\.(png|jpe?g|gif|svg)$/, type: "asset", parser: { dataUrlCondition: { maxSize: 10 * 1024, }, }, }, { test: /\.(ttf|woff2?)$/, type: "asset/resource", }, ] } ] }, plugins: [ new MiniCssExtractPlugin({ filename: "static/css/[name].css", chunkFilename: "static/css/[name].chunk.css", }), ] }
|
Preload / Prefetch
我们前面已经做了代码分割,同时会使用 import 动态导入语法来进行代码按需加载(我们也叫懒加载,比如路由懒加载就是这样实现的)。
但是加载速度还不够好,比如:是用户点击按钮时才加载这个资源的,如果资源体积很大,那么用户会感觉到明显卡顿效果。
我们想在浏览器空闲时间,加载后续需要使用的资源。我们就需要用上 Preload 或 Prefetch 技术。
Preload:告诉浏览器立即加载资源。
Prefetch:告诉浏览器在空闲时才开始加载资源
它们共同点:
都只会加载资源,并不执行。
都有缓存。
它们区别:
Preload加载优先级高,Prefetch加载优先级低。
Preload只能加载当前页面需要使用的资源,Prefetch可以加载当前页面资源,也可以加载下一个页面需要使用的资源。
总结:
当前页面优先级高的资源用 Preload 加载。
下一个页面需要使用的资源用 Prefetch 加载。
它们的问题:兼容性较差。
Preload 相对于 Prefetch 兼容性好一点。
1 2 3 4 5 6 7 8
|
const PreloadWebpackPlugin = require("@vue/preload-webpack-plugin"); new PreloadWebpackPlugin({ rel: "preload", as: "script", }),
|
Network Cache
更新前:math.xxx.js, main.js 引用的 math.xxx.js
更新后:math.yyy.js, main.js 引用的 math.yyy.js, 文件名发生了变化,间接导致 main.js 也发生了变化
runtime 文件只保存文件的 hash 值和它们与文件关系,整个文件体积就比较小,所以变化重新请求的代价也小
1 2 3 4 5
| optimization: { runtimeChunk: { name: (entrypoint) => `runtime~${entrypoint.name}.js`, }, }
|
Core-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
|
parser: "@babel/eslint-parser",
import "core-js";
import "core-js/es/promise";
module.exports = { presets: [ [ "@babel/preset-env", { useBuiltIns: "usage", corejs: { version: "3", proposals: true } }, ], ], };
|
PWA
开发 Web App 项目,项目一旦处于网络离线情况,就没法访问了。
渐进式网络应用程序(progressive web application - PWA):是一种可以提供类似于 native app(原生应用程序) 体验的 Web App 的技术。
其中最重要的是,在 离线(offline) 时应用程序能够继续运行功能。
内部通过 Service Workers 技术实现的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
const WorkboxPlugin = require("workbox-webpack-plugin"); new WorkboxPlugin.GenerateSW({ clientsClaim: true, skipWaiting: true, }),
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); }); }); }
|