我报名参与金石方案1期应战——切割10万奖池,这是我的第2篇文章。
相关文章
《Flutter Web – 让 Web 与 App UI 共同的另一种可能》
《Flutter Web – 一种取巧的 CDN 方案》
《Flutter Web – ts_to_dart_facade 东西链及示例(附 git)》
布景
算最近工作里产出的干货,记录下心得。
与上文一脉相承,上文展现了怎样运用 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。(因为原有代码仍是有封装一层,所以改造上只需改封装的那一层即可,量并不算多。)
在 Web 项目里也是如此,结构 WebBridgeRegister
完结相同的接口。但实际上就不是调用 MethodChannel 的桥接,而是上文所说的 TS 通讯 API,与 TS 业务层通讯。
详细也是举例 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)的又一个途径 。
后续
这项目也还在进行中,还有哪些坑后续笔者遇到再共享 ~
假如对你开发学习有丝丝效果,请点个赞,谢谢。[快乐]