新励步课件体系介绍

前言

想必不少“投身于教育行业”的前端工程师们都绕不过“课件”这个话题,对于前端来讲,课件项目是教育公司相比互联网公司特有的需求之一,对于公司来讲也是及其重要的。目前教育行业我了解到的生产 h5 课件的方式大体分为如下三种,每种方式也是各有优劣,下面是个人理解:css

  1. ppt 制做,经过三方或者自研平台转换成 h5
    这种方式可能更适合初创团队或者是开发资源较少的团队,并且通常配合着其余教学服务一块儿使用(好比直播),这样几乎能够彻底不须要技术人员的支持,固然劣势也是明显的,首先这通常要依托于三方平台,其次是编辑端通常须要在 ppt 上完成,文件易丢失和泄漏。
  2. 研发手写课件,好比用 cocos creator 根据教研老师提供的 ppt 生产课件
    这种”流水线“是比较主流的方式之一,每每经过研发手动编写出的课件更加灵活,效果更好更生动,可以完成更复杂的课件。缺点想必不少伙伴也是深有体会,这种方式人力成本巨大,要有一支固定的游戏开发团队,须要教研、设计、研发、测试共同协做才能完成一讲课件。
  3. 提供编辑平台,教研在此平台直接制做并输出 h5 课件
    第三种方式也是比较主流的方式之一,据我所知目前也有不少团队在从第二种方式转到这种方式。这种方式通常是由开发提供课件平台,教研老师本身在平台上制做课件,这样必定程度的释放了开发人员,下降了协做成本,开发课件效率更高,周期更短。固然它也是有劣势的,通常这种编辑平台的开发难度比较高,其次就是前期教研老师对于这个平台的学习成本也不小。

有时候对于团队来讲,三种方式不是互斥的,大部分状况三种方式是并行的,会根据内容的类型、复杂度等方面去折中选择。对于咱们团队来讲,其实也是三种共存的,不过大部份内容生产使用第三种方式,下面我来给你们介绍一下励步课件的技术体系。前端

业务场景分析及技术难点

介绍技术以前,首先我来简单介绍下咱们课件使用的业务场景:webpack

  1. 咱们的课件分为两种,线上课件和线下课件,两种课件在内容相互衔接,风格没有明显差别,有区别的是线上课件须要作实时的师生互动,分为师生两端,教师用 PC 端,学生 ipad 居多,线下课主要是线下校区使用利用白板播放,线下除了课件播放,还须要一些辅助教学的功能。
  2. 支持了英语、数学、语文三个学科
  3. 生产线上课件与线下课件是同一个部门的教研老师。

其次咱们再来结合业务看下咱们要面临的技术难点:web

  1. 性能要求高:对于上课的产品,要求是彻底接近离线的体验,举例来讲,图片、音视频等资源不容许出现缓冲等待;
  2. 容错要求高:灾备方案要考虑,好比断网,课件也须要可以正常播放;
  3. 风险高:对于上课场景来讲,基本上一两分钟的系统不可用都是不能忍受的;
  4. 交互复杂:最后一点主要针对编辑端,作过编辑器的伙伴大体都了解,可以实现 ppt 那种功能交互是异常复杂的。

那么结合以上的业务场景和遇到的困难,我来给你们一一介绍咱们的处理方式,在此以前为了让你们更好的理解,我先放几张咱们总体的功能模块图以及功能演示图。数据库

功能模块图:canvas

课件编辑器:后端

课件播放器:性能优化

编辑器

目前咱们的编辑器大体能够分为如下几大模块的功能:微信

  • 元件系统:这也是咱们编辑器的核心功能,支持添加文字、图形、图片、音频、视频以及 iframe,每种元件都支持若干种属性的更改。
  • 互动系统:这其中还分为事件、动画、题型三个模块。websocket

    • 事件模块:咱们能够给元件作事件绑定操做,好比我能够给某个元件设置点击事件,点击后隐藏某个另外的元件,或者播放动画、播放音视频,再或者跳转到某一页等等。
    • 动画模块:咱们支持自定义动画功能,当前支持折线动画,能够设置播放时长、循环播放等属性
    • 题型模块:课件中能够设置拖拽,选择,连线等题型
  • 通用模块:包括了基础的功能,好比复制粘贴元件,组合,撤销,图片裁剪,帧动画制做,资源库,页码操做等等

那么在实现以上功能的时候,有几个关键的技术点与你们分享。

Canvas vs Dom

相信若是你也作过相似的产品,在初期作技术调研的时候必定也会在 CanvasDom 之间纠结,实际上用两种方式均可以实现这类功能,市面上也都有成功的案例,但咱们在开始以前还要针对咱们的业务场景来综合评估,首先咱们来梳理一下这两种方式的优缺点:
首先对于 Canvas 来讲,

  • 优势

    1. 元素多的状况下,性能表现更好
    2. 不须要过多考虑重绘的问题
    3. 对于图片处理更加方便
    4. 三方资源较多
  • 缺点

    1. 上手门槛较高
    2. 元素少的时候会产生无效的画布区域
    3. 不支持音视频、gif 图

其次对于 Dom 来讲,

  • 优势

    1. 能够利用 css,元素样式控制方便
    2. 调试方便,能够直接在控制台抓到元素
    3. Dom API 更加完善便捷
  • 缺点

    1. 元素多时性能开销大
    2. 对于不规则图形实现麻烦

