前端工程化之webpack(万字)

前端工程化之 Webpack

::: warning 本文基于 webpack 5.x 版本进行介绍,对于 webpack 4.x 版本可能存在部分功能不兼容的情况。 :::

一、webpack 简介

webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示具体的内容。

二、webpack 入门

1.起步

安装 webpack 和 webpack-cli。

zsh 复制代码
pnpm i webpack webpack-cli -D

安装成功, 设置 build 命令:

json 复制代码
// package.json
{
  "name": "01-getting-started",
  "version": "0.1.0",
  "main": "index.js",
  "author": "condingandsleeping <<1162300237@qq.com>>",
  "license": "MIT",
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "webpack": "^5.98.0",
    "webpack-cli": "^6.0.1"
  }
}

新建 src 目录,并在其中新建 index.jsheading.js 文件。

js 复制代码
// index.js
import createHeading from './heading.js';

const heading = createHeading();

document.body.append(heading);
js 复制代码
// heading.js
export default () => {
  const element = document.createElement('h2');

  element.textContent = 'Hello world';
  element.addEventListener('click', () => {
    alert('Hello webpack');
  });

  return element;
};

执行 npm run build 命令,默认会打包生成 dist/main.js 文件。

新建 index.html 文件,引入 dist/main.js 文件。

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Webpack</title>
  </head>
  <body>
    <!-- 引入打包后的文件 -->
    <script src="dist/main.js"></script>
  </body>
</html>

最终文件目录:

zsh 复制代码
- dist
  - main.js
- src
  - index.js
  - heading.js
- package.json
- index.html

全局安装 serve 命令, 在当前目录下执行 serve . 命令,会启动本地服务器,访问 http://localhost:3000/ 地址,可以看到页面正常输出。

2.配置文件

webpack除了可以通过命令行参数进行配置外,还可以通过在根目录创建 webpack.config.js 文件进行一些打包配置。

1.1 entry & output

通过配置 entry 属性指定入口文件,output 属性指定打包后的文件名和路径。

js 复制代码
// webpack.config.js
const path = require('path');
module.exports = {
  entry: './src/main.js', // 入口文件
  output: {
    filename: 'bundle.js', // 打包后的文件名
    path: path.join(__dirname, 'output'), // 打包后的文件路径
  },
};
1.2 mode

通过配置 mode 属性指定打包模式。默认为 production

js 复制代码
const path = require('path');
module.exports = {
  ...
  mode: 'development', // 打包模式 - 开发模式
  ...
};
模式 说明
development 开发模式下,Webpack 会自动优化打包速度,添加一些调试过程中的辅助
production 生产模式下,Webpack 会自动优化打包结, 压缩、tree shaking
none none 模式下,Webpack 就是运行最原始的打包,不做任何额外处理
1.3 loader

webpack 默认只能打包 js 文件,如果项目中引入了其他文件类型,例如 csslessscssjsonhtmlimage 等文件类型,则需要配置不同的 loader 来对不同的文件类型进行转换, loader可以是同步的,也可以是异步的。

loader 配置方式是配置在 module.rules 属性中:

  • rules 是一个数组,可以配置多个加载器规则。
  • 每个规则都是一个对象,包含 testusetest 属性是一个正则表达式,用于匹配要加载的文件,use 配置对应的 loader, 可以配多个。

简单示例:

js 复制代码
module.exports = {
  ...
  module: {
    rules: [
      {
        test: /.css$/, // 匹配所有 .css 结尾的文件
        use: [
          'style-loader', // 将 CSS 代码注入到 HTML 页面中
          'css-loader' // 转换 css 代码
        ]
      }
    ]
  }
  ...
}

注意: loader 是链式调用的,执行顺序是从后往前 ,上面的配置表示先使用 css-loader 转换 css 文件,再使用 style-loaderCSS 代码注入到 HTML 页面中。

下面介绍一些常用的 loader

css-loader: 将CSS 文件用 require 的方式引入,并返回一个 css 代码

