之前幾天,攜程將它收購的旅行推薦網站Trip.com導向一個新的域名:www.trip.skyscanner.com。但在11月19日,攜程將Trip.com變成了其OTA內容的一個英語版本,Trip.com網站主頁與攜程長期以來官網Ctrip.com 的英語版存在一些差異。
從19日攜程重啟Trip.com網站開始,Trip.com主頁就刪除了任何關于網站屬于中資企業所有的提法,不過,在Trip.com的“關于我們”頁面,提到Trip.com是攜程集團的一部分。
Trip.com網站標志性的攜程標志(Ctrip logo)沒有了。網站有一個強調去中國城市的旅行產品的宣傳方框,但它提供去往全球200個國家的5000個城市的旅行產品。
截止目前,Trip.com公司尚未發表任何正式評價,不過,Trip.com網站似乎是攜程全球擴張戰略的一個新的支柱。
值得注意的是,攜程高管決定押注于利用Trip.com這個域名來擴張其OTA業務,而不是利用攜程于去年收購的元搜索引擎子公司天巡(Skyscanner)。
者簡介
攜程前端框架團隊,為攜程集團各業務線在PC、H5、小程序等各階段提供優秀的Web解決方案。當前主要專注方向包括:新一代研發模式探索,Rust構建工具鏈路升級、Serverless應用框架開發、在線文檔系統開發、低代碼平臺搭建、適老化與無障礙探索等。
2022,攜程PC版首頁終于迎來了首次改版,完成了用戶體驗與技術棧的全面升級。
作為與用戶連接的重要入口,舊版PC首頁已經陪伴攜程走過了22年,承擔著重要使命的同時,也遇到了很多問題:
維護/更新困難
祖傳代碼黑盒邏輯過多,產品也難以推動新需求的上線,舊版首頁已經不能滿足高速發展的業務需求。
技術棧陳舊且不統一
互聯網技術日新月異,舊版首頁的整體架構設計和技術棧都相對落后,且大首頁中各個組件的研發涉及多事業部合作,存在技術選型差異的問題,增加了維護成本。
用戶體驗有待改善
舊版攜程首頁的設計風格沿用至今,在視覺和交互層面上,都已經難以滿足用戶不斷提升的互聯網體驗和審美需求。
綜合上述情況,為了給用戶提供更好的服務,攜程首頁的整體改造迫在眉睫。
攜程首頁改造需要考慮的核心問題包括以下幾個方面:
技術選型
為了優化首屏性能,提升用戶體驗,攜程新版首頁采用服務端渲染模式。在技術選型上,考慮到我們希望應用層是輕量的,只做頁面HTML拼接和響應兩件事情,最終決定基于Node.js構建應用載體,客戶端則統一使用公司主流的React技術棧。
跨團隊合作
首頁作為攜程的重要門戶,涉及多業務線的流量入口。如圖1所示,我們可以將整個頁面進行切割,按業務線劃分成多個組件模塊。
圖1 攜程首頁業務模塊切分圖
可以看到,整個頁面的研發是需要框架部門和各個事業部業務團隊緊密合作才能完成的,這就需要一整套完善的跨團隊合作模式。其中,我們希望業務團隊只需要關注業務邏輯的實現,完成組件模塊的開發??蚣軋F隊則負責提供:
監控及維護
上線后,我們需要時刻關注應用狀態,及時響應異常情況。因此,需要對應用及組件進行埋點監控。除此之外,由于需要跨團隊合作,對于業務組件,我們希望各個業務團隊不僅可以實現開發/構建自由,彼此獨立互不影響,在監控及版本管理上也能實現自控。因此,我們將各個業務組件包裝成Node.js應用,開發人員可以直接在發布系統查看組件版本,完成發布/回退,也可以通過應用ID在埋點管理平臺查看組件的相關埋點。
三、整體架構設計
圖2 攜程首頁架構設計圖
基于上述需求分析,攜程新版首頁的整體架構設計如圖2所示,可以分為四個部分:
業務模塊開發
我們將攜程首頁拆分為多個業務模塊,由各業務團隊負責完成相應組件的開發。與常規React組件開發不同的是,首先,開發人員需要在配置文件中設置好模塊相關配置,如組件唯一ID;其次,組件開發需遵循一些規則,如為防止出現樣式污染,我們強制使用CSS Modules;最后,我們支持服務端渲染組件,可以在服務端生命周期中拉取數據,然后在服務端/客戶端使用。為了更好的輔助業務團隊完成組件開發,框架團隊會提供腳手架幫助創建組件模版,搭建開發環境,模擬完整首頁場景。
業務模塊構建
業務模塊開發完成后,就需要構建/發布至生產環境。整個構建過程會在Pipeline中完成,開發人員git push代碼后會自動觸發?;诓煌膃ntry及配置,我們會使用webpack分別完成客戶端及服務端代碼的生產態構建,并將客戶端構建產物(js+css)上傳至靜態資源管理系統。
之后,我們會將服務端構建產物(js)連同組件及靜態資源版本相關信息包裝成一個Job應用,該應用中會有一個定時任務負責推送當前版本信息,觸發組件完成服務端渲染,這里我們是使用定時器來實現定時任務的管理。最后,需要由開發人員在發布系統中將構建好的應用鏡像部署到生產環境,完成組件的發布。
業務模塊服務端渲染
業務模塊的服務端渲染主要包括兩部分:
我們將相關功能實現封裝成云函數,作為服務提供出去。由于部分組件對服務端渲染具有數據更新的需求。因此,上文我們提到過,Job應用中會有一個定時任務,負責觸發組件進行服務端渲染,這里也就是會觸發云函數的調用。
應用頁面組裝
最后,我們就需要在應用中將所有的業務模塊拼裝起來,定時從Redis中獲取組件相關信息,組裝成首頁html返回到客戶端。
四、整體架構的核心功能實現
對應上述首頁架構設計,我們簡要介紹下部分核心功能的實現:
4.1 搭建組件開發環境,模擬首頁場景
我們會在開發階段提供腳手架輔助業務團隊開發組件,其中一項重要功能就是搭建組件開發環境。常規的webpack搭建React開發環境我們這里就不贅述了,為了實現開發環境的統一標準化,我們還做了以下事情:
import React from 'react'
import ReactDOM from 'react-dom'
import Comp from '__COMP_PATH__'
const render=async()=> {
let data
// 獲取服務端傳遞到客戶端數據
const container=document.getElementById('__MFE___MODULE___DATA__')
if (container && container.textContent) {
try {
data=JSON.parse(container.textContent)
} catch(e) {
console.log(e)
}
}
const root=document.getElementById('__MODULE__')
// 客戶端渲染組件
if (module.hot) {
ReactDOM.render(<Comp serverData={data} />, root)
} else {
ReactDOM.hydrate(<ErrorBoundary><Comp serverData={data} /></ErrorBoundary>, root)
}
}
render()
對于服務端entry,則需要調用服務端生命周期拉取數據,并調用renderToString()完成渲染:
import React from 'react'
import { renderToString } from 'react-dom/server'
import Comp from '__COMP_PATH__'
const render=async()=> {
let data
// 執行服務端生命周期
if (Comp.getInitialProps) {
data=await Comp.getInitialProps(_ctx)
}
// 沙盒中傳入setMfeData方法,見下文中服務端渲染組件實現
setMfeData(data)
// 服務端渲染組件,返回html
return renderToString(<Comp serverData={data} />)
}
export default render()
搭建首頁場景。我們希望開發人員在組件開發時,就可以看到其嵌入在整個首頁中的效果,而不是只能看到自己的組件。因此,我們在服務端處理頁面請求時,通過以下方式搭建了首頁場景:
4.2 SSR-Service 服務端渲染組件
我們會在沙盒中運行服務端構建生成的代碼(可結合上文中服務端entry看),完成組件渲染,得到服務端生命周期中返回的數據及組件html。
const vm=require('vm')
const render=async ({content, request})=> {
// content即為服務端構建生成的代碼
const script=new vm.Script(content)
let moduleObj={
exports: {}
}
let mfeEnv='prod'
let mfeData
// 基于云函數中的request模擬req
const _req={
url: request.rawPath,
query: request.queryStringParameters,
headers: request.headers
}
let sandBox={
...global,
process,
require,
module: moduleObj,
console,
_ctx: {
req: _req,
env: mfeEnv,
},
setMfeData: (data)=> {
mfeData=data
}
}
// 沙盒中運行,執行服務端渲染
const ctx=vm.createContext(sandBox)
script.runInContext(ctx)
const comp=await sandBox.module.exports.default
return {
comp,
mfeData
}
}
4.3 整體頁面組裝
在首頁應用中,我們會定時從redis中獲取組件相關信息,拼裝首頁html,在有客戶端請求進入時,直接返回緩存中的最新html。
let indexCache=''
const renderPage=async (content)=> {
// 加載首頁html
const $=cheerio.load(content)
// 更新組件
for (let module of modules) {
try {
// moduleData為從redis取到的數據
let data=moduleData[module] || ''
if (!data) {
continue
}
data=typeof data=='string' ? JSON.parse(data) : data
const {comp, version, mfeData, style}=data
// 更新組件相關的html,link,script標簽
parse(module, comp, $, version, mfeData, style)
} catch(e) {
console.log(e)
}
}
// 生成html
const payload=$.html()
if (!payload) {
throw Error('renderPage error - html is null')
}
// 更新緩存
indexCache=payload
}
五、公共組件的渲染原理及技術細節
前面說的是島嶼式架構之首頁的整體架構和獨立組件渲染的核心實現,其中有些獨立組件(左側菜單欄,頭部等)除了在大首頁中使用,還會在其他的頁面中使用,這里就稱為公共組件。
5.1 公共組件需求點和痛點分析
在開始開發公共組件前,需要整理一下目前各個事業部的接入需求、成本及痛點。所以總結了以下問題點:
各個事業部的站點技術架構不同
由于各個事業部的站點技術架構不同。有的事業部可能是服務端渲染,有的可能為客戶端渲染 。在服務端渲染中,技術棧又可能出現 JAVA 和NODE 。而在客戶端渲染中,各個事業部技術棧也不統一,有React、JQuery或者Vue等等前端框架。這里的問題是各個事業部的技術棧的錯綜復雜,如果分開維護會帶來不同的版本及很高的維護成本。
所有頁面中的公共組件有變更時能否統一熱更新
當公共組件的改動或有問題需要修復時,不能讓所有的頁面都去變更公共組件,而是應該我們變更后,所有頁面上的公共組件會靜默生效,各個事業部無需關心公共組件的變更。
公共組件的樣式如何不對頁面造成巨大影響
由于各個業務方的樣式風格不同并且還存在一些全局的公共樣式,如何才能保證每個接入方為下圖的頁面布局方式,其頁面組成的方式為陰影部分是事業部所維護的組件,其他是公共組件。
由于歷史原因,舊版的公共組件已經使用了很多年了,新版頭尾和舊版的頭尾布局構造不同,要如何設計,才能使其改動最小,而不是去做很大的改動去適配公共組件。新舊版大首頁頁面布局變化如下圖:
公共組件的渲染性能問題
在背景中提到的不同形態的公共組件(比如有些不需要左側菜單或者頭部樣式的不同),如何在客戶端能第一時間展示給用戶相應組件形態并且支持搜索引擎優化(SEO)。當多個公共組件在頁面中如何能快速進行加載及渲染。
5.2 解決公共組件問題和痛點
問題一:各個事業部的站點技術架構不同
前面提到了各個業務支線的技術棧不統一的問題,并且還存在服務端和客戶端渲染的情況,如果為了多個技術棧去維護多個公共組件維護成本極高,且沒有辦法做到一套代碼多端使用。這里就從服務端和客戶端渲染分析,提供的相應解決的方案
CSR(客戶端渲染)
在CSR中,技術棧也不同。由于有React、Vue、jQuery,所以我們需要提供的應該是一個原生JS的公共組件,這樣能保證維護成本。但是大首頁的首屏技術棧已經為React,再去開發及維護一套原生JS組件顯得冗余。所以需要一個方案來支持多技術棧運行,并且能夠兼容我們大首頁首屏的技術棧。
最終的方案是使用Preact,它很輕量,重點是它可以幫助我們解決多技術棧運行并且能夠兼容React??扇f一有頁面同樣在使用 Preact 和我們沖突怎么辦? 這里將Preact單獨打包出來common包并且重名了全局的變量。這樣即使頁面使用了Preact也不會和我們有沖突,在webpack的 externals 的選項中可以配置組件需要的包名。
{
//...
externals: {
preact: 'xxxxxx'
}
// ...
}
SSR(服務端渲染)
在SSR中,在技術棧上選擇了Preact,Preact 它同樣支持 SSR,可以構建一個服務端的JS來支持SSR。因此我們的問題就迎刃而解了,我們在組件構建的時候多生成一份 Preact 的 SSR 的 JS,用沙盒執行服務端渲染輸出HTML并存儲。我們調研了以前的老的公共組件,全部攜程的業務線存在的技術棧只有兩種:JAVA、NODE,這樣就提供兩個接入方式的外殼即可——兩套不同語言的SDK及接入方式,其內部都是獲取統一的公共組件HTML字符串供頁面使用。
{
// ...
resolve: {
extensions: ['.ts', '.tsx', '.js'],
alias: isPreact ? {
"react": "preact/compat",
"react-dom": "preact/compat", // Must be below test-utils
"react/jsx-runtime": "preact/jsx-runtime"
} : {},
}
// ...
}
(React輕松轉換Preact)
問題二:所有頁面中的公共組件有變更時能否統一熱更新
當公共組件更新或者修復緊急的某些問題,不應該影響業務方頁面,應該是自動進行更新,當用戶訪問頁面時,看到就是最新的公共組件,因此我們沒有做類似npm包多版本的方式進行管理。
基于問題一的基礎上:
SSR(服務端渲染)的情況
SSR的服務端的HTML從哪里來?HTML怎么樣才能是最新的?
我們需要構建出來一份服務端的JS在沙盒中輸出HTML,存儲在了 Redis 中,將多個公共組件統一構建出了多個HTML,分別存放在 Redis 里。業務方接入JAVA、NODE的SDK其實要做的只有一件事:守護進程定時的去 Redis 里拿到最新的 HTML 結果。
CSR(客戶端渲染)的情況
CSR如何保持為最新公共組件的?
需要一臺機器同多語言技術棧SDK一樣,定時從 Redis 里讀取數據,對外暴露一個接口,供客戶端的JS調用。這樣,每次用戶訪問頁面的時候,客戶端JS會發起請求,保證用戶所看到的的內容永遠是最新的。
問題三:樣式問題
目前新版的相比之前舊版的公共組件在樣式和交互上更加復雜。由于左側菜單的存在,使得布局構造不同,而且各個事業部的頁面樣式可能五花八門,很難保證不會影響自身樣式和事件等問題。
比如:如果使用flex的布局,需要在最外層套用一個div,如果不套用的話則需要在body元素上添加flex樣式,但是不能保證其他的事業部的頁面的 body 是否有其他的樣式,甚至body 內是否存在其他的div元素等。還有很多事業部的頁面的類似滾動等事件監聽都是在body上進行監聽的,所以如果外層套取div,這種形式會讓原來頁面的事件監聽滾動非常麻煩,各個事業部原來監聽body的事件,需要一一進行改動。
觀察老項目發現,之前的公共組件骨架有個最外層的div元素,并且有一個名為"container"的id,我們要做的就是將左側的菜單 fixed 在左側就好了.關于css的fixed的兼容性:
(樣式屬性兼容情況)
但是此時有個問題是,我們的左菜單是可以展開或收起的。所以在展開和收起的時候需要一個全局的通信機制,當左側的組件變化時,在組件的內部應該觸發全局的通信鉤子,通知 id 為container 的div元素跟隨左菜單變化,達到 flex 布局的效果。
(左側菜單展開)
(左側菜單收起)
問題四:性能問題
基于問題 1/2/3 大概已經擬定了技術的方向,并且已經能在各個事業部行的通了,證明思路是沒有問題的,但是還有些個瑣碎的問題需要考慮:
為了解決上面的問題,我們考慮了先準備一個預渲染的HTML占位,類似骨架屏的意思,此時就可以先進行骨架屏的渲染,之后再異步拉取渲染,來解決異步渲染白屏等待時間的問題。
為了解決這個問題,我們的那臺跑沙盒JOB機器就可以繼續做這件事情。因為每個組件構建后有資源的版本,我們需要將版本存儲一份,一旦新的組件構建后,拉取其他公共組件的資源版本,將多個JS組裝在一起。同時因為我們用了 Preact ,抽取了 Preact 為一個共同依賴,將它放在最前面,保證它的最先執行。
六、公共組件的數據動態配置系統
介紹完攜程新版大首頁的公共組件的渲染原理及技術細節,接下來就是公共組件中的數據如何支持動態配置。
6.1 為什么需要組件數據動態配置系統
攜程PC版首頁進行改版的過程中,按業務線將整個頁面劃分成了多個組件模塊,每個組件模塊內都有需要展示的業務數據。而頁面上線之后,隨著產品需求的變更,業務數據會被頻繁的更新,如果每次更新數據都發布一次模塊代碼的話,這個成本和風險很大。
因此,將代碼和數據分開發布是很有必要的,當組件數據有改動時無需發布組件,搭建一個專門用于發布大首頁數據配置的管理系統勢在必行。
6.2 組件數據動態配置系統的需求分析
攜程大首頁數據配置管理系統的核心功能是完成數據配置的發布,并保證發布的可靠性和安全性,為了實現這個目標,此管理系統應制定一套完整的數據檢驗規范和發布流程,其中主要功能包括:
6.3 組件數據動態配置系統架構設計
圖1 大首頁數據配置管理系統架構設計
數據配置管理系統的架構設計 (如圖1 所示),為了實現需求分析中的四塊主要功能,整個管理系統主要搭建了兩個應用:
前端應用:以可視化界面的形式提供本地上傳配置文件,預覽數據效果以及更新頁面等功能,同時完成了數據校驗和預覽檢測。
Node服務:主要負責數據配置的處理及發布,將前端應用上傳的數據配置保存到QConfig系統中。
其中,前端應用提供的預覽功能的架構設計如下圖2 所示:
圖2 預覽功能架構設計
前端應用:負責提供數據配置和展示頁面效果。
服務端渲染應用:調用組件渲染函數,根據數據配置渲染出當前組件HTML,并從Redis拉取其他組件的HTML,而后組裝成一個完整頁面的HTML吐給前端應用。
Redis:存儲所有組件模塊的HTML。
前面部分介紹了數據配置管理系統的架構設計,這里就架構中核心功能部分的實現進行詳細介紹,主要包括:
數據配置規范及數據校驗
本地上傳的數據配置最終要傳給組件渲染出來,而數據配置的上傳者不一定是組件的開發者,上傳者并不一定清楚組件所需數據的類型和結構,那么如何保證上傳的數據與組件要求的數據結構保持一致呢?
這就需要管理系統制定一套數據配置規范來約束上傳的數據,然而不同的組件,其數據結構是不同的,那么每個組件都應有一套自己的規范。管理系統提供了兩種制定數據規范的方式:
規范制定完成之后管理系統會將其存儲起來,每次有上傳者上傳某一組件的數據配置后(為方便上傳者修改數據,管理系統規定數據配置以JSON文件的形式提供),系統會根據組件的數據規范校驗上傳的數據配置,如果校驗通過則會展示上傳數據與線上數據的差別,上傳者可進行預覽操作;如果校驗未通過,則提示未通過原因及具體的不規范數據,上傳者不可進行后續的預覽操作,需重新上傳數據配置,直到校驗通過。
組件及頁面預覽
此部分功能的核心實現在SSR Service 服務端渲染組件中(上文中有詳細介紹,這里不贅述),主要分為以下幾個步驟完成:
七、總結
本文通過攜程新版首頁項目系統的介紹了其整體架構設計,組件開發,數據配置的整個流程及實現原理,是對島嶼式架構的一次實踐。希望能對大家今后跨團隊組件式開發的項目有所收獲。
作者:前端框架團隊
來源:微信公眾號:攜程技術
出處:https://mp.weixin.qq.com/s/2EzL1ZrPCe_ZFGPjsrcp9Q
手速的時刻又到了!現在搶票渠道又增多啦,本周五大麥網、攜程網在線訂票與濟南西客站東廣場游客服務中心現場訂票渠道正常開啟。
【搶票方法1】:登錄大麥網(https://www.damai.cn/jn/),注冊后憑居民身份證實名預訂“明湖秀”演出座票。每場演出每張身份證僅能下單一次,一單可以憑2張身份證預訂2張0元門票,數量有限,先訂先得,訂完為止。
【搶票方法2】:登陸【電腦】攜程網頁版景區頁面http://piao.ctrip.com/dest/t4379371.html
【搶票方法3】:登陸【攜程APP】,直接搜索“明湖秀”或“泉城夜宴明湖秀”
【搶票方法4】:微信小程序,搜索小程序“攜程”進入“景點門票”板塊,選擇城市“濟南”后,進入《泉城夜宴明湖秀》頁面,憑居民身份證實名預訂“明湖秀”演出票。
【搶票方法5】:線下搶票,5月25日(周五)上午9:00-10:00,在濟南西客站東廣場游客服務中心開啟“明湖秀”座票現場免費預訂工作。
訂票須知:
通過大麥網或攜程網預訂門票成功的觀眾,將收到手機短信通知,大家可以在演出當天下午14:00-19:00,憑居民身份證到大明湖景區西南門換取紙質票,演出前憑身份證與紙質門票驗票入場觀演。
在濟南西客站東廣場游客服務中心直接取得紙質門票的觀眾,演出前直接憑身份證與紙質門票驗票入場觀演。