新勵步課件體系介紹

前言

想必不少「投身於教育行業」的前端工程師們都繞不過「課件」這個話題,對於前端來講,課件項目是教育公司相比互聯網公司特有的需求之一,對於公司來講也是及其重要的。目前教育行業我瞭解到的生產 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

相關文章
相關標籤/搜索