当前位置:   article > 正文

来自字节大佬首发:基于Flutter的Hybrid Webview容器实践

hybrid容器

关注 前端瓶子君,回复“交流”

加入我们一起学习,天天进步

授权自作者:王旭@字节跳动

原文链接:https://github.com/wayshon/hybrid_webview_flutter  (欢迎Star)

背景

Flutter 是一个 UI 框架,实际开发中除了常见的 widget 还需要如地图、webview等 Native 组件。

一种方法是 Flutter 通知 Native 唤起 Native 界面,如之前的扫码插件。缺点是 Native 组件很难和 Flutter 组件进行组合。

第二种是通过 Flutter 提供的 PlatformView(AndroidView/UIKitView) 将 Native 组件嵌入到 Flutter的组件树。使 Flutter 能够像控制普通 widget 那样控制 Native 组件。

目标: Flutter 中嵌入 webview widget,这个 webview 需要受 flutter 控制,且能够与 flutter 通信。

思路

具体实现

target 1: 实现 webview 插件

1、创建插件:

flutter create -i objc --template=plugin hybrid_webview_flutter

自动生成 HybridWebviewFlutterPlugin 类,打开 Runner.xcworkspace

2、在 info.flist 添加 io.flutter.embedded_views_preview: YES。PlatformView 功能默认关闭,不配置这行就没法使用

3、创建 webview 类,实现 FlutterPlatformView 协议,在构造函数里获取 flutter 传递过来的参数,创建 webview,创建 FlutterMethodChannel 并设置 block 回调。

  1. // 注册flutter 与 ios 通信通道
  2. NSString* channelName = [NSString stringWithFormat:@"com.calcbit.hybridWebview_%lld", viewId];
  3. _channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:messenger];
  4. __weak __typeof__(self) weakSelf = self;
  5. [_channel setMethodCallHandler:^(FlutterMethodCall *  call, FlutterResult  result) {
  6.  [weakSelf onMethodCall:call result:result];
  7. }];

4、创建工厂类 WebviewFactory,实现 FlutterPlatformViewFactory 协议,实现协议中的 createWithFrame 方法并返回步骤3创建的 webview

  1. //用来创建 ios 原生view
  2. - (nonnull NSObject<FlutterPlatformView> *)createWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args {
  3.     //args 为flutter 传过来的参数
  4.     Webview *webView = [[Webview alloc] initWithWithFrame:frame viewIdentifier:viewId arguments:args binaryMessenger:_messenger];
  5.     return webView;
  6. }

5、步骤1生成的 HybridWebviewFlutterPlugin 注册插件的方法 registerWithRegistrar 中添加一行注册 WebviewFactory

[registrar registerViewFactory:[[WebviewFactory alloc] initWithMessenger:registrar.messenger] withId:@"com.calcbit.hybridWebview"];

6、根目录 lib/ 下新建 hybrid_webview.dart 文件,创建 HybridWebview Widget,build 返回 UiKitView。UiKitView 接收的 viewType 与步骤5注册 Factory 时的 withId 一致。

creationParams 可传递参数给步骤3。

creationParamsCodec 标准平台通道使用标准消息编解码器,以支持简单的类似JSON值的高效二进制序列化 参考StandardMessageCodec

onPlatformViewCreated 在 UiKitView 创建完成后执行,可获取到 Native 组件的 viewId,注册 MethodChannel,这时候 channel 可与步骤3创建的 webview 进行通信

  1. Widget buildWebView() {
  2.     return UiKitView(
  3.       viewType: "com.calcbit.hybridWebview",
  4.       creationParams: {
  5.         "url": widget.url,
  6.       },
  7.       //参数的编码方式
  8.       creationParamsCodec: const StandardMessageCodec(),
  9.       //webview 创建后的回调
  10.       onPlatformViewCreated: (id) {
  11.         //创建通道
  12.         _channel = new MethodChannel('com.calcbit.hybridWebview_$id');
  13.         //设置监听
  14.         nativeMessageListener();
  15.       },
  16.       gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>[
  17.         new Factory<OneSequenceGestureRecognizer>(
  18.           () => new EagerGestureRecognizer(),
  19.         ),
  20.       ].toSet(),
  21.     );
  22. }