那么结合他们各自的优缺点,咱们并无单纯的选取某一种方案,而是把两者结合起来,也就是说两种咱们都用了!下面咱们来介绍是如何结合使用的。

画布元素 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 以上(课间数据是指对于课件的描述数据,好比元素的位置,课件的页码,题型等,不包括静态资源)。因此对于咱们来讲,如何组织这些数据变的尤其关键,组织很差会对后期维护以及性能形成很大的影响。

在设计数据存储结构以前,要考虑清楚目标,那么我在设计以前大体考虑了两点:

  • 尽量让数据小
  • 数据结构清晰、简单

结合咱们的场景举个例子,咱们要执行下面一系列操做:

  1. 在画布上添加两个元素:圆形 A,方形 B,
  2. 给 B 添加一段折线动画
  3. 给 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
/>

组件内部包含了数据处理,课件、题型、动画等课件内容的展现,以及答题结果展现、处理回调等功能。各个使用方在调用的时候只须要传入指定格式的课件数据,课件就能够渲染出来了。如下我来介绍几个与其相关的技术点。

实现线上实时互动

实时互动的意思是老师和学生均可以操做课件,而且相互可以看到,通常实现这种需求有两种方式,一种是直接录屏直播,学生可以保证看到老师全部的交互,但若是想让学生和学生之间互动就比较难实现了;另一种是全部用户都打开课件,相似在线游戏,经过传递消息来实现同步,咱们目前使用的就是这种。

实现这功能,重点须要处理状态同步。 提及来容易作起来其实挺费劲的,细节比较多,列举几个问题,你们也能够思考如何实现:

  1. 学生中途进课的时候,课件如何处理?
  2. 上课过程当中把程序切到后台,如何处理?
  3. 丢包的时候如何补偿?
  4. 如何让课件秒翻页?
  5. 音视频状态如何作到同步?
  6. 课件中答题步骤如何作到同步?
  7. 动画如何同步?

除去后端相关的内容,咱们直接说课件端主要须要作哪些工做,说 3 点关键的功能点:

  1. 操做回调
    根据咱们的课件特色,操做大体又能够分为两类,画布元素操做以及外挂元素操做,画布元素上的操做咱们借助 fabric.js 很容易捕获到,外挂元素就会稍微复杂一些了,主要针对音视频,须要咱们手动去绑定对应的事件了,好比 video.addEventListener('play', this.update);
  2. 发送、接收操做消息
    经过操做回调,那么组件外层会获取到相应的状态改变,咱们经过 websocket 去发送消息。这里咱们须要注意尽量的让传输数据更小。好比若是一个元素的位置发生改变,咱们只须要发送idlefttop 数据
  3. 实现组件受控
    实现组件受控实际上是难点,与第一条相似,其中细节很是多,不过仍是能够分为画布元素和外挂元素来考虑,画布元素大致上能够经过 fabric.js 中 API LoadFromJSON 能够实现,而外挂元素就须要咱们去手工封装音频、视频受控组件去处理了。此外还有答题步骤等,这里我就不详细描述了。

迄今为止,咱们对于 iframe 以及 Gif 图的状态同步尚未实现,若是您有解决方案,期待您的不吝赐教。

性能及线下离线方案

前面有说过,性能对于播放器而言也是个比较大的挑战。对于性能优化,咱们的工做分为两部分:播放器组件和使用方。组件内部的优化点相对比较零散,网上前端性能优化的方案也不少,咱们基本上也是从那些方面作优化,我简单列举几点,这里我不详细介绍:

  1. 去除对游戏引擎的依赖,动画改成本身实现,这样可以大幅度的减少 js 体积
  2. 预加载,提早两页去预加载一页的静态资源(图片,视频,音频)
  3. 对于图片,优先使用 webp;其次借助阿里云 oss 的功能,咱们会针对不一样尺寸的设备加载不一样尺寸的图片
  4. 字体文件、固定图片的合并压缩
  5. cdn,开启国际加速,上课前提早预热
  6. webpack 打包优化
  7. 资源多域名
  8. ... ...

下面我重点来讲一下咱们的线下课离线方案。

离线,顾名思义就是不依赖网络能够正常播放课件,之因此要作离线主要出于两点考虑:

  • 应对网络、服务不可用等突发问题
  • 资源彻底本地化,可以大幅度提高性能体验

如何实现呢?咱们利用 Electron + 校区公盘实现了资源本地化的功能。 具体实现流程咱们来看下面这张图,包含了咱们资源整个的生命周期:用户上传-> 资源加密 -> 同步到校区公盘 -> 播放课件 -> 获取课件数据 -> 本地化资源 -> 资源解密 -> 渲染课件

基于这套方案咱们基本上能够作到课件本地化,以前测试过,课件中一个 150M 的视频文件,基本能够在 2 秒以内彻底缓冲完。

放在最后

其实完整的课件系统还衍生出诸多周边辅助产品,咱们也不例外,会有不少辅助工具,好比:

  • 快速导出 ppt 工具
  • 静态资源导出工具
  • 课件数据批量修复工具
  • 客户端文件检查工具
  • 课件数据可视化编辑工具

以上就是我对于励步课件系统中比较重要的几个功能点的简介,但愿对你们有所帮助,其实还有不少细节问题一篇文章讲不清楚,若是有什么问题或者指导欢迎知音楼联系 郑庆鑫,或者加微信 zqx362965772

相关文章
相关标签/搜索