查看原文
其他

干货 | 前端跨端业务整合的探索与实践

Jeff/Harry 携程技术 2022-06-07

作者简介

 

Jeff,携程前端开发经理,对前端自动化技术感兴趣,推动了团队使用cucumber进行UI自动化测试。Harry,携程前端开发工程师,秉持“Don’t make me think“的理念向用户交付页面、向同事协作工程。


为应对携程国际化的需求,机票前端团队开始业务统一化的步伐,Ctrip和Trip的业务整合和代码复用成为面临的困难和挑战。在实践过程中,团队积累了大量的经验,下文是机票实现业务统一化、技术中台化、迭代敏捷化的思路和方法。

一、背景



Trip与Ctrip为独立运行的两个站点,虽存在各自品牌化的差异,其业务功能有着极高的一致性。两个站点相互独立开发与维护存在着以下的问题:

1.1 技术架构不统一


Trip与Ctrip使用的开发技术栈存在较大差异。Trip订后场景在APP端使用Native iOS、Android开发,H5/PC端采用React技术;Ctrip订后项目使用可在iOS及Android双端运行的基于React Native的CRN①框架,在H5端采用CRN-WEB②进行动态打包将CRN代码生成对应H5页面。
两个站点整体技术架构上多种技术方案并行,相同的业务逻辑需要在各端分别实现,在打包发布流程中,各端需要通过不同的方式进行相关操作(如MCD③、Ares④、PAAS⑤等)。再加上订后场景业务维护复杂性比较高,开发周期冗长,两个站点分别开发的效率不容乐观。

1.2 功能迭代存在冗余


由于技术架构的不统一导致在业务维护上需要分别进行开发迭代,在开发效率上存在很大的冗余,同时开发团队需要面对多种技术栈,学习成本和开发成本都非常高。相同的业务在多团队重复开发时也难以实现功能与进度上的对齐。

1.3 发布与监控分散


正由于第一点提到的,Trip与Ctrip使用了不同的技术框架分别进行开发,其打包、发布及日常埋点监控、数据维护存在极大差别且难以整合。Trip的iOS、Android使用的是MCD③平台使用双频道分别进行打包与发布,而H5页面需结合Ares④平台的打包以及PAAS⑤的发布管理。相对而言,Ctrip对于APP、H5端使用MCD打包发布,PC端使用Ares与Captain。对于不同的开发代码,相关的数据埋点纬度与落地方式也不同,分散的埋点数据给之后的统一分析带来了困难。
因此TRIP和CTRIP技术架构的统一,共同维护一套流程,那将极大解放开发资源而着重于更有意义的业务探索,也缩小了其后维护流程的成本。在改造过程中,我们将技术栈统一,将原先iOS、Android、H5替换为CRN架构,将PC替换为React架构,并在此基础上建造了模块化的基础组件,打造前端中台化产品。本文将针对前端中台化改造的探索做出阐述。

 整体架构图
// 章节尾注① CRN:Ctrip React Native,携程对于React Native的再封装,提供多种业务部门可以直接使用的基础工具;② CRN-Web:携程提供的将CRN/RN转为H5页面的工具,使APP页面能在浏览器上展示。③ MCD:携程无线持续交付平台。含多类型项目的集成、测试、发布、运营等多种服务。④ Ares:携程提供一套前端研发整体解决方案。从编码、测试、编译、发布等多个环节整合前端资源的开发。⑤ PAAS:携程研发服务平台。一站式提供多种服务产品。

二、面临的挑战



面对庞大繁复的业务逻辑、Ctrip站点与Trip站点的表现差异,中台化开发两边的产品线并不是一个简单的改造。就前端而言,将现有的国内站点代码直接套用于国际站点是一个海量级的改造工作,但经过仔细拆分,难点分类列出并逐个击破其实是一个可量化好控制的迭代流程。 
1)组件共用;2)样式拆分;3)暗黑模式的适配;4)多语言的适配(i18n)5)国际单位的本地化(l10n)6)基于Gitlab pipeline的自动化测试7)发布与监控
针对以上各个方面,机票前端开发做出一些探索供大家探讨。