target 2: flutter 与 native 通信

native 调用 flutter

target 1 步骤3 创建的 FlutterMethodChannel channel 可以调用 invokeMethod 方法传递消息名,参数给 flutter,并设置 flutter 回调

回调参数 result 可能是 flutter Feature 返回值,也有可能是 flutter 运行时报错

flutter 调用 native

有了 target 1 步骤 6 onPlatformViewCreated 里创建的 channel,使用 channel.invokeMethod 调用 native 方法,第一个参数为消息名,第二个为可选参数。返回一个 Future(类似 js 的 Promise)

在 target 1 步骤 3 OC 创建 FlutterMethodChannel 时的 block 可接收到 flutter 的调用信息。第一个参数 FlutterMethodCall 包含了 flutter 调用的消息名与参数,第二个参数 FlutterResult 是一个回调函数,传递给 flutter 返回值。

为了扩展性,这里将 invokeMethod 的第一个参数固定为 __flutterCallJs,第二个参数固定为数组,数组第一个参数固定为 js 的目标方法。这样只是用 __flutterCallJs 就不用每增加一个方法就去修改 native 的代码。Native 调 flutter 的消息名不固定是因为我们能够经常修改 flutter,但是不会经常修改 native

  1. -(void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result{
  2.     if ([[call method] isEqualToString:@"__flutterCallJs"]) {
  3.         NSString *action = [call.arguments firstObject];
  4.         NSArray *params;
  5.         if ([call.arguments count] > 1) {
  6.             params = [call.arguments subarrayWithRange:NSMakeRange(1, [call.arguments count] -1)];
  7.         } else {
  8.             params = @[];
  9.         }
  10.         //  在主线程更新 webview,不然会崩
  11.         dispatch_async(dispatch_get_main_queue(), ^{
  12.             [self->_context[@"__flutterCallJs"] callWithArguments:@[action, params, ^(JSValue *value) {
  13.                 NSArray *arr = [value toArray];
  14.                 result(arr);
  15.             }]];
  16.         });
  17.     } else if ([[call method] isEqualToString:@"evaluateJavaScript"]) {
  18.         // 注入 js
  19.         NSString* jsString = [call arguments];
  20.         dispatch_async(dispatch_get_main_queue(), ^{
  21.             [self->_webView stringByEvaluatingJavaScriptFromString:jsString];
  22.         });
  23.     }
  24. }

下图列举了 native 与 flutter 值的转换

通过实践限制 flutter 调 oc 限制的参数为 bool, int, double, string,List, Map, Null Set不能传会报错,map 的 key 必须为 string,不然 flutter 传给 OC 没问题,OC传给 js 的时候会剔除掉。如 {'a':1,2:2} 传到 js 就变成了 {'a':1}

target 3: webview 与 native 通信

native 调用 js

在 target 2 中 OC 已经能够接收到 flutter 传递过来的消息,这时候 OC 需要将消息传给 js。可以通过 KVC 获取到 UIWebView 的 JSContext(WKWebView取不到context但是可以通过消息形式)

在 webview 定义全局函数 __flutterCallJs 用来接收 OC 传递过来的值。

JSContext 执行 __flutterCallJs 透传 flutter 传过来的参数,并多传一个 block 参数,block 在 js 里会变成函数,js 侧调用这个函数类似 callback

OC 的 block 接收到 js 执行的回调,调用 FlutterResult,将回调结果返回给 flutter

除了获取 js context 执行 js,webview 常见的还有注入 js,可以接收 flutter 传来的 js string 注入到 webview

js 调用 native

1、JSContext 直接注入 bolck,js 调用这个函数

  1. _context[@"globalFuction"] = ^(JSValue *value) {
  2.  NSLog("%@", value);
  3. };

2、通过 JSExport 协议,只有 JSExport 里声明的方法才会被 js 访问到

定义一个 JSExport 协议,并在 Class A 实现,将 A 实例化并作为全局变量注入到 JSContext,这里为了方便直接在 webview 定义实现 JSExport,将 当期实例 self 注入到 JSContext

  1. //定义一个JSExport protocol
  2. @protocol JSExportProtocol <JSExport>
  3.  JSExportAs(jsCallFlutter, - (void)jsCallFlutter:(JSValue *)action params:(JSValue *)params callback:(JSValue *)callback);
  4. @end
  5. //将self添加到context中
  6. _context[@"__OCObj"] = self;
  7. };

