有道互动内容引擎Ceramics的业务实践

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"前言","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Ceramics 是一款 HTML5 互动内容引擎,主要为有道互动教学场景提供 Web 互动内容开发解决方案帮助开发者快速生产高质量的题目内容。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文将从业务场景出发,介绍 Ceramics 的技术实现,并讲述其如何高效地为高质量的互动内容生产赋能,希望能给对 Web 互动技术感兴趣的前端开发人员提供一些参考。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"一、背景","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1. 互动内容","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"互动内容是有道在线教学中的重要组成部分,应用场景广泛例如:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"直播课堂","attrs":{}},{"type":"text","text":"中,通过教师端控制向学生端推送互动内容,学生在观看直播或课后回放时,均可收到互动题目,作答后可提交答案至教师端;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"AI教学视频","attrs":{}},{"type":"text","text":"中,穿插互动内容,根据用户的作答结果播放不同的后续视频片段;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"App、小程序","attrs":{}},{"type":"text","text":"中,课后通过复习闯关、自由练习、错题本等形式让用户进行知识点巩固,辅导老师在后台可查看用户答题情况进行专项指导;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"对外合作、市场活动、用户学前测试","attrs":{}},{"type":"text","text":"等页面中穿插互动题目来进行拉新引流。互动内容通过趣味性的交互过程、及时合理的反馈来激发学习兴趣,吸引注意力,让用户在互动式、探索式的趣味课堂中更高效地获取知识,巩固学习。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/18/1870ae4e8bb8d3fc1d4f85806ce725bc.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"图1:互动内容的应用场景","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2. 题目模板","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一道互动题目由题目模板和题目数据组成:研发人员根据教研需求开发出包含交互逻辑、动画效果、正误判断的题目模板,教研根据题目模板的数据结构配置题目数据。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前团队已开发近300个题目模板(下文简称题板),涵盖","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong","attrs":{}}],"text":"填空、选择、拖拽、绘图、连线","attrs":{}},{"type":"text","text":"等题型。通过良好的接口设计和数据结构定义,一个题目模板可以适用于多种教学模式,题目模板之间也可以互相组合,形成交互更丰富的题目模板。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/50/502d98f0fd0ad9e5508c0522e95607dd.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"图2:互动题目、题目模板、互动引擎之间的关系","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以图3的两类点选消除题目为例,","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong","attrs":{}}],"text":"判题逻辑、场景风格、动画效果","attrs":{}},{"type":"text","text":"等有差异,但题目交互相似度较高。基于此,将两类题目用一个题目模板开发,保留较高的配置度,数据结构中定义背景图片、选项图片、动效形态、判题模式、题干文本等字段,即可通过一个题目模板支持不同玩法的多道题目。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/2e/2e88539bbb4fe0b26b6f102ac6ff230f.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"图3:一个题目模板支持多道题目","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.互动引擎","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"互动引擎是互动研发的基础,一款好的互动引擎能大大降低研发成本。从上文的图2可以看到,互动题目引擎提供了渲染、用户交互、动画、声音、资源管理等能力,开发人员基于其可以开发出符合业务场景的题板,题板根据不同配置数据来渲染出定制化的题目内容。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"为了应对业务的高速发展,保障高效稳定地生产高质量的互动内容,少儿项目组的前端团队自研了一款高性能、好拓展、易于开发的互动内容引擎:","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong","attrs":{}}],"text":"Ceramics","attrs":{}},{"type":"text","text":"。Ceramics 是基于TypeScript+React 开发的 HTML5 互动引擎,包含用户交互、声音、动画、渲染、资源管理等功能,Web 开发人员利用其提供的 API 和组件,仅需较低的学习成本即可快速开发出 iOS、Android、Web、H5 多端适配的互动题板。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"自 Ceramics 投入使用后,目前已陆续为少儿组的多学科多条业务线提供内容生产服务。下文将从架构设计、用户交互、动画效果、组件设计等方面展开介绍 Ceramics 的技术实现,希望能给对互动技术感兴趣的前端开发人员提供一些参考。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"二、技术实现","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1. 基础架构","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先我们先来看一下Ceramics的整体架构。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/4c/4cc3a30928ab25c2fe08521678a209b0.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"图4:Ceramics架构","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"互动引擎简单来说就是将题目数据渲染到界面的工具,由数据层、视图层和业务逻辑层构成。数据带动视图,渲染引擎根据逻辑将数据渲染到画布上。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"数据以","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}}],"text":" ","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong","attrs":{}}],"text":"JSON","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}}],"text":" ","attrs":{}},{"type":"text","text":"形式存储于配置文件中,配置文件通常包含以下信息:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"UI层配置","attrs":{}},{"type":"text","text":":如页面布局,元素数量、尺寸、定位等。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"脚本配置","attrs":{}},{"type":"text","text":":如元素出现的时机,动画播放的顺序,用户交互行为,反馈等。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"资源配置","attrs":{}},{"type":"text","text":":如背景图、音效、动效的资源地址等。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"题目数据配置","attrs":{}},{"type":"text","text":":题目具体的信息,如文案,题干,图片,答案,读题语音等。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Web 开发人员的主要工作在视图层和业务逻辑层,即如何将配置文件可视化,包括题板的核心逻辑、具体交互的实现、以及判题的计算。我们通常在 Canvas 区域实现复杂动画效果和绘制,使用 DOM 实现布局,使用 TSX+hooks 编写逻辑代码。为了减少这一步的工作量,引擎本身提供了:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"动画能力","attrs":{}},{"type":"text","text":":包括 CSS 动画、JS 动画、骨骼动画、AE 动画的显示和控制,并将常用动效封装成动效库。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"音频播放能力","attrs":{}},{"type":"text","text":":支持 mp3 音频文件的播放和语音合成 tts 文件的播放。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"渲染能力","attrs":{}},{"type":"text","text":":基于 HTML、Canvas、WebGL 的 2D、3D 渲染。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"用户交互","attrs":{}},{"type":"text","text":":封装了拖拽、按压、点击、输入、手写、切割、3D交互等用户交互事件。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"适配与兼容","attrs":{}},{"type":"text","text":":经过完善测试的移动端适配和兼容,支持 iOS 9(Safari 9),Android23(Chrome 44),Web、微信小程序等平台。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"性能优化","attrs":{}},{"type":"text","text":":通过懒加载、资源打包优化等手段对引擎进行了优化,通过 Sentry 监控、3D 动画帧率检测等保障引擎的可靠性。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基于上述基础能力,我们封装了一些常用的原子组件和功能插件,如手写板、计算器、算盘等,及游戏化常用的的闯关、倒计时、引导动画等;还封装了通用题板组件,实现拖拽、选择、填空、连线等基础题型的逻辑和交互。原子组件之间可互相组合封装成功能插件,通用题板组件和功能插件可组合搭配形成业务题板组件。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述能力和组件及详细的接入文档和开发文档,降低了互动题板的开发门槛,Web开发人员无需提前学习 2D、3D、图形学、游戏开发的知识,也可以快速上手开发满足业务需求的互动题板。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2、交互能力","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"交互能力是用户和屏幕产生互动的基础,常见的用户交互包括拖拽、按压、点击、输入、手写、切割、3D 场景下的交互等,互动题板在 Web 端和移动端都可使用,需要同时考虑鼠标和手指触摸的交互。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Ceramics 在交互能力的设计中,采用了游戏开发中流行的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":" ECS","attrs":{}},{"type":"text","text":"(entity-component-system)设计模式作为底层架构。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ECS 的一个核心理念是","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"组合优于继承,","attrs":{}},{"type":"text","text":"即将不变的部分使用继承以方便复用,将多变的部分用组合来方便拓展。这样的设计模式可以保证引擎的高扩展性。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在互动题目中,每一个元素(小动物、草地、石块、气球等)都是一个实体 entity,每个实体可以绑定一个或多个组件,每个组件描述不同的能力(移动、拖拽、点击等),通过组合的方式给实体赋予一个或多个能力,也可以动态添加或删除能力。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以一个最简单的按钮为例,它本身是一个没有任何能力的实体,用一个
元素表示。按钮可以有按压按钮和开关按钮,分别应用于不同场景。我们自定义了 usePress 的 hook 和 useToggle 的 hook ,统一处理 Web 端和移动端的 mouse 和 touch 交互,抹平浏览器间的差异,对外暴露简洁 API ;基于 usePress 封装了 useButton 组件,表示按钮可被按压的能力;基于 useToggle 和 useButton 封装了 useToggleButton 组件,表示开关按钮的基本能力。最后,给按钮组件绑定 useButton 或 useToggle ,成为 Button 组件和 ToggleButton 组件,即为按钮赋予了对应的能力。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f7/f7a8baa6d37e32234ba4c1257539b054.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"图5:按钮组件交互能力设计","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"例如拖拽的交互,如图6所示,简单来说就是物体拖到指定的热区的行为,正误判断就是在指定时机对热区中的物体与答案设定中的物体进行比较。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e3/e30ae3f5862e13d79e0795e1a2256791.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"图6:拖拽物体与热区","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可拖拽物体、不可拖拽物体本质上都是同样没有能力的实体。将拖拽能力封装于独立的组件,处理 mouse 和 touch 时的移动行为以及拖拽物体与热区的碰撞检测。给可拖拽物体赋予了 draggable 的能力,给目标热区赋予 droppable 的能力,并通过拖拽组件暴露的接口进行后续逻辑处理,即可实现拖拽行为。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除了交互能力的实现之外,ECS 的设计思想在 Ceramics 的很多模块中都有体现,例如通过自定义的 hooks 来表示能力,可以实现更大程度的逻辑复用。Ceramics 中有很多自定义 hooks ,如 useQuestion hooks ,将做题流程整合在一起,包括校验答案、根据校验结果正误反馈、累计错误次数等;useViewport hooks ,封装移动端适配能力;useAnime hooks ,封装了动画操作能力,如播放、暂停、重播等。不同题板引入相同 hooks 可以统一代码风格,提高开发效率,新的开发者也更好理解,易于上手。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/0d/0df36e80f56333462a6789dcef11153f.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"图7:useQuestion hooks","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3. 动画效果","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"动画是互动内容的一个重要组成部分。逼真趣味的动画效果不仅能辅助展示教学内容,还可以帮助提升用户沉浸感,吸引注意力,激发学习兴趣,更好地留存用户。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"常见的 Web 动画方案主要有序列帧、CSS 动画、JS 动画、Canvas、WebGL 等,Ceramics 通过引入常用的动画库来支持多种动画形式,针对业务场景和兼容进行二次封装。对于一些简单动画,研发只需要简单的API调用即可实现效果。一些前端实现成本较高的复杂动效,由设计师通过 AE、Spine 等工具制作,研发引入 Ceramics 中相应的组件即可实现动画的渲染和控制。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"3.1 Lottie","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Lottie 是 Airbnb 推出的可应用与 Web, Android, iOS 等多个平台的动效解决方案。动画师用AE(After Effects)制作出动画后,通过 AE Bodymovin 插件可以导出在移动端和 Web端渲染动画的 json 文件。前端通过 Lottie 库可以对包含动画信息的 json 文件进行解析和渲染。Lottie 动画使用简单,高效高性能,效果还原能力强,跨平台支持性好,在 iOS,Android, Web,小程序端都有良好的呈现。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d8/d8935597851ac9afd96ad9f8ee4452c5.gif","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"图8:lottie动画","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"3.2 Spine","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Spine 是游戏开发中常用的2D 骨骼动画制作工具,通过 Spine 制作的动画可以导出为 json + atlas + png 文件。Spine Runtime 是 Spine 官方提供的可以解析 spine 导出的动画文件的库,其中 HTML Canvas runtime 提供了 Web 端渲染 Spine 动画的能力。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Spine 动画可应用于不同的引擎和语言,因此在需要跨端协作的业务线,设计师可以用一套 spine 动画适配不同应用,提升工作效率,避免重复劳动。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e2/e2a4baf08a153220f1f78761f0c723e3.gif","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"图9:spine动画","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"3.3 基于Anime.js的动效库","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong","attrs":{}}],"text":"Anime.js","attrs":{}},{"type":"text","text":" 是一个轻量的 Javascript 动画库,可对 CSS 属性、SVG、DOM 和 JavaScript 对象进行动画。Anime.js 的内置交错系统使复杂的动画和重叠动画变得简单。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Ceramics 基于 Anime.js 开发了更符合业务需求的动效库,封装了题板通用的动效,如:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"强调(缩放、更换样式、抖动、闪烁)","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"元素入场出场动画(渐入、渐出、闪现、掉落)","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"元素入场出场动画(渐入、渐出、闪现、掉落)","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"动效库也是独立发布的 ","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong","attrs":{}}],"text":"npm","attrs":{}},{"type":"text","text":" 包,可以服务于多个工程,提高开发效率。项目中直接引入@ceramics-ae中的动效组件使用,给","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong","attrs":{}}],"text":"DOM","attrs":{}},{"type":"text","text":"元素添加动效名称和参数配置即可实现效果。利用","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}}],"text":" ","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong","attrs":{}}],"text":"timeline","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":" ","attrs":{}},{"type":"text","text":"可以对单步动画进行串联,支持单次播放、循环播放、时间轴等功能。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"import { Teeter } from \"@ceramics-ae/core\"\nTeeter.play({ el }) //抖动\nStyleChange.play({el,config: {style: { color: \"#F5636C\",background: `center/100% no-repeat url(${OptionStyle[type].bg_wrong})`}}}) //更改css样式\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"3.4 WEB 3D","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Web端实现3D效果,当前最流行的技术就是 WebGL 。WebGL基于 OpenGL ,OpenGL 是一个跨平台的 2D、3D 绘图标准,WebGL 把 OpenGL 与 JS 结合在一起,从而在 Web 端展示 3D 场景和模型。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"WebGL 功能强大,提供了大量的基础绘图 API ,但上手门槛较高,代码的复杂度高,需要开发者具有前置的图形学知识和数学知识。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Three.js 是一款 WebGL 开源引擎,对 WebGL 的底层渲染细节和复杂的数据结构进行了封装,开发人员通过简单的 API 调用即可配置灯光,顶点着色器、片元着色器,自动生成矩阵,视角控制、射线等,快速进行 3D 模型制作。我们基于 Three.js 制作了魔方、立体图形等3D效果,开发简单,效果还原度高,在移动端也能保持良好的性能和兼容。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/1d/1da69cca3f96afb8a715aa6177bad8e3.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"图10:web 3D","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一组高质量的动效往往是视觉、动效师、前端开发合力的产物,开发者根据不同的应用场景、素材依赖、开发成本、性能和兼容性选择不同的动画方案,可以在表达出丰富的动画效果的同时降低设计师与研发的协作成本,提高团队工作效率。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4. 题板组件设计","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Ceramics 作为一款互动内容引擎,目标之一是高效地为多业务方生产题目模板,我们希望每一个题目模板都能够服务于更多业务方,当新的业务方有需求时可以更快地接入。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基于此,我们将每个题板封装成题板组件,独立开发,独立发布为 npm 包;题板与业务解耦、题板与题板解耦,由上层业务方负责控制多个题板间的组合、切换等逻辑;题板组件也可以作为子组件被其他题板引入,进行二次封装,更大程度提高题板的复用性。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c8/c83526f6a9c61dcc57c3d51f302d3eac.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"图11:题板组件与业务方的调用关系","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常当上层业务方使用题板时,需要收集题板内部的状态来实现外部的业务逻辑,例如:题板加载成功时去除过渡动画和背景音乐、用户答错时计入错题本、答对时切换下一题等,题板内部也需要根据外部的数据更新状态,例如:用户在不同的游戏模式中进入题目,题板应切换对应的界面和逻辑。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"针对以上业务场景,题板组件定义了一套通用的接口:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/7e/7e52e9852e13b0864b1b62c8764a0fa5.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"图12:题板组件接口","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上层业务方安装 npm 包,按需引入题板,根据接口定义传入 props 即可完成接入。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"import { CeramicsProvider, DragCetegoryQuestion } from \"@ceramics-math/core\"\n\n //设备配置数据\n\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"题板组件之间相互独立,但又拥有相似的逻辑和组件依赖。为了更好地重用代码、管理项目依赖,我们引入了","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":" ","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong","attrs":{}}],"text":"Lerna","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":" ","attrs":{}},{"type":"text","text":"框架进行多包管理,将多个题板组件作为子项目合并到一个父项目中,重复的代码逻辑也抽取成该父项目下的另一个独立子项目。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/75/75f3f06646020c0296a2642331d31981.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"图13:多包管理的结构","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"多包管理带来的","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong","attrs":{}}],"text":"收益","attrs":{}},{"type":"text","text":"主要有:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所有子项目可以共享同一份打包、测试配置;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"子项目内部可以像引入其他依赖包一样引入其他子项目,Lerna 会自动识别并导向内部项目,实时同步更新;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"处理依赖包的依赖关系更加方便,通过 Lerna bootstrap 命令安装所有子项目的依赖包,在安装依赖时依赖提升,即把所有项目 npm 依赖文件都提升到根目录下,避免相同依赖包在不同项目安装多次。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通过Lerna的命令可以很方便地执行所有组件的build、test和组件发布流程,更新组件中相应的依赖版本号并发布,还会在组件发布之后打 tag 以及生成 changelog。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此外,由于不同的题板在打包发布配置、接口定义、移动端适配规则、组件依赖等存在较多的相似性,为了方便新同事上手,避免接口遗漏,统一代码风格和数据结构,我们还定义了初始化模板,引入自动化构建工具 Plop 协助快速创建题板组件。通过交互式命令行输入,即可动态地在指定目录下生成题板组件,包括初始化代码和配置文件。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/cb/cbad27a2f590d12e1b41068d1238a249.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"图14:交互式命令行","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/05/05d4698c89fa202c2f22fa8bf5c7bbbf.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"图15:初始化模板与生成的文件","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如本章节所述,明确的接口定义、方便快捷的接入方式、多包管理和自动化构建的引入,使得上层业务方可以快速接入题板组件库,题板组件的开发、迭代和维护也更加容易。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"5. 原子组件和插件","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"为了进一步提高开发效率,我们根据常见的业务场景,封装了许多原子组件和功能插件,开发人员通过简单的组件引入和传参即可引入。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/5d/5d5951c5213c1ca970218f4a99685596.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"图16:原子组件和功能插件","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们把界面中的最小元素理解为","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong","attrs":{}}],"text":"原子组件","attrs":{}},{"type":"text","text":",如按钮、标题、展示板、选项、进度条等,根据 props 的不同配置样式和交互。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/6d/6dd376b79db77ed60ae339fee5579750.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"图17:原子组件","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"功能插件由多个原子组件组合而成,耦合了一些业务逻辑,常用插件如手写板、计算器、算盘等,及游戏化常用的的闯关、倒计时、引导动画等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/21/2131669e164c25bd32250af548aa7746.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"图18:功能插件","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根据不同业务场景,功能插件可以适配不同的UI和动效。下图为","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong","attrs":{}}],"text":"键盘组件","attrs":{}},{"type":"text","text":"在不同题板中的样式:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/38/38fad08e4d1bf8dba31b60c5b757c3e1.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"图19:键盘组件","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一个题板组件中会有多个原子组件和功能插件。原子组件和功能插件内部可能依赖其他原子组件,也可能会依赖一些通用的 hooks 、全局 Context 、动画、 utils 等。原子组件和功能插件也是多包工程的子项目,可以独立导出并发布为 npm 包,不仅可被任意题板组件引入,也可被其他原子组件引入,还可以被引入到其他项目中。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"6. 通用题板","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除了上文中的原子组件和插件,Ceramics 还内置了通用题板组件来进一步提高开发效率。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通用题板为一些最基础的题目模板,如通用拖拽、通用选择、通用填空、通用连线等。通用题板封装了基本逻辑和交互,更多的业务逻辑则以数据的形式表达,这样可以提高单个题目模板的复用性,使得一个通用题板支持N种交互和样式相似度较高的题板需求。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通过对多个题板的归类和抽象,我们总结出来几类基础题板的基本要素:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如下图中的多个拖拽题,由画布、热区、可拖拽物体、不可拖拽物体组成。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/87/87137171873ba09ac37dd9149021db70.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"图20:拖拽题基本元素","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同理,选择题板的基础元素有画布、可点击物体、不可点击物体;填空有画布、物体、填空框、选项框;连线有画布、可连线物体、不可连线物体。物体的类型包括图片、动画、文字等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除了基础元素外,题板的反馈也有相似之处:例如答对后出现正确动画,答错后出现错误动画、连线匹配成功或拖拽正确时A物体消失替换成B物体等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"与上文中提到的 ECS 设计模式一致,当给物体赋予了可拖拽、可点击、可连线等交互能力,并给每次交互都添加相应的反馈,即可实现一个题目的基本互动。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基于此,我们抽象出了通用题板,在题板中处理元素渲染、用户交互和提交判断逻辑,至于页面中哪些元素可点击、哪些可拖拽、每个元素的数量、样式、位置和动画,不同时机的反馈等,都可以用数据来表达,通过数据带动视图进行渲染。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们封装了一些基础类,比较常用的有 Entity(物体),Feedback(反馈),Area(热区)、ShowCondition(出现时机)等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"页面中的所有元素都属于 Entity 类,EntityType 表示物体的类型,有图片、动画、文本、标题、提交按钮等,不同类型的物体有不同的配置项和渲染方式。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"给entity添加不同的属性即可赋予其不同的交互,如 ","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong","attrs":{}}],"text":"clickable ","attrs":{}},{"type":"text","text":"表示时可被点击的选项,","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong","attrs":{}}],"text":"draggable ","attrs":{}},{"type":"text","text":"表示可被拖拽的物体,","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong","attrs":{}}],"text":"droppable ","attrs":{}},{"type":"text","text":"表示可被放置的热区,droppable 的热区还可以配置形状、容量、吸附精确度等属性。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"export interface Entity extends Rectangle {\n id: string | number // 物体的唯一id\n name: string // 物体的名称\n entityType?: EntityType // 物体的类型\n draggable?: boolean // 可拖拽\n droppable?: boolean // 可放置\n clickable?: boolean // 可点击\n linkable?: boolean //可连线\n zIndex?: number // 层级,0-999,越大层级越高\n show?: ShowCondition // 何时显示,默认always\n feedbacks?: Feedback[]\n [key: string]: any\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"feedbacks 字段用来配置一组反馈事件,反馈主要由时机和行为定义。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\nexport interface Feedback {\n type: FeedbackType | string // 反馈时机, FeedbackType和AND,OR组合而成的字符串\n action: FeedbackAction // 反馈的行为\n data: FeedbackData // 反馈行为需要的数据\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"反馈的时机(type)有:点击、放入热区、热区已满、拖对热区、拖错热区、开始连线、连线结束、动画播放完成、整体判题正确、整体判题错误等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"反馈的行为(action)有:显示物体、隐藏物体、播放音频、替换图片、检查答案等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同一个动作可能触发多个反馈,一个反馈也可以在不同的触发时机被触发。通过Feedbacks可以把反馈步骤串联起来,类似于动画脚本,使得一个常规的题板具有更丰富的玩法。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"经过上述拆解,通用题板的设计思路也逐渐清晰了起来。以下图中的通用选择题板为例:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/55/550bebc46be7d46324dc9156d0a45811.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"图21:通用选择题板","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong","attrs":{}}],"text":"选择题","attrs":{}},{"type":"text","text":"的核心交互流程为:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"black"}}],"text":"点击可点元素,触发点击反馈;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"black"}}],"text":"满足提交条件,触发提交反馈,进行判题,触发正/误反馈;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"black"}}],"text":"满足题目结束条件,题目结束不可作答,否则可以继续作答。","attrs":{}}]}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong","attrs":{}}],"text":"提交答案","attrs":{}},{"type":"text","text":"的时机主要有3种:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"black"}}],"text":"无提交按钮时,点击可点元素立即提交;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"black"}}],"text":"无提交按钮时,点击元素数量与允许提交的数量相等后立即提交;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"black"}}],"text":"有提交按钮按钮时,点击提交按钮提交。","attrs":{}}]}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"选择题支持单选、多选、部分选择、无正确答案的选择(如投票)。因此","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong","attrs":{}}],"text":"判断答案的条件","attrs":{}},{"type":"text","text":"也不同:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"black"}}],"text":"单选 & 多选;用户答案与题目正确答案一致;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"black"}}],"text":"部分选择:用户答案与题目正确答案部分一致;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"black"}}],"text":"无正确答案的选择:任意选项均为正确。","attrs":{}}]}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面是通用选择的基本数据结构:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"interface QuestionConfig {\n canvas: Canvas //画布\n answer: Answer //正确答案\n entities: Entity[] //物体\n submit?: SubmitEntity //提交按钮\n audio?: { //音效\n correct?: string\n wrong?: string\n click?: string\n }\n aeTemplate?: string //动画模板\n maxCheckNum?: number //最大错误次数\n allowMultipleChoice?: boolean //是否允许多选\n allowPartChoice?: boolean // 是否允许部分选择\n showHint?: boolean // 是否出现引导\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如上文所述,题目由题板和数据组成,通用题板中主要的业务逻辑包括:处理上述渲染和交互,定义配置数据的数据结构,根据配置数据进行相应的展示和反馈。例如,根据 entities 渲染物体,给 clickable 的物体绑定点击事件,根据 entity 中的 feedbacks 字段展示反馈;根据 submit 字段处理提交逻辑和提交按钮的渲染;根据 answer、allowPartChoice、allowMultipleChoice 等字段进行题目判断。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通过数据驱动页面的渲染,使得一个题板可以满足大部分同类型的教研需求,教研可以在配套的题板可视化配置界面中配置数据来创作新的题板,无需研发介入;研发人员也可以基于通用题板进行二次封装,通用题板可以作为组件引入到其他题板中,多个通用题板可以组合形成新的题板,大幅降低了新题板的开发成本。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"三、总结与展望","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文主要介绍了有道互动题目引擎Ceramics在","attrs":{}},{"type":"text","marks":[{"type":"color","attrs":{"color":"#F5222D","name":"red"}},{"type":"strong","attrs":{}}],"text":"架构设计、用户交互、动画效果、组件设计","attrs":{}},{"type":"text","text":"等方面的技术实现,从研发视角讲述互动题目的生产过程,以及题目引擎是如何让互动内容的生产链路更高效地运转起来的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前 Ceramics 还在成长阶段,未来我们将持续迭代,不断丰富开发工具链。我们将打造低代码开发平台,拼搭式可视化开发题板,使得编写交互逻辑更加容易;打通设计图到代码的转换通道,开发素材库,优化 UI 和研发的合作体验;不断提高高效运行和渲染能力,向游戏化迈进,支持更复杂的游戏玩法,更炫酷的动画效果;增加实时互动,多人对战的能力,进一步提升直播课等场景的互动性与趣味性。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们的愿景是是打造一款高性能的 Web 互动引擎,让前端工程师更低成本地开发高质量的互动内容。未来我们将不断扩大 Ceramics 的影响力,为更多业务赋能。","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章