style-loader:将 CSS 代码注入到 HTML 页面中,通过 <style> 标签添加样式

less-loader:用于加载和转换 less 文件

sass-loader:用于加载和转换 sass 文件

file-loader: 用于处理文件,例如图片、字体等

url-loader:用于处理文件,并将它们转为 base64 编码,减少请求数量

babel-loader:用于加载和转换 ES6 代码,将其转换为浏览器可以识别的代码

html-loader:用于加载和转换 HTML 文件

常用的 loader 配置示例:

js 复制代码
module.exports = {
  ...
  module: {
    rules: [
      {
        test: /.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'], // babel 预设
          },
        },
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /\.scss$/,
        use: ['style-loader', 'css-loader', 'sass-loader'],
      },
      {
        test: /\.(png|jpe?g|gif|svg)$/i,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 10 * 1024, // 图片大小小于 10kb 时,会自动转为 base64 编码 否则会启用 file-loader
              name: '[name].[ext]', // 输出文件名
            },
          },
        ],
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/i,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[name].[ext]',
            },
          },
        ],
      },
      {
        test: /\.html$/,
        use: [
          {
            loader: 'html-loader',
            attrs: ['img:src', 'a:href'], // 处理 img 和 a 标签的 src 属性
          },
        ],
      },
    ],
  },
  ...
}

当以上 loader 不满足我们的需求时,我们还可以编写自定义 loader 来处理不同的文件类型。

自定义 markdown-loader

js 复制代码
const marked = require('marked');

// 导出一个函数,接收 源代码 作为参数
module.exports = (source) => {
  // 处理 markdown 文件
  const html = marked(source);
  // 返回 html 字符串交给下一个 loader 处理
  return html;
};

webpack.config.js 中配置 markdown-loader

js 复制代码
module.exports = {
  ...
  module: {
    rules: [
       {
        test: /.md$/, // 匹配所有 .md 结尾的文件
        use: [
          'html-loader', // 将转换好的 html 代码交给 html-loader 处理
          './markdown-loader' // 自定义的 markdown-loader
        ]
      }
    ],
  },
  ...
}
1.4 plugin

pluginwebpack 的强大功能之一,它可以扩展 webpack 的功能, 它会运行在 webpack 构建流程的不同阶段, 贯穿了 webpack的整个编译周期。它可以解决 loader 无法实现的一些问题。

基本使用:

js 复制代码
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
  ...
  plugins: [
    new webpack.ProgressPlugin(), // webpack内置插件 - 显示打包进度
    new CleanWebpackPlugin(), // 自动清理打包目录
    new HtmlWebpackPlugin() // 自动生成 html 文件
  ]
  ...
}

常用的 plugin

  • CleanWebpackPlugin:自动清理打包目录
  • HtmlWebpackPlugin:自动生成 html 文件
  • CopyWebpackPlugin:复制文件到打包目录
  • DefinePlugin :定义环境变量
  • HotModuleReplacementPlugin: 热更新插件
  • MiniCssExtractPlugin:提取 css 文件到一个单独的文件中

常用的 plugin 配置示例:

js 复制代码
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
  ...
  plugins: [
    new CleanWebpackPlugin(), // 自动清理打包目录

    new HtmlWebpackPlugin({
      title: 'webpack plugin', // 设置生成的 html 文件的 title, html中需要使用<%= htmlWebpackPlugin.options.title %> 指定
      meta: {
        viewport: 'width=device-width', // 设置生成的 html 文件的 viewport
      },
      template: './src/index.html', // 指定生成的 html 文件的模板
    }),
    new CopyWebpackPlugin({
      patterns: ['public'], // 复制 public 文件到打包目录
    }),
    new webpack.HotModuleReplacementPlugin(), // 热更新插件
  ],
  ...
};

html模版文件:

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

使用 DefinePlugin 注入全局成员变量:

