通用詳情頁(yè)的打造,你學(xué)會(huì)了嗎?
背景介紹
圖片
大家都知道,詳情頁(yè)承載了站內(nèi)的核心流量。它的量級(jí)到底有多大呢?
我們來(lái)看一下,日均播放次數(shù)數(shù)億次,這么大的流量,其重要程度可想而知。
在這樣一個(gè)頁(yè)面,每一個(gè)功能都是大量業(yè)務(wù)的匯總點(diǎn)。
作為用戶核心消費(fèi)場(chǎng)景,詳情頁(yè)不僅需要承接各種業(yè)務(wù)的轉(zhuǎn)化,還要負(fù)責(zé)展示各業(yè)務(wù)在播放頁(yè)的功能。
可以說(shuō),播放頁(yè)的代碼復(fù)雜度屬于客戶端最高的代碼之一,這不僅因?yàn)椴シ彭?yè)本身的功能復(fù)雜,還因?yàn)樗枰诤洗罅客獠繕I(yè)務(wù)功能。
復(fù)雜的功能自然會(huì)帶來(lái)較高的代碼復(fù)雜度,而高代碼復(fù)雜度往往意味著高代碼維護(hù)成本。
明確需求
圖片
我們來(lái)看一下沒(méi)有做這個(gè)項(xiàng)目之前的狀態(tài)。如圖所示,他們分別為三個(gè)業(yè)務(wù)團(tuán)隊(duì)各自維護(hù)。頁(yè)面間相互獨(dú)立。能力無(wú)法復(fù)用。
圖片
通過(guò)這個(gè)項(xiàng)目,我們要將他們?nèi)诤铣闪艘粋€(gè)頁(yè)面。產(chǎn)品的訴求就是將他們?nèi)诤蠟橐粋€(gè),來(lái)達(dá)到多業(yè)務(wù)形態(tài)統(tǒng)一的目標(biāo)。
圖片
但是,這三個(gè)詳情頁(yè)并不像產(chǎn)品想象的那么簡(jiǎn)單。
每個(gè)業(yè)務(wù)都有自己的特殊形態(tài),如大型活動(dòng)態(tài)、主客態(tài)、播單態(tài)、PUGV/OGV態(tài)等一系列業(yè)務(wù)形態(tài)。
每種形態(tài)都有自己的特殊邏輯,而且這些業(yè)務(wù)形態(tài)間還可以互相切換。
需求分析
為了更好地達(dá)成目標(biāo),我們需要進(jìn)行如下思考:
- 從業(yè)務(wù)角度:
要解決多業(yè)務(wù)形態(tài)不統(tǒng)一的問(wèn)題。例如,產(chǎn)品既想要UGC大型活動(dòng)的能力,又想要OGV的多視角功能。
但這兩個(gè)能力在之前分別是兩個(gè)業(yè)務(wù)團(tuán)隊(duì)各自開(kāi)發(fā)的,無(wú)法復(fù)用,產(chǎn)品在業(yè)務(wù)選擇上無(wú)法兼得。
- 從效率角度:
要解決迭代方式不統(tǒng)一的問(wèn)題。例如,進(jìn)度條體驗(yàn)優(yōu)化需求,產(chǎn)品在給UGC團(tuán)隊(duì)提需求的同時(shí),還要復(fù)制一份給OGV團(tuán)隊(duì)。
兩個(gè)業(yè)務(wù)方的開(kāi)發(fā)和測(cè)試都需要進(jìn)入這個(gè)項(xiàng)目,并且雙方的開(kāi)發(fā)進(jìn)度和排期可能不一致。如果產(chǎn)品強(qiáng)烈要求同一版本上線,還需要協(xié)調(diào)各方資源。
- 從質(zhì)量角度:
要解決如何保障穩(wěn)定性的問(wèn)題。例如,多團(tuán)隊(duì)協(xié)作,之前都是組內(nèi)同事協(xié)作開(kāi)發(fā),現(xiàn)在融入了兩個(gè)新的業(yè)務(wù)團(tuán)隊(duì),我們?cè)撊绾伪U戏€(wěn)定性。
- 從團(tuán)隊(duì)角度:
要解決如何讓新人快速上手的問(wèn)題。正常情況下,新人想要進(jìn)入開(kāi)發(fā)必須對(duì)這個(gè)系統(tǒng)足夠了解后才行。
更何況現(xiàn)在變成了三個(gè)業(yè)務(wù)融合的頁(yè)面。有沒(méi)有一種手段,讓新人無(wú)需關(guān)心復(fù)雜的業(yè)務(wù)形態(tài)和業(yè)務(wù)邏輯,只需要關(guān)注自己的需求?
具體方案
針對(duì)以上問(wèn)題,我們可以總結(jié)出通用詳情頁(yè)框架必須滿足以上三點(diǎn),分別為:復(fù)用性,靈活性,穩(wěn)定性
圖片
接下來(lái)我們繼續(xù)對(duì)多業(yè)務(wù)形態(tài)進(jìn)行分析。
首先我們從橫向上進(jìn)行拆解,通過(guò)對(duì)比,我們可以發(fā)現(xiàn)
多業(yè)務(wù)形態(tài)間其實(shí)有很多的相同模塊。如互動(dòng),彈幕發(fā)送框,相關(guān)推薦等。
從縱向上進(jìn)行拆解,我們也可以發(fā)現(xiàn)很多相同模塊,如彈窗管理器,主題組件,轉(zhuǎn)場(chǎng)組件等。
那么從橫向和縱向上我們發(fā)現(xiàn),多種業(yè)務(wù)形態(tài)間其實(shí)有很多可以復(fù)用的能力。
圖片
基于前面的思考,我們?cè)O(shè)計(jì)了一套通用詳情頁(yè)的框架。將其分為三層:
- 業(yè)務(wù)層:將業(yè)務(wù)模塊分為兩類,能夠在多業(yè)務(wù)間復(fù)用的模塊抽象到通用業(yè)務(wù),業(yè)務(wù)獨(dú)有模塊則由各業(yè)務(wù)自行負(fù)責(zé)。
- 組件層:抽象出各種通用組件,業(yè)務(wù)方可自由選取和組裝。
- 框架層:抽象生命周期管理、數(shù)據(jù)管理等核心邏輯,以此來(lái)保證整個(gè)詳情頁(yè)的穩(wěn)定性。
這樣我們就初步解決了復(fù)用性的問(wèn)題,但是隨之而來(lái)的就是靈活性問(wèn)題。
圖片
我們以實(shí)際場(chǎng)景為例,相關(guān)推薦模塊在課堂態(tài)不展示,但是在ugc和ogv下需要展示,另外他的點(diǎn)擊事件在ugc和ogv下還會(huì)出現(xiàn)差異。
同時(shí)相關(guān)推薦模塊還強(qiáng)依賴簡(jiǎn)介模塊。因?yàn)楹?jiǎn)介模塊也是一個(gè)通用組件,業(yè)務(wù)方可以自由替換。
如果哪天業(yè)務(wù)方替換了了簡(jiǎn)介模塊,那相關(guān)推薦模塊將無(wú)法正常運(yùn)行。
從相關(guān)推薦這個(gè)例子我們可以得出如果想讓業(yè)務(wù)模塊復(fù)用,必須滿足兩個(gè)條件。
- 支持業(yè)務(wù)異化,即允許業(yè)務(wù)能插入自定義邏輯,否則現(xiàn)在抽象的通用模塊在迭代的過(guò)程一定會(huì)變成非通用,或者里面摻雜各種if else邏輯來(lái)支持異化。
- 必須保證模塊間相互獨(dú)立,因?yàn)樗袠I(yè)務(wù)邏輯在此框架下都變成了模塊,模塊是可以由業(yè)務(wù)方自由選擇的。
圖片
引入依賴注入
因此,我們需要在流程和模塊中加入依賴注入的能力,用于業(yè)務(wù)方實(shí)現(xiàn)差異化邏輯。
業(yè)務(wù)方可自行插入自己的業(yè)務(wù)邏輯,并選擇或替換業(yè)務(wù)模塊。來(lái)解決模塊間的耦合。
定義依賴注入容器
public class BlocStore {
typealias StoreLock = RecursiveLock
typealias StoreTable = [String: BlocTable]
private let lock: StoreLock = StoreLock()
private lazy var storeTable: StoreTable = [:]
}
extension BlocStore {
public func register<Service>(service: Service.Type = Service.self, to: Bloc.Type) {
let key = "\(service)"
lock.lock()
defer { lock.unlock() }
serviceTable[key] = to
}
@discardableResult
public func optional<Service>(service: Service.Type = Service.self) -> Service? {
let key = "\(service)"
lock.lock()
defer { lock.unlock() }
let service = resolve(bloc)
return s
}
}
// Bind and unbind
extension BlocStore {
public func bindBloc(bloc: Bloc) {
}
public func unbindBloc<T: Bloc>(_ blocType: T.Type) {
}
}
// BlocLifeCycle
extension BlocStore {
func onStart(bloc: Bloc?) {
bloc?.onStart()
}
func onPause(bloc: Bloc?) {
bloc?.onPause()
}
func onResume(bloc: Bloc?) {
bloc?.onResume()
}
func onStop(bloc: Bloc?) {
bloc?.onStop()
}
}
組件注冊(cè)
// 業(yè)務(wù)方根據(jù)業(yè)務(wù)邏輯可以注入不同的實(shí)現(xiàn)
register(service: XXXProtocol.self, to: ABloc.self) // A業(yè)務(wù)形態(tài)
register(service: XXXProtocol.self, to: BBloc.self) // B業(yè)務(wù)形態(tài)
組件解析
let s: XXXProtocol = store.optional()
引入scope
scope分為頁(yè)面級(jí)和業(yè)務(wù)級(jí)兩種scope:
class VDScope {
public static let core = "store.core.scope"
public static let biz = "store.biz.scope"
}
定義 Scope 管理來(lái)管理模塊的生命周期:
- Page scope的生命周期與頁(yè)面保持一致,Biz scope與業(yè)務(wù)形態(tài)的生命周期保持一致。
- 即在頁(yè)面形態(tài)發(fā)生變化時(shí),框架層會(huì)自動(dòng)將bizscope下的所有模塊進(jìn)行銷毀。
public class BlocStore {
typealias ScopeTable = [String: String]
...
func bizTypeDidChanged() {
// 銷毀上一個(gè)bizscope下所有模塊
xxxx
// 初始化新bizscope下模塊
xxx
}
}
這樣,新人進(jìn)入開(kāi)發(fā)時(shí)無(wú)需關(guān)注當(dāng)前業(yè)務(wù)形態(tài)或業(yè)務(wù)形態(tài)切換的問(wèn)題,達(dá)到快速上手的目的。
如何保障吞吐速度和質(zhì)量穩(wěn)定?
在開(kāi)發(fā)資源和測(cè)試資源不變的情況下,業(yè)務(wù)范圍擴(kuò)大了,我們?cè)撊绾伪U贤掏滤俣群唾|(zhì)量的穩(wěn)定呢?
圖片
我們可以將策略分為三個(gè)階段:
1.開(kāi)發(fā)階段:
對(duì)于核心流程添加全鏈路日志,如果發(fā)現(xiàn)不符合預(yù)期的數(shù)據(jù)則直接拋出異常。
同時(shí)進(jìn)行技術(shù)埋點(diǎn)上報(bào)。如果是對(duì)于核心流程的修改,強(qiáng)制添加AB降級(jí)方案。
2.測(cè)試階段:
有些bug非常隱蔽,在用戶體驗(yàn)上可能沒(méi)有任何差異,但內(nèi)部流程或數(shù)據(jù)可能已經(jīng)發(fā)生異常。
對(duì)于類似問(wèn)題,測(cè)試根本無(wú)法發(fā)現(xiàn)。導(dǎo)致此類問(wèn)題流入線上的風(fēng)險(xiǎn)。我們可以通過(guò)添加監(jiān)控和告警,讓我們及時(shí)發(fā)現(xiàn)問(wèn)題。
3.灰度/線上階段:
我們可以通過(guò)添加監(jiān)控和告警,讓我們及時(shí)發(fā)現(xiàn)問(wèn)題。
具體實(shí)施方案:
首先,我們對(duì)通用詳情頁(yè)里核心流程添加了全鏈路日志,并為日志服務(wù)添加了兩項(xiàng)額外能力:
如果發(fā)現(xiàn)日志類型為Error,內(nèi)部自動(dòng)觸發(fā)DEBUG彈窗提醒,并上報(bào)技術(shù)埋點(diǎn),達(dá)到對(duì)線上穩(wěn)定性的監(jiān)控。
圖片
同時(shí),搭建離在線數(shù)據(jù)報(bào)表和異常告警,進(jìn)一步保障穩(wěn)定性。
至此,搭建了通用詳情頁(yè)從發(fā)現(xiàn)問(wèn)題到定向拉取再到快速定位的閉環(huán)。