我报名参与金石方案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 项目:

Flutter Web - 高雅的兼容 Flutter App 代码

笼统层独立成一个 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 上呈现的坑

Flutter Web - 高雅的兼容 Flutter App 代码

报错如上,原因是它自身的完结 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 天时刻

Flutter Web - 高雅的兼容 Flutter App 代码

人力(shi)开释(ye)的又一个途径 。

后续

这项目也还在进行中,还有哪些坑后续笔者遇到再共享 ~

假如对你开发学习有丝丝效果,请点个赞,谢谢。[快乐]