为什么80%的码农都做不了架构师?>>>   

写在前面

现在视频业务越来越流行了,播放器也比较多,作为前端工程师如何打造一个属于自己的播放器呢?最快最有效的方式是基于开源播放器深度定制,至于选择哪个开源播放器仁者见仁智者见智,可以参考开源播放器列表选择适合自己业务的播放器。

我们的播放器选择了排名第一的video.js播放器,截至目前该播放器在Github拥有13,991 star, 4,059 fork,流行程度可见一斑。为了让大家更多的了解它,我们细数下优点:

  1. 免费开源

    这个意味着什么就不多说了,附上项目地址

  2. 兼容主流浏览器

    在国内的前端开发环境往往需要支持到低级版本的IE浏览器,然后随着Flash的退化,很多公司没有配备Flash开发工程师,video.js提供了流畅的Flash播放器,而且在UI层面做到了和video的效果,实属难得,比如全屏。

  3. UI自定义

    不管开源项目在UI方面做的如何漂亮,对于各具特色的业务来说都要自定义UI,一个方便简单的自定义方式显得格外重要,更何况它还自带了编译工具,只能用一个”赞“字形容。具体怎么实现的,这里先简单描述下是使用JavaScript(es6)构建对象,通过Less编写样式规则,最后借助Grunt编译。

  4. 灵活插件机制

    video.js提供一个插件定义的接口,使插件开发简单易行。而且社区论坛也提供了一些好用的插件供开发者使用。附插件列表

  5. 比较完善的文档

    完善的文档对于一个稳定的开源项目是多么的重要,video.js提供了教程、API文档、插件、示例、论坛等。官方地址

  6. 项目热度

    开源作者对项目的维护比较积极,提出的问题也能很快给予响应。开发者在使用过程中出现问题算是有一定保障。

书归正传,要想更自由的驾驭video.js,必然要了解内部原理。本文的宗旨就是通过核心代码演示讲解源码运行机制,如果有兴趣,不要离开,我们马上开始了……

组织结构

由于源码量较大,很多同学不知道从何入手,我们先来说下它的组织结构。

├── control-bar

├── menu

├── popup

├── slider

├── tech

├── tracks

├── utils

├── big-play-button.js

├── button.js

├── clickable-component.js

├── close-button.js

├── component.js

├── error-display.js

├── event-target.js

├── extend.js

├── fullscreen-api.js

├── loading-spinner.js

├── media-error.js

├── modal-dialog.js

├── player.js

├── plugins.js

├── poster-image.js

├── setup.js

└── video.js

其中control-bar,menu,popup,slider,tech,tracks,utils是目录,其他是文件。video.js是个非常优秀的面向对象的典型,所有的UI都是通过JavaScript对象组织实现的。

video.js是个入口文件,看源码可以从这个文件开始。

setup.js处理播放器的配置安装即data-setup属性。

poster-image.js处理播放器贴片。

plugins.js实现了插件机制。

player.js构造了播放器类也是video.js的核心。

modal-dialog.js处理弹层相关。

media-error.js定义了各种错误描述,如果想理解video.js对各语言的支持,这个文件是必看的,它是桥梁。

loading-spinner.js实现了播放器加载的标志,如果不喜欢默认加载图标在这里修改吧。

fullscreen-api.js实现各个浏览器的全屏方案。

extend.js是对node 继承 and babel’s 继承的整合。

event-target.js 是event类和原生事件的兼容处理。

error-display.js 主要处理展示错误的样式设置。

component.js 是video.js框架中最重要的类,是所有类的基类,也是实现组件化的基石。

close-button.js 是对关闭按钮的封装,功能比较单一。

clickable-component.js 如果想实现一个支持点击事件和键盘事件具备交互功能的组件可以继承该类,它帮你做了细致的处理。

button.js 如果想实现一个按钮了解下这个类是必要的。

big-play-button.js 这个按钮是视频还未播放时显示的按钮,官方将此按钮放置在播放器左上角。

utils目录顾名思义是一些常用的功能性类和函数。

tracks目录处理的是音轨、字幕之类的功能。

tech目录也是非常核心的类,包括对video封装、flash的支持。

slider目录主要是UI层面可拖动组件的实现,如进度条,音量条都是继承的此类。

popup目录包含了对弹层相关的类。

menu目录包含了对菜单UI的实现。

control-bar目录是非常核心的UI类的集合了,播放器下方的控制器都在此目录中。