三、解决方案



3.1 组件化开发


为了使一套代码能驱动仍存在差异的Ctrip与Trip流程,首先需要将公用的且因平台存在差异的模块或功能抽象化为组件。机票订后流程开发技术栈基于React Native + Redux的技术框架,控制流程逻辑的action和reducer一层可以高度重用。然而和视觉相关的View层需要做品牌化区别、不同平台的语言需要不同的翻译结果、响应同一操作的服务请求与底层处理逻辑也会有些许不同。由此搭建一套兼容两端的公共组件库是拼接一切业务的基石。
为了满足各个功能的兼容性,公共库包含了原子UI组件、业务基础组件、基础处理事件、公共工具等四象限纬度的组件:

 公共库四象限组件
原子UI组件:包含了页面展示的基础UI,包括Icon、Card、Button等。其组件与上下文无关,更多是在针对Ctrip及Trip不同平台进行品牌化差异的样式处理(详见第2小节)、基础事件的绑定和必要的曝光点击等埋点的处理。
基础业务组件:是针对一个原子业务块的UI封装,例如机票卡片、进度轴、运价明细卡片等,通常需要依赖上下文数据的传入。一个业务组件虽然依赖的数据源往往是一致的,但其组装起的基础UI组件、页面的排版格式往往存在一定的差异。封装之后的业务组件可使业务开发无需考虑其中的展示、排版逻辑,使用统一的数据源及事件操作可达到相同效果。
基础处理事件:包含了相同性质的业务事件处理,例如Ajax请求的发送、业务埋点的发送、多语言的转换(详见第4小节)、国际单位的本地化转换(详见第5小节)等。
公共工具:则提供与上下文无关的纯函数处理工具,例如电话号码的正则校验、url参数的转换等等。保证对其调用不会受方法以外的环境影响,也不会影响对外的环境。

3.2 样式拆分


在追求流程统一化的前提下,Ctrip和Trip的品牌化视觉体验还是有很大的差异。两端针对字号、颜色、头部样式、弹窗样式、甚至圆角都有各自的标准。 

Ctrip & Trip 字号大小映射表

Ctrip & Trip 颜色映射表