js 复制代码
const webpack = require('webpack');

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
  },
  plugins: [
    new webpack.DefinePlugin({
      // 值要求的是一个代码片段
      STRING: '"string"',
      TRUE: true,
      TRUE_STRING: JSON.stringify(true),
      OBJECT: { foo: 'bar' },
      OBJECT_STRING: JSON.stringify({ foo: 'bar' }),
      NUMBER: 123,
      NUMBER_STRING: JSON.stringify(123),
      FUNCTION: function () {},
      FUNCTION_STRING: '"function () {})"',
    }),
  ],
};

main.js 中使用:

js 复制代码
console.log(STRING);
console.log(STRING_STRING);
console.log(TRUE);
console.log(TRUE_STRING);
console.log(OBJECT);
console.log(OBJECT_STRING);
console.log(NUMBER);
console.log(NUMBER_STRING);
console.log(FUNCTION);
console.log(FUNCTION_STRING);

打包过后的结果:

js 复制代码
/******/ (() => {
  // webpackBootstrap
  console.log('string');
  console.log('string');
  console.log(true);
  console.log(true);
  console.log({ foo: bar });
  console.log({ foo: 'bar' });
  console.log(123);
  console.log(123);
  console.log(function () {});
  console.log('function () {})');
  /******/
})();

需要注意的是,在注入全局变量时,如果该值为字符串,它将被作为代码片段来使用。如果是一个对象,它的值将被作为代码片段来使用。

编写plugin

plugin 插件是一个具有 apply 方法的 JavaScript 对象。apply 方法会被 webpack compiler 调用,并且在整个编译生命周期都可以访问 compiler 对象。