这时候 js 全局链就会有 __OCObj 对象,调用 __OCObj.jsCallFlutter 传递参数给 OC,约定 最后一个参数为 callback,js Function 到 OC 里面会转换成 block

OC 通过 FlutterMethodChannel 调用 flutter 获得返回值后通过这个 block 触发 js 的 callback

  1. #pragma mark - jsExport
  2. - (void)jsCallFlutter:(JSValue *)action params:(JSValue *)params callback:(JSValue *)callback {
  3.     NSString *actionName = [NSString stringWithFormat:@"%@", action];
  4.     NSArray *arr = [params toArray];
  5.     [self->_channel invokeMethod:actionName arguments:arr result:^(id  _Nullable result) {
  6.         if ([result isKindOfClass:[NSClassFromString(@"FlutterError") class]]) {
  7.             [callback callWithArguments:@[[result valueForKey:@"_message"], [NSNull null]]];
  8.         } else {
  9.             id results;
  10.             if (result) {
  11.                 results = result;
  12.             } else {
  13.                 results = [NSNull null];
  14.             }
  15.             //  在主线程更新 webview
  16.             dispatch_async(dispatch_get_main_queue(), ^{
  17.                 [callback callWithArguments:@[[NSNull null], results]];
  18.             });
  19.         }
  20.     }];
  21. }

经实践,限制 js 传给 OC 的值为 boolean, number, string, array, obj, null/undefined

null/undefined 都会转成 null,fn/set/map都会在OC变成空字典 {},{1: 'a'} 到了 OC key 也会转成 string

target 4: cookie 共享

webView/OC,RN/OC cookie 都是共享的。但是 flutter 比较奇怪,用过的 dart:io 与 dio 都不自动带上cookie,查看了 dio_cookie_manager 与 cookie_jar 的实现,发现 dio 是利用这两个库自己在 dart 维护了 cookie 信息,然后添加到 dio.interceptors 里,随 request 带上,监听 response 存储。

  1. // dio & dio_cookie_manager 代码
  2. Future onRequest(RequestOptions options) async {
  3.     var cookies = cookieJar.loadForRequest(options.uri);
  4.     cookies.removeWhere((cookie) {
  5.       if (cookie.expires != null) {
  6.         return cookie.expires.isBefore(DateTime.now());
  7.       }
  8.       return false;
  9.     });
  10.     String cookie = getCookies(cookies);
  11.     if (cookie.isNotEmpty) options.headers[HttpHeaders.cookieHeader] = cookie;
  12.   }
  13.   @override
  14.   Future onResponse(Response response) async => _saveCookies(response);
  15.   _saveCookies(Response response) {
  16.     if (response != null && response.headers != null) {
  17.       List<String> cookies = response.headers[HttpHeaders.setCookieHeader];
  18.       if (cookies != null) {
  19.         cookieJar.saveFromResponse(
  20.           response.request.uri,
  21.           cookies.map((str) => Cookie.fromSetCookieValue(str)).toList(),
  22.         );
  23.       }
  24.     }
  25.   }

由于这种实现相当于把 response 的 cookie 维护在 dart 层面,所以 OC 的请求就不会有这些信息,webView 环境也不会有。

然后?

与其将 cookie 信息维护在 dart,为什么不直接维护在 OC,那样OC/webView的请求还能带上。

方案
  • OC 与 webView 的 cookie 是互通的,不用手动处理

  • dart req/res 调用 MethodChannel 读/存 cookie

  • OC 存 cookie 至 NSHTTPCookieStorage,并同步至 webView 的 document.cookie

实现

1、 dart 存取 cookie 存到 OC

  1.   // 存
  2.   static Future<bool> setCookie({ String domain, String name, String value, int exp }) async {
  3.     bool result = await _channel.invokeMethod('setCookie', [domain, name, value, exp]);
  4.     return result;
  5.   }
  6.   // 取
  7.   static Future<List<Map>> getCookie(String url) async {
  8.     final List res = await _channel.invokeMethod('getCookie', url);
  9.     List<Map> listMap = new List<Map>.from(res);
  10.     return listMap;
  11.   }

