webpack|【webpack】从零开始配置webpack系列(进阶篇)

从零开始配置webpack(进阶篇)
文章目录

  • 从零开始配置webpack(进阶篇)
    • 前言
    • 进阶一:代码分离
      • 入口起点
      • 防止重复
        • 入口依赖
        • SplitChunksPlugin
      • 动态导入
      • 懒加载
      • 预获取/预加载模块
    • 进阶二:缓存
      • 输出文件的文件名
      • 缓存第三方库
      • 将 js 文件放到一个文件夹中
    • 进阶三:拆分开发环境和生产环境配置
      • 公共路径
        • 基于环境设置
        • Automatic publicPath
      • 环境变量
      • 拆分配置文件
      • npm 脚本
      • 提取公共配置
      • 合并配置文件

前言 从零开始配置webpack系列(基础篇)
从零开始配置webpack系列(进阶篇)
从零开始配置webpack系列(优化篇)
从零开始配置webpack系列(原理篇)


进阶一:代码分离 代码分离特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。
常用的代码分离方法有三种:
  • 入口起点:使用 entry 配置手动地分离代码。
  • 防止重复:使用 Entry dependencies 或者 SplitChunksPlugin 去重和分离 chunk。
  • 动态导入:通过模块的内联函数调用来分离代码。

入口起点
在 src 目录下创建 another-module.js 文件:
//another-module.js //这个模块依赖了 lodash ,需要安装一下: npm install lodash --save-devimport _ from 'lodash' console.log(_.join(['Another', 'module', 'loaded!'], ' '))

修改配置文件:
module.exports = { entry: { index: './src/index.js', another: './src/another-module.js', }, output: { filename: '[name].bundle.js' }, }

查看打包后的文件:

两个入口的 bundle 文件都被链接到了 app.html 中。
这种方式的确存在一些隐患:
  • 如果入口 chunk 之间包含一些重复的模块,那些重复模块都会被引入到各个 bundle 中。
  • 这种方法不够灵活,并且不能动态地将核心应用程序逻辑中的代码拆分出来。

防止重复
入口依赖 配置 dependOn option 选项,这样可以在多个 chunk 之间共享模块
module.exports = { entry: { index: { import: './src/index.js', dependOn: 'shared', }, another: { import: './src/another-module.js', dependOn: 'shared', }, shared: 'lodash', } }

index.bundle.js 与 another.bundle.js 共享的模块 lodash.js 被打包到一个单独的文件 shared.bundle.js 中。

SplitChunksPlugin SplitChunksPlugin 插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。使用这个插件,将之前的示例中重 复的 lodash 模块去除:
module.exports = { entry: { index: './src/index.js', another: './src/another-module.js' }, optimization: { splitChunks: { chunks: 'all', }, }, }

使用 optimization.splitChunks 配置选项之后,现在应该可以看出, index.bundle.js 和 another.bundle.js 中已经移除了重复的依赖模块。需要注意的是,插件将 lodash 分离到单独的 chunk,并且将其从 main bundle 中移除,减轻了大小。

动态导入
第一种,也是推荐选择的方式是,使用符合 ECMAScript 提案的 import() 语法 来实现动态导入。
第二种,则是 webpack 的遗留功能,使用 webpack 特定的 require.ensure。

懒加载
懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把 你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用 或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体 体积,因为某些代码块可能永远不会被加载
创建一个 math.js 文件,在主页面中通过点击按钮调用其中的函数:
export const add = () => { return x + y } export const minus = () => { return x - y }

编辑 index.js 文件:
const button = document.createElement('button') button.textContent = '点击执行加法运算' button.addEventListener('click', () => { import(/* webpackChunkName: 'math' */ './math.js').then(({ add}) => { console.log(add(4, 5)) }) }) document.body.appendChild(button)

这里有句注释,我们把它称为 webpack 魔法注释: webpackChunkName: ‘math’ , 告诉webpack打包生成的文件名为 math 。
第一次加载完页面, math.bundle.js 不会加载,当点击按钮后,才加载 math.bundle.js 文件。

预获取/预加载模块
在声明 import 时,使用下面这些内置指令,可以让 webpack 输出 “resource hint(资源提示)”,来告知浏览器:
  • prefetch(预获取):将来某些导航下可能需要的资源
  • preload(预加载):当前导航下可能需要资源
