什么是 module Federation
module Federation
(上面简称 MF
) 是 webpack5
推出的最新的概念
有用过 webpack
的小伙伴都晓得, 在咱们打包时, 都会对资源进行分包, 或者应用异步加载路由的计划,
这样打进去的包(也叫 chunk
), 在咱们应用时, 就是一个独自的加载
在过来, chunk 只是在一个我的项目中应用, webpack5
中, chunk
通过 门路 /fetch 等形式也能够提供给其余利用应用
这便是 MF
的倒退由来, 不得不说这是一个很有想象力的 API
MF
的粒度, 最小能够是一个组件 / 组件库, 最大能够是一个页面, 取决于你怎么应用
根底应用
首先咱们来看 ModuleFederationPlugin
的一些根底配置参数:
const {ModuleFederationPlugin} = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({// options}),
],
};
参数
file filename
name
是容器的名称, filename
是具体的文件名入口
如果没有 filename
则以 name
为文件名, 须要留神的是 name
须要是惟一值
new ModuleFederationPlugin({
name: 'app2', // 名称
filename: 'remoteEntry.js', // 入口文件
// 打包后, 就会主动打出 remoteEntry.js
// 他的内容就是 exposes 参数中的映射
})
exposes
在这个配置里你能够裸露你想要的所有内容
new ModuleFederationPlugin({
name: 'app2', // 名称
filename: 'remoteEntry.js', // 入口文件
exposes: {'./Widget': './src/Widget',},
})
exposes
的 key
不能是一个间接的名称, 如 'Widget': './src/Widget',
这样会报错
shared
应用 shared
能够最大限度地缩小依赖关系的反复,因为近程依赖于主机的依赖关系。如果主机短少一个依赖项,近程只在必要时下载其依赖项
应用 dependencies
是为了共享模块的版本和 package.json
中的版本保持一致。如果不统一则会打印正告
const deps = require('./package.json').dependencies;
new ModuleFederationPlugin({
name: 'app2',
filename: 'remoteEntry.js',
exposes: {'./Widget': './src/Widget',},
shared: {
moment: deps.moment,
react: {
requiredVersion: deps.react,
import: 'react', // 所提供的模块应该被搁置在共享范畴内。如果在共享范畴内没有找到共享模块或版本有效,这个提供的模块也作为后备模块。shareKey: 'react', // 所申请的共享模块在这个键下从共享范畴中被查找进去。shareScope: 'default', // 共享范畴的名称。singleton: true,
},
'react-dom': {requiredVersion: deps['react-dom'],
singleton: true,
},
},
})
对于 singleton
这个参数只容许在共享范畴内有一个繁多版本的共享模块(默认状况下禁用)。一些库应用全局的外部状态(例如 react, react-dom)。
因而,在同一时间只有一个库的实例在运行是很要害的。
remotes
一般来说,remote
是应用 URL
配置的,示例如下
new ModuleFederationPlugin({
name: "app1",
remotes: {app2: 'app2@http://localhost:3002/remoteEntry.js',}
})
当然 remotes
还能够有其余的扩大, 在前面会具体阐明
援用 MF
MF
插件组合了 ContainerPlugin
和 ContainerReferencePlugin
所以它既是一个入口, 也是一个进口
所以咱们再应用 MF
时, 也是须要增加对应插件:
new ModuleFederationPlugin({
name: 'mainApp',
remotes: {app2: 'app2@http://localhost:3002/remoteEntry.js',},
// 省略 shared
})
运行时截图:
之后咱们就能够间接应用组件了:
import App2Widget from 'app2/Widget';
function App() {
// 当成失常组件一样应用
return (
<div>
<h1>Dynamic System Host</h1>
<h2>main App</h2>
<App2Widget/>
</div>
);
}
remotes 的计划
环境变量
在不同的环境中应用不同的链接, 能够解决 pro
和 dev
的不同环境问题
然而在大型利用中, 环境较多, 配置 (增加 / 批改 url
) 就比拟麻烦了
new ModuleFederationPlugin({
name: "Host",
remotes: {RemoteA: `RemoteA@${env.A_URL}/remoteEntry.js`,
RemoteB: `RemoteB@${env.B_URL}/remoteEntry.js`,
},
})
Webpack External Remotes Plugin
有一个不便的 Webpack
插件,由 MF
的创建者之一 Zack Jackson 开发,称为 external-remotes-plugin。它容许咱们应用模板在运行时解析 URL
plugins: [
new ModuleFederationPlugin({
name: "Host",
remotes: {RemoteA: "RemoteA@[window.appAUrl]/remoteEntry.js",
RemoteB: "RemoteB@[window.appBUrl]/remoteEntry.js",
},
}),
new ExternalTemplateRemotesPlugin(),]
在从近程应用程序加载任何代码之前, 咱们加能够定义 window
的属性来灵便地定义咱们的 URL
这种办法是齐全动静的,能够解决咱们的用例,但这种办法仍有一点限度:
咱们不能齐全管制加载的生命周期。
Promise
基于 promise
的获取计划, 此计划在官网也有所提及
然而你也能够向 remote 传递一个 promise,其会在运行时被调用。你应该用任何合乎下面形容的 get/init 接口的模块来调用这个 promise。例如,如果你想传递你应该应用哪个版本的联邦模块,你能够通过一个查问参数做以下事件:
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
app1: `promise new Promise(resolve => {const urlParams = new URLSearchParams(window.location.search)
const version = urlParams.get('app1VersionParam')
const remoteUrlWithVersion = 'http://localhost:3001/' + version + '/remoteEntry.js'
const script = document.createElement('script')
script.src = remoteUrlWithVersion
script.onload = () => {
// the injected script has loaded and is available on window
// we can now resolve this Promise
const proxy = {get: (request) => window.app1.get(request),
init: (arg) => {
try {return window.app1.init(arg)
} catch(e) {console.log('remote container already initialized')
}
}
}
resolve(proxy)
}
document.head.appendChild(script);
})
`,
},
// ...
}),
],
};
请留神当应用该 API
时,你 必须 resolve
一个蕴含 get/init
API
的对象。
在 promise
中咱们创立一个 script
标签, 同时增加动静 URL
, 不过此计划是比拟死板的, 因为 url
仍旧是写在配置中
Dynamic Remote Containers
在 webpack
官网中有一种计划, 即动静近程容器
这种计划就是像加载 react
子利用一样加载 MF
利用
咱们的插件能够不必设置 remotes
:
plugins: [
new ModuleFederationPlugin({
name: "Host",
remotes: {},}),
]
外围加载程序:
function loadComponent(scope, module) {return async () => {
// 初始化共享作用域(shared scope)用提供的已知此构建和所有近程的模块填充它
await __webpack_init_sharing__('default');
const container = window[scope]; // or get the container somewhere else
// 初始化容器 它可能提供共享模块
await container.init(__webpack_share_scopes__.default);
const factory = await window[scope].get(module);
const Module = factory();
return Module;
};
}
webpack_init_sharing 是一些 webpack 编译的变量, 最初运行时都是会转换成 webpack_require
webpack_require 是 webpack 运行时援用文件内容的办法
container
指的是咱们在 webpack
配置的 remotes
中配置的一个利用。
module
指的是另一个 exposes
字段中的定义。
最初封装获取到 hooks
:
// 这里的 url, scope, module 都是能够通过接口什么的异步获取, 做到齐全动静
// 原理还是通过 script 标签加载 js 代码
const {Component: FederatedComponent, errorLoading} = useFederatedComponent('http://localhost:3002/remoteEntry.js', 'app2', './Widget');
<Suspense fallback={'loading...'}>
{errorLoading
? `Error loading module "${module}"`
: FederatedComponent && <FederatedComponent />}
</Suspense>
其中 remoteEntry
就是咱们的入口了, 762 和 700, 这两个是 moment
的主题和多语言文件
而 630 则是 Widget
文件的内容
子组件独自加载 moment
, 而没有 react
, react-dom
, 就是因为咱们的 shared
配置
整个 Dynamic Remote Containers
的加载流:
具体例子可点此查看
基于 MF 的框架
这里也有一个框架是基于 MF
的: EMP
该框架通过灵便的共享配置, 可自定义抉择 MF
/ CDN
/ ES import
/ Dll
多种形式来共享库
扩大的可能性
MF
目前的 定位在于公共组件库 / 业务库的复用、对立, 然而他能作为利用级别的载体吗
在 qiankun
中, 子利用的获取, 是通过 fetch
来获取实例, 并在沙盒中解析的
然而 MF
的组件就不能实现这种场景, 因为咱们一开始援用的是入口文件, 后续具体文件不能通过接口获取
只能通过 script
来拿, 然而这样就没有了 js
环境的隔离
所以在利用级别上, 目前的论断是: 能够当做一个乞丐版微前端的框架
然而也须要留神各个利用间接的反复关系, JS/CSS
的隔离问题, 同时他也短少对应生命周期来治理
在这方面, qiankun
是一个成熟的案例实现, MF
值得尝试的场景还是在业务公共组件这一块
总结
MF
曾经引来了许多的尝鲜者, 他是一种可扩大的解决方案,在独立的应用程序之间共享代码,对开发者来说十分不便。
然而也存在以下的问题点:
- 须要应用
webpack5
, 当初很多老我的项目是在用webpack4
, 而又有一些新我的项目应用了vite
shared
依赖不能tree sharking
- 代码执行不能应用沙箱隔离, 不太举荐做到利用级别
援用
- https://www.syncfusion.com/blogs/post/what-is-webpack-module-…
- https://juejin.cn/post/7005450458009600036
- https://oskari.io/blog/dynamic-remotes-module-federation/