cubegao

Flutter 微前端架构探索与实践

2024-11-22

一、为什么需要在 Flutter 中讨论「微前端」

微前端(Micro Frontends)本质上是将 Web 前端领域的微服务思想引入客户端架构:将单一应用拆分为多个独立的、可独立开发、独立部署、独立运行的子应用,由主框架组合成一个完整应用

你可能会问:Flutter 是客户端框架,讨论「微前端」是不是生搬硬套?

实际上,当项目规模达到企业 IM 这个级别——300+ 页面、50+ 开发者、多个业务团队并行开发——微前端的核心诉求在 Flutter 项目中同样存在:

  1. 独立开发与部署:工作台团队不想等聊天团队合完代码再上线。
  2. 技术栈解耦:部分轻应用是 H5 实现,不能强迫所有业务都用 Flutter。
  3. 增量迁移:从原生 iOS/Android 逐步迁移到 Flutter 时,需要新老页面共存。
  4. 故障隔离:一个子应用的崩溃不应该导致整个 App 白屏。

本文记录了我们在企业 IM 项目中落地微前端架构的完整过程,包括架构设计、容器方案选型、子应用通信机制和鸿蒙平台的适配经验。

二、整体架构:壳工程 + 子应用容器

2.1 架构图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌─────────────────────────────────────────────┐
│ 壳工程 (Shell) │
│ ┌─────────────────────────────────────┐ │
│ │ 子应用管理器 (App Manager) │ │
│ │ 注册 / 加载 / 生命周期 / 路由分发 │ │
│ ├───────────┬───────────┬─────────────┤ │
│ │ 原生子应用 │ Flutter子应用 │ Web 子应用 │ │
│ │ 容器 │ 容器 │ 容器 │ │
│ └───────────┴───────────┴─────────────┘ │
│ ┌─────────────────────────────────────┐ │
│ │ 共享服务层 │ │
│ │ 用户信息 / 网络 / 存储 / 日志 / 埋点 │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘

2.2 壳工程的职责

壳工程(Shell)是应用的入口,本身不做业务。它的核心职责:

  1. 子应用注册与发现:维护子应用清单,包括标识、入口、版本、加载策略。
  2. 路由分发:根据 URL 匹配规则将请求路由到对应的子应用。
  3. 生命周期管理:控制子应用的加载、暂停、恢复、销毁。
  4. 共享服务注入:向子应用提供统一的用户认证、网络请求、数据存储、埋点上报能力。

壳工程的核心代码量控制在 5000 行以内,保证自身稳定。

三、三类子应用容器的方案

3.1 Flutter 子应用容器

这是我们最常用的容器类型,承载聊天、通讯录、会议等核心业务模块。

实现基于组件的动态加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class FlutterSubAppContainer extends StatefulWidget {
final String appId;

@override
Widget build(BuildContext context) {
return FutureBuilder<FlutterSubApp>(
future: _loadSubApp(appId),
builder: (context, snapshot) {
if (snapshot.hasData) {
return snapshot.data!.build(context);
}
return const LoadingView();
},
);
}
}

Flutter 子应用的组件代码可以随 App 打包,也可以通过 CodePush 动态下发配置来启用/禁用。但受 Flutter AOT 编译的限制(插件化文章中详细分析过),Flutter 子应用无法在 Release 模式下动态加载新的 Dart 代码。

3.2 原生子应用容器

对于已经成熟的原生页面(如 iOS 端的设置页面、Android 端的系统通知管理),不需要强行用 Flutter 重写。原生子应用容器通过 Platform Channel 实现原生页面在 Flutter 容器中的嵌入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class NativeSubAppContainer extends StatelessWidget {
final String appId;

@override
Widget build(BuildContext context) {
return PlatformViewLink(
viewType: 'native_sub_app_$appId',
surfaceFactory: (context, controller) {
return AndroidView(
viewType: 'native_sub_app_$appId',
creationParams: {'appId': appId},
creationParamsCodec: StandardMessageCodec(),
);
},
);
}
}

在企业 IM 的迁移过程中,这个容器发挥了重要作用:当我们开始用 Flutter 重写通讯录模块时,旧的原生通讯录页面通过这个容器嵌入 Flutter 界面,用户看不到任何变化。新模块开发完成后,切换容器类型即可无缝上线。

3.3 Web 子应用容器

