当前位置: 首页 > 科技观察

IslandsArchitecture在携程新首页的实践

时间:2023-03-12 02:06:35 科技观察

作者简介携程前端框架团队在PC、H5、小程序等阶段为携程集团各业务线提供优秀的Web解决方案。目前主要关注方向包括:新一代研发模式探索、Rust构建工具链接升级、Serverless应用框架开发、在线文档系统开发、低代码平台建设、老龄化无障碍探索等一、项目背景2022年,携程PC版首页终于迎来了第一次改版,完成了用户体验和技术栈的全面升级。作为连接用户的重要入口,旧版PC首页已经陪伴携程22年。在承担重要使命的同时,也遇到了很多问题:维护/更新难,祖传代码黑盒逻辑过多,新产品推广困难。随着需求的上线,旧版首页已不能满足快速发展的业务需求。技术栈陈旧,不统一。互联网技术日新月异。旧版首页整体架构设计和技术栈相对落后,大首页各个组件的研发涉及多个业务部门的协作。存在技术选型差异化的问题,增加了维护成本。用户体验有待提高。旧版携程首页的设计风格沿用至今。在视觉和交互方面,已经难以满足用户不断提升的互联网体验和审美需求。基于以上情况,为了给用户提供更好的服务,携程首页的整体改造迫在眉睫。2、需求分析携程首页改造需要考虑的核心问题包括以下几个方面:技术选型为优化首屏性能,提升用户体验,携程新首页采用服务端渲染方式.在技??术选型上,考虑到我们希望应用层是轻量级的,只做两件事:页面HTML拼接和响应,最终决定基于Node.js构建一个应用载体,客户端使用公司主流的React技术栈。跨团队合作首页作为携程的重要入口,涉及多个业务线的流量入口。如图1所示,我们可以将整个页面按照业务线划分为多个组件模块。图1携程首页业务模块切分图。由此可见,整个页面的研发需要框架部门和各业务单元业务团队的密切配合。这就需要一套完整的跨团队合作模型。其中,我们希望业务团队只需要专注于业务逻辑的实现,完成组件模块的开发。框架团队负责提供:组件模块服务端/客户端构建方案组件模块服务端渲染方案应用层,实现页面组装和响应组件模块开发环境监控维护上线后需要时刻关注应用状态和应对异常情况。因此,需要对应用程序和组件进行嵌入式监控。另外,由于需要跨团队协作,对于业务组件,我们希望各个业务团队不仅能够实现开发/构建自由,相互独立,互不影响,而且能够实现自主可控。监控和版本管理。因此,我们将各个业务组件打包成一个Node.js应用。开发者可以直接在发布系统中查看组件版本完成发布/回滚,也可以通过应用ID在埋点管理平台上查看组件的相关埋点。3.整体架构设计图2携程首页架构设计图基于以上需求分析,携程新版首页的整体架构设计如图2所示,主要分为四个部分:业务模块开发我们对携程首页进行了拆分分为多个业务模块,每个业务团队负责完成相应组件的开发。不同于常规的React组件开发,首先开发者需要在配置文件中设置模块相关的配置,比如组件的唯一ID;其次,组件开发需要遵循一些规则,比如为了防止样式污染,我们强制使用CSSModules;最后,我们支持服务端渲染组件,可以在服务端生命周期拉取数据,然后使用在服务器/客户端上。为了更好的协助业务团队完成组件开发,框架团队将提供脚手架,帮助创建组件模板,搭建开发环境,模拟完整的首页场景。业务模块构建业务模块开发完成后,需要构建/发布到生产环境。整个构建过程会在Pipeline中完成,开发者gitpush代码后会自动触发。我们会根据不同的入口和配置,使用webpack完成客户端和服务端代码的生产态构建,并将客户端构建产物(js+css)上传到静态资源管理系统。之后我们将服务端构建产品(js)连同组件和静态资源版本信息一起打包成一个Job应用。在这个应用中,会有一个定时任务负责推送当前版本信息,并触发组件完成服务端渲染。这里我们是使用定时器来实现定时任务管理。最后,开发者需要在发布系统中将构建好的应用镜像部署到生产环境中,完成组件的发布。业务模块服务端渲染业务模块服务端渲染主要包括两部分:在沙箱中完成服务端渲染,将组件相关信息和渲染后的html存储在Redis中,将相关功能打包成云函数,分别是随着服务的推出而提供。因为有些组件有服务端渲染的数据更新需求。所以我们上面提到,在Job应用中会有一个定时任务,负责触发组件在服务端渲染,这里也会触发云函数的调用。应用页面组装最后,我们需要组装应用中的所有业务模块,定时从Redis获取组件相关信息,组装首页html返回给客户端。4.整体架构核心功能的实现对应于上述首页架构设计,我们简单介绍以下核心功能的实现:4.1搭建模拟首页场景的组件开发环境。我们会在开发阶段提供脚手架,协助业务团队开发组件,其中一个重要的功能就是搭建组件开发环境。webpack是如何搭建React开发环境的,这里不再赘述。为了实现统一规范的开发环境,我们还做了以下工作:将webpack和babel的相关配置封装到cli中,有选择地提供可配置项,以规范组件开发环境关闭入口。这里要注意,服务端和客户端的入口是不一样的。对于客户端的入口,需要获取服务端传输过来的数据,通过调用ReactDOM.render()完成渲染:importReactfrom'react'importReactDOMfrom'react-dom'importCompfrom'COMP_PATH'constrender=async()=>{letdata//获取从服务器传递到客户端的数据constcontainer=document.getElementById('__MFE___MODULE___DATA__')if(container&&container.textContent){try{data=JSON.parse(container.textContent)}catch(e){console.log(e)}}constroot=document.getElementById('__MODULE__')//客户端渲染组件if(module.hot){ReactDOM.render(,root)}else{ReactDOM.hydrate(,root)}}render()为服务端入口,需要调用服务端-sidelifecycle拉取数据,并调用renderToString()完成渲染:nc()=>{letdata//执行服务器生命周期if(Comp.getInitialProps){data=awaitComp.getInitialProps(_ctx)}//在沙箱中输入setMfeData方法,查看服务端渲染组件实现setMfeData(data)//服务端渲染组件,返回htmlreturnrenderToString()}exportdefaultrender()构建首页场景我们希望开发者在开发组件时能够看到嵌入整个首页的效果,而不是只能看到自己的组件。因此,在服务器端处理页面请求时,我们通过以下方式构建首页场景:读取首页html文件(第一次从线上拉取),解析/处理首页html,去除在线脚本/链接当前组件相关标签,在沙箱中添加开发状态构建产品的服务端渲染组件,在首页html中替换组件html4.2SSR-Service服务端渲染组件。我们会在沙箱中运行服务端构建生成的代码(结合上面的服务终端入口),完成组件渲染,获取服务端生命周期返回的数据和组件html。constvm=require('vm')constrender=async({content,request})=>{//content是服务器构建生成的代码constscript=newvm.Script(content)letmoduleObj={exports:{}}letmfeEnv='prod'letmfeData//模拟reqconst_req={url:request.rawPath,query:request.queryStringParameters,headers:request.headers}letsandBox={...global,process,require,module:moduleObj,console,_ctx:{req:_req,env:mfeEnv,},setMfeData:(data)=>{mfeData=data}}//在沙箱中运行,进行服务端渲染constctx=vm.createContext(sandBox)script.runInContext(ctx)constcomp=awaitsandBox.module.exports.defaultreturn{comp,mfeData}}4.3整体页面组装在首页应用中,我们会定时从redis中获取组件相关信息,组装首页页面html,客户端请求进来时,直接返回缓存中最新的html。letindexCache=''constrenderPage=async(content)=>{//加载主页htmlconst$=cheerio.load(content)//更新组件for(letmoduleofmodules){try{//moduleData从redis获取让data=moduleData[module]||''if(!data){continue}data=typeofdata=='string'?JSON.parse(data):dataconst{comp,version,mfeData,style}=data//更新组件相关的html、link、script标签parse(module,comp,$,version,mfeData,style)}catch(e){console.log(e)}}//生成htmlconstpayload=$.html()if(!payload){throwError('renderPageerror-htmlisnull')}//更新缓存indexCache=payload}5.公共组件的渲染原理和技术细节上面我说的是孤岛架构的整体主页架构和独立组件渲染的核心实现。一些独立的组件(左侧菜单栏、页眉等)不仅在大主页中使用,在其他页面中也会使用。它们在这里被称为公共组件。5.1公共组件需求点和痛点分析在开始开发公共组件之前,需要梳理一下目前各业务单元的接入需求、成本和痛点。因此,总结出以下问题:每个业务单元的站点技术架构不同,因为每个业务单元的站点技术架构不同。有的划分可能是服务端渲染,有的可能是客户端渲染。在服务端渲染中,技术栈中可能会出现JAVA和NODE。在客户端渲染中,各业务单元的技术栈并不统一,有React、JQuery、Vue等前端框架。这里的问题是各个业务单元的技术栈错综复杂。如果分开维护,会导致版本不同,维护成本高。能不能把所有页面的公共组件都统一热更新?当公共组件发生变化或者有问题需要修复时,我们不能让所有的页面都去改变公共组件。相反,我们应该更改所有页面上的公共组件。会默默生效,各业务部门无需关心公共组件的变化。公共组件的样式如何才能不对页面产生巨大的影响由于各个业务方的样式各不相同,并且存在一些全局公共样式,如何保证每个接入方都有如下图所示的页面布局方式,它的页面组成方式是shadow有的是分部维护的组件,有的是公共组件。由于历史原因,老版本的公共组件已经使用多年。新版头尾的布局结构与旧版不同。如何设计它以最小化更改而不是进行大更改以适应公共组件。大首页新旧版本布局变化如下:公共组件渲染性能问题。对于后台提到的不同表单的公共组件(比如有些不需要左侧菜单或者不同的header样式),如何在客户端第一个?时间向用户显示相应的组件形状并支持搜索引擎优化(SEO)。当页面上有多个公共组件时,如何快速加载和渲染。5.2解决公共组件和痛点问题问题一:各业务部门站点技术架构不同维护多个公共组件的维护成本极高,没办法做到一套代码多端。这里从服务端渲染和客户端渲染来分析,CSR中提供了对应的解决方案CSR(client-siderendering),技术栈也不同。既然有React、Vue、jQuery,我们需要提供的应该是原生JS的公共组件,可以保证维护成本。但是大首页的首屏技术栈已经是React了,再去开发和维护一套原生的JS组件就显得多余了。所以需要一个解决方案来支持多种技术栈的运行,兼容我们大首页和首屏的技术栈。最终的解决方案是使用Preact,它非常轻量级,重点是可以帮我们解决多技术栈操作,兼容React。但是如果一个页面也在使用Preact并且与我们发生冲突怎么办?这里将Preact单独打包到common包中,并重命名了全局变量。这样即使页面使用了Preact,也不会和我们发生冲突。在webpack的externals选项中,可以配置组件需要的包名。{//...externals:{preact:'xxxxxx'}//...}SSR(服务端渲染)在SSR中,Preact是在技术栈上选择的。Preact也支持SSR,你可以自己构建服务端渲染JS来支持SSR。因此,我们的问题就解决了。我们在构建组件时额外生成一份Preact的SSRJS,使用沙箱进行服务端渲染输出HTML并存储。我们调研过去老旧的公共组件,携程的所有业务线只有两个技术栈:JAVA和NODE,所以我们只需要提供两个接入壳——两套不同语言的SDK和接入内部方法是获取一个统一的公共组件HTML字符串供页面使用。{//...解析:{扩展名:['.ts','.tsx','.js'],别名:isPreact?{"react":"preact/compat","react-dom":"preact/compat",//必须在test-utils下面"react/jsx-runtime":"preact/jsx-runtime"}:{},}//...}(ReacteasilyconvertsPreact)问题2:所有页面中的公共组件是否可以统一更新?当更新公共组件或修复一些紧急问题时,不应影响业务页面。它应该自动更新。当用户访问该页面时,他们将看到最新的。公共组件,所以我们不采用类似于npmpackagemulti-version的方式来管理它们。基于问题1:在SSR(ServerRendering)的情况下,SSR服务器的HTML从何而来?HTML如何保持最新?我们需要构建一个服务端JS在沙箱中输出HTML,存储到Redis中,从多个公共组件构建多个HTML,分别存储到Redis中。实际上,业务方访问JAVA和NODESDK只有一件事:daemon进程定时去Redis获取最新的HTML结果。对于CSR(客户端呈现),CSR如何针对公共组件保持最新?需要一台机器定时从Redis读取数据,并对外暴露一个接口供客户端JS调用,就像多语言技术栈SDK一样。这样,每次用户访问页面时,客户端JS都会发起请求,保证用户看到的内容始终是最新的。问题三:样式问题目前新版本的公共组件在样式和交互上比老版本更加复杂。由于左侧菜单的存在,布局结构不同,各业务部门的页面样式可能不同,很难保证不影响自己的风格和事件。例如:如果你使用flex布局,你需要在最外层应用一个div。如果不应用,需要给body元素添加flex样式,但是不能保证其他业务部门页面的body是否有其他样式,甚至不能保证body里面是否有其他div元素等。还有很多业务部门的页面在body上监听的还有滚动等事件,所以如果外层设置为div,这种形式会让原页面的事件监听和滚动很麻烦,而各个业务部门用来监控body的事件,需要一一更改。观察老项目,我们发现之前的公共组件骨架有一个最外层的div元素和一个名为“container”的id。我们要做的就是把左边的菜单固定在左边。关于cssFixedcompatibility:(Style属性兼容性)但是这时候有个问题,我们的左侧菜单是可以展开或者收起的。因此,在展开和折叠时需要一个全局的通信机制。当左边的组件发生变化时,应该在组件内部触发一个全局通信钩子,通知容器id的div元素跟随左边菜单的变化,实现布局的灵活性。影响。(左边菜单展开)(左边菜单收起)问题4:性能问题根据问题1/2/3,技术方向大概已经拟好了,已经可以在各种工作了业务部门,证明思路是没问题的,但是还有一些琐碎的问题需要考虑:因为是定时从服务器拉取,所以不是第一次拉取或者在客户端渲染的情况,让请求返回HTML进行异步渲染是不是太长了?为了解决上述问题,我们考虑先准备一个预渲染的HTMLplaceholder,类似于骨架屏的意思。这时候可以先渲染骨架屏,然后异步拉取渲染,解决异步渲染白屏等待的问题。是否可以合并多个公共组件的客户端JS资源,Preact公共包也合并打包在一起。为了解决这个问题,我们的沙盒JOB机器可以继续这样做。因为每个组件在构建后都有一个资源版本,所以我们需要存储一份版本。一个新组件构建完成后,拉取其他公共组件的资源版本,将多个JS组装在一起。同时,因为我们使用了Preact,所以我们将Preact抽取出来作为一个公共依赖放在最上面,以保证它的第一次执行。6、公共组件的数据动态配置体系介绍完携程新版大首页公共组件的渲染原理和技术细节,接下来就是公共组件中的数据如何支持动态配置。6.1为什么要动态配置组件数据?携程PC版首页改版时,将整个页面按照业务线划分为多个组件模块,每个组件模块包含需要展示的业务数据。页面上线后,业务数据会随着产品需求的变化而频繁更新。如果每次更新数据都发布模块代码,成本和风险会很高。因此,需要将代码和数据分开发布。当组件数据发生变化时,不需要释放组件。搭建一个专门发布大首页数据配置的管理系统势在必行。6.2组件数据动态配置系统需求分析数据审核规范及发布流程,主要功能包括:规范数据配置上传格式,比较本地配置数据与在线配置数据的差异,制定不同组件模块的数据校验规则,以及用于在数据配置发布前验证数据的合法性效果预览确保不与其他在线组件模块交互。更新在线页面。6.3组件数据动态配置系统架构设计图1大主页数据配置管理系统架构设计数据配置管理系统架构设计(如图1所示),为了实现需求分析中的四大主要功能,整个管理系统主要构建两个应用:前端应用:以可视化界面的形式提供本地上传配置文件,预览数据效果和更新页面,同时完成数据校验和预览检测。节点服务:主要负责数据配置的处理和发布,将前端应用上传的数据配置保存到QConfig系统中。其中,前端应用提供的预览功能的架构设计如下图2所示:图2预览功能的架构设计预览功能的实现主要依赖三部分(如图2所示):前端应用:负责提供数据配置和展示页面效果。服务端渲染应用:调用组件渲染函数,根据数据配置渲染当前组件HTML,从Redis中拉取其他组件的HTML,然后组装成一个完整的HTML页面吐出给前端应用.Redis:存储所有组件模块的HTML。6.4数据配置管理系统核心功能的实现上一节介绍了数据配置管理系统的架构设计,这里详细介绍该架构核心功能的实现,主要包括:数据配置规范和验证组件和页面预览数据配置规范和数据验证本地上传的数据配置最终会传递给组件进行渲染,但是数据配置的上传者不一定是组件的开发者,上传者也不一定知道组件需要的数据的类型和结构,那么如何保证上传的组件的数据和组件需要的数据结构一致呢?这就需要管理系统制定一套数据配置规范,对上传的数据进行约束。但是,不同的组件具有不同的数据结构,因此每个组件都应该有自己的一套规范。管理系统提供了两种制定数据规范的方式:输入组件的基本信息,包括详细的数据结构:数据名称、数据类型、强制或可选等。当使用TypeScript(推荐的组件开发语言)开发组件时),你可以上传.d.ts声明文件,系统会根据这个文件解析出具体的组件信息和数据结构。规范制定后,由管理系统存储。上传者每次上传某个组件的数据配置(为了方便上传者修改数据,管理系统规定数据配置以JSON文件形式提供),系统会根据该组件.根据数据规范验证上传的数据配置。如果验证通过,将显示上传数据与在线数据的差异,上传者可以进行预览操作;如果校验不通过,会提示失败原因和具体数据不规范,上传者不能进行后续的预览操作,需要重新上传数据配置,直到校验通过。组件及页面预览这部分功能的核心实现在SSRService服务端渲染组件中(上面有详细介绍,这里不再赘述),主要分为以下几个步骤来完成:应用的组件渲染功能收到对应的组件数据后,标准配置数据,通过Props将数据传递给组件,然后渲染当前组件的HTML。从Redis中取出其他模块的HTML,与当前组件的HTML拼接在一起。为了保证预览的可靠性(减少其他模块出错时对当前组件的影响),其他模块与生产HTML拼接。为什么一定要将其他模块的HTML拼接在一起才能预览?为了测试配置数据发布后对其他组件模块的影响,如果有影响则不能发布,以保证在线页面的安全。7.总结本文介绍了携程新首页项目系统的总体架构设计、组件开发、数据配置过程和实现原理,是孤岛架构的一次实践。希望能为大家以后的跨团队组件开发项目有所收获。