本文将介绍在具体业务实践中,携程玩乐团队一套代码多端复用的一些实践与经验,希望能给面对同样问题的同学提供些思路和参考。
在多端开发实践之前,玩乐的前端开发存在如下一些问题:
1)技术栈架构繁杂且陈旧
到2019年2月,整个玩乐前端系统随着不同需求的叠加积累以及其他原因,造成了不同业务线的基础架构,技术栈不一致。当时的页面技术栈包括:.net页面、imvc页面、nextjs页面、jquey页面等,这些不同技术栈的应用从写法、结构、思想等方面都有不少出入。在公司全面推行React技术栈的背景下,诸如 JQuery、.net的前端技术架构显得十分陈旧,各个技术栈之间形成了天然的技术壁垒,不利于代码维护和人员培养。
2)统一产品需求,需要多端多渠道实现
改造前业务团队是按照端和渠道来进行具体的需求划分,如下图所示:
这就意味着前端需要支持国内、海外包括 PC、H5、Hybrid、RN七个(海外版没有Hybrid页面)端的需求代码实现,如果按照改造前的模式,将会有同学为了这个需求更改6个仓库的代码。可以预见,如果这种模式不更正,前端开发将是在来回切换仓库中度过的。
3)发布困难,监控麻烦
正是由于技术架构不统一,所以改造前的应用从打包到发布,其间的流程、打包技术存在很大差异,发布变成了一件手动且不可预期的工作。改造前的页面只是接入了一些公司日志框架,对于一些业务性能指标、细颗粒度的指标并未记录,这也导致了排查问题流程长,效率低下,开发人员只关注完成代码并不知道页面性能等问题。
针对前端系统存在的问题,我们进行了一些调研和不同框架之间的横向比对。遗憾的是目前并没有一套架构设计可以同时运行在多端,所以我们开始了对多端架构的持续开发与改造的过程,重点措施如下:
1)架构升级
对代码结构和业务逻辑实现做合理划分,在不同层级架构上做合理的事,并且对代码做测试,引入新的日志系统,接入新的打包发布流程。
2)面向组件化的开发策略
尽可能多的将需求逻辑抽象成各个组件,再将这些组件拼接成页面。基于这个角度,我们将组件划分成基础组件和业务组件两个部分。
3)支持PC/H5/RN同时预览
作为多端开发的实践,为了确保开发效率,需要满足一处修改多端同时可见。
为了能让RN和非RN的代码同时运行,如果按照改造前的做法会出现如下一些问题:
1)由于是一套代码,在发布的时候会出现不断改代码配置然后再去发布的问题;
2)代码逻辑划分不明确,会出现一类逻辑,到处都有的问题;
3)非RN打包中会出现RN的某块代码,从而造成打包代码体积过大和其他不可预知的问题;
针对以上几点,我们对前端架构做了升级,具体措施如下。
为了确保一次修改多端发布,我们按功能模块将仓库划分为四个层级,如下图所示:
1)基础、组件类库
这个仓库提供了页面加载所需要的一些基础服务,比如 React.Component的二次封装、页面信息生成、AJAX,fetch服务,定位服务等;以及一些与业务场景无关的组件,比如:多语言组件、弹框组件、价格组件等。
2)业务组件类库和SSR仓库
业务组件类库为业务开发需要关注的仓库,所有的业务开发调试都是基于这个仓库的。SSR仓库提供几部分功能,包括SEO,SSR会读取业务组件仓管内不同页面的生命周期,从而在不同的页面内生成不同的SEO内容,以下为页面生命周期函数的逻辑,其中data为服务端渲染的数据:
路由生成,这部分通过配置文件完成,具体配置文件内容如下:
配合SSR仓库,业务组件类库可以实现本地服务端渲染开发。
3)工程化仓库
这个仓库的提供的服务主要包括:
4)发布仓库
发布所依赖的仓库必定包含所有发布所用到的资源,包括代码和配置,之前的一贯做法是把前后端所涉及到的代码和配置都统一放到一个仓库内进行发布,但是在多端统一的背景下并不适用。这种场景下代码应当只有一套,变化的只有配置文件,结果就是开发仓库只有一个,而发布仓库有多个,发布仓库被架空,里面只发布配置,下图为一个发布仓库的目录结构:
其中 app.config.js 为应用配置信息,app.js为入口文件,package.json为发布所依赖资源,其配置也很简单,如下图所示:
经过如上的仓库拆分,我们能够让开发者只专注于业务组件的开发,其余的基础服务功能都能做到在一次开发,稳定使用的同时完成多端的同时开发和发布。
由于是面向组件化的开发策略,下面简单介绍下组件多端开发的一些细节与实现:
这部分主要针对目前所要接入的第三方组件,包括多语言组件、头尾组件、locale组件等。在定制API和上下文参数的时候,我们尽量向上扩展API。
比方说,我们的代码内都不允许直接取中文,所有取文案的地方都是使用封装的translate组件去拿对应文案。这样的好处是,在使用、解释层面ctrip和trip保持了统一,另一方面也为以后ctrip可能存在文案走配置服务做了铺垫,其他服务和组件API的封装也遵循这个原则。
我们遵循约定大于配置的原则,以单个页面为例,以下为页面的一些约定配置:
经过以上的约定划分,我们的开发模式变成了开发人员拿到需求,先研究三端的view层代码结构,确定出View的大致结构,然后先按H5的逻辑开发一套action和与之匹配的H5View。然后再在此基础上做加法,去书写RN的view,PC的view绝大多数情况下可以通过结构和样式抹平差异,只需要在pc.scss去书写差异化的css即可。
由于PC和H5页面大多数情况下只需要通过样式就可以做到多端实践,所以在多数情况下并没有index.pc.tsx和index.h5.tsx,只有一个公用的index.tsx,所以我们不能手动去引入平台样式。
我们做了这样的处理,一是对单位做换算,.scss书写的主单位还是px,由于某些原因我们在H5上的展示单位是rem,所以在打包的after-emit阶段,通过插件把px换算成rem;二是在打包的buildModule阶段,动态的将页面依赖的css放进css依赖树,从而实现动态打包效果。如下为实现代码:
在做前后端同构的时候,以fetch模块为例,假设只是简单的判断前端用前端的ajax模块,服务端用服务端的request模块,逻辑看起来没问题。但是事实上如果在打包逻辑上没有做比较复杂的改进,那么是不可能打包成功的。
所以通常的做法,是在逻辑中先声明一个为空的变量,在运行时初始化这个变量,那么在代码执行到该处逻辑变的有意义,而打包由于是静态检测所以并不会把任何不需要的代码打进包内。这里在处理RN模块和正常的H5模块的时候也采用类似的技术:
1)我们会在有RN模块的场景下声明一个rn-polyfill.ts,大致内容如下:
2)在index.rn.ts中对变量赋值:
3)在我们的初始化RN页面(比如index.ios.js)中引入 index.rn.ts。
这样的话,我们就实现了模块打包的分离。
由于篇幅原因,其他模块(测试、日志监控等)的介绍这里就不进行了。
先介绍下改进后的效果,组内成员除开业务逻辑整理,可以做到无缝替换开发;和之前开发模式对比,同一个需求多端完成时间,节省了近四成。目前框架功能依旧在不断地迭代和更新,近期希望能消除view和style层需要同时书写的情况,从而让开发迭代更加效率和速度。
作者介绍:
Neo,携程前端开发工程师,负责玩乐前端架构相关开发工作。
领取专属 10元无门槛券
私享最新 技术干货