Webpack Module Federation实战:微前端架构
Webpack Module Federation是Webpack 5引入的一个特性,它支持微前端架构,允许不同的Web应用之间共享模块,而不需要运行时的容器或服务器端的构建步骤。
项目结构
假设有两个独立的React应用:app1和app2,其中app2将通过Module Federation作为远程模块被app1消费。
- app1:主应用
- app2:作为远程微应用
app2配置
首先,在app2中配置Webpack以使其成为可被其他应用消费的远程微应用。
webpack.config.js (app2)
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
output: {
publicPath: 'http://localhost:3002/', // 公共路径
uniqueName: 'app2', // 应用唯一标识
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
new ModuleFederationPlugin({
name: 'app2', // 微应用名称
library: { type: 'var', name: 'app2' }, // 导出方式
filename: 'remoteEntry.js', // 输出文件名
exposes: {
'./Button': './src/Button', // 暴露的模块
},
}),
],
};
app1配置
接下来,在app1中配置Webpack以消费来自app2的模块。
webpack.config.js (app1)
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
output: {
publicPath: 'http://localhost:3001/',
},
plugins: [
new ModuleFederationPlugin({
name: 'app1',
remotes: {
app2: 'app2@http://localhost:3002/remoteEntry.js', // 引入app2的远程模块
},
}),
],
};
app1中的消费代码
在app1中,你可以像导入本地模块一样直接导入来自app2的模块。
App.js (app1/src)
import React from 'react';
import { Button } from 'app2/Button';
function App() {
return (
<div>
<h1>Welcome to App1</h1>
<Button text="Click me!" />
</div>
);
}
export default App;
启动应用
分别启动app1和app2,app1会成功地从app2加载并显示Button组件。
ModuleFederationPlugin
:是Webpack的核心插件,用于配置模块联邦。它允许应用声明自己暴露哪些模块给其他应用,以及从哪些远程应用消费模块。exposes
:在app2的配置中,通过exposes字段指定了要暴露给其他应用的模块路径,这里暴露了./src/Button作为一个名为Button的模块。remotes
:在app1的配置中,通过remotes字段定义了远程应用的名称(app2)及其远程Entry文件的URL。这使得app1能够找到并加载app2的模块。- 动态导入:在实际应用中,通常使用动态导入(import()表达式)来异步加载远程模块,以避免阻塞主应用的加载。
在实际的微前端项目中,除了基本的配置外,还需要考虑以下高级特性:
共享库:
- 可以通过Module Federation共享公共库,避免每个应用都包含相同的库副本,从而减小包体积和加载时间。
- 在app1和app2的Webpack配置中,添加共享库配置,如下所示:
shared: {
react: { singleton: true, version: '17.0.2' },
'react-dom': { singleton: true, version: '17.0.2' },
},
懒加载:
通过动态导入远程组件,可以实现按需加载,只在用户需要时才加载微应用,提高页面加载速度。 示例代码:
import React, { lazy, Suspense } from 'react';
const Button = lazy(() => import('app2/Button'));
function App() {
return (
<div>
<h1>Welcome to App1</h1>
<Suspense fallback={<div>Loading...</div>}>
<Button text="Click me!" />
</Suspense>
</div>
);
}
export default App;
路由集成:
为了实现微应用之间的路由切换,可以使用single-spa
或react-router-dom
的Route组件,配合Microfrontends库来处理路由导航。
示例代码(使用react-router-dom
):
import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import App1 from './App1';
import App2 from './App2';
function MainRouter() {
return (
<Router>
<Switch>
<Route path="/app1" component={App1} />
<Route path="/app2" component={App2} />
</Switch>
</Router>
);
}
export default MainRouter;
全局状态管理:
- 要在微应用之间共享状态,可以使用Redux、MobX或Context API。需要确保在每个微应用中使用相同的状态管理解决方案,并通过Module Federation共享状态管理库。
样式隔离:
- 为了避免样式冲突,可以使用CSS Modules或Scope Hoisting,确保每个微应用的样式仅影响其自身。
错误处理:
- 添加全局错误处理,捕捉并处理微应用中可能抛出的错误。
性能优化:
- 使用Webpack的Tree Shaking和Code Splitting进一步减小包大小。
- 使用HTTP/2和Service Worker提高加载速度和离线可用性。
通信和协调:
微应用之间可能需要进行通信,例如传递数据或触发事件。可以使用事件总线、MessageChannel API或自定义钩子函数实现跨应用通信。 示例(使用事件总线):
// 在app1中发布事件
import { eventBus } from './eventBus';
eventBus.emit('customEvent', data);
// 在app2中订阅事件
import { eventBus } from './eventBus';
eventBus.on('customEvent', (data) => {
console.log('Received data:', data);
});
加载和渲染顺序:
确保微应用的加载和渲染顺序,避免依赖于未加载的微应用。可以使用single-spa的bootstrap和mount生命周期钩子进行控制。
服务端渲染(SSR):
如果需要服务端渲染,可以使用single-spa-react-server-rendering或next.js等库,确保微应用在服务器端也能正确渲染。
兼容性和更新策略:
考虑到微应用可能由不同团队和时间点开发,需要制定兼容性和更新策略。例如,使用特定的Webpack版本,或者确保微应用的API兼容性。
测试和部署:
单独测试每个微应用,同时确保整体应用的集成测试。 使用CD/CD(持续部署/持续交付)策略,自动化部署过程,确保微应用的快速迭代和更新。
监控和日志:
实施统一的日志和监控系统,以便跟踪微应用的性能、错误和用户体验。
用户体验:
优化微应用之间的切换体验,如使用骨架屏或过渡动画,减少用户感知的延迟。
安全和权限:
考虑微应用的安全性和权限控制,确保每个微应用只能访问必要的数据和资源。
转载自:https://juejin.cn/post/7379149008357900297