下面这个 prefetch 的简单示例中,编辑 index.js 文件:
const button = document.createElement('button') button.textContent = '点击执行加法运算' button.addEventListener('click', () => { import(/* webpackChunkName: 'math',webpackPrefetch:true*/'./math.js').then(({ add }) => { console.log(add(4, 5)) }) }) document.body.appendChild(button)

添加第二句魔法注释: webpackPrefetch: true
告诉 webpack 执行预获取。这会生成并追加到页面头部,指示着浏览器在闲置时间预取 math.js 文件。
启动服务,在浏览器上查看:
我们发现,在还没有点击按钮时, math.bundle.js 就已经下载下来了。
与 prefetch 指令相比,preload 指令有许多不同之处:
  • preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
  • preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载
  • preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会 用于未来的某个时刻。 浏览器支持程度不同
创建一个 print.js 文件
export const print = () => { console.log('preload chunk.') }

【webpack|【webpack】从零开始配置webpack系列(进阶篇)】修改 index.js 文件:
const button2 = document.createElement('button') button2.textContent = '点击执行字符串打印' button2.addEventListener('click', () => { import(/* webpackChunkName: 'print', webpackPreload: true */ './print.js').then(({ print }) => { print(4, 5) }) }) document.body.appendChild(button2)

启动服务,在浏览器上查看:
仔细观察,发现 print.bundle.js 未被下载,因为我们配置的是 webpackPreload , 是在父 chunk 加载时,以并行方式开始加载。点击按钮才加载的 模块不会事先加载的。

进阶二:缓存 以上,我们使用 webpack 来打包我们的模块化后的应用程序,webpack 会生成一个可部署的 /dist 目录,然后把打包后的内容放置在此目录中。只要 /dist 目录中 的内容部署到 server 上,client(通常是浏览器)就能够访问此 server 的网站及其 资源。而最后一步获取资源是比较耗费时间的,这就是为什么浏览器使用一种名为 缓存 的技术。可以通过命中缓存,以降低网络流量,使网站加载速度更快,然而,如果我们在部署新版本时不更改资源的文件名,浏览器可能会认为它没有被更新,就 会使用它的缓存版本。由于缓存的存在,当你需要获取新的代码时,就会显得很棘 手。
通过必要的配置,以确保 webpack 编译生成的文件能够被客户端缓存,而在文件内容变化后,能够请求到新的文件。

输出文件的文件名
我们可以通过替换 output.filename 中的 substitutions 设置,来定义输出文件的 名称。webpack 提供了一种使用称为 substitution(可替换模板字符串) 的方式,通 过带括号字符串来模板化文件名。其中, [contenthash] substitution 将根据资源 内容创建出唯一 hash。当资源内容发生变化时, [contenthash] 也会发生变化。
修改配置文件:
module.exports = { output: { filename: '[name].[contenthash].js', }, };

bundle 的名称是它内容(通过 hash)的映射。如果我们不做修改,然后 再次运行构建,文件名会保持不变。

