AS3中的插件模式开发(二)

以一个视频播放器为例,分析插件机制的具体执行方式。

本系列文章将在新博客更新,链接在此,请移步阅读。


上次我分析了插件模式的一般特征,并且对主体应该具备的功能进行了设计。这次,我尝试拿具体的案例来分析如何设计插件,如何使插件和主体进行简单的联动。

先看例子:视频播放器。

视频播放器非常常见,就拿我以前做的播放器当例子,请看下图:

视频播放器
视频播放器

上方是最主要的部分——视频播放窗口,下面是控制面板,如此便组成了最简单的播放器。然而实际生产当中我们还会遇到各种各样的特殊需求,比如:片头广告、(播放完毕后)更多推荐、(时间轴上)关键点预告等等;这些需求,在不同的应用场景下又会呈现出不同的组合。比如,当作为页面视频广告时,需要监测有多少用户观看,观看了多长时间,而不需要其它增强功能;当嵌入到专门的视频文章页时,就需要将片头广告、更多推荐都包含进去;当被某个小网站借去改换外观重新上线时,只需要保留基本的播放功能就好,因为没有专人来维护其它内容。

如此这般因时间、地点不同而对产品功能产生了不同的需求,就适用插件模式了。要应用插件模式,首先咱们得区分主体插件

主体

对于播放器而言,主体自然包括“可视的视频主体”和“视频播放的基本控制器”两部分。具体到这个产品,我把外部参数作为“配置信息”也当成主体的一部分包含进来。于是主体就包含三个部分:

  1. 视频主体
  2. 基础控制面板(包括播放、暂停、音量调节等等)
  3. 配置信息

这三个部分互相关联,具有较高的耦合度,如下图所示。配置信息用来提供影片的相关信息(这里只标出getMovieSrc的方法),提供给视频主体读取;控制面板利用视频主体提供的方法来控制视频的播放、暂停等操作,并根据视频头来显示播放进度。

主体结构简图
主体结构简图

插件

相对来说,插件的构成比较自由,每个功能都可以独立成插件,于是统计一下需要,就归纳出如下这些插件:

  1. 片头广告
  2. 缩略图占位符
  3. 时间轴关键点提示
  4. 更多推荐
  5. 统计
  6. 画面热区
    鼠标划过,浮现半透明的层,显示一些信息
  7. 关键点编辑插件
  8. 热区编辑插件

插件的复杂度跟功能有关,有些插件的逻辑和素材很复杂,比如4;有些则很简单,比如1。插件的复杂程度和它所处的位置无关,再复杂的插件也是插件。

插件的逻辑彼此不同,这里也不再画图了。

联动

回忆一下上一篇文章,关于插件模式的架构:主体 <=> PluginManager <=> IPlugin。一直到现在我都没有提PluginManager,似乎前后矛盾的样子。其实在这个案例当中,我并没有使用它。为什么呢?我考虑过以下几点,把插件和主体放在了一起:

  1. 全功能播放器的体积不过几十K(包括可视素材),拆成更小的模块,会加重用户加载的负担
  2. 视频并非网站主营业务,所以也没有一个针对视频的配置后台,拆开之后会给其他同事部署swf带来问题
  3. 播放器的功能虽然很多,但相对固定,不太可能有即时加载的需要。

最终方案里,PluginManager的功能由文档类来继承。同时,在主体三大类当中,不体现任何插件的内容,插件存放在plugin包中,这样就实现了物理区隔。于是,总的指导思想不变,只是表现形式稍微变了下,插件模式的优点依然保留。

最后插件接口IPlugin是这个样子:

IPlugin
IPlugin

现在,我拿最简单的“缩略图”来示范插件的加载和联动。因为文档类即PluginManager,所以在其中直接声明插件实例就可以:

....
var thumb:ThumbnailPlugin = new ThumbnaiPlugin();
thumb.install(this); // this为此插件的父容器
thumb.fill(video, controller, data);
....

在ThumbnaiPlugin这个类中,我们需要侦听controller的PLAY事件,以便适时的移除插件,相关代码是这样的:

public function install(container:Sprite):void {
  container.addChild(this);
}
public function fill(video:IVideo, controller:IController, data:IData):void {
  load(data.getPluginData().thumbnail);
  controller.addEventListener(PlayEvent.PLAY, playHandler);
}

private function load(url:String):void {
  // 加载并显示图片
}
private function playHandler(event:PlayEvent):void {
  if (parent != null) {
    parent.removeChild(this);
  }
  controller.removeEventListener(PlayEvent.PLAY, playHandler);
}

插件从IData当中取到自己需要的缩略图路径信息,加载并显示缩略图。当用户单击播放按钮开始播放时,插件会侦听到PlayEvent.PLAY,并移除自己。

总结

从始至终,主体并不关心插件的存在,插件从主体中取得自己关心的时间点和数据,执行自己的逻辑。

接下来,我会更进一步,探讨那些复杂的插件该如何完成任务,以及主体该如何配合它(们)。