webpack5 新特性

发布时间 2023-11-17 17:35:05作者: 柯基与佩奇

webpack5 已经发布,将主要涉及的新特性及这些特性的使用方法总结了一下。

英文文档地址:webpack

中文文档地址:webpack.docschina

github 地址:github

1、内置静态资源构建能力 —— Asset Modules

在 webpack 5 之前,通常使用:

  • raw-loader 将文件导入为字符串
  • url-loader 将文件作为 data URI 内联到 bundle 中
  • file-loader 将文件发送到输出目录

资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:

  • asset/resource 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。
  • asset/inline 导出一个资源的 data URI。之前通过使用 url-loader 实现。
  • asset/source 导出资源的源代码。之前通过使用 raw-loader 实现。
  • asset 在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源体积限制实现。

1.1 type 分别为asset/resourceasset/inlineasset/source

webpack.config.js

module: {
  rules: [
    {
      test: /\.(png|jpg|jpeg|gif)$/,
      type: "asset/resource",
    },
    {
      test: /\.svg/,
      type: "asset/inline",
    },
    {
      test: /\.txt/,
      type: "asset/source", // 原样将txt文件中的文本内容注入到打包文件中
    },
  ];
}

src/index.js

import imgUrl from "./assets/img/pic.jpeg";
import svgUrl from "./assets/img/delete.svg";
import txt from "./assets/example.txt";

//添加图片资源
let img = document.createElement("img");
img.src = imgUrl; // imgUrl: 'file:///Users/yujian2018/work/learning/project/webpack5/dist/assets/img/f972bcf4.pic.jpeg'
img.style.width = "150px";
img.style.height = "150px";
document.body.appendChild(img);