js 复制代码
class MyPlugin {
  apply(compiler) {
    console.log('MyPlugin 启动');
    // emits hooks : 输出 asset 到 output 目录之前执行
    compiler.hooks.emit.tap('MyPlugin', (compilation) => {
      // compilation 可以理解为此次打包的上下文
      for (const name in compilation.assets) {
        // 遍历所有资源
        if (name.endsWith('.js')) {
          // 处理 js 文件
          const contents = compilation.assets[name].source();
          const withoutComments = contents.replace(/\/\*\*+\*\//g, ''); // 去除注释
          compilation.assets[name] = {
            source: () => withoutComments,
            size: () => withoutComments.length,
          };
        }
      }
    });
  }
}

module.exports = MyPlugin;

webpack.config.js 中配置 MyPlugin

js 复制代码
const MyPlugin = require('./my-plugin.js')
module.exports = {
  ...
  plugins: [
    new MyPlugin(), // 自定义的 plugin
  ],
  ...
};

plugin的具体的生命周期有很多,可以参考官方文档

1.5 devServer

devServer 提供用于开发的 HTTP服务器。集成了实时编译和自动刷新等功能。

首先,安装 webpack-dev-server

zsh 复制代码
pnpm i webpack-dev-server -D

可以使用命令行启动 devServer

zsh 复制代码
pnpm webpack serve

它会将打包结果存放在内存中,并监听文件变化,自动重新编译和自动刷新浏览器。

webpack 5.x 版本中,默认开启 静态资源目录为 public目录,用于访问静态资源,可以设置 staticfalse,关闭静态资源目录功能。注意:在 4.x的版本中是使用 contentBase 配置静态资源目录。

::: tip 为什么要配置静态资源目录,是因为在开发环境中频繁拷贝 public 目录中的静态资源到 dist 目录,很耗费性能,因此配置静态资源目录可以让打包后的结果直接访问 public 目录。 :::

还可以通过指定 openport 来开启自动打开浏览器和修改端口号:

js 复制代码
module.exports = {
  ...
  devServer: {
    open: true, // 自动打开浏览器
    port: 3000, // 修改端口号
  },
  ...
}

此外,在解决开发环境跨域问题时,还可以配置proxy属性,用过 vite 的应该不陌生。

js 复制代码
module.exports = {
  ...
  devServer: {
    proxy: {
      '/api': {
        target: 'http://server.com',  // 目标服务器地址
        changeOrigin: true,
        pathRewrite: {
          '^/api': '', // 重写路径
        },
      },
    },
  },
  ...
}

其他的属性还有很多,可以参考官方文档

1.6 source map

source map 是一种映射关系,它将编译后的代码映射回源代码,方便开发者调试。

webpack 5.x 版本中,默认开启 source map,可以通过配置 devtool 属性来修改 source map 的生成方式。

source map的类型有很多种,接下来,主要以 source-mapeval 两种生成方式进行介绍。

首先,在 webpack.config.js 中配置 devtoolsource-map

js 复制代码
module.exports = {
  ...
  devtool: 'source-map', // 开启 source map
  ...
}

然后在, 在 main.js 中添加一端错误的代码:

js 复制代码
import createHeading from './heading.js';

const heading = createHeading();

document.body.append(heading);

console.log1('error'); // 错误代码

然后进行打包,可以看到 dist 目录下生成了 main.jsmain.js.map 文件。

进入 dist目录下,使用 serve .命令启动本地服务器,访问 http://localhost:3000/ 地址,可以看到控制台输出了错误信息,以及对应的源代码的所在的行信息。

在来看一个 eval 的例子:

js 复制代码
module.exports = {
  ...
  devtool: 'eval', // 开启 eval source map
  ...
}

同样进行打包, dist 目录下并没有生成 map文件:

而是在 打包生成的文件中,使用 eval函数执行代码,只能映射到编译后的代码, 不能映射到源代码, 因此错误只能定位到文件。

除此之外,source map 还有很多种类型, 每种类型生成的效率和速度都不同,因此需要根据具体情况进行选择。

初次构建速度监视模式再次构建速度是否适合生产 以及生成质量四个维度进行对比:

devtool build rebuild production quilty
(none) ​ 快 ​ 快 低(不生成 .map 文件)
eval ​ 快 ​ 快 低(不生成 .map 文件,内联在代码中)
eval-cheap-source-map 较快 较快 较低(仅映射行,不映射列)
eval-cheap-module-source-map 较快 较快 中等(映射行和模块,不映射列)
eval-source-map 较慢 较慢 ​(映射行、列和模块)
cheap-source-map 较快 较快 较低(仅映射行,不映射列)
cheap-module-source-map 较快 较快 中等(映射行和模块,不映射列)
inline-source-map 较慢 较慢 ​(映射行、列和模块,内联在代码中)
source-map 较慢 较慢 是(可选) ​(生成单独的 .map 文件)
hidden-source-map 较慢 较慢 是(可选) ​(生成 .map 文件,但不关联代码)
nosources-source-map 较慢 较慢 是(可选) ​(生成 .map 文件,但不包含源代码内容)

以上就是各种 devtool 的类型的特点,在开发中可以灵活选择,这里比较推荐这种方式:

  • 开发模式选择eval-cheap-source-map:开发阶段,生成速度较快,错误信息可以映射到行。
  • 生产模式选择 none:生成速度最快,不生成 .map 文件,不会暴露源代码。
1.7 HMR

hmr 全称 Hot Module Replacement,即热模块替换,是 webpack 提供的一种在不刷新浏览器的情况下更新模块的功能。

开启 hmr 功能,需要在 webpack.config.js 中配置 devServerhot 属性:

js 复制代码
module.exports = {
  ...
  devServer: {
    hot: true, // 开启 hmr
  },
  ...
}

对于 css 文件是可以自动热更新的, 而对于js文件 则需要在入口文件中使用 module.hot.accept 方法,告诉 webpack 哪些模块需要热更新,执行怎样的更新:

js 复制代码
if (module.hot) {
  let lastEditor = editor;
  // 第一个参数传递模块路径,第二个参数传递回调函数
  module.hot.accept('./editor', () => {
    // editor 模块更新了,需要这里手动处理热替换逻辑
    const value = lastEditor.innerHTML;
    document.body.removeChild(lastEditor);
    const newEditor = createEditor();
    newEditor.innerHTML = value;
    document.body.appendChild(newEditor);
    lastEditor = newEditor;
  });
}

::: tip 在 Webpack 5.x 中,​ 不需要手动添加 HotModuleReplacementPlugin。当你设置 devServer.hot: true 时,Webpack 会自动启用该插件。 :::

三、webpack 进阶

1.tree shaking

tree shakingwebpack 提供的一种优化功能,它可以自动删除没有使用的代码,减少打包后的文件大小。具体是通过在 optimization 选项中的usedExportsminimize配置搭配使用来实现,这个功能在 production 模式中默认开启。

下面具体来讲解这两个配置的用法:

首先,在 src 目录下创建 components.js 文件:

js 复制代码
export const Button = () => {
  return document.createElement('button');

  console.log('dead-code'); // 无用的代码
};

export const Link = () => {
  return document.createElement('a');
};

export const Heading = (level) => {
  return document.createElement('h' + level);
};

main.js 中只引入 components.js 文件中的 Button 组件:

js 复制代码
import { Button } from './components';

document.body.appendChild(Button());

webpack.config.js 配置中将mode设置为 none,方便查看变化,同时配置 entryoutput属性:

js 复制代码
module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
  },
};

