一、前言
本文将介绍在具体业务实践中,携程玩乐团队一套代码多端复用的一些实践与经验,希望能给面对同样问题的同学提供些思路和参考。
1.1 背景
在多端开发实践之前,玩乐的前端开发存在如下一些问题:
1)技术栈架构繁杂且陈旧
到 2019 年 2 月,整个玩乐前端系统随着不同需求的叠加积累以及其他原因,造成了不同业务线的基础架构,技术栈不一致。当时的页面技术栈包括:.net 页面、imvc 页面、nextjs 页面、jquey 页面等,这些不同技术栈的应用从写法、结构、思想等方面都有不少出入。在公司全面推行 React 技术栈的背景下,诸如 JQuery、.net 的前端技术架构显得十分陈旧,各个技术栈之间形成了天然的技术壁垒,不利于代码维护和人员培养。
2)统一产品需求,需要多端多渠道实现
改造前业务团队是按照端和渠道来进行具体的需求划分,如下图所示:
这就意味着前端需要支持国内、海外包括 PC、H5、Hybrid、RN 七个(海外版没有 Hybrid 页面)端的需求代码实现,如果按照改造前的模式,将会有同学为了这个需求更改 6 个仓库的代码。可以预见,如果这种模式不更正,前端开发将是在来回切换仓库中度过的。
3)发布困难,监控麻烦
正是由于技术架构不统一,所以改造前的应用从打包到发布,其间的流程、打包技术存在很大差异,发布变成了一件手动且不可预期的工作。改造前的页面只是接入了一些公司日志框架,对于一些业务性能指标、细颗粒度的指标并未记录,这也导致了排查问题流程长,效率低下,开发人员只关注完成代码并不知道页面性能等问题。
1.2 举措
针对前端系统存在的问题,我们进行了一些调研和不同框架之间的横向比对。遗憾的是目前并没有一套架构设计可以同时运行在多端,所以我们开始了对多端架构的持续开发与改造的过程,重点措施如下:
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 为服务端渲染的数据:
路由生成,这部分通过配置文件完成,具体配置文件内容如下:
IS_TRIP、IS_CTRIP、IS_ONLINE、IS_H5 用于指定页面属性,为 false 则渲染的时候不会生成该配置的页面;
PATH:用于指定页面路由;
CHILDREN:用于指定页面的子页面,用于 SPA 应用;
ALIA:非必传,默认为页面文件名称,用于获取多语言服务;
配合 SSR 仓库,业务组件类库可以实现本地服务端渲染开发。
3)工程化仓库
这个仓库的提供的服务主要包括:
init 安装所有依赖,可以支持多版本的 RN 依赖
watch 热更新
dev 能同时运行 crn、h5、online
ares 集成 ares 平台打包静态资源
precommit 提交前自动化 review 代码规范
publish 自动化发布 nodejs 资源
4)发布仓库
发布所依赖的仓库必定包含所有发布所用到的资源,包括代码和配置,之前的一贯做法是把前后端所涉及到的代码和配置都统一放到一个仓库内进行发布,但是在多端统一的背景下并不适用。这种场景下代码应当只有一套,变化的只有配置文件,结果就是开发仓库只有一个,而发布仓库有多个,发布仓库被架空,里面只发布配置,下图为一个发布仓库的目录结构:
其中 app.config.js 为应用配置信息,app.js 为入口文件,package.json 为发布所依赖资源,其配置也很简单,如下图所示:
经过如上的仓库拆分,我们能够让开发者只专注于业务组件的开发,其余的基础服务功能都能做到在一次开发,稳定使用的同时完成多端的同时开发和发布。
三、具体措施
由于是面向组件化的开发策略,下面简单介绍下组件多端开发的一些细节与实现:
3.1 服务和 API 封装
这部分主要针对目前所要接入的第三方组件,包括多语言组件、头尾组件、locale 组件等。在定制 API 和上下文参数的时候,我们尽量向上扩展 API。
比方说,我们的代码内都不允许直接取中文,所有取文案的地方都是使用封装的 translate 组件去拿对应文案。这样的好处是,在使用、解释层面 ctrip 和 trip 保持了统一,另一方面也为以后 ctrip 可能存在文案走配置服务做了铺垫,其他服务和组件 API 的封装也遵循这个原则。
3.2 代码职责划分
我们遵循约定大于配置的原则,以单个页面为例,以下为页面的一些约定配置:
fetch:这一层负责页面的网络请求获取,在做 SSR 渲染的时候,代码会以这个文件入口获取页面所需要的数据,并以 props 的形式传到页面内;
view:这一层用来描述不同平台的视图展示,不通入口文件的 UI 结构基本一致,不包括任何的非 UI 展示逻辑;
fomat:重新组装从 restful 下发的数据,经过这一层验证和转换后,后续逻辑都是即拿即用;
action:这一层是多端统一开发的关键,所有的页面非 UI 展示逻辑都需要写在这里;
style:这一层配合 view 层决定了页面具体的展示样子,但和正常开发不同,这部分的模块不需要手动引入,具体做法后面会介绍。
经过以上的约定划分,我们的开发模式变成了开发人员拿到需求,先研究三端的 view 层代码结构,确定出 View 的大致结构,然后先按 H5 的逻辑开发一套 action 和与之匹配的 H5View。然后再在此基础上做加法,去书写 RN 的 view,PC 的 view 绝大多数情况下可以通过结构和样式抹平差异,只需要在 pc.scss 去书写差异化的 css 即可。
3.3 动态引入样式
由于 PC 和 H5 页面大多数情况下只需要通过样式就可以做到多端实践,所以在多数情况下并没有 index.pc.tsx 和 index.h5.tsx,只有一个公用的 index.tsx,所以我们不能手动去引入平台样式。
我们做了这样的处理,一是对单位做换算,.scss 书写的主单位还是 px,由于某些原因我们在 H5 上的展示单位是 rem,所以在打包的 after-emit 阶段,通过插件把 px 换算成 rem;二是在打包的 buildModule 阶段,动态的将页面依赖的 css 放进 css 依赖树,从而实现动态打包效果。如下为实现代码:
3.4 运行时注入依赖
在做前后端同构的时候,以 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,携程前端开发工程师,负责玩乐前端架构相关开发工作。
本文转载自公众号携程技术(ID:ctriptech)。
原文链接:
评论