let svg = document.createElement("img");
svg.src = svgUrl; // svgUrl: data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDUyLjYgKDY3NDkxKSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT7liKDpmaQ8L3RpdGxlPgogICAgPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+CiAgICA8ZGVmcz4KICAgICAgICA8cGF0aCBkPSJNNy4zMzMzMzMzMywxLjMzMzMzMzMzIEwxMC41MzMzMzMzLDEuMzMzMzMzMzMgQzEwLjYwNjk3MTMsMS4zMzMzMzMzMyAxMC42NjY2NjY3LDEuMzkzMDI4NyAxMC42NjY2NjY3LDEuNDY2NjY2NjcgTDEwLjY2NjY2NjcsMi41MzMzMzMzMyBDMTAuNjY2NjY2NywyLjYwNjk3MTMgMTAuNjA2OTcxMywyLjY2NjY2NjY3IDEwLjUzMzMzMzMsMi42NjY2NjY2NyBMMTAsMi42NjY2NjY2NyBMMTAsMTIuOCBDMTAsMTMuMDk0NTUxOSA5Ljc2MTIxODUzLDEzLjMzMzMzMzMgOS40NjY2NjY2NywxMy4zMzMzMzMzIEwxLjIsMTMuMzMzMzMzMyBDMC45MDU0NDgxMzMsMTMuMzMzMzMzMyAwLjY2NjY2NjY2NywxMy4wOTQ1NTE5IDAuNjY2NjY2NjY3LDEyLjggTDAuNjY2NjY2NjY3LDIuNjY2NjY2NjcgTDAuMTMzMzMzMzM1LDIuNjY2NjY2NjcgQzAuMDU5Njk1MzY3NiwyLjY2NjY2NjY3IDQuNzI4OTIyNjVlLTE2LDIuNjA2OTcxMyAzLjMzMDY2OTA3ZS0xNiwyLjUzMzMzMzMzIEwwLDEuNDY2NjY2NjcgQy05LjAxODA1MDAxZS0xOCwxLjM5MzAyODcgMC4wNTk2OTUzNjY3LDEuMzMzMzMzMzMgMC4xMzMzMzMzMzMsMS4zMzMzMzMzMyBMMy4zMzMzMzMzMywxLjMzMzMzMzMzIEwzLjMzMzMzMzMzLDAuMTMzMzMzMzM1IEMzLjMzMzMzMzMzLDAuMDU5Njk1MzY3NiAzLjM5MzAyODcsMS4yNDU0OTM3OGUtMTYgMy40NjY2NjY2NywxLjExMDIyMzAyZS0xNiBMNy4yLDAgQzcuMjczNjM3OTcsMi40ODA4NzU0ZS0xNiA3LjMzMzMzMzMzLDAuMDU5Njk1MzY3NiA3LjMzMzMzMzMzLDAuMTMzMzMzMzM1IEw3LjMzMzMzMzMzLDEuMzMzMzMzMzMgWiBNMy42NjY2NjY2Nyw0LjY2NjY2NjY3IEMzLjU5MzAyODcsNC42NjY2NjY2NyAzLjUzMzMzMzMzLDQuNzI2MzYyMDMgMy41MzMzMzMzMyw0LjggTDMuNTMzMzMzMzMsOS44NjY2NjY2NyBDMy41MzMzMzMzMyw5Ljk0MDMwNDYzIDMuNTkzMDI4NywxMCAzLjY2NjY2NjY3LDEwIEw0LjMzMzMzMzMzLDEwIEM0LjQwNjk3MTMsMTAgNC40NjY2NjY2Nyw5Ljk0MDMwNDYzIDQuNDY2NjY2NjcsOS44NjY2NjY2NyBMNC40NjY2NjY2Nyw0LjggQzQuNDY2NjY2NjcsNC43MjYzNjIwMyA0LjQwNjk3MTMsNC42NjY2NjY2NyA0LjMzMzMzMzMzLDQuNjY2NjY2NjcgTDMuNjY2NjY2NjcsNC42NjY2NjY2NyBaIE02LjMzMzMzMzMzLDQuNjY2NjY2NjcgQzYuMjU5Njk1MzcsNC42NjY2NjY2NyA2LjIsNC43MjYzNjIwMyA2LjIsNC44IEw2LjIsOS44NjY2NjY2NyBDNi4yLDkuOTQwMzA0NjMgNi4yNTk2OTUzNywxMCA2LjMzMzMzMzMzLDEwIEw3LDEwIEM3LjA3MzYzNzk3LDEwIDcuMTMzMzMzMzMsOS45NDAzMDQ2MyA3LjEzMzMzMzMzLDkuODY2NjY2NjcgTDcuMTMzMzMzMzMsNC44IEM3LjEzMzMzMzMzLDQuNzI2MzYyMDMgNy4wNzM2Mzc5Nyw0LjY2NjY2NjY3IDcsNC42NjY2NjY2NyBMNi4zMzMzMzMzMyw0LjY2NjY2NjY3IFoiIGlkPSJwYXRoLTEiPjwvcGF0aD4KICAgIDwvZGVmcz4KICAgIDxnIGlkPSLliKDpmaQiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxyZWN0IGZpbGw9IiNGRkZGRkYiIG9wYWNpdHk9IjAiIHg9IjAiIHk9IjAiIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PC9yZWN0PgogICAgICAgIDxnPgogICAgICAgICAgICA8ZyBpZD0iNF9JY29uLzBfYmFzZS9iZy3mm7/mjaIiPjwvZz4KICAgICAgICAgICAgPGcgaWQ9Imljb24vZ3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIuNjY2NjY3LCAxLjMzMzMzMykiPgogICAgICAgICAgICAgICAgPG1hc2sgaWQ9Im1hc2stMiIgZmlsbD0id2hpdGUiPgogICAgICAgICAgICAgICAgICAgIDx1c2UgeGxpbms6aHJlZj0iI3BhdGgtMSI+PC91c2U+CiAgICAgICAgICAgICAgICA8L21hc2s+CiAgICAgICAgICAgICAgICA8dXNlIGlkPSJNYXNrIiBmaWxsPSIjODM4NjhGIiB4bGluazpocmVmPSIjcGF0aC0xIj48L3VzZT4KICAgICAgICAgICAgPC9nPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+
document.body.appendChild(svg);

let txtEl = document.createElement("div");
txtEl.innerHTML = txt; // txt: 这里是纯文本内容
document.body.appendChild(txtEl);

1.2 type 为asset

对于type: asset,webpack 将按照默认条件,自动地在 resource 和 inline 之间进行选择:小于 8kb 的文件,将会视为 inline 模块类型,否则会被视为 resource 模块类型。

也可以通过在 webpack 配置的 module rule 层级中,设置 Rule.parser.dataUrlCondition.maxSize 选项来修改此条件:

module:
  { rules: [{ test: /\.(png|jpg|jpeg|gif)$/, type: "asset", ? // 自定义设置
      parser
    : { dataUrlCondition: { maxSize: 8 * 1024 } } }]
  }

1.3 自定义输出文件名

默认情况下,asset/resource 模块以 [hash][ext][query] 文件名发送到输出目录。

可以通过在 webpack.config.js 将 output.assetModuleFilename 和 Rule.generator.filename 结合使用来定制化文件的输出目录:

output: {
  filename: 'main.js',
  path: path.resolve(__dirname, 'dist'),
  assetModuleFilename: 'images/[hash][ext][query]'
},

module: {
  rules: [
    {
      test: /\.(png|jpg|jpeg|gif)$/,
      type: 'asset/resource',
      parser: {
        dataUrlCondition: {
          maxSize: 8 * 1024
        }
      },
      generator: {
        // [ext]前面自带"."
        filename: 'assets/img/[hash:8].[name][ext]',  //自定义输出目录
      }
    }
  ]
}

注意:Rule.generator.filename 与 output.assetModuleFilename 相同,并且仅适用于 asset 和 asset/resource 模块类型。

2、文件缓存

在 webpack4 中,会使用 cache-loader 缓存一些性能开销较大的 loader ,或者是使用 hard-source-webpack-plugin 为模块提供一些中间缓存。在 Webpack5 之后,默认就为集成了一种自带的缓存能力(对 module 和 chunks 进行缓存)。通过如下配置,即可在二次构建时提速。

cache: {
  type: 'filesystem',
  // 默认缓存到 node_modules/.cache/webpack 中
  // 也可以自定义缓存目录,cache.cacheDirectory 选项仅当 cache.type 被设置成 filesystem 才可用。
  // cacheDirectory:path.resolve(__dirname,'node_modules/.cac/webpack'),
  buildDependencies : {
    // 2. 将配置添加为 buildDependency 以使配置更改时缓存失效
    config : [ __filename ]

    // 3. 如果有其他构建所依赖的东西可以在这里添加它们
    // 请注意,webpack、加载器和从配置中引用的所有模块都会自动添加
  }
}

3、更好地 treeshaking

未使用的导出内容不会被打包生成。 将 mode 工作模式改为 production 就会自动开启。

3.1、 嵌套 treeshaking(Nested tree-shaking)

module1.js

import * as module2 from "./module2";
export function fun1() {
  console.log("fun1");
}

export function fun2() {
  console.log("fun2");
}
export { module2 };

module2.js

export function fun3() {
  console.log("fun3");
}

export function fun4() {
  console.log("fun4");
}

export const num1 = 111;
export const num2 = 222;

index.js

import * as module1 from "./module1";
console.log(module1.module2.num1);

webpack4 和 webpack5 的打包结果对比:

4.jpg

5.jpg

3.2、 内部模块 treeshaking(Inner-module tree-shaking)

webpack 4 没有分析模块的导出和导入之间的依赖关系。webpack 5 有一个新选项 optimization.innerGraph,它在生产模式下默认启用,它对模块中的符号运行分析以找出从导出到导入的依赖关系。

import { something } from "./something";

function usingSomething() {
  return something;
}

export function test() {
  return usingSomething();
}

innerGraph 将计算出 something 仅在使用 test 导出时使用。这允许将更多导出标记为未使用并从包中省略更多代码。

当 "sideEffects": false 设置,这允许省略甚至更多的模块。 在此示例中,./something 当 test 导出未使用时将被省略。

3.3 commonjs treeshaking

webpack 5 添加了对某些 CommonJs 结构的支持,允许消除未使用的 CommonJs 导出并跟踪 require()调用中引用的导出名称。

4、模块联邦

模块联邦本身是一个普通的 Webpack 插件 ModuleFederationPlugin,插件有几个重要参数:

  • name 当前应用名称,需要全局唯一。
  • remotes 可以将其他项目的 name 映射到当前项目中。
  • exposes 表示导出的模块,只有在此申明的模块才可以作为远程依赖被使用。
  • shared 是非常重要的参数,制定了这个参数,可以让远程加载的模块对应依赖改为使用本地项目的 React 或 ReactDOM。

使用 Module Federation 时,每个应用块都是一个独立的构建,这些构建都将编译为容器。

容器可以被其他应用或者其他容器应用。

一个被引用的容器被称为 remote, 引用者被称为 host,remote 暴露模块给 host, host 则可以使用这些暴露的模块,这些模块被成为 remote 模块。

主要代码:

app_remote 项目中的 weback.config.js

new ModuleFederationPlugin({
  name: "app_remote",
  filename: "remoteEntry.js",
  exposes: {
    // 远程应用暴露出的模块名
    "./Button": "./src/components/Button.vue",
  },
  shared: ["vue", "element-ui"],
});

host 项目中的 weback.config.js