先看一下什么都不配置的打包结果,通过搜索关键字可以发现,无用的代码和未引用的代码也被打包进了最终的结果中,并导入了出去:

然后,在 webpack.config.js 中配置 optimization.usedExports 属性为 true

js 复制代码
module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
  },
  optimization: {
    // 模块只导出被使用的成员
    usedExports: true,
  },
};

再次打包,可以看到,无用的代码和未引用的代码还是被打包了进去,只是未引用的代码没有使用,被编辑器标记成了灰色:

最后,在 webpack.config.js 中配置 optimization.minimize 属性为 true

js 复制代码
module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
  },
  optimization: {
    // 模块只导出被使用的成员
    usedExports: true,
    // 压缩代码,并剔除无用的代码
    minimize: true,
  },
};

再次打包,可以看到,代码进行了压缩和变量替换,通过关键字搜索,没有发现未引用的代码,也没有发现无用的代码。表明 tree shaking 功能起作用了。

需要注意的是,如果配置了 babel-loader,并将代码转换成了其他模块例如 commonjs,那么 tree shaking 功能将失效,因为 webpack 无法识别其他模块的依赖关系。可以modules 设置为 false,或者使用默认,也就是auto来关闭 ESM 模块的转换

js 复制代码
 module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              // 如果 Babel 加载模块时已经转换了 ESM,则会导致 Tree Shaking 失效
              // ['@babel/preset-env', { modules: 'commonjs' }]
              // 可以将 modules 设为 false 或使用默认配置,也就是 auto,这样 babel-loader 会自动关闭 ESM 转换
              ['@babel/preset-env', { modules: false }]
              // ['@babel/preset-env', { modules: 'auto' }]

            ]
          }
        }
      }
    ]
  },

总结:

  • usedExports 配置可以让 webpack 只导出被使用的成员。可以理解成对未引用的代码进行标记(编辑器的置灰功能)
  • minimize 配置可以让 webpack 压缩代码,减少文件大小,并剔除无用的代码
  • 如果使用 babel-loader 进行了 ESM 模块的转化,则会导致 tree shaking 失效

2.scope hoisting

scope hoisting 是指将每个模块合并到同一个模块中。具体是通过在 optimization 选项中的concatenateModules配置来实现,下面来讲解这个配置的用法:

同样的代码,在 webpack.config.js 中将 optimization.concatenateModules 属性设置为 true

js 复制代码
module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
  },
  optimization: {
    // 合并模块
    concatenateModules: true,
  },
};

再次打包,可以看到,所有的模块被合并到了同一个作用域下,最终的结果简化了不少。

3.side effects

side effectswebpack 提供的一种优化功能,它可以自动分析模块的依赖关系,并分析出模块的副作用,从而决定是否要将模块打包到一个文件中。具体是通过在 optimization 选项中的sideEffects配置来实现,这个功能在 production 模式中默认开启。

