关于javascript:新励步课件体系介绍

2次阅读

共计 6612 个字符,预计需要花费 17 分钟才能阅读完成。

前言

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

  1. ppt 制作,通过三方或者自研平台转换成 h5
    这种形式可能更适宜初创团队或者是开发资源较少的团队,而且个别配合着其余教学服务一起应用(比方直播),这样简直能够齐全不须要技术人员的反对,当然劣势也是显著的,首先这个别要依靠于三方平台,其次是编辑端个别须要在 ppt 上实现,文件易失落和透露。
  2. 研发手写课件,比方用 cocos creator 依据教研老师提供的 ppt 生产课件
    这种”流水线“是比拟支流的形式之一,往往通过研发手动编写出的课件更加灵便,成果更好更活泼,可能实现更简单的课件。毛病想必很多搭档也是深有体会,这种形式人力老本微小,要有一支固定的游戏开发团队,须要教研、设计、研发、测试独特合作能力实现一讲课件。
  3. 提供编辑平台,教研在此平台间接制作并输入 h5 课件
    第三种形式也是比拟支流的形式之一,据我所知目前也有很多团队在从第二种形式转到这种形式。这种形式个别是由开发提供课件平台,教研老师本人在平台上制作课件,这样肯定水平的开释了开发人员,升高了合作老本,开发课件效率更高,周期更短。当然它也是有劣势的,个别这种编辑平台的开发难度比拟高,其次就是后期教研老师对于这个平台的学习老本也不小。

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

业务场景剖析及技术难点

介绍技术之前,首先我来简略介绍下咱们课件应用的业务场景:

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

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

  1. 性能要求高:对于上课的产品,要求是齐全靠近离线的体验,举例来说,图片、音视频等资源不容许呈现缓冲期待;
  2. 容错要求高:灾备计划要思考,比方断网,课件也须要可能失常播放;
  3. 危险高:对于上课场景来说,基本上一两分钟的零碎不可用都是不能忍耐的;
  4. 交互简单:最初一点次要针对编辑端,做过编辑器的搭档大抵都理解,可能实现 ppt 那种性能交互是异样简单的。

那么联合以上的业务场景和遇到的艰难,我来给大家一一介绍咱们的解决形式,在此之前为了让大家更好的了解,我先放几张咱们整体的功能模块图以及性能演示图。

功能模块图:

课件编辑器:

课件播放器:

编辑器

目前咱们的编辑器大抵能够分为以下几大模块的性能:

  • 元件零碎:这也是咱们编辑器的外围性能,反对增加文字、图形、图片、音频、视频以及 iframe,每种元件都反对若干种属性的更改。
  • 互动零碎:这其中还分为事件、动画、题型三个模块。

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

那么在实现以上性能的时候,有几个要害的技术点与大家分享。

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

正文完
 0