为了解决两端样式的适配,公共库封装了技术样式表组件。改造初期对于整个流程针对字号和颜色进行了一次整理,将流程所使用到的字号和颜色总结到了一张基准样式常量表,再将常量表再跟进国际站点的标准重填入对应的值,并写入样式表组件库。之前写到样式表里的字号和颜色全部改为引用样式表里的常量,而用哪张表则取决于当前是哪个站点的APP。抽离常量的过程虽然繁琐,换来的是两端的代码可以尽可能得使用一张样式表。
const BasicIBU = { // Fonts subHead: 20, // sub head headline: 18, // title headlineS: 16, // title bodyText: 15, // body callout: 14, // title textNormal: 14, bodyTextS: 13, footnote: 13, // void caption1: 12, //caption // ... // Colors black: '#0F294D', // 基础黑色字 secondaryBlack: '#455873', // 基础黑色字 二级黑 tertiaryBlack: '#8592A6', // 基础浅黑色字 三级黑 titleBlack: '#042950', // 标题黑色字 grey: '#ACB4BF', // 基础灰色 switchGrey: '#AAA', // 切换按钮灰 lightGrey: '#F5F7FA', // 基础浅灰 lineGrey: '#DDDDDD', // 边框灰 placeholderGray: '#F0F2F5', // 呼吸态灰 // ...}IBU基础样式常量表截取
modalTitle: { textAlign: 'center', fontSize: BasicStyles.headline, color: BasicStyles.black,}一个简单的样式表

有了字号和颜色的基础,可以在这基础上开发出两端共用的基础UI组件与基础业务组件。例如页面头部、选择弹框、抽屉浮层等。因为基础组件的交互逻辑一致,不同的只是两端(或者三端:国际站点针对IOS端和Android端有不同标准)的表现样式,所有的公共组件都是针对逻辑写了一份共用的JS逻辑以及针对渲染层级写一份共用的JSX Dom表。JSX Dom表上绑定的StyleSheet则是针对性得读取三张表(FBU、IBU IOS、IBU Android)的样式内容。而样式表中的字体、颜色使用基础样式表的封装便可按图索骥渲染不同的品牌样式。

公共组件目录结构
同样的,在业务开发过程中,非基础组件的View层也需要区别开发。因为业务逻辑相同,各个业务场景往往只需制作一套JSX Dom表,而对于FBU和IBU往往需要两套样式表。

业务组件目录结构

3.3 暗黑模式的适配


除了常规的两端样式差异以外,为了加强品牌效应,流程还需要支持暗黑模式(DarkMode)。DarkMode在转换时,看似只是将颜色做一个简单的白转黑,黑转白映射转换,实在底层有很多让人头疼的逻辑。
首先并不是白色都转换为统一的白色,明亮模式下白色卡片相互叠加因为有黑色边框或者黑色阴影的隔离,层级区分很自然明细;然而在暗黑模式下,自然黑色的边框和阴影并不能将黑色的卡片有效的区分开来,所以需要将所有白色做语义化区分,不同层级不同语义的白色转换为不同深浅的黑色。如此,一个 #FFF白色,可以根据场景语义化区分为背景纯白ThemeWhite,主要内容白色PrimaryContentWhite,次要内容白色SecondaryContentWhite等,分别对应的暗黑模式色值为#0D131A,#252B31,#333B46。


其次,如上面提到的阴影和边框等拟物色,在暗黑模式下不能转换(自然界中未有过白色的阴影吧)。需要将这些拟物色剥离出来(如阴影的ShadowBlack),在暗黑模式下不做转换。
最后,所有的彩色在亮度更低的暗黑模式下需要转换为饱和度更低的对应颜色。例如警戒红色从#EE3B28映射为#F37668,品牌蓝色从#287DFA映射为#7EB0FC。 

明亮模式&暗黑模式对比图
颜色的映射规则弄清楚了,那怎么把暗黑模式应用到流程的呢。这就要回到在样式品牌化章节提到的基础样式表,FBU站点有一张基础样式表,IBU有一张基础样式表,只需要将原来的IBU基础样式表作为明亮模式的样式,再在此基础上映射出一张暗黑模式表。在APP启动阶段动态得判断当前所在的模式,并加载对应的样式表。

加载基础样式表流程

3.4 多语言的适配(i18n)


国际化的改造自然离不开多语言的适配(i18n即internationalization的简写,由首尾的i、n及中间的18个字母组成)。此次机票订后流程的多语言翻译及加载机制依托于携程的Shark多语言整体解决方案⑥。Shark翻译平台以及框架提供的i18n组件已经是相当成熟的一套系统,这里不再赘述。这次改造的难点还是在如何在已有的流程中抠出需要翻译的文本,以及管理各页面翻译文本的加载。
在流程改造初期,一个繁重但必不可少的工作就是在全流程代码抠出需要翻译的展示词条。为了方便管理以及优化资源分配,整个业务层将词条分页整理为多个数组:其中全流程都使用的基础词条(如“确定”、“取消”等)单独列为一个数组;而页面独有的词条根据页面纬度分别建组。数组里的每个词条实体包含一个键值对,键为提供给Shark平台翻译唯一标记的key,值为其key对应的默认简体中文文案。
// 通用KEYexport const sharkCommon = { goToDetailPage: { id: 'key.flight.postservice.common.godetail', defaultMessage: '去订单详情', }, calendarTitle: { id: 'key.flight.postservice.calendar.title', defaultMessage: '选择日期', }, // ...}// 退票选程页用的KEYexport const refundSelectTrip = { changeTagTxt: { id: 'key.flight.postservice.refund.select.change.tag.txt', defaultMessage: '航班调整', }, refundBtnTxt: { id: 'key.flight.postservice.refund.select.refund.btn.txt', defaultMessage: '申请退票', }, // ...}shark keys

页面加载时,根据各个页面使用情况动态加载去加载翻译文本。每个页面继承了一个基础的页面组件(CommonBasePage),组件加载后(于RN的生命周期componentDidMount)首先需要锁住页面的渲染展示加载态,这时两条业务线的加载逻辑略有不同。
针对Trip站点,加载态时需要拿着当前页面渲染使用的翻译词条给到Shark SDK申请翻译结果,翻译词条一般包含之前划分好的公用词条组和当前页面特有词条组,Shark SDK会拿词条的key与当前手机配置的区域语言匹配到翻译后文案并返回给业务端,当翻译返回后放开页面的加载态继续进行页面后续的渲染工作。而针对Ctrip站点,不需要向shark平台请求翻译结果,所有内容都已包含翻译键值对的默认翻译中,则直接跳过获取翻译这一步,并取消加载态进入后续的页面渲染。
Shark平台其实也提供了简体中文的翻译,那么为什么不将Ctrip业务线的展示词条也托管到shark平台统一管理呢?
这里有几方面的考虑:首先shark平台的简体中文需要开发和产品自行翻译且维护,和维护键值对的默认中文并无区别;其次,通过Shark SDK加载翻译需要额外的外部依赖调用,且在目前流程是阻塞式的,页面稳定性及页面加载效率来说不如本地读取键值对的方案;最后,Ctrip团队针对业务线已写有大量的UI自动化测试及单元测试且已接入CI/CD持续化构建平台,如若每次测试都需要额外调用Shark SDK,稳定性及自动化构建的效率也会受到挑战(关于自动化测试相关解决方案之后章节有更详细讨论)。

 页面获取翻译流程
在流程上线之后,仍需要对翻译结果查漏补缺,监控可能出现的因漏提翻译或系统错误导致的展示中文的情况。好在前端的所有文字展示都使用Text基础拓展组件,组件在触发渲染时对子元素所包含的字符串做一次正则检测。在Trip环境中若正则检测到中文,则发送一次警告。开发可以方便得通过警告信息关联的订单号或流水号定位到系统展示中文是因为该字段是漏提交了翻译还是系统错误造成的。(除去中文检测,此正则式还可以很方便得检测出null、NaN、undefined等页面的错误展示。)
static checkChildren(children?: ReactNode) { const chineseReg = /[\u4e00-\u9fa5]/; if (StringChecker.needCheck && children && typeof children === 'string') { if (chineseReg.test(children)) { if (!StringChecker.data[children]) { StringChecker.data[children] = DataStatus.DEFAULT; Expose.sendError('Error:find chinese', { str: children }, true); } } }}查找中文并报警

另外,针对一些特殊场景如人名等,可以配置相应的可忽略检查的语言以及业务模块。
// 章节尾注⑥ Shark:携程提供的多语言站点UI文案管理与翻译的一整套解决方案。实现提供原文后交于统一交于翻译团队,并通过其提供的SDK工具于业务代码中抓取下发对应翻译后的多语言结果。

3.5 国际单位的本地化(l10n)


和国际化(i18n)相对的,Trip站点也许考虑各计量单位的本地化(l10n即localization的简写,由首尾的l、n及中间的10个字母组成)。每个国家和地区对于货币、时间、重量、距离等的展示标准各有差异,因此需要根据APP所设置的地区与语言,动态得去转换所展示的计量数据格式。
例如时间的展示,不同的区域会展示如“01/01/2020 Monday”、“2020/01/01 月曜日”等格式。决定时间以何种格式展示,方法类似于上一章节的多语言翻译。基础页面组件(CommonBasePage)加载翻译语言词条时,也会拿手机当前语言及地区向Shark SDK请求对应的基础计量单位展示格式制式包,其中包含了诸如日期、重量、数字等计量单位展示时所使用的标准格式,之后的业务代码再将具体需要转换的数字向对应的格式进行转换。这样就使服务下发或计算出来的唯一格式的时间根据不同的APP设置转换为不同的格式。
货币,重量、距离、数字的千分位展示及小数默认位数等的个数都需要根据不同的地域语言做区分。转换使用方式类似,这里不再赘述。

 Currency转换

3.6 基于Gitlab Pipeline的自动化测试流程


在质量这一块,除了常规的UT之外,机票前端团队做了大量的自动化测试,这些自动化的流程适配于中台化开发的流程中,保证了Ctrip和Trip代码的质量。
我们基于Gitlab,在其Pipeline中加入相应的检测机制,把Soanr、UT、UI自动化等流程融合到流程中,确保每一次代码签入和MR都执行成功。 


在UI自动化测试实现过程中,内核采用的是Cucumber⑦和Puppeteer⑧运行业务代码的H5版本来实现测试。我们将CTRIP和Trip的测试步骤拆分(增加isIBU字段作为标识),在Cucumber底层会根据此标识区分测试Trip站点与Ctrip站点对应的页面。
针对于多语言环境,我们执行在Trip站点用例时会分情况进行判断。如若此用例更多偏向测试业务逻辑而不太在意翻译的内容与质量,运行过程将屏蔽获取翻译的流程,直接使用默认的简体中文内容,这样我们的测试用例断言内容全部都不需要修改,并且屏蔽了因翻译服务不稳定、翻译内容时常变更带来的比确定性。而若此用例需要考虑进测试多语言翻译结果的正确性,则可以给予标识打开翻译流程,实时获取翻译内容进行校验。

 // 章节尾注⑦ Cucumber:一个基于行为驱动(BDD: Behavier Driven Development)的开发测试工具(https://cucumber.io/);⑧ Puppeteer: 一个通过提供高阶API用于控制Chrome或Chromium的Node工具集(https://pptr.dev/)。

3.7 发布与监控


相较于多端分开开发,中台化开发还带来一个好处是发布更加便捷、易于管理、更新更加及时。
原先发布就一个需求而言,全平台上线需要先后于MCD平台分别发布IOS、Android版本,于Ares打包发布H5的静态资源,于PAAS平台将H5打包结果发布生产站点。特别的,IOS、Android需要跟随APP主程序包的更新进行发布,这样便限制了APP端业务的发布节奏。也就是说进行单频道的热更新修复或者紧急需求上线比较困难,并且上线之后未更新的客户端仍无法使用最新的业务逻辑。
进行中台化开发后的订后产品,使用相同的技术栈,在APP端采用CRN框架开发,在IOS、Android、H5统一使用MCD发布系统进行打包发布,避免了多平台发布的差异性。并且使用中台化开发后,所有站点需求会在统一的发布时间节点进行统一发布,意味着Trip站点和Ctrip站点的需求可以统一管理发布上线。使用CRN还可以很方便得在APP内进行热更新,和APP版本发布相解耦,实现了需求的随发布随使用,解决了紧急修复难于上线的困难。

 统一前后的发布逻辑
在监控上,Ctrip和Trip站点接入统一的监控平台,数据采集统一汇总到hickwall中,制作相应的监控面板和告警规则,针对客户端Error统一采集并分发到Bigeyes管理平台,同时针对上面提到的异常空字符串也会做到及时监控、及时告警。

四、小结



以上几点便是机票前端后服团队针对Ctrip和Trip实现中台化开发的一些探索,虽然磕磕碰碰绕过弯路,现在仍有一些待优化的点。但阵痛过后的收获是满满的:现在Ctrip和Trip站点在APP、H5、Online三线业务逻辑统一,相同功能迭代的开发用时减少,多个站点只需要维护一套代码,开发成本得到很大的降低,开发效率也有很大的提高。
【推荐阅读】




 “携程技术”公众号

  分享,交流,成长



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存