new ModuleFederationPlugin({
  name: "app_remote",
  filename: "remoteEntry.js",
  remotes: {
    // 声明需要引用的远程应用
    remote: "app_remote@http://localhost:3000/remoteEntry.js",
  },
  shared: ["vue", "element-ui"],
});

host 项目中使用 remote 项目的组件时, src/app.vue

button: () => import("remote/Button");

遇到的问题:

使用 shared 参数时,如果报错:Uncaught Error: Shared module is not available for eager consumption,则解决方案如下:

新建 bootstrap.js,将 index.js 中的内容粘贴到此文件中。如下:

import Vue from "vue";
import App from "./app.vue";
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";

Vue.use(ElementUI);

new Vue({
  render: (h) => h(App),
}).$mount("#app");

将 index.js 中的内容修改为:

import("./bootstrap");

最终效果图如下:

分别是子应用和主应用,其中普通按钮来自子应用,带了 ele 样式的 button 来自主应用。

remote.jpg

host.jpg

完整的项目代码如下:

app_remote 项目:

webpack.config.js

const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const VueLoaderPlugin = require("vue-loader/lib/plugin");

module.exports = {
  mode: "development", // production  none
  entry: "./src/index.js",
  output: {
    filename: "[name].js",
    path: path.resolve(__dirname, "dist"),
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: "vue-loader",
      },
      {
        test: /\.js$/,
        loader: "babel-loader",
        exclude: /node_modules/,
      },
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"],
      },
      {
        test: /\.(woff|ttf)$/,
        loader: "file-loader",
      },
    ],
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: "app_remote",
      template: path.resolve(__dirname, "./public/index.html"),
      filename: "index.html",
    }),
    new ModuleFederationPlugin({
      name: "app_remote",
      filename: "remoteEntry.js",
      exposes: {
        // 远程应用暴露出的模块名
        "./Button": "./src/components/Button.vue",
      },
      remotes: {
        host: "app_host@http://localhost:9000/remoteEntry.js",
      },
      shared: ["vue", "element-ui"],
    }),
    new VueLoaderPlugin(),
  ],
  devServer: {
    hot: true,
    host: "0.0.0.0",
    port: 3000,
  },
};

src/app.vue

<template>
  <div>
    Hello,{{ name }}
    <Button />
    <List />
  </div>
</template>
<script>
export default {
  components: {
    Button: () => import("./components/Button.vue"),
    List: () => import("host/list"),
  },
  data() {
    return {
      name: "子应用",
    };
  },
};
</script>

src/components/Button.vue

<template>
  <div>
    <button>hahaha</button>
  </div>
</template>

app_host 项目中:

webpack.config.js

const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const VueLoaderPlugin = require("vue-loader/lib/plugin");

module.exports = {
  mode: "development", // production
  entry: "./src/index.js",
  output: {
    filename: "[name].js",
    path: path.resolve(__dirname, "dist"),
    // publicPath: "http://localhost:9000/", //部署后的资源地址
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: "vue-loader",
        include: [path.resolve(process.cwd(), "src")],
      },
      {
        test: /\.js$/,
        loader: "babel-loader",
        exclude: /node_modules/,
      },
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"],
      },
      {
        test: /\.(woff|ttf)$/,
        loader: "file-loader",
      },
    ],
  },
  plugins: [
    new VueLoaderPlugin(),
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: "app_host",
      template: path.resolve(__dirname, "./public/index.html"),
      filename: "index.html",
    }),
    new ModuleFederationPlugin({
      name: "app_host",
      filename: "remoteEntry.js",
      exposes: {
        "./list": "./src/components/list.vue",
      },
      remotes: {
        // 声明需要引用的远程应用
        remote: "app_remote@http://localhost:3000/remoteEntry.js",
      },
      shared: ["vue", "element-ui"],
    }),
  ],
  devServer: {
    hot: true,
    host: "0.0.0.0",
    port: 9000,
  },
};

app.vue

<template>
  <div>
    Hello,{{ name }}
    <Button />
    <el-button type="primary"></el-button>
  </div>
</template>
<script>
export default {
  components: {
    // Button: (resolve) => require(["remote/Button"], resolve),
    Button: () => import("remote/Button"),
  },
  data() {
    return {
      name: "主应用",
    };
  },
};
</script>

src/components/list.vue

<template>
  <div>
    <el-button type="primary">这里使用了element-ui组件库</el-button>
  </div>
</template>
<script>

src/bootstrap.js

import Vue from "vue";
import App from "./app.vue";
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";

Vue.use(ElementUI);

new Vue({
  render: (h) => h(App),
}).$mount("#app");

src/index.js

import("./bootstrap");