在上一篇文章 videojs 源码分析 中简单介绍了 videojs 的关键代码,顺便提到 videojs 的扩展性极高,本文就主要详细介绍 videojs 的扩展性设计。
videojs 源码中有很多优秀的设计,本文谈谈其中四种扩展性设计,分别是注册机制、插件、中间件、钩子。
注册机制 register
注册机制在 videojs 中使用得十分广泛,在源码中搜索 static register
可以发现几处定义,分别是 registerComponent,registerTech 和 registerPlugin。
registerComponent
在 videojs 源码分析 中就已经提到,Component 是 videojs 中所有组件的基类,而 registerComponent 就是 Component 类的静态方法。
videojs 内置的所有组件,都是通过 registerComponent 注册的,Player 使用这些组件的时候,根本不关心这些组件定义在什么地方。
我们在业务中实现的自定义组件,也是通过 registerComponent 注册的。而注册的组件在渲染时都会默认为 div 的 DOM,可以无限扩展。
注册机制中,register 和 get 通常是成对出现的。registerComponent 对应的静态方法就是 getComponent。
getComponent
在 videojs 通过 getComponent 获取已注册的组件,而获取组件的逻辑是在 Component 的 initChildren 函数中根据 options.children 来获取子组件。
比如 ControlBar 就通过遍历 children,调用 getComponent 获取子组件。
ControlBar.prototype.options_ = {
children: [
'playToggle',
'volumePanel',
'currentTimeDisplay',
'timeDivider',
'durationDisplay',
'progressControl',
'liveDisplay',
'seekToLive',
'remainingTimeDisplay',
'customControlSpacer',
'playbackRateMenuButton',
'chaptersButton',
'descriptionsButton',
'subsCapsButton',
'audioTrackButton',
'fullscreenToggle'
]
};
registerTech
Tech 是 videojs 中播放视频的“技术”,而 Tech 在 videojs 中也是可以扩展的,除了浏览器原生支持播放的 mp4 格式之外,还可以扩展为支持 flv、hls、dash 等格式。其扩展方式就是通过 registerTech。比如基于 flv.js 实现的 FLV Tech,比如支持 Youtube 的 videojs/videojs-youtube。
至于 registerPlugin,则用于插件扩展,在后文关于插件的部分再谈。
注册机制的基础结构
注册机制的基础结构最少包含三个部分:
- 类静态方法 register(key, registerValue),key 一般为字符串
- 类静态变量 box,在 register 方法中会将 registerValue 存入 box 中,registerValue 可以是任何类型,而 box 通常是一个对象
- 类静态方法 get(key),通过 key 从 box 获取已注册的 registerValue
除了这个基础结构之外,还可能会有其他的设计,比如注册的优先级,是先注册优先还是后注册优先。
另外需要注意的是,注册机制不一定会使用 register 这个单词,也可能会使用诸如 install,extend,insert,add 等单词。
我们以 xgplayer 为例进行对比,xgplayer 主要设计思想也是参考 videojs。
基础结构定义与使用
videojs
videojs 的注册方法 registerComponent 如下。注意,源码中 registerComponent 比这复杂,还有一些与 videojs 的 Tech 和 Player 有关的代码。为了便于对比,我去除了其中与注册机制关系不大的代码。
videojs 是后注册优先,即同名覆盖。
static registerComponent (name, ComponentToRegister) {
if (typeof name !== 'string' || !name) {
throw new Error(`Illegal component name, "${name}"; must be a non-empty string.`);
}
if (!Component.components_) {
Component.components_ = {};
}
Component.components_[name] = ComponentToRegister;
return ComponentToRegister;
}
static getComponent(name) {
if (!name || !Component.components_) {
return;
}
return Component.components_[name];
}
以全屏切换按钮为例,注册代码如下图所示。
xgplayer
xgplayer 的注册方法 install 如下。与 videojs 不同,xgplayer 是先注册优先,即同名无效。不过 xgplayer 又提供了一个 use 方法来实现同名覆盖。
static install (name, descriptor) {
if (!Player.plugins) {
Player.plugins = {}
}
if (!Player.plugins[name]) {
Player.plugins[name] = descriptor
}
}
static use (name, descriptor) {
if (!Player.plugins) {
Player.plugins = {}
}
Player.plugins[name] = descriptor
}
以弹幕插件为例,注册代码如图所示,在其文档 西瓜播放器 | 插件 中也有相关描述。
注意,在最新源码仓库中,其中 danmu.js 源码文件中移除了 Player.install 的这段代码,并提供了一个 Player.installAll 方法,统一注册多个默认组件。但这个代码改动对注册机制设计本身没有影响。
中间件 Middleware
谈到中间件,大家应该比较熟悉 Express/Koa 的中间件机制,在 ASP.NET 中也有中间件的设计。
关于中间件,videojs 官方博客有一篇详细的文章 videojs-middleware。
videojs 的中间件是以媒体类型 mime type 作为过滤规则的。
videojs 中的 Middleware 主要功能包括以下这些:
- 修改媒体源
- 修改 Tech
- 修改播放器状态
其基本结构如下图所示,其中的 * 就代表针对任意类型的媒体都生效。
在中间件定义中,setSource 是必须的,其中会调用 next 方法。
下面是两个简单的中间件示例,其中 video/mp4 和 video/x-flv 代表只会分别对 MP4 格式和 FLV 格式生效。
一个关于音量设置的中间件示例,除了 setSource 函数之外,还实现了 setVolume 方法,这将导致所有设置音量的操作,都将返回 0.2,实际表现即音量条保持在 0.2,无法调节。
插件 Plugin
前面我们已经提到 registerPlugin,现在我们具体看看 registerPlugin 的详细代码。可以看到 Plugin 在注册的时候就已经被应用了,已经通过 Player.prototype[name]
扩展了 Player 对象。所以,和 registerComponent 、registerTech 不同,videojs 中的通常并不需要 getPlugin 来获取对应的插件。
那么,videojs 中的插件如何生效呢?下面以一个插件作为举例。自定义一个 PerformancePlugin,继承自 videojs Plugin,以 pf
为 key 注册此插件。
然后在 videojs 的初始化参数 options 中,定义 plugins 参数,针对 pf
设置对应的参数,然后我们就可以在 player 中获得 pf
这个属性。
Plugin 的应用主要在 player.js 代码中,包括插件的检查和加载。
钩子 Hooks
Hooks 的设计思路,其实就是在应用的生命周期中找出关键节点,并添加开放地插槽,以便使用者可以通过插槽添加扩展方法。
videojs 中默认提供了两个 Hooks,分别是 beforesetup 和 setup,分别是在 Player 初始化前和 Player 初始化后执行,前者主要用来修改 options,后者则可以获取 Player 对象执行一些操作。
Hooks 实际上与事件有些类似,比如这两个内置 Hooks,如果用事件来实现,使用方式大概就会想下面这样。
videojs.on('beforesetup', cb);
videojs.on('setup', cb);
就像 Webpack 3 和 Webpack 4 中插件定义的区别,同样是 emit Hook,写法不一样,意思差不多。下面是典型 Webpack 4 插件的代码模板写法,兼容 Webpack 3。
function DemoPlugin() {
function emitHook(compilation, callback) {
//...
}
function apply(compiler) {
if (compiler.hooks) {
compiler.hooks.emit.tapAsync('AssetOutputPlugin', emitHook);
} else {
compiler.plugin('emit', emitHook);
}
}
return { apply };
}
videojs 其实提供了完整的 Hooks 机制,完全可以通过 videojs.hook 添加任意名称的 Hook,然后自己选择在合适的时机调用。
下面是一个 setup Hook 的简单示例。
结尾
videojs 源码中集合了众多前端常见的可扩展性设计,其源码就像是一个前端扩展性设计的“小百科”。如果大家能熟练掌握这些可扩展设计的方法,在自己的项目中灵活适当地使用,一定可以设计出高可扩展性的架构。