轻应用中的 H5 页面通过 WebView 容器承载(插件化文章中已详细介绍)。关键不同在于微前端架构下的 Web 子应用需要更完整的 API 支持:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class WebSubAppContainer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return InAppWebView(
initialUrlRequest: URLRequest(url: Uri.parse(subAppConfig.entryUrl)),
initialUserScripts: [
_buildJSBridgeScript(), // 注入 JSBridge
_buildAuthTokenScript(), // 注入登录态
_buildThemeScript(), // 注入主题配置
],
onWebViewCreated: (controller) {
_registerApiHandlers(controller);
},
);
}
}

四、子应用间的通信机制

微前端架构中最容易出问题的就是跨子应用通信。我们的方案是三层通信模型:

4.1 层一:全局事件总线

适用于广播式通信。新的未读消息、用户登录/退出、网络状态变化等全局事件通过总线发布:

1
2
3
4
5
6
7
// 聊天子应用发布新消息事件
MicroAppEventBus.emit(NewMessageEvent(conversationId: '123', message: msg));

// 未读计数组件在壳工程中监听
MicroAppEventBus.on<NewMessageEvent>().listen((event) {
updateUnreadBadge(event.conversationId);
});

4.2 层二:共享状态服务

适用于需要持久化的跨子应用数据。当前用户信息、企业组织架构、全局配置:

1
2
3
4
5
6
7
8
class SharedStateService {
final _userState = BehaviorSubject<UserInfo>.seeded(UserInfo.empty());

ValueStream<UserInfo> get userStream => _userState.stream;
UserInfo get currentUser => _userState.value;

void updateUser(UserInfo user) => _userState.add(user);
}

BehaviorSubject(来自 rxdart)保证新的订阅者能立即获取最新值,避免了「先改变状态再监听,错过了初始值」的时序问题。

4.3 层三:直接路由传参

适用于页面间的一次性数据传递。选人组件选中联系人后返回给发起方:

1
2
3
4
5
6
7
// 由壳工程的路由管理器统一处理
class MicroAppRouter {
Future<T?> push<T>(String subAppId, String route, {Map<String, dynamic>? args}) {
final subApp = _manager.get(subAppId);
return subApp.navigator.push<T>(route, arguments: args);
}
}

五、鸿蒙平台的适配

鸿蒙平台对微前端架构的影响集中在 Web 子应用容器上:

  1. WebView 差异:鸿蒙的 WebView 组件在 JSBridge 的字符串参数大小限制上与 Android 不同。一次传递超过 4KB 的数据会导致 Bridge 调用失败。我们在 JS 侧增加了自动分包逻辑,将大数据拆分为多个小于 4KB 的包,在 Native 侧重组合并。

  2. 沙箱隔离:鸿蒙的应用沙箱机制更严格,子应用的文件存储路径不能相互访问。这反而降低了微前端间数据泄露的风险,但需要在共享存储服务层做额外的跨应用文件访问授权。

六、实践效果与反思

6.1 量化效果

指标 微前端改造前 微前端改造后
团队独立发布周期 2-4 周(统一发版) 1 周(Web 子应用按天)
故障隔离 单模块崩溃整 App 白屏 子应用崩溃不影响其他模块
技术栈自由度 仅 Flutter Flutter / Native / H5 混用
增量迁移效率 N/A 通讯录模块灰度迁移,用户无感知

6.2 主要挑战

挑战一:调试复杂性。微前端架构下,一个 bug 可能涉及壳工程、子应用 A、子应用 B 和共享服务四层。调试时需要同时打开多个工程的断点。我们最终搭建了一个「集成调试模式」——壳工程可以加载本地开发版本的子应用,替代远程版本,方便联调。

挑战二:性能开销。微前端的容器层引入了一级额外的路由分发和上下文注入,首屏加载时间增加了约 50-80ms。对于聊天页面这种高频使用场景,我们选择了「不下放」——聊天作为核心模块直接在壳工程中加载,不走子应用容器。

挑战三:经验成本。微前端对团队的要求高于单体架构。需要有人维护壳工程、有人管理路由注册、有人制定子应用开发规范。小团队不建议强行上微前端。

七、总结

微前端不是 Flutter 的天然能力,但大型项目天然需要它。在企业 IM 项目中,微前端解决了三个核心矛盾:

  1. 发布节奏的矛盾:Web 子应用按天更新 vs 客户端按周发布。
  2. 技术栈的矛盾:H5 轻应用、原生模块、Flutter 模块的共存问题。
  3. 风险隔离的矛盾:一个子应用的异常不影响整体稳定性。

但如果团队规模在 10 人以内、项目页面数在 50 以内,不建议引入微前端。它的收益与项目规模成正比——规模越大,收益越明显;规模不足,额外管理的复杂度反而成为负担。

扫描二维码,分享此文章