通过对组织结构的描述,大家可以,想了解video.js的哪一部分内容可以快速入手。如果还想更深入的了解如何正确使用这些类,请继续阅读继承关系一节。

继承关系

video.js是JavaScript面向对象实现很经典的案例,你一定会好奇在页面上一个DOM节点加上data-setup属性简单配置就能生成一个复杂的播放器,然而在代码中看不到对应的HTML”模板“。其实这都要归功于”继承“关系以及作者巧妙的构思。

在组织结构一节有提到,所有类的基类都是Component类,在基类中有个createEl方法这个就是JavaScript对象和DOM进行关联的方法。在具体的类中也可以重写该方法自定义DOM内容,然后父类和子类的DOM关系也因JavaScript对象的继承关系被组织起来。

为了方便大家查阅video.js所有的继承关系,整理了两个图表,一个是完整版,一个是核心版。

运行机制

video.js源码代码量比较大,我们要了解它的运行机制,首先确定它的主线是video.js文件的videojs方法,videojs方法调用player.js的Player类,Player继承component.js文件的Component类,最后播放器成功运行。

我们来看下videojs方法的代码、Player的构造函数、Component的构造函数,通过对代码的讲解基本整个运行机制就有了基本的了解,注意里面用到的所有方法和其他类对象参照组织结构一节细细阅读就可以掌握更多的运行细节。

  • videojs方法

function videojs(id, options, ready) {

let tag;

// id可以是选择器也可以是DOM节点

if (typeof id === 'string') {

if (id.indexOf('#') === 0) {

id = id.slice(1);

}

//检查播放器是否已被实例化

if (videojs.getPlayers()[id]) {

if (options) {

log.warn(`Player "${id}"

is already initialised.Options will not be applied.`);

}

if (ready) {

videojs.getPlayers()[id].ready(ready);

}

return videojs.getPlayers()[id];

}

// 如果播放器没有实例化,返回DOM节点

tag = Dom.getEl(id);

} else {

// 如果是DOM节点直接返回

tag = id;

}

if (!tag || !tag.nodeName) {

throw new TypeError('The element or ID supplied is not valid. (videojs)');

}

// 返回播放器实例

return tag.player || Player.players[tag.playerId] || new Player(tag, options, ready);

}

[]()

  • Player的构造函数

constructor(tag, options, ready) {

// 注意这个tag是video原生标签

tag.id = tag.id || `vjs_video_$ {

Guid.newGUID()

}`;

// 选项配置的合并

options = assign(Player.getTagSettings(tag), options);

// 这个选项要关掉否则会在父类自动执行加载子类集合

options.initChildren = false;

// 调用父类的createEl方法

options.createEl = false;

// 在移动端关掉手势动作监听

options.reportTouchActivity = false;

// 检查播放器的语言配置

if (!options.language) {

if (typeof tag.closest === 'function') {

const closest = tag.closest('[lang]');

if (closest) {

options.language = closest.getAttribute('lang');

}

} else {

let element = tag;

while (element && element.nodeType === 1) {

if (Dom.getElAttributes(element).hasOwnProperty('lang')) {

options.language = element.getAttribute('lang');

break;

}

element = element.parentNode;

}

}

}

// 初始化父类

super(null, options, ready);

// 检查当前对象必须包含techOrder参数

if (!this.options_ || !this.options_.techOrder || !this.options_.techOrder.length) {

throw new Error('No techOrder specified. Did you overwrite ' +

'videojs.options instead of just changing the ' +

'properties you want to override?');

}

// 存储当前已被实例化的播放器

this.tag = tag;

// 存储video标签的各个属性

this.tagAttributes = tag && Dom.getElAttributes(tag);

// 将默认的英文切换到指定的语言

this.language(this.options_.language);

if (options.languages) {

const languagesToLower = {};

Object.getOwnPropertyNames(options.languages).forEach(function (name) {

languagesToLower[name.toLowerCase()] = options.languages[name];

});

this.languages_ = languagesToLower;

} else {

this.languages_ = Player.prototype.options_.languages;

}

// 缓存各个播放器的各个属性.

this.cache_ = {};

// 设置播放器的贴片

this.poster_ = options.poster || '';

// 设置播放器的控制

this.controls_ = !! options.controls;

// 默认是关掉控制

tag.controls = false;

this.scrubbing_ = false;

this.el_ = this.createEl();

const playerOptionsCopy = mergeOptions(this.options_);

// 自动加载播放器插件

if (options.plugins) {

const plugins = options.plugins;

Object.getOwnPropertyNames(plugins).forEach(function (name) {

if (typeof this[name] === 'function') {

this[name](plugins[name]);

} else {

log.error('Unable to find plugin:', name);

}

}, this);

}

this.options_.playerOptions = playerOptionsCopy;

this.initChildren();

// 判断是不是音频

this.isAudio(tag.nodeName.toLowerCase() === 'audio');

if (this.controls()) {

this.addClass('vjs-controls-enabled');

} else {

this.addClass('vjs-controls-disabled');

}

this.el_.setAttribute('role', 'region');

if (this.isAudio()) {

this.el_.setAttribute('aria-label', 'audio player');

} else {

this.el_.setAttribute('aria-label', 'video player');

}

if (this.isAudio()) {

this.addClass('vjs-audio');

}

if (this.flexNotSupported_()) {

this.addClass('vjs-no-flex');

}

if (!browser.IS_IOS) {

this.addClass('vjs-workinghover');

}

Player.players[this.id_] = this;

this.userActive(true);

this.reportUserActivity();

this.listenForUserActivity_();

this.on('fullscreenchange', this.handleFullscreenChange_);

this.on('stageclick', this.handleStageClick_);

}

