Video.js 博客

Pat O'Neill2017-01-26

功能聚焦:高级插件

注意:高级插件在 Video.js 6.0 中引入,并且仅从该版本开始支持。

如果您使用 Video.js 已有一段时间,您可能熟悉插件的概念:即成为您创建的任何播放器方法的函数。如果您不熟悉 Video.js 插件,我们提供了一份全面的插件指南可供查阅。

这些插件(我们称之为基本插件)轻量级且提供对播放器的完全控制。这非常有用,并且它不会改变——现有插件应该继续工作!

但是,如果您想要更丰富的功能集呢?或者关于如何构建插件的更多指导?或者更多开箱即用的工具来帮助管理复杂的、插件丰富的播放器呢?

嗯,直到 Video.js 6.0 之前,您都必须自己摸索。

引入高级插件

Video.js 的优势之一是其丰富的插件生态系统;因此,在过去的几个月里,我们希望将精力集中在改善插件作者的体验上。

虽然像插件生成器videojs-spellbook这样的项目使得成为插件作者比以往任何时候都容易,但 Video.js 团队认为,提供一个基础 API 和一套约定对于构建 Video.js 插件的未来至关重要。

我们的解决方案是高级插件

高级插件类似于组件

高级插件的设计目标之一是提供一个类似于现有组件系统的 API。我们通过多种方式实现了这一点。

在最低层面,这包括将插件注册函数名称从 videojs.plugin 更改为 videojs.registerPlugin(借鉴了 videojs.registerComponentvideojs.registerTech 的命名方式)。

除了简单的注册方法名称更改之外,高级插件是基于类的。一个简单的高级插件示例如下所示

const Plugin = videojs.getPlugin('plugin');

class HelloWorld extends Plugin {
  constructor(player) {
    super(player);
    this.player.addClass('hello-world');
  }
}

videojs.registerPlugin('helloWorld', HelloWorld);

这个插件可以通过与基本插件相同的方式进行初始化——通过一个播放器方法,其名称与插件的注册名称相匹配。

对于高级插件,此方法是一个工厂函数,它实例化插件类并返回一个实例。

了解所创建的播放器方法将始终是一个函数是很有用的。如果播放器已经拥有高级插件的一个实例,其关联方法将简单地返回预先存在的实例,而不是重新初始化它

const player = videojs('my-player');
const instance = player.helloWorld();

// Logs: 'true'
videojs.log(instance === player.helloWorld());

helloWorld 方法将返回此插件对象,直到它被销毁为止——之后它将再次创建一个新的插件实例。

事件

与组件类似,高级插件可以通过 ononeofftrigger 方法监听和触发事件。

这为插件和其他对象(组件、播放器等)提供了一个松散耦合的通信通道,以管理它们自己的状态并响应彼此状态的变化。

附加事件数据

Video.js 事件系统允许在触发事件时将额外数据作为第二个参数传递给监听器(第一个参数是事件对象本身)。

插件事件在此对象中传递一组一致的属性(包括传递给 trigger 的任何自定义属性)

  • instance:触发事件的插件实例。
  • name:插件的名称字符串(例如 'helloWorld')。
  • plugin:插件类/构造函数(例如 HelloWorld)。

例如,插件事件的监听器可以预期如下内容

const player = videojs('my-player');
const instance = player.helloWorld();

instance.on('some-custom-event', (e, data) => {
  videojs.log(data.instance === instance); // true
  videojs.log(data.name === 'helloWorld'); // true
  videojs.log(data.plugin === videojs.getPlugin('helloWorld')); // true
  videojs.log(data.foo); // "bar"
});

instance.trigger('some-custom-event', {foo: 'bar'});

生命周期

插件和组件之间的另一个相似之处是生命周期的概念——更具体地说,是设置和拆卸过程。

我们通过 JavaScript 中正常的对象创建获得了设置功能,但在对象销毁和确保对象间引用被清理以避免内存泄漏方面,我们则需要自行处理。

Video.js 组件长期以来都具有 dispose 方法和事件,用于从 DOM 和内存中移除组件。高级插件也具备相同的功能

const player = videojs('my-player');

const firstInstance = player.helloWorld();

// Logs: 'true'
videojs.log(firstInstance === player.helloWorld());

firstInstance.on('dispose', () => videojs.log('disposing a helloWorld instance'));

// Logs: 'disposing a helloWorld instance'
firstInstance.dispose();

const secondInstance = player.helloWorld(); 

// Logs: 'false'
videojs.log(firstInstance === secondInstance);

pluginsetup 事件

插件有一个组件不具备的生命周期特性:pluginsetup 事件。

当插件在播放器上初始化时,会触发此事件

const player = videojs('my-player');

player.on('pluginsetup', (e, hash) => {
  if (hash.name === 'helloWorld') {
    videojs.log('A helloWorld instance was created!');
  }
});

// Logs: 'A helloWorld instance was created!'
player.helloWorld();

受 React 启发的有状态性

Video.js 在高级插件和组件方面最令人兴奋的新增功能之一是受 React 启发的有状态性。本质上,这意味着所有插件对象和组件对象都具有一个 state 属性,它是一个普通对象,可用于存储该对象的变量状态。然后,还有一个 setState 方法,用于更新此对象并触发 statechanged 事件。

该系统允许插件和组件利用其事件驱动的特性,通过一致的 API 进行内存中状态变化的通信

// A static property of the constructor can be used to pre-populate state 
// for all instances.
HelloWorld.defaultState = {color: 'red'};

const player = videojs('my-player');
const instance = player.helloWorld();

instance.on('statechanged', (e) => {
  const {color} = e.changes;

  if (color) {
    videojs.log(`The helloWorld color changed from "${color.from}" to "${color.to}"!`);
  }
});

// Logs: 'The helloWorld color changed from "red" to "blue"!'
instance.setState({color: 'blue'}); 

播放器插件感知

最后,如果不解决管理复杂插件组合中更顽固的问题之一(即播放器无法报告它已初始化或未初始化哪些插件),我们就无法添加新的插件基础设施。为此,播放器新增了两个方法:hasPluginusingPlugin。这些方法适用于两种类型的插件。

hasPlugin 方法

此方法报告播放器上是否存在与给定名称匹配的插件

const player = videojs('my-player');

// Logs: 'true'
videojs.log(player.hasPlugin('helloWorld'));

// Logs: 'false'
videojs.log(player.hasPlugin('fooBar'));

此方法不考虑插件是否已初始化,仅报告其是否已注册。

usingPlugin 方法

此方法不仅报告播放器上是否存在插件,还报告该插件当前是否在播放器上处于活动状态

const player = videojs('my-player');

// Logs: 'false'
videojs.log(player.usingPlugin('helloWorld'));

player.helloWorld();

// Logs: 'true'
videojs.log(player.usingPlugin('helloWorld'));

这里有一个需要注意的警告。虽然这适用于两种类型的插件,但只有高级插件才能多次更改此值。基本插件没有内置的生命周期或事件;因此,无法确定它是否已被“销毁”。

去吧,开始编码吧

我们希望这些对插件架构的增补和改进能让 Video.js 插件的编写变得更愉快,并减少确保插件不产生内存泄漏及其他问题所涉及的一些底层繁琐工作。

高级插件的设计使得我们可以在 6.0 版本成熟并获得更多社区反馈时添加新功能。一如既往,我们强烈鼓励用户以任何可能的方式回馈 Video.js 项目

要更全面地讨论插件,请访问Video.js 插件指南