下面具体来讲解这个配置的用法:

首先,在 src 目录下创建 extend.jsglobal.css 文件:

js 复制代码
// 为 Number 的原型添加一个扩展方法
Number.prototype.pad = function (size) {
  // 将数字转为字符串 => '8'
  let result = this + '';
  // 在数字前补指定个数的 0 => '008'
  while (result.length < size) {
    result = '0' + result;
  }
  return result;
};
css 复制代码
body {
  background-color: #e07474; // 给 body 元素添加背景颜色
}

main.js 中引入 extend.jsglobal.css 文件:

js 复制代码
// 样式文件属于副作用模块
import './global.css';

// 副作用模块
import './extend';

console.log((8).pad(3));

webpack.config.js 中打开 sideEffects 配置:

js 复制代码
const HTMLWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
  optimization: {
    sideEffects: true,
  },
  plugins: [new HTMLWebpackPlugin()],
};

然后,在 package.json 中声明 sideEffects 属性为 false

json 复制代码
{
  "name": "webpack-demo",
  "sideEffects": false // 声明 sideEffects 属性为 false
}

然后打包,运行 serce ./dist,启动本地服务器,访问 http://localhost:3000/ 地址,可以看到控制台输出了错误信息,背景色也没有生效:

这是因为在 package.json 中声明了 sideEffects 属性为 false, 表明引入的模块中没有副作用, webpack将没有副作用的模块给移除了。为了解决这个问题,需要在 package.json 中指定哪些模块有副作用,sideEffects 可以设置一个数组,在数组中添加有副作用的模块:

json 复制代码
{
  "name": "webpack-demo",
  "sideEffects": ["./src/extend.js", "*.css"] // 声明 具有副作用的模块
}

再次打包,运行,可以看到,控制台正常输出,背景色也生效了:

4.code splitting

4.1 muti entry

webpack 支持多入口配置,可以配置多个入口文件,生成不同的打包文件

js 复制代码
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'none',
  entry: {
    index: './src/index.js', // 入口文件1
    album: './src/album.js', // 入口文件2
  },
  output: {
    filename: '[name].bundle.js', // 通过 [name] 占位符来生成不同的文件名
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/index.html',
      filename: 'index.html',
      chunks: ['index'], // 指定引入的入口文件
    }),
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/album.html',
      filename: 'album.html',
      chunks: ['album'], // 指定引入的入口文件
    }),
  ],
};

打包会生成 index.bundle.jsalbum.bundle.js,并且分别生成对应的 index.htmlalbum.html 文件:

4.2 split chunks

split chunkswebpack 提供的一种优化功能,在进行多入口文件打包时,公共的模块会进行多次打包,split chunks 可以将公共的模块提取到单独的 chunk 中,具体是通过在 optimization 选项中的splitChunks配置来实现,这个功能在 production 模式中默认开启。

在多入口的基础上增加以下配置:

js 复制代码
  optimization: {
    splitChunks: {
      // 自动提取所有公共模块到单独 bundle
      chunks: 'all'
    }
  },

打包结果如下:

4.3 dynamic import

dynamic importwebpack 提供的一种异步加载模块的功能,可以实现按需加载,从而减少初始加载时间。

首先,在 src 目录下创建 dynamic.js 文件:

js 复制代码
export default function () {
  const element = document.createElement('div');
  element.innerHTML = 'Dynamically loaded module';
  document.body.appendChild(element);
}

main.js 中并通过点击按钮来异步加载 dynamic.js 文件:

js 复制代码
const btn = document.createElement('button');
btn.textContent = 'click me';
document.body.appendChild(btn);
btn.addEventListener('click', () => {
  import('./dynamic').then((module) => {
    module.default();
  });
});

打包,并启动服务器,访问 http://localhost:3000/ 地址:

点击按钮, dynamic.js 文件动态加载进来:

5. CSS 提取

可以通过 mini-css-extract-plugin 插件将CSS带阿妈提取到一个单独的文件中。

