| 導語 直播頁面是一個功能豐富且複雜的頁面,整個頁面幾乎全部由若幹個功能元件構成,在這樣一個背景下,如何通過前期的合理設計來接入這些功能元件,同時提高頁面的擴充性和可維護性。

一、背景
開播了鵝是手Q今年上線的一個直播帶貨平台。
圖檔來源:直播截圖
在實作上,我們采用的是用戶端+h5模式,底層的視訊流屬于用戶端邏輯,頂層的各種挂件屬于h5邏輯。去掉視訊流之後,也就是右邊這張圖,就是一個純h5頁面。

二、功能子產品劃分
在h5頁面裡面,總的來說可以分成兩大塊,一塊是各種類型的挂件,比如禮物動畫,商品櫥窗,另一塊則是一些公共基礎邏輯,比如消息輪詢,挂件接入等。
挂件這裡其實跟我們普通開發功能元件沒有多大的差別,這裡就不展開描述了。而如何把各種類型的挂件組織到一個h5頁面上,就是我們的挂件體系需要考慮的。

三、目标
我們在設計這套體系的時候,設定了幾個目标。
- 高内聚,低耦合。插件在接入這套體系之後,插件和插件之前是耦合開的,不會互相影響。
- 可維護。由于直播項目工程比較大,希望這套體系能承接住近一兩年的需求。特别如果涉及到技術棧的遷移,能夠盡可能減少代碼的重構。
- 可擴充。目标是能夠接入各種各樣功能的插件,避免因為某個插件不适配需要重構這套體系。
- 友善接入。因為在開發插件的時候,任何人都可能會來參與開發。那麼我們要保證對于插件開發者來說,隻關注自身插件的邏輯,盡量少的去寫公共邏輯。

四、解決方案
- 方案一:vue元件開發
大緻思路是一個挂件即一個元件,所有挂件以vue元件的形式開發。這套方案跟我們平時開發的思路是差不多的。但是會有以下幾個問題:
- 強依賴vue。後續如果遷移vue3.0或者react,那麼整個直播頁面需要重做,包括所有插件。
- 生存周期不好統一。vue雖然有一套生命周期,但這套生命周期是由元件自己控制的,如果我們想讓某個元件初始化或者銷毀,其實是不太好做的。
- 公共檔案大量修改。有過開發vue元件的開發者都比較清楚,我們開發完一個vue元件的時候,需要在父級元件裡面引入,如果涉及到資料互動或者樣式調整,也需要在父級修改。如果元件少還好維護,一旦元件增加之後,這個公共檔案就會變得越來越臃腫,并且還有可能導緻元件與元件之間互相影響。
- 布局不好實作。這點我們放到後面的布局方案講。
基于以上幾點考慮,我們就舍棄了這個方案。然後就有了方案二。
- 方案二:自定義插件體系
這套方案有以下幾個特點。
- 自定義一套生命周期。我們參考了vue的生命周期,自定義一套。提供了擁有初始化,更新,銷毀等功能。把渲染時機交給插件體系來控制,這樣的話渲染機制就由黑盒變成白盒。而開發者隻需要在對應的鈎子函數裡面添加自己的邏輯即可
import { util } from '@tencent/qlib';
// 插件體系子產品,與直播插件獨立
import { BasePlugin, Subscriber, LAYOUT_TYPE, LAYOUT } from
'@packages/basePlugin';
// 插件自身的邏輯元件
import audienceInfo from './src/AudienceInfo.vue';
import storeConfig from './store';
class AudienceInfo extends BasePlugin {
init(): void {
const MyComponent = this.RootVue.extend(audienceInfo);
// 注冊狀态管理器
this.rootStore.registerModule(storeConfig.name, storeConfig);
const componentEl = new MyComponent({
store: this.rootStore,
data: {
uid: 111,
},
}).$mount();
// layer由插件體系配置設定,把生成的DOM插入到layer裡面即可完成渲染,這裡
不限制語言架構,隻要提供html即可
this.layer.appendChild(componentEl.$el);
}
getLayout(): LAYOUT {
return {
width: '100w',
offsetBottom: '0',
zIndex: 3,
layoutType: LAYOUT_TYPE.LEFTBOTTOM,
};
}
subscribeMsg(): Array {
return [];
}
dismiss(): void {
// 銷毀内容
this.rootStore.unregisterModule(storeConfig.name);
}
}
export default AudienceInfo;
- 2.
- 核心邏輯全部用TS編寫。也就是前面代碼裡面的@packages/basePlugin,我們全部采用TS來編寫,確定不依賴第三方架構,友善以後做技術重構。
- 中心管理。布局,輪詢,狀态管理統一收歸到插件體系,插件開發不需要關注如何内部實作,隻需要調用具體的方法即可。
- 基類繼承。所有插件繼承一個公共的基類,保證插件的規範性。
- 補充:布局
然後再來分析下插件布局,目前把插件分為三類:可互動的UI元件,彈窗類型元件,純展示不可互動元件。首先我們做了一個分層處理,把彈窗類型元件放在了最頂層,可互動的UI元件放在第二層,純展示的放在最底層,如圖:
确認好層次之後,再确認每一層的布局。目标是每一層的布局模式都保持一緻。
以第二層為例,這裡面的元件雖然在頁面的任何一個地方,比較雜亂。但仔細觀察,還是有一定的規律的。第二層所有的元件都是從某個角出發,向不同的兩個方向排列的。如下圖所示,比如分享插件,在左下角向右排列,消息元件,在左下角向上排列。
圖檔來源:直播截圖
找到規律之後,我們就可以去布局了。
我們分析了主流的布局方案:
布局類型 | 特點 |
block | 需要元件管理自己的位置資訊,一旦元件膨脹将難以維護 |
flex | 側重一維布局,難以處理複雜的層級關系,不友善精準定位 |
grid | 寫法複雜,不友善精準定位,還有相容性問題(iOS10) |
絕對定位 | 精準定位,但需要大量計算 |
發現都有優缺點。最後我們選擇了第四種方案,絕對定位布局。
這個方案需要解決的最大問題就是位置計算,如果把這個計算交給插件做的話,每個插件接入進來都需要去計算目前已有的插件的位置,勢必會把插件開發變得更複雜。是以我們把這套計算交給了插件體系去做。
插件體系在這八個方位裡面分别用一個變量來存儲目前方位已經占據的空間,當下一個插件接入進來的時候,根據對應的方位裡面的資料來确定位置,同時把目前插件的位置資訊更新到對應的變量裡面。
對于插件開發者來說,隻需要聲明具體的方位,以及自身需要的寬高(上面代碼裡面的getLayout方法),插件體系就可以給插件配置設定好具體的位置了(上面代碼裡面的this.layer)。

五、目标
這套插件體系已經在直播這邊平穩的使用了快半年,也從剛開始的幾個插件到現在的30多個插件,到目前位置,不管收到什麼樣類型的需求挂件,這套體系基本都能夠承接住。也從側面看出來這套體系是基本合格的。而這篇文章的目的,還是希望在遇到類似場景的需求的時候,能給大家一些參考。
資料分析方法入門
從0開始打造UI架構:動态化架構Scrollview實體學算法解析
算力時代将至——我們是否已經做好準備