| 导语 直播页面是一个功能丰富且复杂的页面,整个页面几乎全部由若干个功能组件构成,在这样一个背景下,如何通过前期的合理设计来接入这些功能组件,同时提高页面的扩展性和可维护性。
一、背景
开播了鹅是手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物理学算法解析
算力时代将至——我们是否已经做好准备