Post's cover

H1介绍

为什么需要打包工具?

开发时,我们会使用框架(React、Vue),ES6 模块化语法,Less/SASS 等 CSS 预处理器等语法进行开发。

这样的代码要想在浏览器运行必须经过编译成浏览器能识别的 JS、CSS 等语法,才能运行。

所以我们需要打包工具帮我们做完这些事。

除此之外,打包工具还能压缩代码、做兼容性处理、提升代码性能等。

H1入口 Entry

指示 Webpack 从哪个文件开始打包

jsmodule.exports = { // 相对路径和绝对路径都行 entry: "./src/main.js", };

H1输出 Output

指示 Webpack 打包完的文件输出到哪里去,如何命名等

jsoutput: { // path: 所有文件输出目录,必须是绝对路径 // path.resolve() 方法返回一个绝对路径 // __dirname 当前文件的文件夹绝对路径 path: path.resolve(__dirname, "dist"), // filename: 入口文件打包输出文件名 // 将 js 文件输出到 static/js 目录中 filename: "static/js/main.js", // 自动清空上次打包内容 clean: true, },

H1加载器 Loader

Webpack 本身是不能识别样式资源的,所以我们需要借助 Loader 来帮助 Webpack 解析样式资源。

H2CSS

  • css-loader:负责将 CSS 文件编译成 Webpack 能识别的模块
  • style-loader:会动态创建一个 Style 标签,里面放置 Webpack 中 CSS 模块内容

配置

jsmodule: { rules: [ { // 用来匹配 .css 结尾的文件 test: /\.css$/, // use 数组里面 Loader 执行顺序是 从右到左 use: ["style-loader", "css-loader"], }, ], },

H2Babel

将 ES6 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

js{ test: /\.js$/, exclude: /node_modules/, // 排除node_modules代码不编译 loader: "babel-loader", option: { presets: ["@babel/preset-env"], } },

H1资源

H2对图片资源进行优化

将小于某个大小的图片转化成 data URI 形式(Base64 格式)

  • 优点:减少请求数量
  • 缺点:体积变得更大
javascript{ test: /\.(png|jpe?g|gif|webp)$/, type: "asset", parser: { dataUrlCondition: { maxSize: 10 * 1024 // 小于10kb的图片会被base64处 } } },

H2修改输出资源的路径和名称

js{ test: /\.(png|jpe?g|gif|webp)$/, type: "asset", generator: { // 将图片文件输出到 static/imgs 目录中 // 将图片文件命名 [hash:8][ext][query] // [hash:8]: hash值取8位 // [ext]: 使用之前的文件扩展名 // [query]: 添加之前的query参数 filename: "static/imgs/[hash:8][ext][query]", }, },

H2字体

js{ test: /\.(ttf|woff2?)$/, type: "asset/resource", generator: { filename: "static/media/[hash:8][ext][query]", }, },

H1插件 Plugin

H2ESLint

它是用来检测 js 和 jsx 语法的工具

ESLint 所有规则

webpack.config.js

jsconst ESLintWebpackPlugin = require("eslint-webpack-plugin"); module.exports = { plugins: [ new ESLintWebpackPlugin({ // 指定检查文件的根目录 context: path.resolve(__dirname, "src"), }), ], };

H2HTML

自动在 html 文件里引入打包文件

jsconst HTMLWebpackPlugin = require("html-webpack-plugin"); module.exports = { plugins: [ new HTMLWebpackPlugin({ // 以 public/index.html 为模板创建文件 // 新的html文件有两个特点:1. 内容和源文件一致 2. 自动引入打包生成的js等资源 template: path.resolve(__dirname, "public/index.html"), }), ], };

H1开发服务器 Dev Server

自动编译代码,代码更改后立马呈现效果

jsmodule.exports = { // 开发服务器 devServer: { host: "localhost", // 启动服务器域名 port: "3000", // 启动服务器端口号 open: true, // 是否自动打开浏览器 }, };

H1模式 Mode

H2开发模式

  1. 编译代码。使浏览器能识别运行

    开发时我们有样式资源、字体图标、图片资源、html 资源等,webpack 默认都不能处理这些资源,所以我们要加载配置来编译这些资源

  2. 代码质量检查。树立代码规范

    提前检查代码规范和格式,统一团队编码风格,让代码更优雅美观。

H2生产模式

这个模式下我们主要对代码进行优化,让其运行性能更好。主要优化代码运行性能打包速度

H3CSS 处理

H4提取 CSS 成单独文件

此前 CSS 文件被打包到 JS 文件中,当 JS 文件加载时,style-loader 才会创建一个 <style> 标签来生成样式。现在用 mini-css-extract-plugin 插件来替换 style-loader

webpack.prod.js

jsconst MiniCSSExtractPlugin = require("mini-css-extract-plugin"); module.exports = { module: { rules: [ { // 用来匹配 .css 结尾的文件 test: /\.css$/, // use 数组里面 Loader 执行顺序是从右到左 use: [MiniCSSExtractPlugin.loader, "css-loader"], }, { test: /\.less$/, use: [MiniCSSExtractPlugin.loader, "css-loader", "less-loader"], }, { test: /\.s[ac]ss$/, use: [MiniCSSExtractPlugin.loader, "css-loader", "sass-loader"], }, { test: /\.styl$/, use: [MiniCSSExtractPlugin.loader, "css-loader", "stylus-loader"], }, ], }, };

H4CSS 兼容性处理

使用 postcss 加载器加强样式代码在旧版浏览器的兼容性。

npm i postcss-loader postcss postcss-preset-env -D

webpack.prod.js

javascriptmodule: { rules: [ { test: /\.less$/, use: [ MiniCSSExtractPlugin.loader, "css-loader", { loader: "postcss-loader", options: { postcssOptions: { plugins: [ "postcss-preset-env", // 能解决大多数样式兼容性问题 ], }, }, }, "less-loader", ], }, ]; }

注意:写在 css 和 less 之间

实际开发中一般不考虑旧版本浏览器,可在 package.json 设置:

json{ // ... "browserslist": ["last 2 version", "> 1%", "not dead"] }

H4CSS 压缩

使用 css-minimizer-webpack-plugin 插件

webpack.prod.js

jsconst CSSMinimizerPlugin = require("css-minimizer-webpack-plugin"); module.exports = { plugins: [ // css压缩 new CSSMinimizerPlugin(), ], };

H3HTML & JS 压缩

默认生产模式开启了 HTML, JS 压缩。

H2开发 & 生产

webpack.dev.js

jsmodule.exports = { output: { path: undefined, // 开发模式没有输出,不需要指定输出目录 filename: "static/js/main.js", // 开启开发服务器后依然要写,其决定虚拟内存中的文件名 // clean: true, // 开发模式没有输出,不需要清空输出结果 }, devServer: { host: "localhost", // 启动服务器域名 port: "3000", // 启动服务器端口号 open: true, // 是否自动打开浏览器 }, };

运行开发模式的指令(地址可变):

bashnpx webpack serve --config ./config/webpack.dev.js

webpack.prod.js

jsmodule.exports = { output: { path: path.resolve(__dirname, "../dist"), // 生产模式需要输出 filename: "static/js/main.js", // 将 js 文件输出到 static/js 目录中 clean: true, }, // 开发服务器可移除 };

为了方便运行不同 mode 指令,可将 package.jsonscript 进行改变:

js"scripts": { "start": "npm run dev", "dev": "npx webpack serve --config ./config/webpack.dev.js", "build": "npx webpack --config ./config/webpack.prod.js" }

H1高级优化

H2提升开发体验

H3SourceMap

开发报错时,提示代码错误位置会不精准,因为代码经过压缩、混淆和合并后,生成的代码与原始源代码差异较大,使得调试非常困难。

SourceMap(源代码映射)是一个用来生成源代码与构建后代码一一映射的文件的方案。

开发模式cheap-module-source-map

  • 优点:打包编译速度快,只包含行映射
  • 缺点:没有列映射
javascriptmodule.exports = { mode: "development", devtool: "cheap-module-source-map", };

生产模式source-map

  • 优点:包含行/列映射
  • 缺点:打包编译速度更慢
jsmodule.exports = { mode: "production", devtool: "source-map", };

H2提升打包构建速度

H3热模块替换 HMR

开发时修改一处代码,Webpack 默认会将所有模块全部重新打包编译,很慢。

HMR (Hot Module Replacement) 能做到只修改某处模块代码,仅它能重新打包编译,其他模块不变,这样打包速度更快。

jsdevServer: { ... hot: true, // 开启HMR功能(只能用于开发环境,生产环境不需要了) },

实际开发我们使用其他 loader 来解决。 比如:vue-loader, react-hot-loader

H3OneOf

打包时每个文件都会经过所有 loader 处理,尽管由于 test 正则 最终只被一个 loader 处理,但都要遍历一遍,比较慢。

OneOf 能让你匹配上 loader 后就停止。

jsmodule: { rules: [ { oneOf: [ { // 用来匹配 .css 结尾的文件 test: /\.css$/, // use 数组里面 Loader 执行顺序是从右到左 use: ["style-loader", "css-loader"], }, ], }, ]; }

H3Include/Exclude

node_modules 不需要编译直接可用,所以在对 JS 文件处理时,要排除 node_modules 下的文件。

js{ test: /\.js$/, // exclude: /node_modules/, // 排除node_modules代码不编译 include: path.resolve(__dirname, "../src"), // 也可以用包含 loader: "babel-loader", },

H3Cache

每次打包 JS 文件都要经过 ESLint 检查 和 Babel 编译,速度较慢。我们可以缓存之前的 ESLint 检查 和 Babel 编译结果,这样第二次打包时速度就会更快了。

jsmodule: { rules: [ { oneOf: [ { test: /\.js$/, // exclude: /node_modules/, // 排除node_modules代码不编译 include: path.resolve(__dirname, "../src"), // 也可以用包含 loader: "babel-loader", options: { cacheDirectory: true, // 开启babel编译缓存 cacheCompression: false, // 缓存文件不要压缩 }, }, ], }, ]; } plugins: [ new ESLintWebpackPlugin({ // 指定检查文件的根目录 context: path.resolve(__dirname, "../src"), exclude: "node_modules", // 默认值 cache: true, // 开启缓存 // 缓存目录 cacheLocation: path.resolve( __dirname, "../node_modules/.cache/.eslintcache" ), }), ];

H3Thread

项目越庞大,打包速度越慢。想继续提升打包速度,主要提升 JS 打包速度,主要就是 ESLint 、Babel、Terser(自带压缩 JS 工具) 三个工具。

我们可以开启多进程同时处理 JS 文件,这样速度比单进程打包更快。(适用于大项目)

先下载 thread-loader

jsconst os = require("os"); const TerserPlugin = require("terser-webpack-plugin"); // cpu核数 const threads = os.cpus().length; { test: /\.js$/, // exclude: /node_modules/, // 排除node_modules代码不编译 include: path.resolve(__dirname, "../src"), // 也可以用包含 use: [ { loader: "thread-loader", // 开启多进程 options: { workers: threads, // 数量 }, }, { loader: "babel-loader", options: { cacheDirectory: true, // 开启babel编译缓存 }, }, ], }, 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: [ // css压缩也可以写到optimization.minimizer里面,效果一样的 new CSSMinimizerPlugin(), // 当生产模式会默认开启TerserPlugin,但是我们需要进行其他配置,就要重新写了 new TerserPlugin({ parallel: threads // 开启多进程 }) ], },

H2减少代码体积

H3Tree Shaking

开发时我们定义了些工具函数库,或引用第三方工具函数库或组件库。若无特殊处理,打包时会引入整个库,但实际我们可能只用了一部分功能。

Webpack 已经默认开启了这个功能,无需其他配置。

H3Babel

Babel 为每个文件都插入了辅助代码,使代码体积过大。Babel 对一些公共方法使用了很小的辅助代码,如 _extend。默认情况下会被添加到每个需要它的文件中。可以将这些辅助代码作为一个独立模块,来避免重复引入。

先下载 babel/plugin-transform-runtime 插件

js{ test: /\.js$/, // exclude: /node_modules/, // 排除node_modules代码不编译 include: path.resolve(__dirname, "../src"), // 也可以用包含 use: [ { loader: "thread-loader", // 开启多进程 options: { workers: threads, // 数量 }, }, { loader: "babel-loader", options: { cacheDirectory: true, // 开启babel编译缓存 cacheCompression: false, // 缓存文件不要压缩 plugins: ["@babel/plugin-transform-runtime"], // 减少代码体积 }, }, ], },

H3Image Minimizer

image-minimizer-webpack-plugin 压缩图片插件

无损压缩 npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D

有损压缩 npm install imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo -D

jsoptimization: { 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", }, }, ], }, ], ], }, }, }), ], },

H2优化代码运行性能

H3Code Split

打包时会将所有 JS 文件打包到一个文件中,体积太大。若只渲染首页,就该只加载首页的 JS 文件。所以需要将打包生成的文件进行代码分割,这样加载的资源少,速度快。

代码分割:

  1. 分割文件:将打包生成的文件进行分割,生成多个 js 文件。
  2. 按需加载:需要哪个文件就加载哪个文件。

H4多入口

jsentry: { main: "./src/main.js", app: "./src/app.js", }, output: { path: path.resolve(__dirname, "./dist"), // [name]是webpack命名规则,使用chunk的name作为输出的文件名。 // 什么是chunk?打包的资源就是chunk,输出出去叫bundle。 // chunk的name是啥呢? 比如: entry中xxx: "./src/xxx.js", name就是xxx。注意是前面的xxx,和文件名无关。 // 为什么需要这样命名呢?如果还是之前写法main.js,那么打包生成两个js文件都会叫做main.js会发生覆盖。(实际上会直接报错的) filename: "js/[name].js", clear: true, },

H4提取公共模块

jsoptimization: { // 代码分割配置 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: 2, // 我们定义的文件体积太小了,所以要改打包的最小文件体积 minChunks: 2, priority: -20, reuseExistingChunk: true, }, }, }, }

H4按需加载,动态导入

jsdocument.getElementById("btn").onclick = function () { // 动态导入 --> 实现按需加载 // 即使只被引用了一次,也会代码分割 import("./math.js").then(({ sum }) => { alert(sum(1, 2, 3, 4, 5)); }); };

H4单入口

开发时我们可能是单页面应用(SPA)

jsmodule.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", // 对所有模块都进行分割 }, };

H4给动态导入文件取名

jsdocument.getElementById("btn").onClick = function () { // eslint会对动态导入语法报错,需要修改eslint配置文件 // webpackChunkName: "math":这是webpack动态导入模块命名的方式 // "math"将来就会作为[name]的值显示。 import(/* webpackChunkName: "math" */ "./js/math.js").then(({ count }) => { console.log(count(2, 1)); }); };

webpack.prod.js

jsoutput: { path: path.resolve(__dirname, "../dist"), // 生产模式需要输出 filename: "static/js/[name].js", // 入口文件打包输出资源命名方式 chunkFilename: "static/js/[name].chunk.js", // 动态导入输出资源命名方式 assetModuleFilename: "static/media/[name].[hash][ext]", // 图片、字体等资源命名方式(注意用hash) clean: true, },

H3Preload / Prefetch

即便已做代码分割,同时会使用 import 动态导入语法来进行代码按需加载(懒加载)。但加载速度还不够好,比如:用户点击按钮时才加载这个资源,若资源体积大,那么会有明显卡顿效果。

我们想在浏览器空闲时间,加载后续需要使用的资源。我们就需要用上  Preload  或  Prefetch  技术。

PreloadPrefetch
加载时间告诉浏览器立即加载资源在浏览器空闲时才开始加载资源。
也可以加载下个页面的所需资源。
优先级
兼容性较低

webpack.prod.js

jsconst PreloadWebpackPlugin = require("@vue/preload-webpack-plugin"); plugins: [ new PreloadWebpackPlugin({ rel: "preload", // preload兼容性更好 as: "script", // rel: 'prefetch' // prefetch兼容性更差 }), ];

H3Network Cache

开发时都会对静态资源使用缓存来优化,这样浏览器二次请求能直接读取缓存,但这样会有个问题,因为前后输出的文件名是一样的,都叫 main.js,一旦将来发布新版本,因为文件名没有变化导致浏览器会直接读取缓存,不会加载新资源,项目也就没法更新了。

所以我们通过给文件名加上哈希值,确保更新前后文件名不一样,这样浏览器就会加载新资源而不是使用旧缓存。

  • fullhash(webpack4 是 hash):每次修改任何一个文件,所有文件名的 hash 至都将改变。所以一旦修改了任何一个文件,整个项目的文件缓存都将失效。
  • chunkhash:根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值。我们 js 和 css 是同一个引入,会共享一个 hash 值。
  • contenthash:根据文件内容生成 hash 值,只有文件内容变化了,hash 值才会变化。所有文件 hash 值是独享且不同的。
jsoutput: { 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][ext]", // 图片、字体等资源命名方式(注意用hash) clean: true, },

H3Core-js

过去咋使用 Babel 对 JS 代码进行了兼容性处理。它能将 ES6 的一些语法进行编译转换,比如箭头函数、扩展运算符等。但 async 函数、promise 对象、数组的一些方法(includes)等,它没法处理。

babel.config.js(自动按需引入)

jsmodule.exports = { // 智能预设:能够编译ES6语法 presets: [ [ "@babel/preset-env", // 按需加载core-js的polyfill { useBuiltIns: "usage", corejs: { version: "3", proposals: true } }, ], ], };

H3PWA

渐进式网络应用程序 (progressive web application - PWA):是一种可以提供类似于 native app(原生应用程序)离线体验的 Web App 的技术。

H1参考

文档

视频

Next

Related Posts