前端进阶:跟着开源项目学习插件化架构

本文在介绍微内核架构相关概念以后,阿宝哥将带你们从🍉视频播放入手,一步步分析微内核(插件化)架构设计三部曲。css

1、微内核架构简介

1. 1 微内核的概念

微内核架构(Microkernel Architecture),有时也被称为插件化架构(Plug-in Architecture),是一种面向功能进行拆分的可扩展性架构,一般用于实现基于产品的应用。微内核架构模式容许你将其余应用程序功能做为插件添加到核心应用程序,从而提供可扩展性以及功能分离和隔离。html

微内核架构模式包括两种类型的架构组件:核心系统(Core System)和插件模块(Plug-in modules)。应用逻辑被分割为独立的插件模块和核心系统,提供了可扩展性、灵活性、功能隔离和自定义处理逻辑的特性。前端

图中 Core System 的功能相对稳定,不会由于业务功能扩展而不断修改,而插件模块是能够根据实际业务功能的须要不断地调整或扩展。微内核架构的本质就是将可能须要不断变化的部分封装在插件中,从而达到快速灵活扩展的目的,而又不影响总体系统的稳定。git

微内核架构的核心系统一般提供系统运行所需的最小功能集。许多操做系统使用的就是微内核架构,这也是它名字的由来。从商业应用程序的角度来看,核心系统通常是通用业务逻辑,没有特殊状况、特殊规则或复杂情形下的自定义代码。github

插件模块是独立的模块,包含特定的处理、额外的功能和自定义代码,来向核心系统加强或扩展额外的业务能力。一般插件模块之间也是独立的,也有一些插件是依赖于若干其它插件的。重要的是,尽可能减小插件之间的通讯以免依赖的问题。数据库

1.2 微内核架构的优势

  • 灵活性高:总体灵活性是对环境变化快速响应的能力。因为插件之间的低耦合,改变一般是隔离的,能够快速实现。一般,核心系统是稳定且快速的,具备必定的健壮性,几乎不须要修改。
  • 可测试性:插件能够独立测试,也很容易被模拟,不需修改核心系统就能够演示或构建新特性的原型。
  • 性能高:虽然微内核架构自己不会使应用高性能,但一般使用微内核架构构建的应用性能都还不错,由于能够自定义或者裁剪掉不须要的功能。

介绍完微内核架构相关的基础知识,接下来咱们将以西瓜视频播放器为例,分析一下微内核架构在西瓜视频播放器中的应用。安全

阅读阿宝哥近期热门文章(感谢掘友的鼓励与支持🌹🌹🌹):服务器

2、西瓜视频播放器简介

西瓜视频播放器一款带解析器、能节省流量的 HTML5 视频播放器。它从底层解析 MP四、HLS、FLV 探索更大的视频播放可控空间。babel