Component的构造函数

constructor(player, options, ready) {

// 之前说过所有的类都是继承Component,不是所有的类需要传player

if (!player && this.play) {

// 这里判断调用的对象是不是Player本身,是本身只需要返回自己

this.player_ = player = this; // eslint-disable-line

} else {

this.player_ = player;

}

this.options_ = mergeOptions({}, this.options_);

options = this.options_ = mergeOptions(this.options_, options);

this.id_ = options.id || (options.el && options.el.id);

if (!this.id_) {

const id = player && player.id && player.id() || 'no_player';

this.id_ = `$ {

id

}

_component_$ {

Guid.newGUID()

}`;

}

this.name_ = options.name || null;

if (options.el) {

this.el_ = options.el;

} else if (options.createEl !== false) {

this.el_ = this.createEl();

}

this.children_ = [];

this.childIndex_ = {};

this.childNameIndex_ = {};

// 知道Player的构造函数为啥要设置initChildren为false了吧

if (options.initChildren !== false) {

// 这个initChildren方法是将一个类的子类都实例化,一个类都对应着自己的el(DOM实例),通过这个方法父类和子类的DOM继承关系也就实现了

this.initChildren();

}

this.ready(ready);

if (options.reportTouchActivity !== false) {

this.enableTouchActivity();

}

}

这里通过主线把基本的流程演示一下,轮廓出来了,更多细节还请继续阅读。

插件机制

一个完善和强大的框架都会继承插件运行功能,给更多的开发者参与开发的机会进而实现框架功能的补充和延伸。我们来看下video.js的插件是如何运作的。

  • 插件的定义

如果之前用过video.js插件的同学或者看过插件源码,一定有看到有这句话videojs.plugin= pluginName,我们来看下源码:

import Player from './player.js';

// 将插件种植到Player的原型链

const plugin = function (name, init) {

Player.prototype[name] = init;

};

// 暴露plugin接口

videojs.plugin = plugin;

不难看出,原理就是将插件(函数)挂载到Player对象的原型上,接下来看下是怎么执行的。

  • 插件的运行

if (options.plugins) {

const plugins = options.plugins;

Object.getOwnPropertyNames(plugins).forEach(function (name) {

if (typeof this[name] === 'function') {

this[name](plugins[name]);

} else {

log.error('Unable to find plugin:', name);

}

}, this);

}

在Player的构造函数里判断是否有插件这个配置,如果有则遍历执行。

UI”继承”的原理

在继承关系一节中有提到video.js的所有DOM生成都不是采用的传统模板的方式,都是通过JavaScript对象的继承关系来实现的。

在Component基类中有个createEl方法,在这里可以使用DOM类生成DOM实例。每个UI类都会有一个el属性,会在实例化的时候自动生成,源代码在Component的构造函数中:

if (options.el) {

this.el_ = options.el;

} else if (options.createEl !== false) {

this.el_ = this.createEl();

}

每个UI类有一个children属性,用于添加子类,子类有可能扔具有children属性,以此类推,播放器的DOM结构就是通过这样的JavaScript对象结构实现的。

