一、什么是Webpack
一种打包工具
为什么要打包?
用户通过浏览器浏览项目,对于项目里面的每一个资源(css js html 以及其他资源)都需要发送一次http请求。
对于vue和less等不能直接被浏览器识别的文件,需要通过装载器转换成可识别文件。
打包可以通过plugin把文件压缩合并
二、基础
基本配置
五大核心概念
- entry(入口)
指示 Webpack 从哪个文件开始打包
- output(输出)
指示 Webpack 打包完的文件输出到哪里去,如何命名等
- loader(加载器)
webpack 本身只能处理 js、json 等资源,其他资源需要借助 loader,Webpack 才能解析
- plugins(插件)
扩展 Webpack 的功能
- mode(模式)
主要由两种模式:
- 开发模式:development
- 生产模式:production
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 path = require("path");
module.exports = { entry: "./src/main.js", output: { path: path.resolve(__dirname, "dist"), filename: "main.js", }, module: { rules: [], }, plugins: [], mode: "development", };
|
处理样式资源
Webpack 本身是不能识别样式资源的,所以我们需要借助 Loader 来帮助 Webpack 解析样式资源
我们找 Loader 都应该去官方文档中找到对应的 Loader,然后使用
npm i css-loader style-loader -D
npm i less-loader -D
npm i sass-loader sass -D
npm i stylus-loader -D
css-loader
:负责将 Css 文件编译成 Webpack 能识别的模块
style-loader
:会动态创建一个 Style 标签,里面放置 Webpack 中 Css 模块内容
less-loader
:负责将 Less 文件编译成 Css 文件
sass-loader
:负责将 Sass 文件编译成 css 文件
sass
:sass-loader
依赖 sass
进行编译
stylus-loader
:负责将 Styl 文件编译成 Css 文件
此时样式就会以 Style 标签的形式在页面上生效
处理js资源
针对 js 兼容性处理,我们使用 Babel 来完成
npm i babel-loader @babel/core @babel/preset-env -D
1 2 3 4
| module.exports = { presets: ["@babel/preset-env"], };
|
针对代码格式,我们使用 Eslint 来完成
npm i eslint-webpack-plugin eslint -D
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| module.exports = { extends: ["eslint:recommended"], env: { node: true, browser: true, }, parserOptions: { ecmaVersion: 6, sourceType: "module", }, rules: { "no-var": 2, }, };
|
处理Html
npm i html-webpack-plugin -D
开发服务器
npm i webpack-dev-server -D
CSS处理
提取css成单独文件
Css 文件目前被打包到 js 文件中,当 js 文件加载时,会创建一个 style 标签来生成样式
这样对于网站来说,会出现闪屏现象,用户体验不好
我们应该是单独的 Css 文件,通过 link 标签加载性能才好
npm i mini-css-extract-plugin -D
CSS兼容性处理
npm i postcss-loader postcss postcss-preset-env -D
CSS 压缩
npm i css-minimizer-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 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
| const path = require("path"); const ESLintWebpackPlugin = require("eslint-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = { entry: "./src/main.js", output: { path: path.resolve(__dirname, "dist"), filename: "static/js/main.js", clean: true, }, module: { rules: [ { 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, }, mode: "development", };
|
生产模式
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
| 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 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: [ { 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|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"), }), new MiniCssExtractPlugin({ filename: "static/css/main.css", }), new CssMinimizerPlugin(), ], mode: "production", };
|
总结
本章节我们学会了 Webpack 基本使用,掌握了以下功能:
- 两种开发模式
- 开发模式:代码能编译自动化运行
- 生产模式:代码编译优化输出
- Webpack 基本功能
- 开发模式:可以编译 ES Module 语法
- 生产模式:可以编译 ES Module 语法,压缩 js 代码
- Webpack 配置文件
- 5 个核心概念
- entry
- output
- loader
- plugins
- mode
- devServer 配置
- Webpack 脚本指令用法
webpack
直接打包输出
webpack serve
启动开发服务器,内存编译打包没有输出
三、高级
1. 提升开发体验
SourceMap
SourceMap(源代码映射)是一个用来生成源代码与构建后代码一一映射的文件的方案。
它会生成一个 xxx.map 文件,里面包含源代码和构建后代码每一行、每一列的映射关系。当构建后代码出错了,会通过 xxx.map 文件,从构建后代码出错位置找到映射后源代码出错位置,从而让浏览器提示源代码文件出错位置,帮助我们更快的找到错误根源。
https://webpack.docschina.org/configuration/devtool/
2. 提升打包构建速度
HotMoudleReplacement(HMR/热模块替换)
做到只替换修改的某个模块代码,而不是全部代码都重新打包
1 2 3 4 5 6 7 8 9
| module.exports={ devSever:{ host: "localhost", port: "3000", open: true, hot: true } }
|
此时css样式经过style-loader处理,已经具备HMR功能了,但是js还不行。
js通过 vue-loader 和 react-hot-loader 来解决
OneOf
打包时每个文件都会经过所有loader处理,虽然因为test正则会跳过不符合的文件,但还是会浪费时间。通过OneOf来使文件只能匹配上一个loader,已经被某个loader匹配上的就不会被剩下的loader匹配
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| module.exports={ module: { rules: [ { oneOf: [ { test: /\.css$/, use: ["style-loader", "css-loader"], }, ] } ] } }
|
Include/Exclude
include
只处理include包含的文件
Exclude
处理exclude外的文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const path = require("path"); module.exports={ module: { rules: [ { oneOf: [ { test: /\.js$/, include: path.resolve(__dirname, "../src"), loader: "babel-loader", }, ] } ] } }
|
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 35 36 37 38
| const path = require("path"); const ESLintWebpackPlugin = require("eslint-webpack-plugin");
module.exports={ module: { rules: [ { oneOf: [ { test: /\.js$/, include: path.resolve(__dirname, "../src"), loader: "babel-loader", option: { 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
多线程处理
多进程打包:开启电脑的多个进程同时干一件事,速度更快。
需要注意:请仅在特别耗时的操作中使用,因为每个进程启动就有大约为 600ms 左右开销。
下载包
npm i thread-loader -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 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
| 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 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|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"), 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, }), new HtmlWebpackPlugin({ template: path.resolve(__dirname, "../public/index.html"), }), new MiniCssExtractPlugin({ filename: "static/css/main.css", }), ], optimization: { minimize: true, minimizer: [ new CssMinimizerPlugin(), new TerserPlugin({ parallel: threads }) ], }, mode: "production", devtool: "source-map", };
|
3. 减少代码体积
Tree Shaking
Tree Shaking
是一个术语,通常用于描述移除 JavaScript 中的没有使用上的代码。
注意:它依赖 ES Module
。
Babel
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
| const path = require("path"); const ESLintWebpackPlugin = require("eslint-webpack-plugin");
module.exports={ module: { rules: [ { oneOf: [ { test: /\.js$/, include: path.resolve(__dirname, "../src"), loader: "babel-loader", option: { cacheDirectory: true, cacheCompression: false plugins: ["@babel/plugin-transform-runtime"], } }, ] } ] } }
|
Image Minimizer
压缩图片
1 2 3 4 5
| 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
|
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
| const os = require("os"); const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
module.exports = { 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", }, }, ], }, ], ], }, }, }), ], }, }
|
若出现报错
1 2
| Error: Error with 'src\images\1.jpeg': '"C:\Users\86176\Desktop\webpack\webpack_code\node_modules\jpegtran-bin\vendor\jpegtran.exe"' Error with 'src\images\3.gif': spawn C:\Users\86176\Desktop\webpack\webpack_code\node_modules\optipng-bin\vendor\optipng.exe ENOENT
|
安装两个文件到node_modules中
4. 优化代码运行性能
Code Split
将打包的js文件分开,渲染哪个页面加载哪个界面的js。
- 分割文件
- 按需加载
npm i webpack webpack-cli html-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 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", };
|
如果存在重复模块,则可以通过修改配置文件,将公共模块提取出来,单独打包为一个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 41 42 43 44 45 46 47 48
| const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = { optimization: { splitChunks: { chunks: "all", cacheGroups: { default: { minSize: 0, minChunks: 2, priority: -20, reuseExistingChunk: true, }, }, }, }, };
|
Preload/Prefetch
都只加载资源,不执行
都会缓存
兼容性差,preload稍好一点
Preload 立即加载资源
Prefetch 浏览器在空闲时才开始加载资源
Network Cache
对于静态资源,我们会使用缓存来优化,但是由于前后的文件名一样,浏览器会直接读取缓存,不会加载新资源。
我们通过这个确保文件名不同
有个问题,会引起引用该文件的hash值也发生变化
通过提取出一个runtime文件进行解决
Core-js
用来做兼容性处理
PWA
5. 总结
我们从 4 个角度对 webpack 和代码进行了优化:
- 提升开发体验
- 使用
Source Map
让开发或上线时代码报错能有更加准确的错误提示。
- 提升 webpack 提升打包构建速度
- 使用
HotModuleReplacement
让开发时只重新编译打包更新变化了的代码,不变的代码使用缓存,从而使更新速度更快。
- 使用
OneOf
让资源文件一旦被某个 loader 处理了,就不会继续遍历了,打包速度更快。
- 使用
Include/Exclude
排除或只检测某些文件,处理的文件更少,速度更快。
- 使用
Cache
对 eslint 和 babel 处理的结果进行缓存,让第二次打包速度更快。
- 使用
Thead
多进程处理 eslint 和 babel 任务,速度更快。(需要注意的是,进程启动通信都有开销的,要在比较多代码处理时使用才有效果)
- 减少代码体积
- 使用
Tree Shaking
剔除了没有使用的多余代码,让代码体积更小。
- 使用
@babel/plugin-transform-runtime
插件对 babel 进行处理,让辅助代码从中引入,而不是每个文件都生成辅助代码,从而体积更小。
- 使用
Image Minimizer
对项目中图片进行压缩,体积更小,请求速度更快。(需要注意的是,如果项目中图片都是在线链接,那么就不需要了。本地项目静态图片才需要进行压缩。)
- 优化代码运行性能
- 使用
Code Split
对代码进行分割成多个 js 文件,从而使单个文件体积更小,并行加载 js 速度更快。并通过 import 动态导入语法进行按需加载,从而达到需要使用时才加载该资源,不用时不加载资源。
- 使用
Preload / Prefetch
对代码进行提前加载,等未来需要使用时就能直接使用,从而用户体验更好。
- 使用
Network Cache
能对输出资源文件进行更好的命名,将来好做缓存,从而用户体验更好。
- 使用
Core-js
对 js 进行兼容性处理,让我们代码能运行在低版本浏览器。
- 使用
PWA
能让代码离线也能访问,从而提升用户体验。