(图片来源 —— http://h5player.bytedance.com/)markdown

它的功能特点是从底层解析 MP四、HLS、FLV 探索更大的视频播放可控控件并拥有如下特色:

  1. 易扩展:灵活的插件体系、PC/移动端自动切换、安全的白名单机制;

  2. 更丰富:强大的 MP4 控制、点播的无缝切换、有效的带宽节省;

  3. 较完整:完整的产品机制、错误的监控上报、自动的降级处理。

上手西瓜视频播放器只需三步:安装、DOM 占位、实例化便可完成播放器的使用。

xgplayer-quick-start

(图片来源 —— pingan8787)

西瓜视频播放器主张一切设计都是插件,小到一个播放按钮大到一项直播功能支持。 想更好的自定义播放器完成本身业务的契合,理解插件机制是很是重要的,播放器自己有不少内置插件,好比报错、loading、重播等,若是你们想自定义效果能够关闭内置插件,本身开发便可。

默认状况下插件是自启动的,若是自定义插件不想自启动或者不想改变播放器默认的执行机制,建议以继承播放器类的方式开发。为了实现 "一切设计都是插件" 的主张,西瓜视频播放器团队采用了微内核的架构,下面咱们开始来分析一下西瓜视频播放器的微内核实践。

3、西瓜视频播放器微内核实践

微内核架构模式包括两种类型的架构组件:核心系统和插件模块。在西瓜视频播放器中核心系统是由 Player 类来实现,该类对应的 UML 图以下所示:

(https://github.com/bytedance/xgplayer/blob/master/packages/xgplayer/src/player.js)

而插件模块主要就是西瓜视频播放器中的各类内置插件,好比控制条的音量控制组件、播放器贴图、播放器画中画和播放器下载控件等,除了上面提到的插件以外,目前西瓜视频播放器总共提供了 22 个插件,完整的内置插件以下图所示:

(西瓜视频播放器内置插件)

对于微内核的核心系统设计来讲,它涉及三个关键技术:插件管理、插件链接和插件通讯。下面咱们将围绕这三个关键点来逐步分析西瓜视频播放器是如何实现的。

3.1 插件管理

核心系统须要知道当前有哪些插件可用,如何加载这些插件,何时加载插件。常见的实现方法是插件注册表机制。核心系统提供插件注册表(能够是配置文件,也能够是代码,还能够是数据库),插件注册表含有每一个插件模块的信息,包括它的名字、位置、加载时机(启动就加载,或是按需加载)等。

在分析西瓜视频播放器插件管理机制前,咱们先来看一下 xgplayer/packages/xgplayer/src 目录结构:

├── control
│ ├── collect.js │ ├── cssFullscreen.js │ ├── danmu.js │ ├── .... │ └── volume.js ├── error.js ├── index.js ├── player.js ├── proxy.js ├── style │ ├── index.scss │ ├── ... │ └── variable.scss └── utils  ├── animation.js  ├── database.js  ├── ...  └── util.js 复制代码

经过观察以上目录结构,咱们能够发现西瓜视频播放器的插件都统一存放在 control 目录下。那么如今问题来了,这些插件是如何被加载的?何时被加载?要回答这个问题,咱们从该项目的入口出发:

// packages/xgplayer/src/index.js
import Player from './player' // ① import * as Controls from './control/*.js' // ② import './style/index.scss' // ③ export default Player // ④ 复制代码

index.js 文件中,咱们发如今第二行代码中使用了 import * as Controls from './control/*.js' 语句批量导入播放器的全部内置插件。该功能是借助 babel-plugin-bulk-import 这个插件来实现的。

除了使用上述插件以外,还能够借助 Webpack context API 来实现,经过执行 require.context 函数获取一个特定的上下文,就能够实现自动化导入模块。在前端工程中,若是遇到从一个文件夹引入不少模块的状况,可使用这个 API,它会遍历文件夹中的指定文件,而后自动导入模块,而不须要每次显式的调用 import 导入模块。

Webpack context API 的使用示例以下:

const contextRequire = require.context("./modules", true);
 const modules = []; contextRequire.keys().forEach((filename) => {  if (filename.match(/^\.\/[^_][\w/]*\.([tj])s$/)) {  modules.push(contextRequire(filename));  } }); 复制代码

好的,回到正题。如今咱们已经知道西瓜视频播放器的全部内置插件,都是经过 babel-plugin-bulk-import 这个插件在构建阶段完成加载的。若是不想使用播放器中的内置控件,能够经过ignores 配置项关闭,使用本身开发的相同功能插件进行替换:

new Player({
 el:document.querySelector('#mse'),  url: 'video_url',  ignores: ['replay'] // 默认值[] }); 复制代码

下个环节,咱们来分析西瓜视频播放器的内置插件是如何链接到核心系统的。

3.2 插件链接

插件链接是指插件如何链接到核心系统。一般来讲,核心系统必须指定插件和核心系统的链接规范,而后插件按照规范实现,核心系统按照规范加载便可。

要了解西瓜视频内置插件是如何链接到核心系统,我就须要来分析已有的内置的插件,这里咱们以简单的 loading 内置插件为例:

// packages/xgplayer/src/control/loading.js
import Player from '../player'  let loading = function () {  let player = this;  let util = Player.util;  let container = util.createDom('xg-loading', `  <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewbox="0 0 100 100">  <path d="M100,50A50,50,0,1,1,50,0"></path>  </svg>  `, {}, 'xgplayer-loading')  player.root.appendChild(container) }  Player.install('loading', loading) 复制代码

(https://github.com/bytedance/xgplayer/blob/master/packages/xgplayer/src/control/loading.js)

在以上代码中,最重要的是最后一行,即 Player.install('loading', loading) 这一行。顾名思义,install 方法是用来安装插件其具体实现以下:

// packages/xgplayer/src/player.js
class Player extends Proxy {  static install (name, descriptor) {  if (!Player.plugins) {  Player.plugins = {}  }  Player.plugins[name] = descriptor  } } 复制代码

经过观察以上代码可知,install 方法支持两个参数 namedescriptor,分别表示插件名称和插件描述器。当调用 Player.install 方法后,会把插件信息注册到 Player 类的 plugins 命名空间下。须要注意的是,这里仅仅是完成插件的注册操做。在利用 Player 类建立播放器实例的时候,才会进行插件初始化操做,代码以下:

class Player extends Proxy {
 constructor(options) {  if (  this.config.controlStyle &&  util.typeOf(this.config.controlStyle) === "String"  ) {  // ...  // 从服务器成功获取配置信息后,  // 再调用self.pluginsCall()  } else {  this.pluginsCall();  }  } } 复制代码

Player 类构造函数中会调用 pluginsCall 方法来初始化插件,其中 pluginsCall 方法的具体实现以下:

class Player extends Proxy {
 pluginsCall() {  let self = this;  if (Player.plugins) {  let ignores = this.config.ignores;  Object.keys(Player.plugins).forEach(name => {  let descriptor = Player.plugins[name];  // 忽略ignores配置项关闭的插件  if (!ignores.some(item => name === item)) {  if (["pc", "tablet", "mobile"].some(type => type === name)) {  if (name === sniffer.device) {  setTimeout(() => {  descriptor.call(self, self);  }, 0);  }  } else {  descriptor.call(this, this);  }  }  });  }  } } 复制代码

了解完上述知识,咱们再来介绍一下如何自定义西瓜视频播放器插件。在西瓜视频播放器中,自定义插件只有两个步骤:

1. 开发插件

// pluginName.js
import Player from 'xgplayer';  let pluginName=function(player){  // 插件逻辑 }  Player.install('pluginName',pluginName); 复制代码

2. 使用插件

import Player from 'xgplayer';
 let player = new Player({  id: 'xg',  url: '//abc.com/**/*.mp4' }) 复制代码

好的,咱们继续进入下一个环节,即分析西瓜视频播放器核心系统和插件模块之间是如何通讯的。

3.3 插件通讯

插件通讯是指插件间的通讯。虽然设计的时候插件间是彻底解耦的,但实际业务运行过程当中,必然会出现某个业务流程须要多个插件协做,这就要求两个插件间进行通讯;因为插件之间没有直接联系,通讯必须经过核心系统,所以核心系统须要提供插件通讯机制

这种状况和计算机相似,计算机的 CPU、硬盘、内存、网卡是独立设计的配置,但计算机运行过程当中,CPU 和内存、内存和硬盘确定是有通讯的,计算机经过主板上的总线提供了这些组件之间的通讯功能。

一样,咱们以西瓜视频播放器的内置插件为切入点来分析插件通讯机制,下面咱们以 poster 内置插件为例。poster 插件用于设置播放器的封面图,该图是当播放器初始化后在用户点击播放按钮前显示的图像。

该插件的使用方式以下:

new Player({
 el:document.querySelector('#mse'),  url: 'video_url',  poster: '//abc.com/**/*.png' // 默认值"" }); 复制代码

该插件的对应源码以下:

import Player from '../player'
 let poster = function () {  let player = this;  let util = Player.util  let poster = util.createDom('xg-poster', '', {}, 'xgplayer-poster');  let root = player.root  if (player.config.poster) {  poster.style.backgroundImage = `url(${player.config.poster})`  root.appendChild(poster)  }   // 监听播放事件,播放时隐藏封面图  function playFunc () {  poster.style.display = 'none'  }  player.on('play', playFunc)   // 监听销毁事件,执行清理操做  function destroyFunc () {  player.off('play', playFunc)  player.off('destroy', destroyFunc)  }  player.once('destroy', destroyFunc) }  Player.install('poster', poster) 复制代码

(https://github.com/bytedance/xgplayer/blob/master/packages/xgplayer/src/control/poster.js)

经过观察源码可知,该插件首先经过监听播放器的 play 事件来隐藏 poster 海报。此外还会监听播放器的 destory 事件来实现清理操做,好比移除 play 事件的监听器和 destroy 事件。

要实现上述功能,在源码中是经过 player 实例提供的 onoffonce 三个方法来实现,相信大多数读者对这三个方法都很熟悉了,它们分别用于实现添加监听(on)、移除监听(off)和单次监听(once)。

那么上述的三个方法来自哪里呢?经过阅读西瓜视频播放器的源码,咱们发现上述方法是 Player 类经过继承 Proxy 类,在 Proxy 类中又经过构造继承的方式继承于来自 event-emitter 第三方库的 EventEmitter 类来实现的。

poster 插件中的监听了播放器的 playdestroy 事件,那这些事件是何时会触发呢?下面咱们来分别分析一下:

1. play 事件

// packages/xgplayer/src/proxy.js
this.ev = ['play', 'playing', 'pause', 'ended', 'error', 'seeking',  'seeked','timeupdate', 'waiting', 'canplay', 'canplaythrough',  'durationchange', 'volumechange', 'loadeddata'].map((item) => {  return {  [item]: `on${item.charAt(0).toUpperCase()}${item.slice(1)}`  } });  this.ev.forEach(item => {  self.evItem = Object.keys(item)[0]  let name = Object.keys(item)[0]  self.video.addEventListener(Object.keys(item)[0], function () {  if (name === 'error') {  if (self.video.error) {  self.emit(name, new Errors('other',  self.currentTime, self.duration,  self.networkState, self.readyState,  self.currentSrc, self.src,  self.ended, {  line: 41,  msg: self.error,  handle: 'Constructor'  }))  }  } else {  self.emit(name, self)  } }); 复制代码

(https://github.com/bytedance/xgplayer/blob/master/packages/xgplayer/src/proxy.js)

在西瓜视频播放器初始化的时候,会经过调用 Video 元素的 addEventListener 方法来监听各类原生事件,在对应的事件处理函数中,会调用 emit 方法进行事件派发。

2. destory 事件

// packages/xgplayer/src/player.js
function destroyFunc() {  this.emit("destroy");  // fix video destroy https://stackoverflow.com/questions/3258587/how-to-properly-unload-destroy-a-video-element  this.video.removeAttribute("src"); // empty source  this.video.load();  if (isDelDom) {  parentNode.removeChild(this.root);  }  for (let k in this) {  delete this[k];  }  this.off("pause", destroyFunc); } 复制代码

(https://github.com/bytedance/xgplayer/blob/master/packages/xgplayer/src/player.js)

在西瓜视频播放器销毁时,会调用 destroyFunc 方法,在该方法内部,会继续调用 emit 方法来发射 destroy 事件。以后,若其它插件有监听 destroy 事件,那么将会触发对应的事件处理函数,执行相应的清理工做。而对于插件之间的通讯,一样也能够借助 player 播放器对象上事件相关的 API 来实现,这里就再也不展开。

前面咱们已经从插件管理、插件链接和插件通讯这三方面分析了西瓜视频播放器是如何实现微内核架构,下面咱们用一张图来总结一下主要的内容:

4、总结

本文以西瓜视频播放器为例,详细介绍了微内核架构的设计要点与实现。其实西瓜视频播放器除了提供大量的内置插件以外,它也提供了一些功能插件,如 flv 和 hls 功能插件,从而来知足不一样的播放场景。

此外,经过分析西瓜视频播放器,咱们发现要设计一个功能完善的组件是颇有挑战的一件事,要考虑很是多的事情,这里我以思惟导图的形式简单整理了一下,有兴趣的读者能够参考一下。

想进一步了解西瓜视频播放器的读者,能够阅读我以前整理的 "西瓜视频播放器功能分析" 这篇文章。

(https://www.yuque.com/docs/share/a86a12a1-77c4-4f78-854b-af185f90bec4?#)

5、参考资源

本文使用 mdnice 排版

相关文章
相关标签/搜索