|
Flutter for Web在贝壳找房容灾降级中的应用
Flutter for Web在贝壳找房容灾降级中的应用
肖鹏
贝壳产品技术
贝壳产品技术 “贝壳产品技术公众号”作为贝壳官方产品技术号,致力打造贝壳产品、技术干货分享平台,面向互联网/O2O开发/产品从业者,每周推送优质产品技术文章、技术沙龙活动及招聘信息等。欢迎大家关注我们。 242篇内容
2021年09月17日 16:10
一. 背景1.1 Flutter for Web的发展现状在2019年举办的Google IO开发者大会上,Flutter 发布了1.5版本,新加入对 Web 端的支持,即Flutter for Web。在经历了多个版本的迭代之后,随着Flutter 2的发布,Flutter Web正式进入stable渠道。1.2 贝壳找房Flutter使用现状贝壳找房从2018年开始调研并接入Flutter框架,在贝壳的所有App中已经有24款app接入Flutter,App接入率超过70%,在贝壳的B端App A+&Link App中超过88%的新页面都在使用Flutter进行开发。随着Flutter在贝壳的大量使用,如何快速解决Flutter线上问题,做到及时止损成为我们非常关注的一个问题。针对这个问题,我们想到了Flutter for Web。如上图当Flutter页面出现问题时,把修改后的Flutter代码编译成web降级包下发到客户端,把出错的页面通过路由拦截的方式跳转至降级包中的Flutter Web页面,就能实现Flutter页面降级,在不需要重新发版的情况下做到线上问题及时止损。二. Flutter for Web探索及主要问题和解决方案2.1 操作系统判断问题有了以上思路,我们首先尝试将Flutter项目编译成web直接运行在浏览器或者客户端web容器上。我们发现,浏览器会报如上图错误。以Platform._operatingSystem方法为突破口,我们首先看一下flutter build web编译出的未压缩的main.dart.js,发现下面代码:_Platform__operatingSystem:function(){throwH.wrapException(P.UnsupportedError$("Platform._operatingSystem"));}我们发现Flutter Web产物的实现直接抛出异常(不仅_Platform__operatingSystem方法抛出异常, dart:io整个库在Flutter for Web都是不支持的)。那这段代码是如何添加到我们的main.dart.js里的呢?我们首先看一下Flutter for Web的前端编译流程。2.1.1 Flutter for Web前端编译Flutter for web编译的前端部分和Flutter for native的编译过程类似,都是通过源码生成中间dill文件。当我们调用flutter build web时会调用flutter_tools/lib/src/build_system/targets/web.dart中下面的代码:finalListsharedCommandOptions=[globals.artifacts.getArtifactPath(Artifact.engineDartBinary),'--disable-dart-dev',globals.artifacts.getArtifactPath(Artifact.dart2jsSnapshot),'--libraries-spec=${globals.fs.path.join(globals.artifacts.getArtifactPath(Artifact.flutterWebSdk),'libraries.json')}',...decodeDartDefines(environment.defines,kExtraFrontEndOptions),if(buildMode==BuildMode.profile)'-Ddart.vm.profile=true'else'-Ddart.vm.product=true',for(finalStringdartDefineindecodeDartDefines(environment.defines,kDartDefines))'-D$dartDefine',];finalProcessResultkernelResult=awaitglobals.processManager.run([...sharedCommandOptions,'-o',environment.buildDir.childFile('app.dill').path,'--packages=.packages','--cfe-only',environment.buildDir.childFile('main.dart').path,//dartfile]);if(kernelResult.exitCode!=0){throwException(kernelResult.stdout+kernelResult.stderr);}这段代码会调用dart2js的dart2jsSnapshot命令,输入是项目的main.dart和dart sdk的web实现,最终将dart代码转换为dill kernel文件。其中dart sdk的web实现以json的形式进行索引,json内容如下:"dart2js":{"libraries":{…."_http":{"uri":"../dart-sdk/lib/_http/http.dart"},"io":{"uri":"../dart-sdk/lib/io/io.dart","patches":"io_patch.dart","supported":false},"isolate":{"uri":"../dart-sdk/lib/isolate/isolate.dart","patches":"../dart-sdk/lib/_internal/js_dev_runtime/patch/isolate_patch.dart","supported":false},….}}我们看到dart:io库的supported字段是false,也就是说dart2js不支持dart:io库(isolate库也是不支持的)。这就是为什么Flutter for Web 调用Platform._operatingSystem抛出异常了。dart:io在我们的app中有着大量使用,如果Flutter for Web不支持dart:io,我们就需要针对不同的平台有不同的实现,那我们的代码就可能会出现针对平台的判断,比如下面的代码:if(kIsWeb){//web逻辑}else{//native逻辑}由于我们已有的代码都只适配了native,这样就需要对已有代码进行修改。使用这种方案的话成本就非常高,那如何在不修改原有代码的情况下实现一套代码既运行在native又运行在web呢?2.1.2 操作系统判断问题解决方案首先,我们发现Flutter for Web提供了dart调用js的能力,代码如下js_util.callMethod(html.window,'isAndroid',[])我们使用js/js_util.dart中的callMethod()方法调用window的isAndroid/isIOS方法,就能判断当前是运行在Android/iOS系统上。那么如何在不修改业务代码的情况下替换原有的isAndroid/isIOS调用呢?这里就用到了我们的面向切面库Beike AspectD(https://mp.weixin.qq.com/s/tVnUXtmINMFwi8ySQHdxwg)。我们将所有调用isAndroid/isIOS的地方通过aop的能力改为调用window的对应的方法,那么问题就顺利解决了。2.2 Platform Channel问题在后面的探索中我们又还发现了native与Flutter Web无法通信的问题。为了理解这个问题,我们先从Flutter和Flutter for Web的架构说起。从Flutter(左)和Flutter for Web(右)的架构我们发现, 两个平台framework层是一致的,为开发者提供了丰富的布局和基础库。Flutter for native的engine层实现了与底层操作系统的交互,Flutter for Web的browser层则是按照浏览器的标准API实现了web端的引擎。那么如果只是使用Flutter Web提供原有能力,我们是无法让Flutter Web和底层操作系统或者native进行通信的。Flutter中与native通信比较常用的是Platform Channel,本文以MethodChannel为例。在Flutter中,负责Flutter与native交互的MethodChannel需要engine层来实现数据的传输。但是在Flutter for Web中没有了这个能力,Flutter Web和native是无法进行通信的。为了打通Flutter for web与native通信的通道,我们设计了三层结构来实现Flutter Web和native的通信通道。以iOS侧调用Flutter Web侧为例。首先,在native侧,我们实现了一套自己的Native Channel来管理native对FlutterChannels的调用和Flutter的回调。我们使用了运行时技术来hook FlutterMethodChannel中的-(void)invokeMethod: arguments:和-(void)setMethodCallHandler:等方法。当业务方注册FlutterMethodChannel时,也会在我们的Native Channel注册,当业务方调用或者注册回调时,也会在我们的Native Channel调用或者注册回调。同样的,在Flutter侧,我们也实现了自己的MethodChannel。我们再次运用Beike AspectD的aop能力对MethodChannel的Future invokeMethod和void setMethodCallHandler等方法进行了hook,当接收到native侧的调用时,会调用到我们hook的方法里进行处理。中间的通信层我们使用了JavaScript与native通信的能力建立了桥接来打通Native Channel层和Flutter Web Channel层的调用。整个通道的具体调用流程如下:1) App启动后,当Flutter侧有method channel注册时,Flutter Web Channel会生成相应的channel,将调用方法名和对应的处理方法进行存储。2) 当native侧调用method channel的某一个方法时,被hook的method channel会调用到我们实现的Native Channel层。3) Native Channel层会接收native传过来的函数名、参数和回调,生成判断调用唯一性的uniqueid,将回调和uniqueid绑定并存储在内存中。然后将函数名、uniqueid和参数通过js桥接透传至Flutter Web Channel侧。4) Flutter Web Channel侧会遍历所有Flutter Web注册的所有method channel,查找对应的处理方法,找到后调用该方法。5) Flutter Web业务处理完成后,Flutter Web Channel层会将结果和uniqueid通过js桥接透传至Native Channel层。6) Native Channel层通过uniqueid查找到存储的回调,将返回的结果传给调用方。这样,就完成了一次从native到Flutter Web侧的调用与回调。Flutter Web调用native的方案类似,本文就不详细介绍了。在后来的开发过程中,我们发现只是打通native和Flutter Web的通道还是不够的。比如我们发现另外一个问题,对于Flutter for native使用正常的一些API,web并没有相应的实现。下面跟大家分享一下这个问题和我们的解决方案。2.3 dart:io文件系统API问题dart:io中有另一个重要的包file.dart,开发者可以使用这个包来进行文件的操作。由于Flutter for Web不支持dart:io,所以使用file.dart库的代码也都会调用失败。我们可以同样使用aop的方式来替换file.dart中api的实现然后通过我们上边实现的通道来调用native侧的实现,但是dart为我们提供了更加简便的方式,IOOverides类。这个类提供了让我们复写dart:io库中api的方法。比如我们要实现readAsBytes()方法来实现文件读取,我们只需要实现类似下面代码即可:@pragma("vm:entry-point")classFileOverrideextendsFileSystemEntityimplementsFile{@override@pragma("vm:entry-point")FuturereadAsBytes()async{returnawaitFlutterFileOverridePlugin.overrideReadAsBytes(path);}}FlutterFileOverridePlugin是我们实现的plugin,需要Android和iOS侧分别实现文件读取的代码并将数据返回。三. 容灾降级方案设计解决了以上的几个主要问题,我们就可以将Flutter Web页面运行在客户端的web容器中。但是只有Flutter Web支持是不够的,我们需要一套完整的方案来支持客户端Flutter页面的容灾降级。整个方案我们从构建、降级配置和客户端支持把整个容灾降级系统分为了六个模块。下面介绍一下Flutter容灾降级的整体架构。主要包括以下几部分:1. 持续集成平台。用于完成降级包的集成,能够做到自动配置集成代码。当工程师发现Flutter线上问题之后,可将修复后的Flutter代码上传,然后触发任务,任务拉取最新的代码后会触发Flutter Web编译,编译完成后,任务会对产物进行裁剪,然后将修改后的产物进行压缩上传。2. 包管理平台。如上图,当持续集成打包完成之后,会在包管理平台注册。用户可在包管理平台针对不同的应用、系统和版本进行包的新增、下载及上下线操作。3. 配置平台。主要负责降级配置管理,可针对不同的app、平台和页面等配置降级包。在配置时,用户需要指定目标URL和替换URL,目标URL是指需要降级的页面的路由URL,替换URL是我们降级包中相应Flutter Web页面的URL。我们也可以通过将目标URL设置为AllFlutterPages来将目标页面指定为所有Flutter页面,这样当客户端要跳转至Flutter页面时,路由拦截器会自动将该Flutter页面的路由URL转化为降级包中对应的Flutter Web页面的URL然后进行跳转。支持Flutter全页面的降级是为了应对Flutter引擎出现问题导致Flutter页面大面积出错的情况。4. Native客户端除了实现上面说到的包下载、配置下载、路由拦截器和路由转换器之外,还实现了Flutter Web的容器,该容器主要实现了以下功能:- 在客户端搭建本地服务,加载降级包。- 加载Flutter Web通道所需要的JS文件。- 实现了贝壳Flutter容器对应的方法(主要是页面的生命周期方法),当这些方法被调用时容器会通过JS桥接调用Flutter Web相应的方法。5. JS层主要是我们随着客户端安装包一起发布的JS文件。6. Flutter Web主要支持了Flutter Web和native 的通道。这一部分代码内置在Flutter Web降级包中。四. 总结贝壳找房Flutter团队主要利用了Flutter跨平台的特性,将Flutter for Web运用在了Flutter的容灾降级并已接入到线上app中使用,为贝壳找房Flutter越来越多的运用提供了又一道保障。我们也还有很多待完善的地方,比如使用IOOverides复写dart:io方法的方案,IOOverides中有差不多30个方法,如果要实现所有的方法需要耗费比较长的时间,我们现在仅实现了我们业务场景使用到的api。除了将Flutter for Web运用于Flutter容灾降级。贝壳Flutter团队也在探索使用Flutter进行多端一体化的开发,充分利用Flutter的跨端特性,一份代码,可以同时运行在iOS/Android和web端。在使用Flutter for Web中,我们也遇到了产物大、滑动卡顿等一系列问题,后续会跟大家分享在Flutter多端一体化过程中遇到的挑战与方案。经过三年的积累,Flutter的运用已为贝壳找房客户端的开发大幅提效,经过不断的积累与沉淀,我们相信Flutter也能够帮助我们提升整个大前端的效率。
预览时标签不可点
Flutter15移动端37Flutter · 目录#Flutter上一篇Flutter的Widget之间的通信方式及状态管理下一篇Flutter流畅度优化神器-开源组件keframe详解关闭更多小程序广告搜索「undefined」网络结果
|
|