互联网教育行业风起云涌,而高品质在线授课平台是每个互联网教育公司的核心和基石。本文是tutorabc前端负责人和君在LiveVideoStackCon 2017上的分享整理,主要介绍了在线授课系统Tutormeet 前端开发实践,包括技术选型、性能优化、持续交付实践以及APM系统。
演讲 / 和君
整理 / LiveVideoStack
大家好,感谢LiveVideoStack提供分享的机会,今天的分享主题是《高品质的互动在线课堂与开发实践》,是基于我公司的一个授课平台产品展开介绍的。首先,自我介绍下,我有着十多年的前后端开发经验,最近几年的重点在前端架构和前端技术体系搭建等工作,曾在途牛等互联网公司工作,现任TutorABC前端负责人。
今天的主题会围绕五个方面去讲,如何打造一个高品质的在线授课平台?
- 技术选型方向
- 优化方向
- 如何提升课堂的互动性
- 工程相关的持续交付
- 前端APM
首先介绍下我们的产品,全称是TutorMeet (简称TM ),上图是授课平台的教师端界面,可以看到布局还是比较合理的:有视频区域、聊天区域、以及教材白板区域,最下面是学生列表。学生端的界面是看上去会更加明亮轻快风格,但总体布局与上图类似,包括一个双向白板,辅助的教师控制的功能,这里只列举了PC端的效果图,其他还包括微信端和APP端。
一、技术选型
1)为什么选择WebRTC?
我们选择了WebRTC作为音视频的技术栈,其实主要是基于以下几个方向考虑的:因为WebRTC是基于浏览器的,无需下载,也无需装载任何的插件;且开发成本低,官方有详细的文档说明,WebRTC的技术社区也相当完善, API常用的也就大概十多个,因此基本上花个半天时间就能熟悉WebRTC是如何开发前端的;以及有Google的出身背景,开源且安全;目前浏览器对它的支持也变得越来越友好。最后,Flash将于 2020年彻底退役,这也是转型到WebRTC的最重要原因。
2)WebRTC浏览器占比
通过上面的WebRTC的浏览器占比图,可以看到截止到2017年9月,在全球领域内支持WebRTC的浏览器已经占比达到了70%,基本上涵盖了当代的主流浏览器,值得一提的是,8月Safari发布的11版本也进行了支持,这就相当于WebRTC已经将所有“当代浏览器”的阵地全部攻陷,而这个比例未来还会越来越高。
3)WebRTC APIs
WebRTC的前端相关API是相当简单的,主要分了以下三个模块:
- 用于处理音视频方面的MediaStream;
- 用于建立连接,保持连接,监控连接和断开连接的RTCPeerConnection;
- 用于双向数据通道的RTCDataChannel。
其常用的API非常简单,这里做了简单的罗列,基本上掌握了这些就可以真正开始动手了,其中 chrome://webrtc-internals是Google专门为开发者创造的监控平台,因此在做WebRTC时可能需要一直将其打开,这是比较重要的一条。
4)WebRTC Polyfills&Adapters
浏览器在不断的升级过程中,这些API也会有所变化,因此需要引入Polyfills或者Adapters来对不同浏览器和浏览器版本之间做兼容和降级处理,这里主要列举了三个比较常用的Adapters:第一个是官方出的Adapters;第二个不仅对支持WebRTC的浏览器会做出支持,对一些不支持的浏览器会通过插件化的方式让它支持,但由于其并非官方出品,且将来随着浏览器对WebRTC支持程度的提升,建议使用第一个即可。
5)不同场景下的技术选型
上图是TutorMeet 的教学场景,从业务形态上大概分为两种形式:
- 大班课和公开课,即一个老师对很多学生。
- 小班课,则是1对1到1对3的模式。
对于这两种业务形态,我们采用不同的技术选型。第一种大班授课方式,采用WebRTC 推流的技术,老师端通过WebRTC跟服务器端建立一个PeerConnection,服务器端通过推流到CDN,可以支持一万个人同时在线观看直播,这个技术基本上和主流的直播网站技术比较接近,支持RTMP和HLS。小班课使用经典的会议模式,支持智能的服务切换,通过判断终端的网络环境去动态切换到对应的服务器。
6)TM 前端架构
上图是TutorMeet 的前端架构。前端UI层是基于React做的,这部分我们会遵循React的一些最佳实践,将Component分为四级,第一级是原子化的Component即Widget;还有一个是纯UI的Component——可能是由一部分各种原子化Component组成的;最终拼装成业务Component,或者说容器Component,它对一些业务逻辑进行封装;我们的状态管理是用Redux做的,所有数据流的的通信只在Redux和业务Component进行,并不和上面组件库里的Component通信。最后我们会将对多媒体的操作、对一些WebSocket消息的操作、对一些工具类工具方法以及教师控制方面做封装。
所有的消息使通过中间件去流转,这里我们借鉴了Express中间件的概念,通过一个通信的Bridge去和媒体服务以及消息服务进行通信。前端所有的UI可以通过配置参数动态化加载需要的组件和模块。值得一提的是,在Facebook最终把React迁到MIT协议上后,我们也将React同时升级到16版本;游戏开发使用的是Egret引擎。开发构建采用了Webpack3.0,Babel / PostCSS,所有的组建开发是基于React Storybook做的,单元测试使用的是Jasmine,以上就是整个TM 前端整体的架构。
7)在线课堂Web前端的特性
相较体验一般的Web应用,我们的交互性会更强,用户在页面滞留的时间也会比较长——一堂课在45—60分钟左右,而我们和一般的Web应用不太一样的地方在于要尽量避免页面刷新,因为老师端刷新会导致所有的学生出现黑屏的现象。并且由于整个页面的功能非常丰富,包括游戏、白板、教材、音视频等等,这也导致了整个静态的资源包体积非常大,那么如何去做一个长时间的稳定流畅在线教育课堂?这就需要针对这些特性进行优化。
二、优化
优化会分为三个大版块去讲:
- 构建时优化,即编译时优化
- 运行时优化,
- 用户体验优化
1)构建时优化:
- 代码分割
当代的Web前端工程肯定离不开构建, 5年前所有的Web页面上,可能写的Source Code就是运行在生产环境上面的代码,但后来引入了Web前端构建的过程,这部分我们基于Webpack实现。因为我们教室类型比较特殊,分为多种应用场景:公开课、大班课、小班课、课程回放等,这和Client端不太一样,一般情况下Client端是将所有的业务形态包到一个App或程序中。但Web由于它的灵活性,可以根据不同的教室类型进行拆分,所以第一件事就是将所有的业务类型拆分成多个入口,这里利用了Webpack的multi-entries特性。
接下来就是对一些大的包进行Code Splitting——做代码的分割,将一些不需要首屏加载的JS单独打包到一个bundle里面,最终它会“按需的”加载到页面里,不会影响首屏的渲染进程。
第三块也是优化比较大的一部分,就是对本地的优化语言包进行按需加载,很多第三方库都有一些语言包的文件,最典型的例子就是moment.js,加上原包可能达到了好几百K,但其实很多东西是不需要的,因此我们要保证只加载在当前环境下需要的语言包即可。经过上述优化整个包的体积会下降很多,这也是最主要的包的优化手段——对包进行瘦身。
- 打包优化
利用Webpack3.0的特性:Tree-shaking、Scope Hoisting。Tree-shaking的作用是移除项目中的dead code,之前我们采用uglify.js移除dead code,原理是根据代码逻辑压缩、把不需要的模块和代码片段从Source Code中移除,Tree-shaking与之不同——是对“需要的模块进行抽取”,进一步减小包的体积,同时它的配置也更加简单。以官方例子为例,上图有三个模块,其中main.js模块引用了前两个模块,但前两个模块在实际中并没有用到,如果在Webpack2.0或者没用Tree-shaking的情况下,会将这两个包同时打进去,这其实是完全没有必要的,而Tree-shaking则会去除不需要的代码。
另一个比较重要的优化是Scope Hoisting,熟悉Webpack的同学都知道,Webpack会将所有的模块都单独生成一个闭包作用域Function,所有的模块都整合到一个数组里,模块引用的时候通过数组的Index去查找。比如前面官方例子中的三个模块,最终打包后会生成一个数组,其中包括三个Function,每个Function里是对应相应的代码,但这样就会有一个问题:在做模块引用时,它找会根据数组中index去查找,这是一种低效的方式。而通过Scope Hoisting把所有闭包里面的模块全部提到了第一级,最终生成代码不会再有数组,这样减少了包的体积,同时又增强了运行效率。
prepack也是比较重要的特性,它是Facebook出的打包优化工具,作用是将在Runtime时的代码估值尽可能前置到Compile-time进行,这样就不用再运行时再过多消耗性能,直接在打包时做求值。由此极大的提升了运行性能,同时也减少了包的体积, 从下图可以看出,从六、七行代码节省到一行代码(由于prepack处于试验阶段,暂时还未用到生产环境)。
通过这种前端构建的方式实现了这一点,Source Code和Production Code差距会越来越大,保证Source Code是可读的、可维护的,而Production Code执行效率是最高的、体积是最小的。以上是构建时优化涉及到的一些知识点。
2)运行时优化
在代码真正运行到线上时,该如何优化代码?我们遵循了Google的RAIL模型,在UI层面上的优化很多都是在遵循这种模型的,它主要分为四块:
- 响应:100ms内做出响应
- 动画:10ms内产生一帧
- 空间:最大化空闲空间
- 加载:1000ms内提供内容
响应就是在人机交互时,人在做交互时机器必须在100毫秒内做出响应;动画也是一样,保证在客户端达到每秒60FPS的效果,这就要求在10ms内将帧算出来,再花一定时间将它渲染到页面上;最大化空闲时间,即尽量让CPU不运作的时间越长越好,这样观看体验会比较流畅。
加载则是首屏第一次Load页面时需要在1000毫秒内提供内容。Google做过相关的实验,首屏加载时间越长,客户流失就越大,若达到了8-9秒,客户根本不会在页面上做任何停留,因此TM 所有的优化都基于这个模型进行的。
这里不得不提的是像素流水线,也就是一帧是怎么做出来的。首先进行JavaScript的运算,然后是Style——对当前元素样式进行计算,接着Layout——元素真正用到页面里面会对布局造成什么影响,最后Paint和Composite分别是浏览器内核往页面上渲染的过程以及对图层进行合并。
我们可以通过Chrome Devtools调试工具看到这五个步骤之间的时间占比。大家可以看到,黄色部分为Scripting时间,紫色是Style和Layout时间,绿色是Paint和Composite时间,该例子在Javascript的计算就花了将近16秒,典型的性能不佳。
针对上述情况分析后,接下来就是Profile部分,这部分基本没有第三方的工具可以选择,可以说Chrome Devtools是“史上最好的Web开发调试工具”。虽然面板看上去比较复杂,但实际在调试前端性能时通常分为以下几步:第一选出波峰的一个值,上图可以看到黄色的地方波峰很高,属于耗时的波段;接着在底下Timeline将耗时帧选出来,每一帧的耗时大小可以通过宽度来判断,选中后会在下面展示出该帧消耗的时长;具体分析主要是Bottom-up、Call Tree和Event Log这三项,通过分析它所有的调用堆栈和每一个步骤损耗的时间,定位到需要优化的点再进行优化。
优化一帧的流程大约分为四步,而中间还有两个比较重要的环节:一是用于分析波段的火焰图,显示当前这个波段内所有帧的调用的堆栈,可以直观的看到哪一帧花费时间长、哪一帧调用堆栈比较深、哪一帧做了一些不必要的操作;第二是内存分析,可以看到内存分析的一个陡然下降是因为浏览器做了一次垃圾回收,但浏览器频繁的做垃圾回收也会影响当前执行性能,它的主要的目的就是定位——定位垃圾回收和是否有内存泄露和大对象有没有释放的情况,基本上就是这六步。
上图是针对在线课堂白板的优化实例,当进行一次profile后,我们发现Scripting时间过长,当定位到问题后采用了一些优化手段:首先是页面上一些JavaScript大的对象进行复用,这就避免了频繁垃圾回收的过程;其次对所有的白板上的笔记进行路径上的优化,这样整个浏览器在渲染到页面上的时间会降低,同时也会使消息通讯时数据量变得更小;节流控制是指对一些连续触发的频繁操作进行频度上的控制,但同时又不能影响用户体验;最后就是对白板上的图形进行拖动、缩放等交互操作的优化。经过上述的优化手段之后,可以发现Scripting时间减少了30%,这是一个比较大的进步。
3)用户体验优化
接下来从用户体验的角度,该如何优化我们的在线课堂?其实和一般Web开发的经验差不多,举例来说:
- 预加载/懒加载:对页面上的教材、视频进行预加载,以及对非首屏图片教材进行懒加载。
- 响应式布局:根据不同分辨率的屏幕,使他的看上去的视频大小和白板教材的大小,正好是最适合的。
- 渐进式用户体验:让用户在不同的设备上都能有所体验。在一些不支持,比如低端的机器上进行降级处理,也能使用在线课堂。
- 层级管理在我们应用中显得特别重要。因为页面特别复杂,导致上面有很多层,包括布局层、上面有内容层、白板层、游戏层、控制层等等,因为层级较多,需要对页面z-index进行管理:对每一层进行一个z-index区段的限定,如第一层限定在0到10。
- Web安全色/安全字体:要保证在不同的终端上看到的东西是一样的。比如在苹果的电脑上用苹果的字体,但在Windows设备上显示不对,不同的字体宽度也不一样,出现换行的问题,尤其是在白板上写字时,可能会有对不齐的情况,极大的影响用户体验。
三、互动性
除了对在线课堂的优化,我们在授课过程的互动性上也做了产品的优化、升级,这是因为K12教学对整个授课内容的趣味性有很高要求,单纯的基于音视频白板教材的在线课堂已经无法满足用户的需求。这里简单列举几个Feature。
1)游戏化白板
首先引入了游戏化白板的概念,这主要用于青少儿教学的场景,即老师可以通过播放一段游戏让学生去玩,从而提升整个在线课堂的互动性。除了老师和学生之间的互动外,学生之间可以通过游戏进行竞赛,同时配备一定的激励机制。这部分是Egret引擎实现的。
2)屏幕分享
屏幕分享用于某些场景下的教学,比如Photoshop软件的教学,需要将操作的页面分享给学生。它的实现是基于Chrome本身的支持,不过必须通过插件的方式去调用,整个屏幕视频流推送是通过WebRTC实现的。
3)回顾课
第三个是录影档结构化,它和视频网站的进度条索引很像,我们针对上课不同的节点来进行分段,每一段都会对应一个缩略图,让学生可以看到它的具体内容,这样学生就可以快速的定位到所需的章节,同时也能大大减少服务器端的流量。
四、持续交付
1)持续交付的目的
对于项目上线后迭代的过程中不出错且稳定运行而言,持续交付很重要。它是为了适应快速迭代的需求,尽量做到自动化,降低人工成本,保证从开发到上线流程中的所有人员可以更紧密的合作,同时也要保证生产环境的上线质量。针对整个前端的持续交付要达成以下几个目的:
- 要保证源码清晰的分支管理,和比较清晰的发布策略;
- 要保证所有核心模块的高覆盖率测试;
- 所有的静态资源都是通过非覆盖,增量式的发布到CDN Server;
- 线上出问题时进行快速回滚。
2)持续交付架构图
上图是持续交付的架构图,所有的第三方模块都通过npm去托管,源码管理使用GitLab,整个CI Server采用Jenkins,Jenkins在发布时会做一些代码风格的检查,Unit Test、Benchmark和端对端测试,通过这些测试后发布到测试环境,通过后再发到生产环境。这样整个发布到测试的过程只需要花费几十秒钟的时间,测试完成到上线的过程会压缩到一个小时,并且也极大的提升了上线的质量。值得一提的是,我们通过Webpack的Define-Plugin 和Multi-entries 实现了差异化按需打包。也就是说整个项目,所有的代码库是一个,但真正生产出来的代码是N份。
3)持续交付技术点
这部分涉及到的技术点主要包括以下几部分:第一是私有的包管理库可以使用Git仓库,或者源码仓库去代替;第二是前端构建的三剑客——Webpack、Node.js和npm script;可能免不了写Shell Script,但这里有个最佳实践就是:前端尽量用前端的技术去解决整个发布过程中的技术点,因此尽量使用node.js npm script代替Shell Script;第四个是Jenkins、Git-webhook,有一些过程是直接通过Web自动触发的;最后是代码风格检查和测试的一些框架,比如eslint、stylelint、jest、benchmark、Nightwatch等等。
五、前端APM
前面这些解决了整个项目上线发布的过程,但真正线上运行时会很多问题出现,需要监控整个上线的运行质量。因此就需要做一套前端APM系统,它主要达成以下几个目的:
- 性能监控:包括首屏加载的指数(比如首屏直接相应速度),内容下载的速度,并且对整个交互过程中可预期的一些耗时操作进行埋点,上报操作所消耗的时间。
- 错误采集:我们对所有未捕获的异常进行了全量采集,对整个页面上所有静态资源、加载失败的异常也进行了采集。针对这种捕获的异常,进行按需采集。
- 业务数据上报展示:为了保证音视频的质量,我们会周期的上报客户端的丢包率,网络延迟等等相关的实时数据。同时也会对当前客户端的类型,包括如浏览器类型、用户IP等进行上报,以及用户的行为和关键流量节点。
后端是采用ELK做的,因为Kibana有强大的数据可视化分析功能,可以节省很多工作量,它可以动态的显示当前的客户端的占比,IP地域分布等情况。
前端APM埋点,我们遵循以下几个最佳实践:
- 对上报数据进行分类、分级,方便后期做报表和ELK中的统计分析。
- 尽量做到无痕埋点,比如说全量采集完全可以通过无痕埋点做到。
- 声明式埋点替代命令式埋点,也就是尽量减少埋点代码对异构代码的侵入性。
- 对业务方面的数据要做到按需采集,减少分析时的噪音,使最终采集数据更加准确。
下图是TM 项目所用到的技术栈,全部都是开源的。
六、总结
接下来我们可能会在以下几个方面进行尝试:
- 首先是引入人工智能,人工智能在在线教育的场景下是非常适合,包括现在比较成熟的人脸识别、语音识别和机器翻译。
- 引入AR/VR概念,给学生带来沉浸式的教学体验。
- 多终端统一,也就是希望将所有终端的开发都收纳到Web 这个技术栈,会引入React Native / Hybrid App/ Electron这些框架。
- WebAssembly,可以使页面具备运行二进制码的功能,让浏览器端计算端性能大步提升,而在音视频领域有很多计算密集型的场景,我相信WebAssembly在接下来一段时间会在这个方面发挥很大的功效。