共计 3597 个字符,预计需要花费 9 分钟才能阅读完成。
特性
webpack 5 引入联邦模式是为了更好的共享代码。在此之前,我们共享代码一般用 npm 发包来解决。npm 发包需要经历构建,发布,引用三阶段,而联邦模块可以直接引用其他应用代码, 实现热插拔效果。对比 npm 的方式更加简洁、快速、方便。
使用方法
1、引入远程 js
2、webpack 配置
3、模块使用
引入远程 JS
假设我们有 app1,app2 两个应用,端口分别为 3001,3002。app1 应用要想引用 app2 里面的 js,直接用 script 标签即可。
例如 app1 应用里面 index.html 引入 app2 应用 remoteEntry.js
webpack 配置
app1 的 webpack 配置:
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
//....
plugins: [
new ModuleFederationPlugin({
name: "app1",
library: {type: "var", name: "app1"},
remotes: {app2: "app2",},
shared: ["react", "react-dom"],
}),
],
};
对于 app2 的 webpack 配置如下
plugins: [
new ModuleFederationPlugin({
name: "app2",
library: {type: "var", name: "app2"},
filename: "remoteEntry.js",
exposes: {"./Button": "./src/Button",},
shared: ["react", "react-dom"],
})
],
可以看到 app1 和 app2 的配置基本相同,除了 app2 多了 filename 和 exposes 以外。
参数解释
name 应用名,全局唯一,不可冲突。
library。UMD 标准导出,和 name 保持一致即可。
remotes 声明需要引用的远程应用。如上图 app1 配置了需要的远程应用 app2.
filename 远程应用时被其他应用引入的 js 文件名称。对应上面的 remoteEntry.js
exposes 远程应用暴露出的模块名。
shared 依赖的包。
1、如果配置了这个属性。webpack 在加载的时候会先判断本地应用是否存在对应的包,如果不存在,则加载远程应用的依赖包。
2、以 app2 来说,因为它是一个远程应用,配置了 [“react”, “react-dom”]
,而它被 app1 所消费,所以 webpack 会先查找 app1 是否存在这两个包,如果不存在就使用 app2 自带包。
app1 里面同样申明了这两个参数,因为 app1 是本地应用,所以会直接用 app1 的依赖。
模块使用
对于 app1/App.js 代码使用 app2 的组件,代码如下:
import React from "react";
const RemoteButton = React.lazy(() => import("app2/Button"));
const App = () => (
Basic Host-Remote
App 1
);
export default App;
具体这一行
const RemoteButton = React.lazy(() => import("app2/Button"));
使用方式为:import(‘ 远程应用名 / 暴露的模块名 ’),对应 webpack 配置里面的 name 和 expose。使用方式和引入一个普通异步组件无差别。
适用范围
由于 share 这个属性的存在,所以本地应用和远程应用的技术栈和版本必须兼容,统一用同一套。比如 js 用 react,css 用 sass 等。
联邦模块和微前端的关系:因为 expose 这个属性即可以暴露单个组件,也可以把整个应用暴露出去。同时由于 share 属性存在,技术栈必须一致。所以加上路由,可以用来实现 single-spa 这种模式的微前端。
使用场景:新建专门的组件应用服务来管理所有组件和应用,其他业务层只需要根据自己业务所需载入对应的组件和功能模块即可。模块管理统一管理,代码质量高,搭建速度快。特别适用矩阵 app,或者可视化页面搭建等场景。
应用
1、next 项目应用 next 项目 1 的 next.config.js
webpack: (config, options) => {const { buildId, dev, isServer, defaultLoaders, webpack} = options;
const mfConf = {
mergeRuntime: true, //experimental
name: "next1",
library: {type: config.output.libraryTarget, name: "next1"},
filename: "static/runtime/remoteEntry.js",
exposes: {"./exposedTitle": "./components/exposedTitle",},
remotes: {
next2: isServer
? path.resolve(
__dirname,
"../next2/.next/server/static/runtime/remoteEntry.js"
)
: "next2",
},
};
if (!isServer) {config.output.publicPath = "http://localhost:3000/_next/";}
withModuleFederation(config, options, mfConf);
return config;
}
next 项目 2 的 next.config.js
webpack: (config, options) => {const { buildId, dev, isServer, defaultLoaders, webpack} = options;
const mfConf = {
mergeRuntime: true, //experimental
name: "next2",
library: {type: config.output.libraryTarget, name: "next2"},
filename: "static/runtime/remoteEntry.js",
remotes: {
next1: isServer
? path.resolve(
__dirname,
"../next1/.next/server/static/runtime/remoteEntry.js"
)
: "next1",
},
exposes: {"./nav": "./components/nav",},
shared: ["lodash"],
};
withModuleFederation(config, options, mfConf);
if (!isServer) {config.output.publicPath = "http://localhost:3001/_next/";}
return config;
}
ps 注意,还要配置
future: {webpack5: true},
vue3 中应用
案例 1 home 项目
new ModuleFederationPlugin({
name: "home",
filename: "remoteEntry.js",
remotes: {home: "home@http://localhost:3002/remoteEntry.js",},
exposes: {
"./Content": "./src/components/Content",
"./Button": "./src/components/Button",
},
}),
案例 2 layout 项目
new ModuleFederationPlugin({
name: "layout",
filename: "remoteEntry.js",
remotes: {home: "home@http://localhost:3002/remoteEntry.js",},
exposes: {},}),
layout 中可以用 home 项目中的组件
import {createApp, defineAsyncComponent} from "vue";
import Layout from "./Layout.vue";
const Content = defineAsyncComponent(() => import("home/Content"));
const Button = defineAsyncComponent(() => import("home/Button"));
const app = createApp(Layout);
app.component("content-element", Content);
app.component("button-element", Button);