2、OC 存取 dart 传过来的值,并在 OC 发送请求时带上这些 cookie

  1. // 读 cookie
  2. NSArray *cookieArray = [NSArray arrayWithArray:[[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]];
  3. // 存 cookie
  4. NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:cookieProperties];
  5. [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
  6. // 请求带上 cookie
  7. NSDictionary *cookieHeaderDic = [NSHTTPCookie requestHeaderFieldsWithCookies:[[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]];
  8. [request setValue:[cookieHeaderDic objectForKey:@"Cookie"] forHTTPHeaderField:@"Cookie"];

3、OC 接收到 dart 传过来的 cookie 时顺带将 cookie 写入 webView

  1.  NSString *jsStr = [NSString stringWithFormat:@"document.cookie='%@=%@;expires=%ld'",name,value,exp];
  2. [_webView stringByEvaluatingJavaScriptFromString:jsStr];

实践应用

WebView 控制 flutter 导航栏右侧 BarButtonItem

  • js 传递配置数组给 flutter,将 callback 存储在 js

  • flutter 根据配置渲染 AppBar actions,设置点击回调将按钮类型回传 js

  • js 根据 flutter 传过来的值调用之前缓存的 callback,调用结果返回给 flutter

写在最后

Flutter 里跑 webview 显然不是明智的做法,flutter 官方默认都关闭 PlatformView 功能。相对于 hybrid 和 RN 只有 JSC 通信,这里的 webview 又多了一层 flutter 通信。

但是特殊场景下也不是不可以这么玩。类似在 RN / 小程序 里跑webview,在小程序里套 webview 减小包体积,避开审核快速迭代的做法不在少数。

也有在微信小程序里利用 miniprograme.navigateTo 触发app.pageNotFound 做 IOC 的,虽然慢了点绕了点,但是提高了开发效率与迭代速度。

Anyway, Keep Balance.

题外话 - 作为页面仔我们做跨端的优劣势

怕被砖,先声明以下为纯扯淡内容

优势
  • 快,一次开发到处跑,对比安卓一堆机型一堆特殊 api,固定 webview 内核简直美滋滋

  • 快,hotreload爽的不行

  • 快,增量热更新绕过审核

  • 快,开发体验,MVVM,声明式开发加上组件库简直拼积木,iOS 命令式开发连尾灯都看不到

  • 快,CSS 牛逼,Android 还得整 xml,iOS 更是惨到手写代码布局

  • 快,开发环境简单,node / web 一把梭,native 一堆奇奇怪怪的配置

劣势
  • 慢,能力限制在 webview 的环境里,webview 限制了上限,扩展功能需要 native 排期施舍

  • 慢,单线程,渲染还会互斥,仰仗 native 帮忙分拆逻辑线程和UI线程进行优化

  • 慢,JIT 干不过 AOT

  • 慢,渲染干不过 native,多了层中间商 webview 赚差价,无限列表 webview 连 native 的尾灯都看不到

    • native 渲染: view -> layout -> renderNode -> 合成 -> GPU渲染

    • webview: html -> dom tree -> render tree -> render layer -> 合成 -> gpu渲染

PS: 听说 flutter 很强,但它并不是前端专属玩具,因为 native 上手更有优势(尤其 Android)

综上, 才是我们的优势啊,钻牛角尖跟 native 比性能,何必呢。

最后

欢迎关注【前端瓶子君】✿✿ヽ(°▽°)ノ✿

欢迎关注「前端瓶子君」,回复「算法」,加入前端算法源码编程群,每日一刷(工作日),每题瓶子君都会很认真的解答哟!

回复「交流」,吹吹水、聊聊技术、吐吐槽!

回复「阅读」,每日刷刷高质量好文!

如果这篇文章对你有帮助,「在看」是最大的支持

》》面试官也在看的算法资料《《

“在看和转发”就是最大的支持

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/425634
推荐阅读
相关标签
  

闽ICP备14008679号