首先,安装 mini-css-extract-plugin 插件:

zsh 复制代码
pnpm i mini-css-extract-plugin -D

然后,在 webpack.config.js 中配置 mini-css-extract-plugin 插件:

js 复制代码
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader'], // 配置 laoder
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin(), // 配置 mini-css-extract-plugin 插件
  ],
};

这样即可将CSS 代码提取到单独的文件中。

6. CSS 压缩

在进行生产模式打包时,webpack 只会将 js文件进行压缩,而不会对 CSS 文件进行压缩,对于 CSS 文件,可以通过 optimize-css-assets-webpack-plugin 插件来压缩打包后的 CSS 文件。

首先,安装 optimize-css-assets-webpack-plugin 插件:

zsh 复制代码
pnpm i optimize-css-assets-webpack-plugin -D

然后,在 webpack.config.js 中配置 optimize-css-assets-webpack-plugin 插件:

js 复制代码
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = {
  mode: 'production',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader'], // 配置 laoder
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin(), // 配置 mini-css-extract-plugin 插件
    new OptimizeCSSAssetsPlugin(), // 配置 optimize-css-assets-webpack-plugin 插件
  ],
};

打包,CSS 文件可以被压缩。

当然也可以配置在 optimization 中的 minimizer 属性中,即只有开启了 minimizer 才会进行压缩:

js 复制代码
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  mode: 'production',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader'], // 配置 laoder
      },
    ],
  },
  optimization: {
    minimizer: [
      `...`, //  使用 `...` 语法,扩展现有的 minimizer,(即 `terser-webpack-plugin`)
      new OptimizeCSSAssetsPlugin(), // 配置 optimize-css-assets-webpack-plugin 插件
    ],
  },
  plugins: [
    new MiniCssExtractPlugin(), // 配置 mini-css-extract-plugin 插件
  ],
};

这样就只有在生产模式下才会进行压缩,其他模式下不会进行压缩,和 js 代码保持一致。

7. 文件名 Hash

在生产模式下,webpack 会对生成的文件名进行哈希,这样可以让文件的过期时间设置长一些,文件名发生变化时,浏览器会重新请求文件,从而避免缓存问题。

可以通过 output.filename 配置项中的 [hash] 占位符来实现:

js 复制代码
module.exports = {
  entry: './src/main.js',
  output: {
    filename: '[name].[hash].js', // 使用 [hash] 占位符来生成哈希文件名
  },
};

hash 有三种:

hash

普通 hash 是项目级别的,每一个文件的 hash 都是相同,当有任何一个文件发生改变,整个项目的 hash 都会改变。

chunkhash

chunkhashchunk级别的,hash,每一个 chunk 中的文件 hash 都是相同的,当某一个文件发生改变,对应的chunk下的每个文件的 hash 都会改变,不会影响到其他 chunk

contenthash

contenthash 是文件级别的,hash,每一个文件中的内容 hash 都不相同,当文件内容发生改变,对应的文件的 hash 都会改变。

相关推荐
Boilermaker19922 分钟前
【Java EE】SpringIoC
前端·数据库·spring
中微子13 分钟前
JavaScript 防抖与节流:从原理到实践的完整指南
前端·javascript
天天向上102428 分钟前
Vue 配置打包后可编辑的变量
前端·javascript·vue.js
芬兰y44 分钟前
VUE 带有搜索功能的穿梭框(简单demo)
前端·javascript·vue.js
好果不榨汁1 小时前
qiankun 路由选择不同模式如何书写不同的配置
前端·vue.js
小蜜蜂dry1 小时前
Fetch 笔记
前端·javascript
拾光拾趣录1 小时前
列表分页中的快速翻页竞态问题
前端·javascript
小old弟1 小时前
vue3,你看setup设计详解,也是个人才
前端
Lefan1 小时前
一文了解什么是Dart
前端·flutter·dart
Patrick_Wilson1 小时前
青苔漫染待客迟
前端·设计模式·架构