Flutter Web - 优雅的兼容 Flutter App 代码
我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章。
背景
算最近工作里产出的干货,记录下心得。
与上文一脉相承,上文展示了如何使用 Flutter UI 绘制 Web 页面的架构形态。
但其实还是过于理想了,真实项目里除非是为了折腾而折腾,大部分应该都是奔着降本增效的目的来使用 Flutter UI 渲染代替 Web UI 渲染。
那如何降本增效?复用 App 的 Flutter UI 其实还没办法完全达到目的,最好的方式是整个 App 的 Flutter UI + 业务 Core 都能无缝迁移到 Web 上。达成开发一套 App 送一套 H5,甚至可以套在各种小程序里,一把梭哈(bu shi)。
实现方案
当然是可以的(只要肯献祭程序猿,新世界的大门总能打开)[手动狗头]。
总体分析下 App 现有的 Flutter Code,可以发现需要改造的点有:桥接适配、路由适配、第三方插件库适配、FFI 环境隔离等。
桥接适配
原有桥接只是针对 App 开发的,通过 Flutter MethodChannel 跟 App Native Code 通信。比如 NetworkPlugin 网络桥接,就是调用 Native 的网络层统一进行网络通信,来保证业务逻辑的一致性。
那在 Flutter Web 下,继续去使用 MethodChannel 并不合适,官方针对不同平台的适配,也是提供了一种最佳实践,每个功能独立提供自身的实现,让外部使用者无感知。
比如 flutter_svg 在针对 Web 的实现上:
export '_file_io.dart' if (dart.library.html) '_file_none.dart';
就是通过判断是否是 Web 环境,来是否引用 _file_none.dart。
但并不适合我们桥接改造,原因是对于 App 项目来说,Web 项目是不存在的。我们期望的也不是侵入式实现,让底层承载更多的事,甚至要最少限度修改原有代码(危楼高百起,能不动就别动)。那在实现上,就采用对桥接层向上抽象一层 GDBridgeAPI,提供一层可实现的接口预留给 Web 项目:
抽象层独立成一个 lib,减少无关依赖。
示例代码:
- 抽象层入口
/// 桥接能力套件
///
/// * 桥定义必传,表示各端都需实现
/// * 桥定义非必传,表示差异化实现,使用前需判断是否支持
class GDBridgeKit {
final INetwork network;
...
GDBridgeKit({
required this.network,
});
}
/// 桥接 API
class GDBridgeAPI {
/// 网络
static INetwork get network {
assert(_kit != null, "必须注册使用");
return _kit!.network;
}
...
static GDBridgeKit? _kit;
/// 注册套件
static register(GDBridgeKit kit) {
_kit = kit;
}
}
- 网络桥接抽象
/// 网络相关
abstract class INetwork {
/// 网络请求
///
/// [url] 请求地址
Future request(
String url,
...
});
}
- App 注册实现者
class NativeBridgeRegister {
static init() {
GDBridgeAPI.register(
GDBridgeKit(
network: _Network(),
),
);
}
}
class _Network extends INetwork {
@override
Future request(
String url,
...
}) {
// 调用原有 Plugins 实现
}
}
在 main() 调用注册
void main() {
/// 注册 Native 桥接
NativeBridgeRegister.init();
...
}
这样,针对 Native Bridge 的架构改造就算完成了,后面就是体力活,把项目中 Bridge 的调用方式替换成 GDBridgeAPI.xxx.xxx。(由于原有代码还是有封装一层,所以改造上只要改封装的那一层即可,量并不算多。)
具体也是举例 Network 这个例子
示例代码:
class _Network extends INetwork {
@override
Future request(
String url,
...
}) async {
var request = GDRequest();
request.path = url;
...
var response = await GDPlugin.network.request(request);
if (response.ok != true) {
throw Exception(response.error);
}
return response.data;
}
}
关键通信就是 GDPlugin.network.request
, 这个是由 TS codegen 生成的代码。
顺便放一下在 Typescript 中是如何定义的。
示例代码:
/*
* 网络插件
*/
export interface PluginNetwork {
/**
* 调用 JS 网络请
* @param request Request
*/
request(request: GDRequest): Promise<GDResponse>
}
...
/**
* Gaoding Web 插件
*/
export class GDPlugin {
/**
* 网络请求
*/
static network: PluginNetwork
...
}
...
declare global {
interface Window {
GDPlugin: GDPlugin
}
}
if (!window.GDGlobal) {
window.GDGlobal = GDGlobal
}
这样在 TS codegen 工具链下就会生成相应的 Flutter 代码,直接链式调用 GDPlugin.network.request
路由适配
在桥接适配中解决了重要的业务调用问题,但还有重要的一点就是路由跳转,这个也是分为2部分需要改造。
路由挂载页面
在 App 中还是用的闲鱼的 flutter_boost
(上山容易下山难),所以并没有办法能直接用在 Web 项目中。
在 Web 项目中是用的正统官方推荐的 go_router。
但好处是 App 上页面开发时都是 Page 形式开发的,那需要做的就是 go_router 挂载所需的页面即可。麻烦的是需处理一下每个页面需要的入参,做一些处理。
示例代码:
// 搜索完成页
GoRoute(
name: RouterURL.searchResult,
path: "/contents",
builder: (context, state) {
return DeferredWidget(
search.loadLibrary,
() {
return search.SearchPage().buildPage({
'keyword': state.queryParams['q'] ?? '',
'page_source': state.queryParams['from'] ?? '',
});
},
placeholder: const DeferredLoadingPlaceholder(),
);
},
),
DeferredWidget 是延迟加载,减少首屏加载时间,这个可以从官方示例中找到写法。
路由重定向
只处理页面挂载还是不够的,App 项目里还会有统一的 URL 路由管理,比如 [custom]://search/search
来处理 App 中各个 Native Page、Flutter Page、Web Page 的跳转关系。
这一部分也不能在 App 项目变更,那我们能做的就是把 RouterPlugin 接出来,做一个统一处理。当然,也就是路由桥接适配在 Web 中的实现。
示例代码:
class _Router extends IRouter {
@override
Future<bool> pop({
...
}) async {
var context = GDNavigatorObserver.instance.navigator?.context;
if (context != null && context.canPop()) {
context.pop();
} else {
GDPlugin.location.href('/');
}
return true;
}
@override
Future push(
String url,
...
) async {
if (redirectFlutterRoute.containsKey(url)) {
// 如果是跳转到 Flutter 页面的路由
GDNavigatorObserver.instance.navigator?.context.pushNamed(
redirectFlutterRoute[url]!,
queryParams: params ?? {},
);
} else if (redirectWebRoute.containsKey(url)) {
// 如果是跳转到 Web 页面的路由
GDPlugin.location.href(redirectFlutterRoute[url]!); // 调用 window.location.href
} else if (url.startsWith("http")) {
// 如果是 Web 链接
GDPlugin.location.open(url);
} else {
debugPrint('url 需接入:$url');
}
}
}
第三方库处理
这里我们项目还好,现只有2个坑:
- flutter_boost 的生命周期兼容问题
我们的解决方式是在 Web 项目中使用一个空实现,page_lifecycle_widget_web.dart
例如:
import 'package:XXX/page_lifecycle_widget.dart'
if (dart.library.html) 'package:XXX/page_lifecycle_widget_web.dart';
- flutter_svg 在 web 上出现的坑
报错如上,原因是它自身的实现 export '_file_io.dart' if (dart.library.html) '_file_none.dart';
在 web 中是使用 _file_none.dart
这里面伪造了一个 File 类产生了冲突。
解决方式 google 了蛮久,其实很简单:
+ dynamic file = File(widget.imageUrl);
return SvgPicture.file(
- File(widget.imageUrl),
+ file,
把 file 定义成 dynamic
绕过编译检查就行了 ...
FFI 处理
对于我们项目来说,用到 FFI 的地方都是有 Web 的方式实现了,所以直接屏蔽掉即可。
成效
比如在 App 上较为复杂的搜索页面,适配到 H5 上正常展示也就不到 1 天时间

人力(shi)释放(ye)的又一个途径 🐶。
后续
这项目也还在进行中,还有哪些坑后续笔者遇到再分享 ~
如果对你开发学习有丝丝作用,请点个赞,谢谢。[开心]
转载自:https://juejin.cn/post/7143130151788740622