客服訂單詳情頁體驗(yàn)升級(jí)之路
一、背景
作為客服域訪問量最大的頁面之一,訂單詳情頁在客服的日常工作中被用來查閱用戶的訂單信息,以此為進(jìn)線的買賣家用戶提供更好的購買服務(wù),進(jìn)而提升用戶的滿意度。無論是一二線客服還是客服管理者,都能在日常使用的系統(tǒng)中直接訪問到詳情頁,因此客服訂單詳情頁的入口也比較多,目前已經(jīng)超過了10處。
圖片
隨著得物業(yè)務(wù)的快速發(fā)展,客服訂單詳情頁需要展示的信息越來越多,需要支持的操作也越來越多,在頁面改版前,一個(gè)頁面首屏就需要展示80條訂單信息,具體數(shù)量會(huì)根據(jù)訂單類型、交易狀態(tài)、物流狀態(tài)等因素而改變。一個(gè)頁面最多情況會(huì)有200條信息、60個(gè)按鈕以及20個(gè)訂單標(biāo)簽(不含用戶標(biāo)簽、商品標(biāo)簽),所以會(huì)出現(xiàn)一個(gè)現(xiàn)象,一個(gè)1920*1080分辨率的顯示器(客服常用的分辨率),鼠標(biāo)滾輪需要滾2次才能從頁面的最上方滑到最下方,詳情頁信息截圖如下所示。
圖片
另一方面,貿(mào)然調(diào)整反而會(huì)降低客服同學(xué)的工作效率。頁面內(nèi)容、交互變化太大對(duì)客服同學(xué)也存有不小的培訓(xùn)成本,因此秉持著變更影響最小化的原則,信息量一直保持著只增不減。
經(jīng)過一段時(shí)間的沉淀與打磨,客服訂單詳情頁無論是客服的使用體驗(yàn),還是開發(fā)體驗(yàn)都得到了顯著提升,訂單相關(guān)的TS問題近半年一直保持清零狀態(tài)。這期間做了很多嘗試和漸進(jìn)式優(yōu)化,本文主要從以下三點(diǎn)具體聊聊對(duì)客服訂單詳情頁體驗(yàn)升級(jí)做的一些思考和優(yōu)化。
- 這么信息全的頁面,幾乎所有客服域平臺(tái)都要能直接訪問查閱,一個(gè)頁面如何多個(gè)系統(tǒng)使用,在保障客服同學(xué)使用體驗(yàn)的同時(shí),還能節(jié)省開發(fā)同學(xué)維護(hù)成本。
- 信息量大數(shù)據(jù)源多導(dǎo)致頁面加載慢,客服同學(xué)經(jīng)常反饋卡頓,如何對(duì)頁面首屏進(jìn)行秒開優(yōu)化,進(jìn)一步提升客服同學(xué)的使用體驗(yàn)。
- 簡(jiǎn)單的改動(dòng)也需要投入資源,信息模塊復(fù)用率低,如何建設(shè)信息自由編排、信息模塊拔插的能力,最大程度解放產(chǎn)研運(yùn)同學(xué)生產(chǎn)力。
二、多入口的頁面復(fù)用
1、多實(shí)例iframe
最早訂單詳情頁只是客服工單系統(tǒng)中的一個(gè)頁面,使用的是Vue2、ElementUI的技術(shù)棧??头蜗到y(tǒng)的定位是后臺(tái)管理系統(tǒng),使用門檻較高,更適合管理者使用,所以需要一個(gè)面向一二線客服的平臺(tái),章魚工作臺(tái)就誕生了。章魚工作臺(tái)是基于qiankun微應(yīng)用搭建的,其子應(yīng)用最開始使用Vue3、Vite和Ant Design,那時(shí),如果要在章魚工作臺(tái)中訪問客服訂單詳情頁屬于跨應(yīng)用、跨技術(shù)棧通信,使用iframe是成本最小,也是初期最合適的一種方式,這也是頁面復(fù)用的第一個(gè)階段:在子應(yīng)用使用多個(gè)iframe容器嵌入工單系統(tǒng)中的訂單詳情頁。
圖片
所有訂單詳情頁的使用方只需要在本地創(chuàng)建iframe容器,然后在嵌入訂單詳情頁的訪問地址時(shí)傳一些必要的參數(shù)如訂單號(hào)、求購單號(hào)就能夠正常訪問了,十分便捷。還可以支持一些高級(jí)配置供使用方選用,如支持通過url query傳參的方式控制頁面樣式,當(dāng)用于外部嵌入時(shí)隱藏主應(yīng)用的菜單和頂部tab欄等等。
圖片
這個(gè)階段,初步解決了頁面復(fù)用的問題。但大家都知道iframe的弊端,一二線客服同學(xué)的日常工作中會(huì)出現(xiàn)大量的頁面切換,這樣一來,iframe的內(nèi)存占用高、加載緩慢的缺點(diǎn)就被放大開來,這個(gè)階段一二線客服經(jīng)常會(huì)反饋頁面卡頓,所以迫切需要進(jìn)一步優(yōu)化。
2、單實(shí)例iframe搭配MF遠(yuǎn)程組件
對(duì)客服工單系統(tǒng)、章魚工作臺(tái)兩個(gè)平臺(tái)的用戶特點(diǎn)、作業(yè)行為進(jìn)行分析,發(fā)現(xiàn)章魚工作臺(tái)的用戶對(duì)頁面的體驗(yàn)要求更高,于是對(duì)詳情頁做了第一次優(yōu)化,步入了第二個(gè)階段:將訂單詳情頁遷移至章魚工單工作臺(tái),其構(gòu)建方式也由Vite變更為Webpack5,子應(yīng)用之間利用Webpack5的Module Federation特性通過遠(yuǎn)程組件的方式進(jìn)行數(shù)據(jù)通信。另一方面,盡管iframe的缺點(diǎn)明顯,但仍是跨技術(shù)棧應(yīng)用間頁面通信的不錯(cuò)選擇,將iframe控制在單實(shí)例容器中可以最大程度限制其對(duì)內(nèi)存的占用。
圖片
這樣一來,詳情頁的入口雖然有10多個(gè),但通信方式都收攏成了三種:遠(yuǎn)程組件、單實(shí)例iframe、本地組件。所有場(chǎng)景都能覆蓋到,后續(xù)各個(gè)入口有復(fù)雜交互的變更或者自定義事件,都能夠在頁面主體做到監(jiān)控和收口,不需要頁面的使用方做額外開發(fā)。
3、技術(shù)實(shí)現(xiàn)
3.1、單實(shí)例iframe通信
內(nèi)容提供方
- 詳情接口響應(yīng)后注冊(cè)message事件。
- 監(jiān)聽iframe父級(jí)攜帶數(shù)據(jù)變化,更新本地頁面數(shù)據(jù)。
- 本地頁面交互事件被遠(yuǎn)端觸發(fā),發(fā)送當(dāng)前的數(shù)據(jù)給遠(yuǎn)端做自定義交互。
/** Vue3 */
/** 1. 詳情接口響應(yīng)后注冊(cè)message事件 */
onMounted(() => {
/** 請(qǐng)求詳情接口 */
fetchOrderDetail(() => {
/** query上打上iframe標(biāo)簽,用于確定注冊(cè)時(shí)機(jī) */
route.query.iframeRoute && watchParentMessage()
})
})
/** 2. 監(jiān)聽iframe父級(jí)攜帶數(shù)據(jù)變化,更新本地頁面數(shù)據(jù) */
const watchParentMessage = () => {
window.addEventListener('message', event => {
const orderNo = event.data.data.orderNo
if (event.data.type === 'ORDER_CHANGE' && orderNo) {
/** 更新訂單信息*/
initStream(orderNo)
}
})
}
/** 3. 本地頁面交互事件被遠(yuǎn)端觸發(fā),發(fā)送當(dāng)前的數(shù)據(jù)給遠(yuǎn)端 */
window.parent.postMessage(
/** payload: 攜帶的數(shù)據(jù)*/
{
type: 'workbenchRoute',
params: {
/** 跳轉(zhuǎn)退貨詳情頁 */
name: 'refundDetail',
query: {
// 攜帶數(shù)據(jù)
},
},
},
/** orgin: 如果想要傳遞給任意窗口,可以將這個(gè)參數(shù)設(shè)置為'*' ,為了安全起見,不建議設(shè)置為'*'*/
'*'
)
內(nèi)容使用方
- 頁面初始化時(shí)注冊(cè)message事件。
- 監(jiān)聽本地訂單單號(hào)變化,將新的數(shù)據(jù)傳給遠(yuǎn)端。
- 監(jiān)聽遠(yuǎn)端交互和數(shù)據(jù)變化,根據(jù)交互類型做不同的本地處理。
/** Vue2 */
/** 1. 頁面初始化時(shí)注冊(cè)message事件*/
mounted() {
window.addEventListener('message', this.callBack, false)
},
/** 2. 監(jiān)聽本地訂單單號(hào)變化,將新的數(shù)據(jù)傳給遠(yuǎn)端*/
watch: {
orderNo(newOrderNo) {
/** 在iframe的contentWindow屬性上掛載postMessage方法*/
detailIframeRef.contentWindow.postMessage(
/** payload: 攜帶的數(shù)據(jù)*/
{
type: 'ORDER_CHANGE',
data: {
orderNo:newOrderNo,
//其他數(shù)據(jù)
},
},
/** orgin: 如果想要傳遞給任意窗口,可以將這個(gè)參數(shù)設(shè)置為'*' ,為了安全起見,不建議設(shè)置為'*'*/
'*',
)
},
},
/** 3. 監(jiān)聽遠(yuǎn)端交互和數(shù)據(jù)變化,根據(jù)交互類型做不同的本地處理 */
method: {
/** callBack: 遠(yuǎn)端事件被觸發(fā)后,處理本地回調(diào)邏輯 */
callBack(event) {
try {
if (event.data.type === 'workbenchRoute') {
switch (event.data.params.name) {
case 'orderdetail':
//跳轉(zhuǎn)訂單詳情的handler
break
case 'detail':
//跳轉(zhuǎn)工單詳情的handler
break
// 其他交互
default:
//兜底處理
}
}
} catch(error) {
//異常處理
}
}
內(nèi)容提供方
- 配置webpack的MF插件,將整個(gè)訂單詳情頁exposes出去
- 維護(hù)詳情頁的props
/** 配置webpack MF插件,將訂單詳情頁exposes出去*/
new ModuleFederationPlugin({
filename: 'remoteEntry.js?[hash]',
library: { type: 'window', name: 'app_ticket' },
name: 'app_ticket',
shared: {
/** 需要共享的依賴 */
},
exposes: {
/** 提供訂單詳情遠(yuǎn)程組件*/
'./OrderDetail': './src/views/orderdetail/Index.tsx',
},
}),
- webpack配置需要建議通信的遠(yuǎn)端應(yīng)用
- 使用defineAsyncComponent注冊(cè)組件
- 像本地組件一樣使用遠(yuǎn)程組件
/** 1.webpack配置遠(yuǎn)端應(yīng)用 */
remotes: {
app_ticket: getRemoteUrl('app_ticket'),
},
/** 2. 使用defineAsyncComponent注冊(cè)組件*/
'OrderDetail': defineAsyncComponent(() => import('app_ticket/OrderDetail')),
/** 3. 像本地組件一樣使用遠(yuǎn)程組件*/
<OrderDetail orderNo={orderNo} {...props} />
4、總結(jié)
頁面使用iframe的首屏耗時(shí)平均在7076ms,非首屏在2594ms,而MF的首屏只需要1279ms,非首屏更是只需428ms,渲染時(shí)間降低了6倍。
圖片
單個(gè)頁面的內(nèi)存占用減少到了之前的1/10以內(nèi),關(guān)于模塊聯(lián)邦和遠(yuǎn)程組件的更多細(xì)節(jié)可以查看Module Federation 在得物客服工單業(yè)務(wù)中的最佳實(shí)踐。
圖片
三、首屏秒開優(yōu)化
上一章節(jié)說到使用MF的方式解決了架構(gòu)層面的卡頓問題,但無緩存下頁面仍要2~3s甚至更久才能刷出訂單信息,這時(shí)要怎么辦?是的,可以改交互、拆接口。但如果數(shù)據(jù)依賴了大量外域服務(wù)、沒有外域產(chǎn)研資源介入,且要在一周時(shí)間做到有效的優(yōu)化,那還能做些什么呢?
1、緩慢原因
由于一些歷史原因,客服訂單詳情頁需要同時(shí)展示100+的訂單信息,所有的訂單信息、訂單操作涉及的字段接近200個(gè),而這么多字段其中90%都在一個(gè)http接口里面,這個(gè)大接口包含了36個(gè)dubbo接口,這些接口來自交易正向、逆向、供應(yīng)鏈、商家、商品、用戶以及其他BU。并行的調(diào)用一定會(huì)出現(xiàn)短板效應(yīng),只要有一個(gè)接口RT(Reaction Time,響應(yīng)時(shí)間)慢,就會(huì)拉慢整個(gè)http接口的響應(yīng)速度,同時(shí)出現(xiàn)Timeout的概率也會(huì)上升,再加上頁面本身對(duì)資源的渲染時(shí)間,無緩存下仍要2~3s甚至更久才能刷出訂單信息。這個(gè)大接口的平均RT在500ms,P99線的RT達(dá)到了1.3s,下圖就是生產(chǎn)環(huán)境下某一次的調(diào)用詳情,耗時(shí)在782ms,降RT優(yōu)化首屏渲染刻不容緩。
圖片
除了接口RT耗時(shí)高的問題,還有首屏接口并行調(diào)用的問題。90%的字段都在一個(gè)大接口里面,剩下10%都是在零零散散的一些小接口里,把這些小接口加起來頁面首屏接口超過6個(gè)。我們知道一個(gè)Chrome頁簽最多并行處理6個(gè)http請(qǐng)求,如果有第7個(gè)接口就會(huì)進(jìn)入到Stalled(熄火)狀態(tài),等待前面的某一個(gè)接口響應(yīng)完畢后再發(fā)起請(qǐng)求,下圖是一個(gè)示例,getTrackTicketInfo接口是頁面首屏的第7個(gè)接口,255.14ms就是需要等待的時(shí)間。
圖片
根據(jù)上述問題現(xiàn)狀,初步的方案就是接口先聚合再拆分,把所有接口的數(shù)據(jù)聚合到一塊,然后再根據(jù)信息模塊拆分成若干個(gè)接口,前端再根據(jù)業(yè)務(wù)場(chǎng)景和用戶行為,去對(duì)拆分出來的接口的調(diào)用時(shí)機(jī)進(jìn)行優(yōu)化。但是,理想很豐滿,現(xiàn)實(shí)很骨感,數(shù)據(jù)量擺在這,很難在短時(shí)間去做到這件事情。光把字段梳理全,數(shù)據(jù)來源理清楚就用掉了兩天時(shí)間,再考慮到成本和收益后,我們的最終方案就是新增快慢接口,快接口的RT要在200ms以內(nèi),所有拖慢RT的數(shù)據(jù)都放到慢接口中,前端再根據(jù)接口的特性將所有接口分為2個(gè)梯隊(duì)在不同時(shí)間進(jìn)行調(diào)用,最大程度的減少頁面的首屏渲染時(shí)間。
2、接口調(diào)用優(yōu)化
2.1、技術(shù)方案
為了控制頁面并行請(qǐng)求接口數(shù)量和頁面數(shù)據(jù)渲染次數(shù),將除了快慢接口之外,所有零散小接口分為如下兩個(gè)梯隊(duì):不依賴詳情大接口反參的首屏信息接口;依賴詳情大接口反參的首屏信息接口、非首屏信息接口。
最終詳情頁首屏接口調(diào)用情況示意圖如下,在能夠聚合依賴大接口反參的首屏信息接口的情況下,頁面只會(huì)渲染兩次,第一次是在快詳情接口和第一梯隊(duì)接口請(qǐng)求回來之后(使用Promise.all控制數(shù)據(jù)的渲染時(shí)機(jī)),第二次就是在慢詳情接口請(qǐng)求回來之后。
圖片
使用Promise.all來保證快詳情接口和第一梯隊(duì)的接口信息同時(shí)渲染,減少頁面渲染次數(shù),從而減少頁面抖動(dòng)的情況。Promise.all有個(gè)缺點(diǎn)就是其中有一個(gè)Promise異常,整個(gè)就會(huì)拋出異常,所以需要對(duì)Promise.all包裹的Promise進(jìn)行二次封裝,保證有一個(gè)Promise報(bào)錯(cuò)不會(huì)干擾其他接口的請(qǐng)求,具體代碼實(shí)現(xiàn)方式如下:
/** 處理promise,保證promise.all使用時(shí)相互獨(dú)立 */
export const handlerPromise = (api, params) => {
return new Promise(resolve => {
api(params)
.then(resolve)
.catch(() => resolve({ error: true }))
})
}
/** 詳情頁首屏請(qǐng)求函數(shù) */
const fetchOrderDetail = (callback?) => {
/** 使用handlerPromise封裝過的promise,保證有一個(gè)報(bào)錯(cuò)不干擾其他接口請(qǐng)求 */
Promise.all([quickDetail(), firstLevel1(), firstLevel2()])
.then(([quickDetailData, firstLevelData1, firstLevelData2]) => {
// 快詳情接口
!quickDetailData?.error && quickDetailHandler(quickDetailData)
// 第一梯隊(duì)接口調(diào)用:不依賴詳情反參的首屏信息接口
!firstLevelData1?.error && firstLevelHandler1(firstLevelData1)
!firstLevelData2?.error && firstLevelHandler2(firstLevelData2)
// 執(zhí)行回調(diào)
nextTick(callback)
})
.then(() => {
// 第二梯隊(duì)接口調(diào)用:依賴詳情反參的首屏信息接口、非首屏接口
secondLevelHandler1()
secondLevelHandler2()
})
// 執(zhí)行慢接口
fetchSlowOrderDetail()
}
接口調(diào)用順序確認(rèn)后還有一點(diǎn)需要注意,因?yàn)槁涌陧憫?yīng)耗時(shí)較高,在客服同學(xué)快速查詢的工作場(chǎng)景中,可能會(huì)在慢接口還在pending,就已經(jīng)切換到下一個(gè)訂單了,頁面單實(shí)例的場(chǎng)景下,這個(gè)時(shí)候如果不處理可能會(huì)將出現(xiàn)數(shù)據(jù)串臺(tái)的情況,不屬于該訂單的數(shù)據(jù)顯示了出來,關(guān)于這一點(diǎn),需要在慢接口的handler上做如下處理。
/** 慢詳情接口請(qǐng)求 */
const fetchSlowOrderDetail = () => {
slowLoading.value = true
orderApi
.getDetail(params)
.then(slowData => {
/** 防止快速切換訂單導(dǎo)致的數(shù)據(jù)串臺(tái)問題 */
if (slowData?.topInfo?.orderNo === orderNo.value) {
orderDetail.value = slowData
}
slowLoading.value = false
})
.catch(() => {
slowLoading.value = false
})
}
2.2、最終效果
優(yōu)化后的Waterfall圖就如下所示,不會(huì)再出現(xiàn)灰色的stalled耗時(shí)了,而且在228ms后首屏的數(shù)據(jù)就已經(jīng)請(qǐng)求回來了。
圖片
3、大接口優(yōu)化
上面一點(diǎn)解決了首屏接口的調(diào)用問題,接下來是對(duì)大詳情接口具體做的一些優(yōu)化:
- 接口協(xié)議由POST改為GET請(qǐng)求,GET的總耗時(shí)是POST的三分之二;Chrome下如果檢測(cè)到GET請(qǐng)求的是靜態(tài)資源,則會(huì)緩存,如果兩次傳輸?shù)臄?shù)據(jù)相同,第二次以后耗費(fèi)的時(shí)間將在10ms以內(nèi)。另一方面也為后續(xù)工作臺(tái)引入Service Worker技術(shù)打下基礎(chǔ)。
- 新增快詳情接口,將大接口中的響應(yīng)耗時(shí)較高的字段整理出來,快接口不再包含這些字段。這些高耗時(shí)的字段新增字段級(jí)別的loading效果,為了避免快慢接口耗時(shí)差異較大,導(dǎo)致一些經(jīng)驗(yàn)豐富的客服同學(xué)誤以為快接口沒返回?cái)?shù)據(jù)的字段是空數(shù)據(jù),但是這個(gè)loading數(shù)量不會(huì)超過3處,保持頁面的整潔易讀。
4、總結(jié)
經(jīng)過上述優(yōu)化,快詳情接口RT只需要平均190ms,從之前大接口的470ms下降了41%,首屏渲染時(shí)間從873ms降至376ms,下降了57%,95分位567ms,下降了62%。
圖片
首屏優(yōu)化效果明顯,很難再看到反饋詳情頁加載緩慢的VOC了,一定程度地提升了客服的平臺(tái)體驗(yàn)滿意度。
四、信息編排、模版插拔能力建設(shè)
解決了頁面卡頓和首屏加載慢的問題,但仍存在一些問題。這一次在產(chǎn)研運(yùn)同學(xué)的通力協(xié)作下,如何進(jìn)一步提升技術(shù)同學(xué)的開發(fā)體驗(yàn)和客服同學(xué)的使用體驗(yàn)?zāi)亍?/p>
1、仍面臨的問題
雖然詳情頁中堆疊的字段已經(jīng)多達(dá)200處,隨著業(yè)務(wù)高速發(fā)展仍會(huì)存在部分信息缺失、不準(zhǔn)確的情況,對(duì)客服日常作業(yè)產(chǎn)生了一定的負(fù)向影響。另一方面,在開發(fā)訂單類需求中,約60%的都是配合外域或者內(nèi)部進(jìn)行字段增刪改,如果建立了訂單信息的編排能力,后續(xù)字段類需求將可配置,從而能夠解放這部分需求的產(chǎn)研生產(chǎn)力,達(dá)到降本增效;同時(shí)若前端模塊能夠支持模塊拔插能力,也能為后續(xù)訂單信息模塊復(fù)用到坐席輔助及其他客服工作臺(tái)提供技術(shù)支撐。
2、信息編排能力建設(shè)
將訂單基本信息及關(guān)聯(lián)信息通過統(tǒng)一Schema維護(hù)。大家知道Schema(結(jié)構(gòu)化的數(shù)據(jù)類型)只要約定的足夠復(fù)雜是可以用來描述所有場(chǎng)景的數(shù)據(jù)的,所以使用Schema第一步就是要控制好這個(gè)邊界,在能夠覆蓋大部分業(yè)務(wù)場(chǎng)景前提下不能太復(fù)雜。首先可將訂單信息做3層細(xì)分:信息塊、信息組、信息元素。做到配置信息塊、信息元素對(duì)于訂單詳情頁的場(chǎng)景來說都不太合適,所以這里選擇約定到信息組的格式。
圖片
2.1、Schema格式
上面圖示中兩組信息組可用下述Schema描述出來,利用數(shù)組的有序性,從左到右、從上到下對(duì)信息組進(jìn)行渲染,實(shí)現(xiàn)訂單信息的編排配置能力。
schemaData : [
{
label: '訂單類型'
text:'普通現(xiàn)貨'
children: [
{
id: 'orderTypeDetail'
text: '詳情',
show: true,
toolType: 'linkBtn', /** linkBtn, primaryBtn, tag */
eventType: 'click', /** dbclick, hover*/
interactiveType: 'popover', /** modal, popover, message*/
children: [
//** popover彈出的內(nèi)容 */
{ label: , text: '', children: //..}]
{ label: , text: ''}
]
},
{
text: '晚到必賠',
show: true
toolType: 'tag',
},
{
text: '退運(yùn)服務(wù)',
show: true
toolType: 'tag',
},
]
},
{
id: 'tradeStatus'
label: '支付狀態(tài)'
text:'已經(jīng)支付'
children: [
{
text: '七天風(fēng)控',
show: true
toolType: 'tag',
},
]
}
],
使用Schema可以滿足大部分的字段渲染場(chǎng)景,但是對(duì)于一些復(fù)雜的交互和自定義的樣式仍需要前端去實(shí)現(xiàn),這個(gè)時(shí)候每個(gè)信息元素中的id就發(fā)揮了作用,前端可以根據(jù)id去綁定交互事件和自定義樣式,具體實(shí)現(xiàn)如下:
/** 信息元素枚舉*/
enum INFO_ElEMENT_MAP {
/** 訂單類型詳情按鈕 */
ORDERTYPE_DETAIL: 'orderTypeDetail'
}
/** 信息元素*/
const infoElementMap = {
INFO_ElEMENT_MAP.ORDERTYPE_DETAIL: {
/** 綁定事件 */
onClick: () => {
orderTypeDetailClickHandler()
},
/** 綁定樣式 */
className: [styles.marginLeft],
},
/** 其他需要添加復(fù)雜交互和樣式*/
}
2.3、模版解析
約定好了Schema和規(guī)范,前端再編寫對(duì)應(yīng)模版解析代碼去渲染頁面,對(duì)應(yīng)渲染圖如下所示。
圖片
最外層的渲染器代碼如下:
const SchemaRender = () => {
//TODO 健壯性代碼
return (
<div>
{schemaData.length ? (
<Row>
{schemaTemplate.map(item => {
// 分隔符
if (item.key === TemplateKeyEnum.dividedLine) {
return <Divider />
}
return (
<Col span={12}>
<InfoItem
key={item.key}
label={item.label}
text={item.text}
infoList={item.children.map(child => {
return {
text: popoverRender(child),
hide: !child.show,
}
})}
/>
</Col>
)
})}
</Row>
) : (
<Skeleton title={false} active paragraph={{ rows: 3 }} />
)}
</div>
)
}
最終,Schema加上模版渲染就能渲染出訂單詳情頁的信息,后續(xù)此類型的需求除了和外域約定字段,就不再需要額外資源投入了。
3、模塊插拔能力建設(shè)
實(shí)現(xiàn)了信息快速編排,還有信息模塊高耦合的問題。其實(shí)不同角色的客服同學(xué)關(guān)注的信息是不一樣的,所以新的訂單詳情頁要根據(jù)客服的身份去展示不同的信息;而且隨著屏幕大小的不同,所適合布局也不同。另一方面,工單詳情、坐席輔助都需要展示訂單信息的某一個(gè)信息模塊(整個(gè)頁面展示就太重了),這時(shí)就需要訂單信息模塊有可插拔的能力了。
3.1、技術(shù)方案
初步方案是后端維護(hù)一個(gè)信息模塊池,提供出一個(gè)接口,前端通過傳一個(gè)標(biāo)識(shí),能夠返回對(duì)應(yīng)標(biāo)識(shí)需要的模塊組合,然后根據(jù)數(shù)據(jù)組合渲染數(shù)據(jù)。這個(gè)方案可以實(shí)現(xiàn)信息可拔插能力。
圖片
上述方案可以解決問題,也比較簡(jiǎn)單,但是控制數(shù)據(jù)的還是前端,這其實(shí)違背了最初建設(shè)信息編排能力的初衷。于是最終改為后端同學(xué)從客服同學(xué)的登錄態(tài)拿到userId,根據(jù)id拿到其所在處理組,是買家處理組就返回買家版訂單信息,賣家版就返回賣家版訂單信息。另一方面,前端也根據(jù)屏幕大小做布局的自適應(yīng)。
3.2、最終效果
- 大屏下頁面布局:
圖片
- 小屏下頁面布局(1440*900以下):
圖片
- 工單詳情使用訂單詳情中的物流記錄、服務(wù)記錄訂單信息模塊:
圖片
4、總結(jié)
從改版以來近8個(gè)迭代的資源投入數(shù)據(jù)來看,訂單需求開發(fā)成本降低了66.7%。
五、灰度和埋點(diǎn)方案
1、灰度方案
新版詳情頁改動(dòng)較大需要根據(jù)客服所在處理組進(jìn)行灰度,但是一線和二線的處理組分配又不太一樣,所以需要根據(jù)入口來源判斷使用哪套AB方案接口。另一方面,訂單詳情頁的入口非常多,所以在每個(gè)入口做灰度不太現(xiàn)實(shí),改動(dòng)較大,所以選擇收口到詳情頁主頁面區(qū)分新老頁面。
1.1、技術(shù)方案
- 根據(jù)來源區(qū)分使用一線AB方案還是二線AB方案,偽代碼如下:
/** 獲取來源區(qū)分IM、工單灰度組信息 */
watch(
() => props.platformCode,
code => {
try {
switch (code) {
case PLATFORM_TYPE.IM:
/** 一線灰度走一線灰度接口 */
isGray.value = true
break
case PLATFORM_TYPE.TICKET:
/** 二線灰度走二線灰度接口 */
isGray.value = true
break
default:
isGray.value = false
} catch {
isGray.value = false
}
},
{
immediate: true,
}
)
- 在主頁面根據(jù)灰度情況渲染詳情模版:
return () => isGray.value ? (
<>
<Button type="link" notallow={clickHandler} >
返回{isOld.value ? '新版' : '老版'}
</Button>
{isOld.value ? <OldDetail {...props} /> : <Detail {...props} />}
</>
) : <OldDetail {...props} />
1.2、總結(jié)
根據(jù)培訓(xùn)進(jìn)度開放灰度名單給客服使用新版頁面,同時(shí)對(duì)新老版頁面的數(shù)據(jù)進(jìn)行監(jiān)控 。支持可監(jiān)控、可灰度、可回滾,確保了在頁面大改動(dòng)情況下的系統(tǒng)質(zhì)量穩(wěn)定。
2、埋點(diǎn)方案
為了體現(xiàn)訂單信息優(yōu)化的收益和價(jià)值,需要對(duì)客服同學(xué)在新老訂單詳情頁的停留時(shí)間、跳出訂單詳情頁次數(shù)進(jìn)行比對(duì)。
- 訂單詳情頁停留時(shí)間:有效的停留時(shí)間越長一定程度能說明頁面的查閱費(fèi)力度越高。
- 訂單詳情頁跳出率:跳出率越高說明當(dāng)前訂單詳情信息不能滿足客服的查閱需求,需要去其他頁面查看。是信息不全、不清晰的一種體現(xiàn)。而且跳出頁面會(huì)重新加載新的頁面,等待時(shí)間會(huì)長于頁面內(nèi)獲取信息,增加客服獲取信息的時(shí)間。
2.1、頁面停留時(shí)間
使用監(jiān)聽路由的方法去計(jì)算頁面停留時(shí)間。這里只對(duì)其中一種做分析,其他兩種類似。
- 確定路由:https://xxx-xxx.xxx/orderdetail/:id
- 只需要考慮上一個(gè)路由是訂單頁面的情況
- 數(shù)據(jù)過濾:小于3s,大于30min視為無效數(shù)據(jù)
- 如果最近的路由是訂單頁面,則重置時(shí)間
/** 數(shù)據(jù)上報(bào) */
const uplog = (current, last, constant, type) => {
/** b: 只用考慮上一個(gè)路由是訂單頁面的情況 */
if (constant === last) {
const nowTime = getNowTime()
const stayTime = nowTime - stayOrderDetailTime.currentTime
/** c: 小于3s,大于30min視為無效數(shù)據(jù) */
if (stayTime > 3000 && stayTime < 1000 * 60 * 30) {
orderDuLog('ORDER_STAY_TIME', {
orderNo: orderNo.value,
stayTime,
type,
})
}
stayOrderDetailTime.currentTime = nowTime
}
/** d:如果最近的路由是訂單頁面,則重置時(shí)間*/
if (constant === current) {
stayOrderDetailTime.currentTime = getNowTime()
}
}
/** 監(jiān)聽路由 */
watch(
() => {
return { name: route.name, id: route.params?.id }
},
throttle(
(currentRoute, lastRoute) => {
/** 從工單出發(fā):只用考慮上一個(gè)路由是訂單頁面的情況 */
uplog(currentRoute?.name, lastRoute?.name, `${globalConfig.backstageCode}_orderdetail`, 'routeChange')
},
100,
{ leading: true, trailing: false }
),
{
deep: true,
immediate: true,
}
)
2.2、頁面跳出次數(shù)
頁面跳出次數(shù)就比較簡(jiǎn)單了,只需要在跳出事件的handler里加上數(shù)據(jù)上報(bào)方法,比如查看商品詳情、退換貨詳情、用戶分期信息、工單詳情等等,最后計(jì)算跳出總量即可。
2.3、總結(jié)
老版頁面平均停留時(shí)間15.6s,新版頁面的平均停留時(shí)間8.5s,客服每次查詢信息的時(shí)長縮短7.1s,可根據(jù)詳情頁P(yáng)V換算日均可降低客服同學(xué)工作時(shí)長。
圖片
同時(shí),客服查詢特定訂單信息時(shí)需要跳到別的頁面查詢,這說明訂單信息是有缺失和或者客服是對(duì)信息準(zhǔn)確性是有懷疑的,老版頁面跳出率11.4%(約8.7次訪問跳出1次),新版頁面跳出率7.92%(約12.6次訪問跳出1次)??头榭从唵涡畔r(shí)跳出的率也下降3.48pp。
圖片
圖片
六、總結(jié)&規(guī)劃
1、總結(jié)
- 關(guān)于動(dòng)態(tài)路由頁面的多平臺(tái)復(fù)用:跨技術(shù)棧使用單實(shí)例的iframe通信,配合雙向的postMessage事件監(jiān)聽用戶行為觸發(fā)交互;微應(yīng)用內(nèi)使用Module Federation通信,在保障客服使用體驗(yàn)的同時(shí),節(jié)省了開發(fā)維護(hù)成本。
MF的首屏需要1279ms,非首屏只需428ms,渲染時(shí)間降低了6倍。
單個(gè)頁面的內(nèi)存占用減少到了之前的1/10以內(nèi)。
- 關(guān)于大數(shù)據(jù)量的頁面首屏優(yōu)化:基于業(yè)務(wù)優(yōu)化接口調(diào)用時(shí)機(jī),保證同時(shí)不超過6個(gè)接口請(qǐng)求,避免出現(xiàn)Stalled耗時(shí),對(duì)大接口RT進(jìn)行優(yōu)化,場(chǎng)景允許的話可改為GET協(xié)議類型,降低首屏響應(yīng)時(shí)間,提升客服體驗(yàn)。
首屏請(qǐng)求6個(gè)以上接口時(shí)不再出現(xiàn)Stalled耗時(shí),大接口改為GET后總耗時(shí)是POST的2/3。
快詳情接口RT只需要平均190ms,從之前大接口的470ms下降了41%,首屏渲染時(shí)間從873ms降至376ms,下降了57%,95分位567ms,下降了62%。
關(guān)于信息編排、模塊拔插能力建設(shè):根據(jù)業(yè)務(wù)分析字段特點(diǎn),約定合適的Schema格式使得信息內(nèi)容可靈活配置;對(duì)用戶的職能特點(diǎn)、設(shè)備情況進(jìn)行分析,使得用戶所訪問頁面的布局、內(nèi)容做到區(qū)分,做到給用戶看到合適的內(nèi)容,降低坐席的查詢信息的費(fèi)力度。
訂單需求開發(fā)成本能夠降低66.7%。
老版頁面平均停留時(shí)間15.6s,新版頁面的平均停留時(shí)間8.5s,客服每次查詢信息的時(shí)長縮短7.1s,根據(jù)訂單詳情頁的PV可換算出每日可降低客服同學(xué)的查詢時(shí)長。
老版頁面跳出率11.4%(約8.7次訪問跳出1次),新版頁面跳出率7.92%(約12.6次訪問跳出1次)??头榭从唵涡畔r(shí)跳出率下降了3.48pp。
2、后續(xù)規(guī)劃
雖然客服訂單詳情頁的使用體驗(yàn)已經(jīng)得到提升,但是體驗(yàn)升級(jí)之路仍在繼續(xù):
- 模塊聯(lián)邦雖然很快,但是對(duì)公共依賴維護(hù)成本較大,也會(huì)導(dǎo)致應(yīng)用構(gòu)建速度下降。后續(xù)會(huì)基于無界對(duì)訂單子應(yīng)用進(jìn)行遷移,建設(shè)專門存放遠(yuǎn)程組件的應(yīng)用容器,提升子應(yīng)用的秒開和快速切換體驗(yàn),同時(shí)也能提升工單子應(yīng)用構(gòu)建速率,解耦橫向訂單功能的發(fā)布??善诖罄m(xù)內(nèi)容。