深入浅出模块联邦(1)

197 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情

共享模块的处理

  1. npm模式模块共享 npm包带来了变革式的依赖好处,同时也带来了升级麻烦,再npm包中的做共享模块都是通过开发一个npm包的功能,最后发布在npm仓库中,每个项目npm i安装

致命缺点: 子包更新后,使用子包的各个项目如何更新,各个项目需要重新编译打包上线。

  1. lib包(umd,esmodule)形式,在html中想要axios的功能,直接script中引用axios的cdn lib包后,就能使用方法了

缺点:

  • 各个lib包容易依赖冲突
  • 编译的时候lib包容易重复很多内容,不易优化
  1. 模块联邦

这个功能比较完美的解决上面几种共享模式下的问题,既可以做到打包发布模块供给后,消费者能够实时保持同步,也可以进行代码构建时候的优化,他可以在一个应用中直接导出使用另外一个应用的模块,相当于两个应用能够互相依赖,却能分别不将对方打包进去。

webpack实现模块联邦

webpack在5版本提供了这个功能,使用时插件配置ModuleFederationPlugin
实操例子:一个项目APP1有一个button1,另一个项目App2有一个button2,现在想实现App1不但有button1,也想有button2。但是又不想将两个button完全拆离成功共享组件,两个button更新互相也能更新

APP1项目入口:

入口主要是引用了一个本地的button1,和importbutton2,这里的import导入方式,不是导入的package.json的npm包,是后面的打包编译配置的形式

import LocalButton from './Button';
import React from 'react';

const RemoteButton = React.lazy(() => import('app2/Button'));

const App = () => (
  <div>
    <h1>Bi-Directional</h1>
    <h2>App 1</h2>
    <LocalButton />
    <React.Suspense fallback="Loading Button">
      <RemoteButton />
    </React.Suspense>
  </div>
);

export default App;

wenpack配置参数简介

  1. name 必须,当前应用的名字,全局唯一ID,通过 name/{expose} 的方式使用
  2. library 可选,打包方式,与 name 保持一致即可
  3. filename 可选,打包后的文件名,对应上面的 remoteEntry.js
  4. remotes 可选,表示当前应用是一个 Host,可以引用 Remote 中 expose 的模块
  5. exposes 可选,表示当前应用是一个 Remote,exposes 内的模块可以被其他的 Host 引用,引用方式为 import(name/{expose})
  6. shared 可选,依赖的包(下面包含了 shared 中包含的配置项)
  • 如果配置了这个属性。webpack在加载的时候会先判断本地应用是否存在对应的包,如果不存在,则加载远程应用的依赖包。
  • 以 app2 来说,因为它是一个远程应用,配置了["react", "react-dom"] ,而它被 app1 所消费,所以 webpack 会先查找 app1 是否存在这两个包,如果不存在就使用 app2 自带包。 app1里面同样申明了这两个参数,因为 app1 是本地应用,所以会直接用 app1 的依赖。
  • shared 配置项指示 remote 应用的输出内容和 host 应用可以共用哪些依赖。 shared 要想生效,则 host 应用和 remote 应用的 shared 配置的依赖要一致。
  • import 共享依赖的实际的 package name,如果未指定,默认为用户自定义的共享依赖名,即 react-shared。如果是这样的话,webpack 打包是会抛出异常的,因为实际上并没有 react-shared 这个包。
  • singleton 是否开启单例模式,true 则开启。如何启用单例模式,那么 remote 应用组件和 host 应用共享的依赖只加载一次,且与版本无关。 如果版本不一致,会给出警告。不开启单例模式下,如果 remote 应用和 host 应用共享依赖的版本不一致,remote 应用和 host 应用需要分别各自加载依赖。
  • requiredVersion 指定共享依赖的版本,默认值为当前应用的依赖版本。- 如果 requiredVersion 与实际应用的依赖的版本不一致,会给出警告。
  • strictVersion 是否需要严格的版本控制。单例模式下,如果 strictVersion 与实际应用的依赖的版本不一致,会抛出异常。默认值为 false。
  • shareKey 共享依赖的别名, 默认值值 shared 配置项的 key 值。
  • shareScope 当前共享依赖的作用域名称,默认为 default。
  • eager 共享依赖在打包过程中是否被分离为 async chunk。eager 为 false, 共享依赖被单独分离为 async chunk; eager 为 true, 共享依赖会打包到 main、remoteEntry,不会被分离。默认值为 false,如果设置为 true, 共享依赖其实是没有意义的。
  • shareScope 所用共享依赖的作用域名称,默认为 default。如果 shareScope 和 share["xxx"].shareScope 同时存在,share["xxx"].shareScope 的优先级更高。
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');
const deps = require('./package.json').dependencies;
module.exports = {
  entry: './src/index',
  mode: 'development',
  devServer: {
    port: 3001,
  },

 
  output: {
    publicPath: 'http://localhost:3001/',
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          presets: ['@babel/preset-react'],
        },
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'app1',
      filename: 'remoteEntry.js',
      remotes: {
        app2: 'app2@http://localhost:3002/remoteEntry.js',
      },
      exposes: {
        './Button': './src/Button',
      },
      // sharing code based on the installed version, to allow for multiple vendors with different versions
      shared: [
        {
          ...deps,
          react: {
            // eager: true,
            singleton: true,
            requiredVersion: deps.react,
          },
          'react-dom': {
            // eager: true,
            singleton: true,
            requiredVersion: deps['react-dom'],
          },
        },
      ],
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

效果:

image.png APP2项目入口

import LocalButton from './Button';
import React from 'react';

const RemoteButton = React.lazy(() => import('app1/Button'));

const App = () => (
  <div>
    <h1>Bi-Directional</h1>
    <h2>App 2</h2>
    <LocalButton />
    <React.Suspense fallback="Loading Button">
      <RemoteButton />
    </React.Suspense>
  </div>
);

export default App;

webpack关键配置

  plugins: [
    new ModuleFederationPlugin({
      name: 'app2',
      filename: 'remoteEntry.js',
      remotes: {
        app1: ['app1@http://localhost:3001/remoteEntry.js'],
      },
      exposes: {
        './Button': './src/Button',
      },
      shared: [
        {
          ...deps,
          react: {
            // eager: true,
            singleton: true,
            requiredVersion: deps.react,
          },
          'react-dom': {
            // eager: true,
            singleton: true,
            requiredVersion: deps['react-dom'],
          },
        },
      ],
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],

效果:

image.png