在Player的构造函数里有一句代码this.initChildren();启动了UI的实例化。这个方法是在Component基类中定义的,我们来看下:

initChildren() {

// 获取配置的children选项

const children = this.options_.children;

if (children) {

const parentOptions = this.options_;

const handleAdd = (child) => {

const name = child.name;

let opts = child.opts;

if (parentOptions[name] !== undefined) {

opts = parentOptions[name];

}

if (opts === false) {

return;

}

if (opts === true) {

opts = {};

}

opts.playerOptions = this.options_.playerOptions;

const newChild = this.addChild(name, opts);

if (newChild) {

this[name] = newChild;

}

};

let workingChildren;

const Tech = Component.getComponent('Tech');

if (Array.isArray(children)) {

workingChildren = children;

} else {

workingChildren = Object.keys(children);

}

workingChildren

.concat(Object.keys(this.options_)

.filter(function(child) {

return !workingChildren.some(function(wchild) {

if (typeof wchild === 'string') {

return child === wchild;

}

return child === wchild.name;

});

}))

.map((child) => {

let name;

let opts;

if (typeof child === 'string') {

name = child;

opts = children[name] || this.options_[name] || {};

} else {

name = child.name;

opts = child;

}

return {name, opts};

})

.filter((child) => {

const c = Component.getComponent(child.opts.componentClass ||

toTitleCase(child.name));

return c && !Tech.isTech(c);

})

.forEach(handleAdd);

}

}

通过这段代码不难看出大概的意思是通过initChildren获取children属性,然后遍历通过addChild将子类实例化,实例化的过程会自动重复上述过程从而达到了”继承“的效果。不得不为作者的构思点赞。如果你要问并没看到DOM是怎么关联起来的,请继续看addChild方法的源码:

addChild(child, options = {}, index = this.children_.length) {

let component;

let componentName;

if (typeof child === 'string') {

componentName = child;

if (!options) {

options = {};

}

if (options === true) {

log.warn('Initializing a child component with `true` is deprecated. Children should be defined in an array when possible, but if necessary use an object instead of `true`.');

options = {};

}

const componentClassName = options.componentClass || toTitleCase(componentName);

options.name = componentName;

const ComponentClass = Component.getComponent(componentClassName);

if (!ComponentClass) {

throw new Error(`Component ${componentClassName} does not exist`);

}

if (typeof ComponentClass !== 'function') {

return null;

}

component = new ComponentClass(this.player_ || this, options);

} else {

component = child;

}

this.children_.splice(index, 0, component);

if (typeof component.id === 'function') {

this.childIndex_[component.id()] = component;

}

componentName = componentName || (component.name && component.name());

if (componentName) {

this.childNameIndex_[componentName] = component;

}

if (typeof component.el === 'function' && component.el()) {

const childNodes = this.contentEl().children;

const refNode = childNodes[index] || null;

this.contentEl().insertBefore(component.el(), refNode);

}

return component;

}

这段代码的大意就是提取子类的名称,然后获取类并实例化,最后通过最关键的一句话this.contentEl().insertBefore(component.el(), refNode);完成了父类和子类的DOM关联。相信inserBefore大家并不陌生吧,原生的DOM操作方法。

总结

至此,video.js的精华部分都描述完了,不知道大家是否有收获。这里简单的总结一些阅读video.js框架源码的心得:

  1. 找准播放器实现的主线流程,方便我们有条理的阅读代码

  2. 了解框架代码的组织结构,有的放矢的研究相关功能的代码

  3. 理解类与类的继承关系,方便自己构造插件或者修改源码的时候知道从哪个类继承

  4. 理解播放器的运行原理,有利于基于Component构造一个新类的实现

  5. 理解插件的运行机制,学会自己构造插件还是有必要的

  6. 理解UI的实现原理,就知道自己如何为播放器添加视觉层面的东西了

  7. 看看我的源码解读吧,能帮一点是一点,哈哈

转载于:https://my.oschina.net/Seas0n/blog/1563827

