Flutter 是一个 UI 框架,实际开发中除了常见的 widget 还须要如地图、webview等 Native 组件。html
一种方法是 Flutter 通知 Native 唤起 Native 界面,如以前的扫码插件。缺点是 Native 组件很难和 Flutter 组件进行组合。前端
第二种是经过 Flutter 提供的 PlatformView(AndroidView/UIKitView) 将 Native 组件嵌入到 Flutter的组件树。使 Flutter 可以像控制普通 widget 那样控制 Native 组件。node
目标: Flutter 中嵌入 webview widget,这个 webview 须要受 flutter 控制,且可以与 flutter 通讯。ios
一、建立插件:web
flutter create -i objc --template=plugin hybrid_webview_flutter
自动生成 HybridWebviewFlutterPlugin 类,打开 Runner.xcworkspace小程序
二、在 info.flist 添加 io.flutter.embedded_views_preview: YES。PlatformView 功能默认关闭,不配置这行就无法使用微信小程序
三、建立 webview 类,实现 FlutterPlatformView 协议,在构造函数里获取 flutter 传递过来的参数,建立 webview,建立 FlutterMethodChannel 并设置 block 回调。api
// 注册flutter 与 ios 通讯通道
NSString* channelName = [NSString stringWithFormat:@"com.calcbit.hybridWebview_%lld", viewId];
_channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:messenger];
__weak __typeof__(self) weakSelf = self;
[_channel setMethodCallHandler:^(FlutterMethodCall * call, FlutterResult result) {
[weakSelf onMethodCall:call result:result];
}];
四、建立工厂类 WebviewFactory,实现 FlutterPlatformViewFactory 协议,实现协议中的 createWithFrame 方法并返回步骤3建立的 webview数组
//用来建立 ios 原生view
- (nonnull NSObject<FlutterPlatformView> *)createWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args {
//args 为flutter 传过来的参数
Webview *webView = [[Webview alloc] initWithWithFrame:frame viewIdentifier:viewId arguments:args binaryMessenger:_messenger];
return webView;
}
五、步骤1生成的 HybridWebviewFlutterPlugin 注册插件的方法 registerWithRegistrar 中添加一行注册 WebviewFactory缓存
[registrar registerViewFactory:[[WebviewFactory alloc] initWithMessenger:registrar.messenger] withId:@"com.calcbit.hybridWebview"];
六、根目录 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 进行通讯
Widget buildWebView() {
return UiKitView(
viewType: "com.calcbit.hybridWebview",
creationParams: {
"url": widget.url,
},
//参数的编码方式
creationParamsCodec: const StandardMessageCodec(),
//webview 建立后的回调
onPlatformViewCreated: (id) {
//建立通道
_channel = new MethodChannel('com.calcbit.hybridWebview_$id');
//设置监听
nativeMessageListener();
},
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>[
new Factory<OneSequenceGestureRecognizer>(
() => new EagerGestureRecognizer(),
),
].toSet(),
);
}
target 1 步骤3 建立的 FlutterMethodChannel channel 能够调用 invokeMethod 方法传递消息名,参数给 flutter,并设置 flutter 回调
回调参数 result 多是 flutter Feature 返回值,也有多是 flutter 运行时报错
有了 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
-(void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result{
if ([[call method] isEqualToString:@"__flutterCallJs"]) {
NSString *action = [call.arguments firstObject];
NSArray *params;
if ([call.arguments count] > 1) {
params = [call.arguments subarrayWithRange:NSMakeRange(1, [call.arguments count] -1)];
} else {
params = @[];
}
// 在主线程更新 webview,否则会崩
dispatch_async(dispatch_get_main_queue(), ^{
[self->_context[@"__flutterCallJs"] callWithArguments:@[action, params, ^(JSValue *value) {
NSArray *arr = [value toArray];
result(arr);
}]];
});
} else if ([[call method] isEqualToString:@"evaluateJavaScript"]) {
// 注入 js
NSString* jsString = [call arguments];
dispatch_async(dispatch_get_main_queue(), ^{
[self->_webView stringByEvaluatingJavaScriptFromString:jsString];
});
}
}
下图列举了 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 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
一、JSContext 直接注入 bolck,js 调用这个函数
_context[@"globalFuction"] = ^(JSValue *value) {
NSLog("%@", value);
};
二、经过 JSExport 协议,只有 JSExport 里声明的方法才会被 js 访问到
定义一个 JSExport 协议,并在 Class A 实现,将 A 实例化并做为全局变量注入到 JSContext,这里为了方便直接在 webview 定义实现 JSExport,将 当期实例 self 注入到 JSContext
//定义一个JSExport protocol
@protocol JSExportProtocol <JSExport>
JSExportAs(jsCallFlutter, - (void)jsCallFlutter:(JSValue *)action params:(JSValue *)params callback:(JSValue *)callback);
@end
//将self添加到context中
_context[@"__OCObj"] = self;
};
这时候 js 全局链就会有 __OCObj
对象,调用 __OCObj.jsCallFlutter 传递参数给 OC,约定 最后一个参数为 callback,js Function 到 OC 里面会转换成 block
OC 经过 FlutterMethodChannel 调用 flutter 得到返回值后经过这个 block 触发 js 的 callback
#pragma mark - jsExport
- (void)jsCallFlutter:(JSValue *)action params:(JSValue *)params callback:(JSValue *)callback {
NSString *actionName = [NSString stringWithFormat:@"%@", action];
NSArray *arr = [params toArray];
[self->_channel invokeMethod:actionName arguments:arr result:^(id _Nullable result) {
if ([result isKindOfClass:[NSClassFromString(@"FlutterError") class]]) {
[callback callWithArguments:@[[result valueForKey:@"_message"], [NSNull null]]];
} else {
id results;
if (result) {
results = result;
} else {
results = [NSNull null];
}
// 在主线程更新 webview
dispatch_async(dispatch_get_main_queue(), ^{
[callback callWithArguments:@[[NSNull null], results]];
});
}
}];
}
经实践,限制 js 传给 OC 的值为 boolean, number, string, array, obj, null/undefined
null/undefined 都会转成 null,fn/set/map都会在OC变成空字典 {},{1: 'a'} 到了 OC key 也会转成 string
webView/OC,RN/OC cookie 都是共享的。可是 flutter 比较奇怪,用过的 dart:io 与 dio 都不自动带上cookie,查看了 dio_cookie_manager 与 cookie_jar 的实现,发现 dio 是利用这两个库本身在 dart 维护了 cookie 信息,而后添加到 dio.interceptors 里,随 request 带上,监听 response 存储。
// dio & dio_cookie_manager 代码
Future onRequest(RequestOptions options) async {
var cookies = cookieJar.loadForRequest(options.uri);
cookies.removeWhere((cookie) {
if (cookie.expires != null) {
return cookie.expires.isBefore(DateTime.now());
}
return false;
});
String cookie = getCookies(cookies);
if (cookie.isNotEmpty) options.headers[HttpHeaders.cookieHeader] = cookie;
}
@override
Future onResponse(Response response) async => _saveCookies(response);
_saveCookies(Response response) {
if (response != null && response.headers != null) {
List<String> cookies = response.headers[HttpHeaders.setCookieHeader];
if (cookies != null) {
cookieJar.saveFromResponse(
response.request.uri,
cookies.map((str) => Cookie.fromSetCookieValue(str)).toList(),
);
}
}
}
因为这种实现至关于把 response 的 cookie 维护在 dart 层面,因此 OC 的请求就不会有这些信息,webView 环境也不会有。
而后?
与其将 cookie 信息维护在 dart,为何不直接维护在 OC,那样OC/webView的请求还能带上。
一、 dart 存取 cookie 存到 OC
// 存
static Future<bool> setCookie({ String domain, String name, String value, int exp }) async {
bool result = await _channel.invokeMethod('setCookie', [domain, name, value, exp]);
return result;
}
// 取
static Future<List<Map>> getCookie(String url) async {
final List res = await _channel.invokeMethod('getCookie', url);
List<Map> listMap = new List<Map>.from(res);
return listMap;
}
二、OC 存取 dart 传过来的值,并在 OC 发送请求时带上这些 cookie
// 读 cookie
NSArray *cookieArray = [NSArray arrayWithArray:[[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]];
// 存 cookie
NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:cookieProperties];
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
// 请求带上 cookie
NSDictionary *cookieHeaderDic = [NSHTTPCookie requestHeaderFieldsWithCookies:[[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]];
[request setValue:[cookieHeaderDic objectForKey:@"Cookie"] forHTTPHeaderField:@"Cookie"];
三、OC 接收到 dart 传过来的 cookie 时顺带将 cookie 写入 webView
NSString *jsStr = [NSString stringWithFormat:@"document.cookie='%@=%@;expires=%ld'",name,value,exp];
[_webView stringByEvaluatingJavaScriptFromString:jsStr];
WebView 控制 flutter 导航栏右侧 BarButtonItem
Flutter 里跑 webview 显然不是明智的作法,flutter 官方默认都关闭 PlatformView 功能。相对于 hybrid 和 RN 只有 JSC 通讯,这里的 webview 又多了一层 flutter 通讯。
可是特殊场景下也不是不能够这么玩。相似在 RN / 小程序 里跑webview,在小程序里套 webview 减少包体积,避开审核快速迭代的作法不在少数。
也有在微信小程序里利用 miniprograme.navigateTo 触发app.pageNotFound 作 IOC 的,虽然慢了点绕了点,可是提升了开发效率与迭代速度。
Anyway, Keep Balance.
怕被砖,先声明如下为纯扯淡内容
PS: 据说 flutter 很强,但它并非前端专属玩具,由于 native 上手更有优点(尤为 Android)
综上,快 才是咱们的优点啊,钻牛角尖跟 native 比性能,何须呢。
The End