共计 6612 个字符,预计需要花费 17 分钟才能阅读完成。
前言
想必很多“投身于教育行业”的前端工程师们都绕不过“课件”这个话题,对于前端来说,课件我的项目是教育公司相比互联网公司特有的需要之一,对于公司来说也是及其重要的。目前教育行业我理解到的生产 h5 课件的形式大抵分为以下三种,每种形式也是各有优劣,上面是我的了解:
- ppt 制作,通过三方或者自研平台转换成 h5
这种形式可能更适宜初创团队或者是开发资源较少的团队,而且个别配合着其余教学服务一起应用(比方直播),这样简直能够齐全不须要技术人员的反对,当然劣势也是显著的,首先这个别要依靠于三方平台,其次是编辑端个别须要在 ppt 上实现,文件易失落和透露。 - 研发手写课件,比方用 cocos creator 依据教研老师提供的 ppt 生产课件
这种”流水线“是比拟支流的形式之一,往往通过研发手动编写出的课件更加灵便,成果更好更活泼,可能实现更简单的课件。毛病想必很多搭档也是深有体会,这种形式人力老本微小,要有一支固定的游戏开发团队,须要教研、设计、研发、测试独特合作能力实现一讲课件。 - 提供编辑平台,教研在此平台间接制作并输入 h5 课件
第三种形式也是比拟支流的形式之一,据我所知目前也有很多团队在从第二种形式转到这种形式。这种形式个别是由开发提供课件平台,教研老师本人在平台上制作课件,这样肯定水平的开释了开发人员,升高了合作老本,开发课件效率更高,周期更短。当然它也是有劣势的,个别这种编辑平台的开发难度比拟高,其次就是后期教研老师对于这个平台的学习老本也不小。
有时候对于团队来说,三种形式不是互斥的,大部分状况三种形式是并行的,会依据内容的类型、复杂度等方面去折中抉择。对于咱们团队来说,其实也是三种共存的,不过大部分内容生产应用第三种形式,上面我来给大家介绍一下励步课件的技术体系。
业务场景剖析及技术难点
介绍技术之前,首先我来简略介绍下咱们课件应用的业务场景:
- 咱们的课件分为两种,线上课件和线下课件,两种课件在内容互相连接,格调没有显著差别,有区别的是线上课件须要做实时的师生互动,分为师生两端,老师用 PC 端,学生 ipad 居多,线下课次要是线下校区应用利用白板播放,线下除了课件播放,还须要一些辅助教学的性能。
- 反对了英语、数学、语文三个学科
- 生产线上课件与线下课件是同一个部门的教研老师。
其次咱们再来联合业务看下咱们要面临的技术难点:
- 性能要求高:对于上课的产品,要求是齐全靠近离线的体验,举例来说,图片、音视频等资源不容许呈现缓冲期待;
- 容错要求高:灾备计划要思考,比方断网,课件也须要可能失常播放;
- 危险高:对于上课场景来说,基本上一两分钟的零碎不可用都是不能忍耐的;
- 交互简单:最初一点次要针对编辑端,做过编辑器的搭档大抵都理解,可能实现 ppt 那种性能交互是异样简单的。
那么联合以上的业务场景和遇到的艰难,我来给大家一一介绍咱们的解决形式,在此之前为了让大家更好的了解,我先放几张咱们整体的功能模块图以及性能演示图。
功能模块图:
课件编辑器:
课件播放器:
编辑器
目前咱们的编辑器大抵能够分为以下几大模块的性能:
- 元件零碎:这也是咱们编辑器的外围性能,反对增加文字、图形、图片、音频、视频以及 iframe,每种元件都反对若干种属性的更改。
-
互动零碎:这其中还分为事件、动画、题型三个模块。
- 事件模块:咱们能够给元件做事件绑定操作,比方我能够给某个元件设置点击事件,点击后暗藏某个另外的元件,或者播放动画、播放音视频,再或者跳转到某一页等等。
- 动画模块:咱们反对自定义动画性能,以后反对折线动画,能够设置播放时长、循环播放等属性
- 题型模块:课件中能够设置拖拽,抉择,连线等题型
- 通用模块:包含了根底的性能,比方复制粘贴元件,组合,撤销,图片裁剪,帧动画制作,资源库,页码操作等等
那么在实现以上性能的时候,有几个要害的技术点与大家分享。
Canvas vs Dom
置信如果你也做过相似的产品,在初期做技术调研的时候肯定也会在 Canvas
和 Dom
之间纠结,实际上用两种形式都能够实现这类性能,市面上也都有胜利的案例,但咱们在开始之前还要针对咱们的业务场景来综合评估,首先咱们来梳理一下这两种形式的优缺点:
首先对于 Canvas 来说,
-
长处
- 元素多的状况下,性能体现更好
- 不须要过多思考重绘的问题
- 对于图片解决更加不便
- 三方资源较多
-
毛病
- 上手门槛较高
- 元素少的时候会产生有效的画布区域
- 不反对音视频、gif 图
其次对于 Dom 来说,
-
长处
- 能够利用 css,元素款式管制不便
- 调试不便,能够间接在控制台抓到元素
- Dom API 更加欠缺便捷
-
毛病
- 元素多时性能开销大
- 对于不规则图形实现麻烦
那么联合他们各自的优缺点,咱们并没有单纯的选取某一种计划,而是把二者联合起来,也就是说两种咱们都用了!上面咱们来介绍是如何联合应用的。
画布元素 vs 外挂元素
回头再来看一下咱们的元件零碎,咱们能够分为两类,一类是 Canvas 反对的,另一类是不反对的,想必大家也猜到了,对于 Canvas 不反对的元件咱们应用了 Dom,总结一下:
- 应用
Canvas
的元件(画布元素):文本、图形、图片(动态图片) - 应用
Dom
的元件(外挂元素):音频、视频、Gif 图、iframe
那么二者在同一个画布上是怎么联合起来的呢?借助上面这个截图来解释一下
原理其实不难,如果我要增加一个外挂元素(音视频、gif、iframe)在画布上,那么在编辑的时候我会把它当做图片来解决,也就是说,用一个图片来在 Canvas 上做占位,咱们能够在画布上随便缩放,旋转等,其次我还会同步渲染一个 video
元素盖在画布下层,并且把 canvas 元素的属性翻译成 css 属性,上面列出段伪代码:
/**
* klass:充当外挂元素的画布元素
*
* */
export function getHtmlElement(klass, i, option = {}, evn = 'editor') {
const basicStyle = {
display: klass.visible ? 'block' : 'none',
position: 'absolute',
transform: `rotate(${klass.angle}deg )`,
...klass.getBoundingRect(),
// ... 其余公共属性
};
switch (klass.type) {
case 'gif':
if (klass.angle) {const canvasZoom = this.canvas.getZoom();
const width = (klass.width + klass.strokeWidth) * canvasZoom * klass.scaleX + 1;
const height = (klass.height + klass.strokeWidth) * canvasZoom * klass.scaleY + 1;
Object.assign(basicStyle, {
left: klass.left * canvasZoom - width / 2,
top: klass.top * canvasZoom - height / 2,
width: width,
height: height
});
}
Object.assign(basicStyle, { pointerEvents: 'none'});
return (![]({klass.getSrc()} />)
);
case 'video':
return(
<video
style={Object.assign(basicStyle, { width: basicStyle.width + 1, height: basicStyle.height + 1})}
src={klass.videoUrl}
// ... 其余 video 属性
/>
)
case 'iframe':
return <iframe key={klass.id} className="el-iframe" src={klass.iframeUrl} style={basicStyle} />;
case 'audio':
// ...
}
课件数据结构
大家晓得,对于这种富前端利用来说,存储的数据会相当大。以咱们的课件零碎举例,画布上每个元素都会有 20-30 个属性,一页课件上可能会有数十甚至上百个元素,每个课件大略会有 15-30 页不等,一讲课件产生的课件数据至多要在 1M 以上(课间数据是指对于课件的形容数据,比方元素的地位,课件的页码,题型等,不包含动态资源)。所以对于咱们来说,如何组织这些数据变的尤为要害,组织不好会对前期保护以及性能造成很大的影响。
在设计数据存储构造之前,要思考分明指标,那么我在设计之前大抵思考了两点:
- 尽可能让数据小
- 数据结构清晰、简略
联合咱们的场景举个例子,咱们要执行上面一系列操作:
- 在画布上增加两个元素:圆形 A,方形 B,
- 给 B 增加一段折线动画
- 给 A 绑定一个点击事件,让点击 A 的时候,B 播放折线动画
那么这个场景个别状况下咱们可能会把数据设计成这样:
const data = [
{
id: "elementA",
name: "圆形",
left: 100,
top: 100,
event: {
type: "click",
target: {
id: "elementB",
// 其余属性
},
},
},
{
id: "elementB",
name: "方形",
left: 100,
top: 100,
event: {
type: "click",
target: {
id: "elementB",
left: 150,
top: 150,
vfx: [
// 动画数据
{name: "point1", left: 20, top: 20},
{name: "point2", left: 50, top: 50},
// ...
],
},
},
// 其余属性
},
];
这样如果数据量小的时候,是没有问题的,获取数据简略,不便咱们开发。但如果数据量多的时候,毛病就会突显: 这种嵌套构造,会使数据质变大,层级过深不易保护。
真实情况咱们是这样解决的,相似数据库一样, 咱们在前端设计了几张表:元件表,动画表,事件表,题型表等。 表与表之间用 id 做关联(主键),数据结构相似上面这样:
const data = {
// 元件表
levelList: [{id: "element1", name: "元件 1",left: 10,top: 10}],
// 动画表
vfxData: [{id: "vfx1", target: "element1", path: [] }],
// 事件表
actionData: [{id: "action1", target: "element1", type: "click"}],
// 题型表
activityData: [{id: "activity1", target: "element1", source: 'element2' type: "fill"}],
};
这样咱们能够更清晰的看到这页数据,都有哪些元件、动画、事件及题型,通过 id 关联也肯定水平的缩小了数据的大小(对于缩小数据体积的问题,咱们在序列化的时候,还会过滤掉一些框架提供的无用属性)。当然这样做也是有毛病的,比方在删除某个元件的时候,咱们须要额定解决相干的表中的数据,这须要咱们在代码中封装出相应的办法。
播放器
在说播放器之前,还是回顾一下下面那张图,咱们的播放器会在多个场景下应用,有线上课、线下课以及其余一些业务零碎中。出于这些思考,咱们把外围播放器抽离成了公共组件,每个应用方在播放器组件的下层去做定制化的性能,那么上面咱们首先来说这个外围播放器组件。
外围播放组件
首先咱们来看下组件的调用形式很简略,相似这样:
<CourseWarePlayer
defaultPage={pid}
data={coursewareData}
onPageChange={(page, currentData) => {this.setState({ currentPlayPage: page, notes: currentData.notes});
}}
options={{
video: {controlsList: "nodownload",},
}}
extraElements={
<Fragment>
<ClassroomWrapper
onClose={this.onToggleClassroom}
scale={this.state.canvas.getZoom()}
/>
</Fragment>
}
onQuestionCommit={this.onQuestionCommit}
// ... other props
/>
组件外部蕴含了数据处理,课件、题型、动画等课件内容的展现,以及答题后果展现、解决回调等性能。各个应用方在调用的时候只须要传入指定格局的课件数据,课件就能够渲染进去了。以下我来介绍几个与其相干的技术点。
实现线上实时互动
实时互动的意思是老师和学生都能够操作课件,并且互相可能看到,个别实现这种需要有两种形式,一种是间接录屏直播,学生可能保障看到老师所有的交互,但如果想让学生和学生之间互动就比拟难实现了;另外一种是所有用户都关上课件,相似在线游戏,通过传递音讯来实现同步,咱们目前应用的就是这种。
实现这性能, 重点须要解决状态同步。 说起来容易做起来其实挺吃力的,细节比拟多,列举几个问题,大家也能够思考如何实现:
- 学生中途进课的时候,课件如何解决?
- 上课过程中把程序切到后盾,如何解决?
- 丢包的时候如何弥补?
- 如何让课件秒翻页?
- 音视频状态如何做到同步?
- 课件中答题步骤如何做到同步?
- 动画如何同步?
除去后端相干的内容,咱们间接说课件端次要须要做哪些工作,说 3 点要害的性能点:
- 操作回调
依据咱们的课件特点,操作大抵又能够分为两类,画布元素操作以及外挂元素操作,画布元素上的操作咱们借助fabric.js
很容易捕捉到,外挂元素就会略微简单一些了,次要针对音视频,须要咱们手动去绑定对应的事件了,比方video.addEventListener('play', this.update);
- 发送、接管操作音讯
通过操作回调,那么组件外层会获取到相应的状态扭转,咱们通过 websocket 去发送音讯。这里咱们须要留神尽可能的让传输数据更小。比方如果一个元素的地位产生扭转,咱们只须要发送id
,left
,top
数据 - 实现组件受控
实现组件受控其实是难点,与第一条相似,其中细节十分多,不过还是能够分为画布元素和外挂元素来思考,画布元素大体上能够通过fabric.js
中 APILoadFromJSON
能够实现,而外挂元素就须要咱们去手工封装音频、视频受控组件去解决了。此外还有答题步骤等,这里我就不详细描述了。
迄今为止,咱们对于 iframe 以及 Gif 图的状态同步还没有实现,如果您有解决方案,期待您的不吝赐教。
性能及线下离线计划
后面有说过,性能对于播放器而言也是个比拟大的挑战。对于性能优化,咱们的工作分为两局部:播放器组件和应用方。组件外部的优化点绝对比拟零散,网上前端性能优化的计划也很多,咱们基本上也是从那些方面做优化,我简略列举几点,这里我不具体介绍:
- 去除对游戏引擎的依赖,动画改为本人实现,这样可能大幅度的减小 js 体积
- 预加载,提前两页去预加载一页的动态资源(图片,视频,音频)
- 对于图片,优先应用 webp;其次借助阿里云 oss 的性能,咱们会针对不同尺寸的设施加载不同尺寸的图片
- 字体文件、固定图片的合并压缩
- cdn,开启国内减速,上课前提前预热
- webpack 打包优化
- 资源多域名
- … …
上面我重点来说一下咱们的线下课离线计划。
离线,顾名思义就是不依赖网络能够失常播放课件,之所以要做离线次要出于两点思考:
- 应答网络、服务不可用等突发问题
- 资源齐全本地化,可能大幅度晋升性能体验
如何实现呢? 咱们利用 Electron + 校区公盘实现了资源本地化的性能。 具体实现流程咱们来看上面这张图,蕴含了咱们资源整个的生命周期: 用户上传 -> 资源加密 -> 同步到校区公盘 -> 播放课件 -> 获取课件数据 -> 本地化资源 -> 资源解密 -> 渲染课件
。
基于这套计划咱们基本上能够做到课件本地化,之前测试过, 课件中一个 150M 的视频文件,根本能够在 2 秒之内齐全缓冲完。
放在最初
其实残缺的课件零碎还衍生出诸多周边辅助产品,咱们也不例外,会有很多辅助工具,比方:
- 疾速导出 ppt 工具
- 动态资源导出工具
- 课件数据批量修复工具
- 客户端文件查看工具
- 课件数据可视化编辑工具
以上就是我对于励步课件零碎中比拟重要的几个性能点的简介,心愿对大家有所帮忙,其实还有很多细节问题一篇文章讲不分明,如果有什么问题或者领导欢送知音楼分割 郑庆鑫
,或者加微信 zqx362965772
。