阅读 304

webpack5新特性

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

英文文档地址:webpack.js.org/

中文文档地址:webpack.docschina.org/

github地址:github.com/webpack/web…

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');
复制代码
文章分类
前端
文章标签