缓存第三方库
将第三方库(library)(例如 lodash )提取到单独的 vendor chunk 文件中,是比较 推荐的做法,这是因为,它们很少像本地的源代码那样频繁修改。因此通过实现以上 步骤,利用 client 的长效缓存机制,命中缓存来消除请求,并减少向 server 获取资 源,同时还能保证 client 代码和 server 代码版本一致。 我们在 optimization.splitChunks 添加如下 cacheGroups 参数并构建:
splitChunks: { cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all', }, }, },


将 js 文件放到一个文件夹中
目前,全部 js 文件都在 dist 文件夹根目录下,我们尝试把它们放到一个文件夹中,修改配置文件
ouotput: { filename: 'scripts/[name].[contenthash].js', }


进阶三:拆分开发环境和生产环境配置 现在,我们只能手工的来调整 mode 选项,实现生产环境和开发环境的切换,且很多 配置在生产环境和开发环境中存在不一致的情况,比如开发环境没有必要设置缓存, 生产环境还需要设置公共路径等等。
本节介绍拆分开发环境和生产环境,让打包更灵活。

公共路径
publicPath 配置选项在各种场景中都非常有用。你可以通过它来指定应用程序中所 有资源的基础路径。
基于环境设置 在开发环境中,我们通常有一个 assets/ 文件夹,它与索引页面位于同一级别。但是,如果我们将所有静态资源托管至 CDN,然后想在生产环境中使用呢? 想要解决这个问题,可以直接使用一个 environment variable(环境变量)。假设 我们有一个变量 ASSET_PATH :
import webpack from 'webpack'; // 尝试使用环境变量,否则使用根路径 const ASSET_PATH = process.env.ASSET_PATH || '/'; export default { output: { publicPath: ASSET_PATH, }, plugins: [ // 这可以帮助我们在代码中安全地使用环境变量 new webpack.DefinePlugin({ 'process.env.ASSET_PATH': JSON.stringify(ASSET_PATH), }), ], };


Automatic publicPath 有可能你事先不知道 publicPath 是什么,webpack 会自动根据 import.meta.url 、 document.currentScript 、 script.src 或者 self.location 变量设置 publicPath。你需要做的是将 output.publicPath 设为 ‘auto’ :
module.exports = { output: { publicPath: 'auto', }, };


环境变量
想要消除 webpack.config.js 在 开发环境 和 生产环境 之间的差异,你可能需要环境变量
webpack 命令行 环境配置的 --env 参数,可以允许你传入任意数量的环境变量。
而在 webpack.config.js 中可以访问到这些环境变量。
例如, --env production 或 --env goal=local 。
npx webpack --env goal=local --env production --progress

对于我们的 webpack 配置,有一个必须要修改之处。通常, module.exports 指向 配置对象。要使用 env 变量,你必须将 module.exports 转换成一个函数:
//... module.exports = (env) => { return { //... // 根据命令行参数 env 来设置不同环境的 mode mode: env.production ? 'production' : 'development', //... } }


拆分配置文件
目前,生产环境和开发环境使用的是一个配置文件,我们需要将这两个文件单独放到不同的配置文件中。如 webpack.config.dev.js (开发环境配置)和 webpack.config.prod.js (生产环境配置)。
在项目根目录下创建一个配置文件夹 config 来存放他们。
webpack.config.dev.js 配置如下:
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", // 将 js 文件输出到 static/js 目录中 // clean: true, // 开发模式没有输出,不需要清空输出结果 }, module: { rules: [ { // 用来匹配 .css 结尾的文件 test: /\.css$/, // use 数组里面 Loader 执行顺序是从右到左 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, // 小于10kb的图片会被base64处理 }, }, generator: { // 将图片文件输出到 static/imgs 目录中 // 将图片文件命名 [hash:8][ext][query] // [hash:8]: hash值取8位 // [ext]: 使用之前的文件扩展名 // [query]: 添加之前的query参数 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/, // 排除node_modules代码不编译 loader: "babel-loader", }, ], }, plugins: [ new ESLintWebpackPlugin({ // 指定检查文件的根目录 context: path.resolve(__dirname, "../src"), }), new HtmlWebpackPlugin({ // 以 public/index.html 为模板创建文件 // 新的html文件有两个特点:1. 内容和源文件一致 2. 自动引入打包生成的js等资源 template: path.resolve(__dirname, "../public/index.html"), }), ], // 其他省略 devServer: { host: "localhost", // 启动服务器域名 port: "3000", // 启动服务器端口号 open: true, // 是否自动打开浏览器 }, mode: "development", };

webpack.config.prod.js 配置如下:
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", // 将 js 文件输出到 static/js 目录中 clean: true, }, module: { rules: [ { // 用来匹配 .css 结尾的文件 test: /\.css$/, // use 数组里面 Loader 执行顺序是从右到左 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, // 小于10kb的图片会被base64处理 }, }, generator: { // 将图片文件输出到 static/imgs 目录中 // 将图片文件命名 [hash:8][ext][query] // [hash:8]: hash值取8位 // [ext]: 使用之前的文件扩展名 // [query]: 添加之前的query参数 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/, // 排除node_modules代码不编译 loader: "babel-loader", }, ], }, plugins: [ new ESLintWebpackPlugin({ // 指定检查文件的根目录 context: path.resolve(__dirname, "../src"), }), new HtmlWebpackPlugin({ // 以 public/index.html 为模板创建文件 // 新的html文件有两个特点:1. 内容和源文件一致 2. 自动引入打包生成的js等资源 template: path.resolve(__dirname, "../public/index.html"), }), ], // devServer: { //host: "localhost", // 启动服务器域名 //port: "3000", // 启动服务器端口号 //open: true, // 是否自动打开浏览器 // }, mode: "production", };

拆分成两个配置文件后,分别运行这两个文件:
开发环境:
[felix] 10-multiple-env $ npx webpack serve -c ./config/webpack.config.dev.js

生产环境:
[felix] 10-multiple-env $ npx webpack -c ./config/webpack.config.prod.js


npm 脚本
每次打包或启动服务时,都需要在命令行里输入一长串的命令。我们将父目录的 package.json 、 node_modules 与 package-lock.json 拷贝到与dist目录同级,
配置 npm 脚本来简化命令行的输入,这时可以省略 npx :
{ "scripts": { "start": "webpack serve -c ./config/webpack.config.dev.js", "build": "webpack -c ./config/webpack.config.prod.js" } }

开发环境运行脚本
[felix] 10-multiple-env $ npm run start [felix] 10-multiple-env $ npm run build


提取公共配置
这两个配置文件里存在大量的重复代码,可以手动的将这些重复的代码单独提取到一个文件里,
创建 webpack.config.common.js ,配置公共的内容
const path = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') const MiniCssExtractPlugin = require('mini-css-extractplugin')const toml = require('toml') const yaml = require('yaml') const json5 = require('json5')module.exports = { entry: { index: './src/index.js', another: './src/another-module.js' }, output: { // 注意这个dist的路径设置成上一级 path: path.resolve(__dirname, '../dist'), clean: true, assetModuleFilename: 'images/[contenthash][ext]', }, plugins: [ new HtmlWebpackPlugin({ template: './index.html', filename: 'app.html', inject: 'body' }), new MiniCssExtractPlugin({ filename: 'styles/[contenthash].css' }) ], module: { rules: [ { test: /\.png$/, type: 'asset/resource', generator: { filename: 'images/[contenthash][ext]' } }, { test: /\.svg$/, type: 'asset/inline' }, { test: /\.txt$/, type: 'asset/source' }, { test: /\.jpg$/, type: 'asset', parser: { dataUrlCondition: { maxSize: 4 * 1024 } } }, { test: /\.(css|less)$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'lessloader'] }, { test: /\.(woff|woff2|eot|ttf|otf)$/, type: 'asset/resource' }, { test: /\.(csv|tsv)$/, use: 'csv-loader' }, { test: /\.xml$/, use: 'xml-loader' }, { test: /\.toml$/, type: 'json', parser: { parse: toml.parse } }, { test: /\.yaml$/, type: 'json', parser: { parse: yaml.parse } }, { test: /\.json5$/, type: 'json', parser: { parse: json5.parse } }, { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'], plugins: [ [ '@babel/plugin-transform-runtime' ] ] } } } ] }, optimization: { splitChunks: { cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all' } } } },//关闭 webpack 的性能提示 performance: { hints:false } }

改写 webpack.config.dev.js :
module.exports = { // 开发环境不需要配置缓存 output: { filename: 'scripts/[name].js', }, // 开发模式 mode: 'development', // 配置 source-map devtool: 'inline-source-map',// 本地服务配置 devServer: { static: './dist' } }

修改 webpack.config.prod.js :
const CssMinimizerPlugin = require('css-minimizer-webpackplugin') module.exports = { // 生产环境需要缓存 output: { filename: 'scripts/[name].[contenthash].js', publicPath: 'http://localhost:8080/' }, // 生产环境模式 mode: 'production', // 生产环境 css 压缩 optimization: { minimizer: [ new CssMinimizerPlugin() ] } }


合并配置文件
如何保证配置合并没有问题呢?使用webpack-merge工具
npm install webpack-merge -D

创建 webpack.config.js ,合并代码
const { merge } = require('webpack-merge')const commonConfig = require('./webpack.config.common.js')const productionConfig = require('./webpack.config.prod.js')const developmentConfig = require('./webpack.config.dev')module.exports = (env) => { switch(true) { case env.development: return merge(commonConfig, developmentConfig) case env.production: return merge(commonConfig, productionConfig) default: throw new Error('No matching configuration was found!'); } }

    推荐阅读