video.js 源码解析相关推荐

  1. video.js 源码分析(JavaScript)

    video.js 源码分析(JavaScript) 组织结构 继承关系 运行机制 插件的运行机制 插件的定义 插件的运行 控制条是如何运行的 UI与JavaScript对象的衔接 类的挂载方式 存储 ...

  2. js怎么调用wasm_Long.js源码解析

    基于现在市面上到处都是 Vue/React 之类的源码分析文章实在是太多了.(虽然我也写过 Vite的源码解析 所以这次来写点不一样的.由于微信这边用的是 protobuf 来进行 rpc 调用.所以 ...

  3. 【Vue.js源码解析 一】-- 响应式原理

    前言 笔记来源:拉勾教育 大前端高薪训练营 阅读建议:建议通过左侧导航栏进行阅读 课程目标 Vue.js 的静态成员和实例成员初始化过程 首次渲染的过程 数据响应式原理 – 最核心的特性之一 准备工作 ...

  4. js define函数_不夸张,这真的是前端圈宝藏书!360前端工程师Vue.js源码解析

    优秀源代码背后的思想是永恒的.普适的. 这些年来,前端行业一直在飞速发展.行业的进步,导致对从业人员的要求不断攀升.放眼未来,虽然仅仅会用某些框架还可以找到工作,但仅仅满足于会用,一定无法走得更远.随 ...

  5. JavaScript数字运算必备库——big.js源码解析

    概述 在我们常见的JavaScript数字运算中,小数和大数都是会让我们比较头疼的两个数据类型. 在大数运算中,由于number类型的数字长度限制,我们经常会遇到超出范围的情况.比如在我们传递Long ...

  6. 史上最全的vue.js源码解析(四)

    虽然vue3已经出来很久了,但我觉得vue.js的源码还是非常值得去学习一下.vue.js里面封装的很多工具类在我们平时工作项目中也会经常用到.所以我近期会对vue.js的源码进行解读,分享值得去学习 ...

  7. 如何将文件地址转为url_Node.js 源码解析 util.promisify 如何将 Callback 转为 Promise

    Nodejs util 模块提供了很多工具函数.为了解决回调地狱问题,Nodejs v8.0.0 提供了 promisify 方法可以将 Callback 转为 Promise 对象. 工作中对于一些 ...

  8. connect.js源码解析

    前言 众所周知,connect是TJ大神所造的一个大轮子,是大家用尾式调用来控制异步流程时最常使用的库,也是后来著名的express框架的本源.但令人惊讶的是,它的源码其实只有200多行,今天也来解析 ...

  9. 【Vue.js源码解析 三】-- 模板编译和组件化

    前言 笔记来源:拉勾教育 大前端高薪训练营 阅读建议:建议通过左侧导航栏进行阅读 模板编译 模板编译的主要目的是将模板 (template) 转换为渲染函数 (render) <div> ...

最新文章

  1. 恭喜你发现了宝藏,编程习惯-日积月累
  2. 第三天:创建型模式--建造者模式
  3. 蛋疼的配置go opengl的记录 running gcc failed: exit status 1 in golang in windows
  4. 【收藏】解决mac问题:打不开,因为它来自身份不明的开发者
  5. 2_1_6 递归与分治策略(汉诺塔问题)
  6. 在Spring Boot中使用内存数据库
  7. 蓝桥杯小朋友排队java_[蓝桥杯][历届试题]小朋友排队 (C++代码)
  8. 10.傅里叶变换——2D中的傅里叶变换,傅里叶变换的应用_5
  9. Netcore 及SDK版本号问题
  10. C++string类常用函数 c++中的string常用函数用法总结
  11. 重装SPS 2003的一点经验
  12. 在scrapy爬虫框架xpath中extract()方法的使用
  13. 初中七年级上计算机试题答案,初中信息技术考试试题(含答案).docx
  14. 三星为什么能超越SONY在世界崛起?
  15. python识别火车票二维码_python实现12306查询火车票
  16. Linux操作系统主机名(hostname)简介
  17. Node.js全局对象
  18. 苹果输入法怎么换行_朋友圈不折叠的N种方法安卓苹果通用
  19. Docker 常见使用
  20. 阿里云点播录制,上传,播放使用说明及遇到的坑

热门文章

  1. win10专业版 安装 docker
  2. jscontent V8 eventLoop
  3. WebService之WADL和WSDL
  4. java防盗链_javaWeb防止恶意登陆或防盗链的使用
  5. 【Nginx】Nginx实现图片防盗链
  6. JimuReport积木报表1.2.0 版本发布,免费的企业级低代码报表
  7. u-boot启动过程分析(一)
  8. 2007MTV超级盛典详细介绍
  9. UML类图及C#实现
  10. 火山小视频怎么伪原创 视频md5很慢