【videojs 系列02】videojs 中四种扩展性设计

3,325 阅读6分钟

在上一篇文章 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 来获取对应的插件。

image.png

那么,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 源码中集合了众多前端常见的可扩展性设计,其源码就像是一个前端扩展性设计的“小百科”。如果大家能熟练掌握这些可扩展设计的方法,在自己的项目中灵活适当地使用,一定可以设计出高可扩展性的架构。