2016年京东为向核心客户提供更优质的购物体验,特别推出京东PLUS会员,旨在全方位提升和丰富用户网购体验,目前京东PLUS会员已成为电商行业付费人数最多的会员体系。作为PLUS的前端开发,我们思考最多的就是如何让页面更快更好的呈现在用户面前,如何用技术为用户提供最好的购物体验。
目前PLUS业务前端技术栈选择是React,虽然我们在React性能优化上做了很多工作,但是由于WebView本身的局限性,和原生相比H5页面的渲染效率和 JavaScript 的执行能力都差一些,所以页面白屏时间长,加载速度和用户体验都比不上原生,同时国内各大厂商机型适配工作浪费了太多开发者的精力。
然后我们尝试了Js2Native的解决方案——React Native(后面简称RN),RN相对于WebView来说,性能和渲染效率都得到了不小的提升,但由于 RN 代码是通过 JS 桥接的方式转换为原生的控件,所以受各个系统间的差异影响非常大,虽然可以开发一套代码,但对各个平台的适配却非常的繁琐和麻烦。
2018 年 12 月初,Google 正式发布了开源跨平台 UI 框架 Flutter 1.0 Release 版本,引起了行业巨大的关注,我们也积极调研在PLUS业务中使用Flutter的可能。2019年Google 开发者大会上,Flutter 1.9正式发布,官方宣布了 Flutter Web 合并到了 Flutter SDK 中,这意味着Flutter Web也越来越成熟。于是我们开始了Flutter在PLUS业务中的尝试。
Flutter比较吸引我们的一些原因:
Flutter有自己的渲染引擎Skia,页面渲染不再受限于Native,页面适配工作将减少。
Flutter支持 JIT 和 AOT 两种编译方式。JIT 编译方式使开发者在开发阶段使用热重载(HotReload),这样在开发时可以省去构建的过程,提高开发效率。而 Release包采用 AOT 的编译方式,使执行效率非常高,让 Release 版本发挥更好的性能。
入坑Flutter需要探索和解决的问题
PLUS业务的用户量比较大,任何新技术的尝试都需要做足调研工作,我们主要从以下几个方面做的调研:
Flutter重写H5页面主要有两个成本——学习成本和开发成本。
学习成本主要就是Dart语言和Flutter Widget框架,作为前端开发如果习惯了原生JS面向过程的设计思维,那么学习使用Dart语言就需要有一个面向对象思维的转变,不过这对程序员来说基本不是事,毕竟我们TS写的也很多了。最大的学习成本在于熟悉Flutter Widget框架,Flutter应用里一切皆Widget,Flutter提供了大量的控件,每个控件又包含大量的属性,这可方便开发者通过组合嵌套的方式构建复杂界面,但是要完全搞明白一个控件也需要花更多的时间。Flutter Widget框架具体查看Widgets Catalog:https://flutter.dev/docs/development/ui/widgets。
开发成本成本主要在于Flutter重写原H5页和客户端的调试。Flutter重写原H5页时间基本可控,虽然刚开始写Flutter时会吐槽控件的嵌套层级太深,但是这个习惯就好了,但是和客户端调试时间就不受前端控制,发现问题只能乖乖等客户端排查解决。
Flutter与H5或者Native进行互交主要包含以下几个方面:H5页面唤起Flutter页面、Flutter页面唤起H5页面、Flutter调用Native功能。
H5页面唤起Flutter页面目前的解决方案是用Scheme协议,Flutter页面唤起H5页面、Flutter调用Native功能都是用桥接的方法直接调用Native能力完成。Flutter提供了三种和Native通信的方式:
这个就不在这里讲了,有兴趣的同学可以自己去了解。
对于前端同学来说,我们有很多调试H5页面的方法,比如 Chrome Dev Tools,我们可以用console.log打印变量,有很多栈信息让你来判断错误和debug,当然Flutter也提供了很多调试页面的方法。
Flutter提供的debugPrint 和 print将消息打印至控制台,不同的是debugPrint 提供了定制打印的能力,也就是说,我们可以向 debugPrint 函数,赋值一个函数声明来自定义打印行为。比如我们将生产环境的 debugPrint 定义为空实现,将开发环境的 debugPrint 定义为同步打印数据,就可以满足开发环境下打日志的需求,也可以保证生产环境下应用的执行信息不会被意外打印,如下所示:
// 将debugPrint指定为空的执行体, 所以它什么也不做
debugPrint = (String message, {int wrapWidth}) {};
// 将debugPrint指定为同步打印数据
debugPrint = (String message, {int wrapWidth}) => debugPrintSynchronously(message, wrapWidth: wrapWidth);
Flutter同时也支持断点调试,界面调试,错误堆栈信息输出。以VSCode为例,点击VSCode的断点调试按钮即可开启调试,调试界面如下:
同时可以点调试工具栏最右边的按钮Open DevTools或者使用快捷键【command+shift+p】打开VSCode工具栏,然后输入Open DevTools打开调试窗口,可以看到Flutter Inspector 所展示的 Widget 树结构,与代码中实现的 Widget 层次是一一对应,如下图所示:
在此界面除了进行布局调试外,还可以使用Flutter Inspector进行布局调优,同时也可以看Flutter页面的各种性能,内存占用情况等。
Flutter中,还有一个很有用的界面调试工具,那就是Debug Painting,即可以给界面绘制布局边界。在VSCode中,开启该绘制功能十分简单,只需要在Flutter App调试的过程中,打开命令面板(cmd+shift+p),输入Flutter Toggle Debug Painting即可启动,它可以很清晰的展示出每个元素的布局边界,迅速帮开发者找出布局出问题的地方,开启界面调试后APP界面展示效果如下图所示:
降级是业务开发中不可缺少的一环,特别是对于Flutter这种刚刚开始使用的新技术,如果线上出现任何问题,都需要第一时间将Flutter页降级到可以正常使用的H5页或者兜底页,保证用户的使用体验。
Flutter页面有两种降级策略,一种是在Flutter页面入口处拉取配置接口,一种是在Flutter容器层拉取配置列表。在容器层做降级更合理一些,目前JDFlutter容器也已经支持各业务在容器层配置降级策略。PLUS业务有一套自己的降级,切量配置系统,可以运用在H5,RN,Flutter业务上,如下图所示:
此系统可以设置页面的切量、配置白名单,头部内容里是配置的是首选的页面打开方式,以Flutter为例,头部内容配置打开Flutter页面的openapp协议时线上入口可以打开Flutter页面,配置H5链接时线上入口即可打开H5页面,达到降级的目的。
H5最大的优势就在于它在客户端集成简单,随时发版,对业务方需求特别友好。PLUS作为正在快速成长的业务,需求量相当大,而且相当多的紧急需求,使用Flutter就对业务在客户端集成和发版速度有了较高的要求。
目前JDFlutter的热更新机制还不完善,Flutter业务发版需要走客户端的发版机制,等待JDFlutter热更新机制成熟后,Flutter业务可以和H5一样随时发版,同时PLUS团队也在探索Flutter界面的动态更新,已经有了不错的方案。
Flutter业务在客户端的集成比较简单,目前因为Flutter自动集成客户端的平台没有做好,需要手动集成客户端。Flutter业务开发完成后使用编译命令:Android为 flutter build aar ,iOS为flutter build ios-framework,将最终生成产物交给客户端同学帮忙集成。整套流程如下图所示:
Flutter在PLUS业务中分三步走:引入期 → 规模化 → 一体化,我们目前还处于引入期到规模化的过渡阶段。我们是京东最早实践H5业务改版Flutter的团队,实践过程中踩了无数的坑,也积累了很多宝贵经验,在客户端同学的帮助下,我们已打通Flutter重构H5页面的整套流程。
在Flutter的实践过程中发现,前端在Flutter底层能做的事比较少,前端的优势在于提高工程化效率,PLUS Flutter工程化架构设计如下:
这套工程化体系将在未来很大程度提升我们Flutter业务的开发效率,这里重点介绍一下我们设计的Flutter MVVM开发架构。
刚开始做Flutter业务开发时,大家可能写的很随意,什么东西都写一起,也不去考虑解耦等问题。但是实际生产开发是不能这样做,否则项目稍大就无法维护。
MVVM开发架构可以很好的解决这个问题。MVVM有三个角色需要扮演View - ViewModel - Model,View是界面,Model是界面数据模型,ViewModel是View 和Model的连接层,它实现了数据的双向绑定,Model中数据的更新会及时的反馈到View上,View上的更新也会及时的反馈给Model。我们的MVVM架构是以Redux为纽带建立的,架构图设计如下:
Redux是一个优雅且实用的状态管理框架,前端同学可能对redux很熟悉,客户端同学可能还没用过,这里大概介绍一下Redux的几个概念:
Redux 的工作流程如图所示,先用户发出 Action ,Store自动给 Middleware 进行处理,再传递给 Reducer , Reducer 会返回新的 State ,通过 Store 触发重新渲染 View。
Flutter使用redux时需要引入redux库和flutter_redux库,flutter_redux可以看做是利用了 Stream 特性的 scope_model 升级版,通过 redux 设计模式来完成解耦和拓展。因为篇幅关系这里不讲flutter_redux具体实现原理,如有需要可以再写文章做详细介绍。这里介绍两个flutter_redux 中的概念:
首先看一下整个PLUS业务开发工程的目录结构:
下面看一下整个工程如何建立和运行:
在pubspec.yaml文件里引入需要用到的第三方库,并执行flutter packages get进行安装:
dependencies:
redux: ^4.0.0
flutter_redux: ^0.6.0
json_annotation: ^2.0.0
dev_dependencies:
build_runner: ^1.1.3
json_serializable: ^2.0.0
这里redux、flutter_redux是flutter redux需要引入的库,json_annotation、json_serializable、build_runner是flutter JSON自动反序列化需要引入的库。
每个接口请求都需要model类来对接口返回的数据进行的反序列化处理。京东APP Flutter网络库向服务器请求数据后,服务器会返回一段Json字符串,如果要想更加灵活的使用数据就需要把Json字符串转化成对象。由于Flutter只提供了Json to Map,所以我们需要对Json进行反序列化处理。目前Flutter反序列化使用比较多的是json_serializable。
首先给Modle类添加注解@JsonSerializable(),并引入与本Model类相同文件名的.g.dart文件,然后执行flutter pub run build_runner build命令就可以自动生成Json序列化和反序列化的方法。
Store 全局只有一个,这里封装一个创建 Store 的方法。创建Store时,传入 Reducer ,初始化的 State 和 Middleware 。
State 创建的时候可以创建一个 AppState,作为整个应用的 state,除此之外,根据不同的页面可以有不同的 State,比如我的页面有一个Notice模块 ,就可以有一个Notice模块的状态 noticeState ,并且 noticeState 会放在 AppState 中作为成员变量进行管理。
Action 是 Widget 发出的消息。对于Notice模块,初始化的时候需求请求接口渲染界面,所以组件初始化的时候会发一个请求接口的Action,一个接口请求建议注册三个Action,分别是接口请求Action、接口请求成功Action、接口请求失败Action。
Middleware是中间件,通过中间件机制,可以解耦业务逻辑,更加灵活的处理异步的操作。它会先于Reducer处理Action,然后将处理结果交给Reducer。页面每个模块都可以有自己的中间件,达到页面各模块解耦的目的,同时也可以引入第三方中间件,比如日志打印、异步处理等中间件。
Reducer 是一个带 State 和 Action 两个参数的纯函数,主要的作用就是返回最新的State。每个Action都有与之对应的Reducer,每一个Middleware都有与之对应的Reducer,Action不一定都有对应Middleware(PS:Action可以不通过中间件直接到Reducer)。和Middleware一样,页面每个模块都可以有自己的Reducer。
这里的viewmodel只是负责ViewModel层数据分发和事件传递的功能,每个view都有一个自己的viewmodel来管理view内的事件和数据。
准备工作做完就可以布局页面了,在项目入口要做的工作是创建 Store,和使用 StoreProider 作为根布局,这样整个Store就和页面关联到一起。
然后将页面各个模块拆出来建立单独的view,给其建立对应的State、Action、Middleware、Reducer、ViewModel,以此类推,一个页面,一个工程可以拆分成很多单独的模块来完成,各模块独立开发互不影响。下面是Notice模块的view,里面不涉及任何逻辑代码。
这里说一下StoreConnector控件,StoreConnector 中有两个标记为@required 的参数,一个是 converter , converter 用于将 store 中的 state 转化为 viewModel,另一个是 builder,builder 的作用是将 viewModel 进一步转化为 UI 布局。同时StoreConnector里也有一些控件生命周期的方法,比Flutter框架里更全,例如onInitialBuild()、onWillChange()、onDidChange()等,可以根据需要灵活使用。
另外,Flutter页面适配可以考虑flutter_rem,按照前端rem原理实现,可以帮助开发者精确还原设计稿。
Flutter Redux在Flutter的工作流程如下图所示:
View发出dispatch action,这个Action可以是接口请求或者事件处理等类型,首先Middleware先响应到这个Action并在这里做各种处理,处理完成后将结果反馈到Reducer,Reducer根据反馈的结果更新State,ViewModel将最新的State分发到各个View,View更新完成。
可能有客户端同学会奇怪为什么需要Flutter Web,作为前端团队需要支持的端比较多,在人力资源有限的情况下,我们不可能一个业务用Flutter开发一遍,再用H5开发一遍,所以Flutter Web对前端团队来说特别重要。同时Flutter Web天生还具备容灾降级的使命,如果线上的Flutter页面出现异常,可以直接降级到Flutter Web页,保证用户的正常使用。
Flutter页面开发完成后运行flutter run -d chrome命令就可以在浏览器里看到相应的Flutter Web页面,使用flutter build web命令可以生成release版Flutter Web页面。下面是PLUS省钱月卡的错误兜底页转web后的效果:
总体还原度接近100%,页面有一部分Canvas 渲染,有一部分用 DOM 填充,性能还是不容乐观。总体实践来看Flutter Web需要解决的问题还有很多:
要在业务使用Flutter Web首先需要保证Flutter插件和Flutter组件支持Flutter Web,目前大多数Flutter插件不支持Flutter Web,希望京东开发者以后写Flutter插件时可以考虑兼容Flutter Web。
Dart 当年天然支持在 Chrome 中使用,并且长期以来一直支持转换为 JavaScript。因此,可以遇见的未来,随着 Flutter 的发展,Flutter Web支持也会越来越好。
通过选取十天H5和Flutter页面的最终渲染时长对比,可以明显的看出Flutter页面相比H5有很大提升的,Flutter是一个不错的选择。
最后推广一下PLUS前端的Git提交信息规范。
提交的格式为:
比如我在Flutter工程里新增了一个页面,commit的格式就是 feat:新增XX页面
PLUS团队为了给用户提供更好的使用体验,在PLUS业务中探索和实践Flutter。因为篇幅的关系,很多基础的东西并没有讲到,比如如何搭建Flutter环境、申请jdFlutter开发工程、flutter在客户端的集成、踩过的坑等等没有说到。前端业务改造Flutter的踩的坑太多,主要是和客户端互交方面,估计又能写一篇文章了,不过踩过的坑都已填平,大家应该不会遇到了。
文章主要分享了一下PLUS Flutter业务开发架构,此架构也可以应用在客户端和RN业务开发上,可以提高合作开发业务的效率。同时也讲了一下我们对Flutter Web研究的最新进展,虽然目前还不太乐观,但是我们会持续完善PLUS Flutter Web的生态,同时我们也在研究Flutter+Serverless的解决方案,希望能和一些正在研究 Flutter 的同学进行交流和学习。
扫一扫
在手机上阅读