目录
背景
当我们开发一个canvas应用的时候,出于效率的考量,免不了要选择一个渲染引擎(比如PixiJS)或者更强大一点的游戏引擎(比如Cocos Creator、Layabox)。
渲染引擎通常会有Sprite的概念,一个完整的界面会由很多的Sprite组成,如果编写复杂一点的界面,代码里面会充斥创建精灵、设置精灵位置和样式的“重复代码”,最终我们得到了极致的渲染性能却牺牲了代码的可读性。
游戏引擎通常会有配套的IDE,界面通过拖拽即可生成,最终导出场景配置文件,这大大方便了UI开发,但是游戏引擎一般都很庞大,有时候我们仅仅想开发个好友排行榜。
基于以上分析,如果有一款渲染引擎,既能用配置文件的方式来表达界面,又可以做到轻量级,将会大大满足我们开发轻量级canvas应用的场景。
本文会详细介绍开发一款可配置化轻量级渲染引擎
需要哪些事情,代码开源至Github:https://github.com/wechat-miniprogram/minigame-canvas-engine。
配置化分析
我们首先期望页面可配置化,来参考下Cocos Creator的实现:对于一个场景,在IDE里面一顿操作,最后场景配置文件大致长下面的样子:
// 此处省略n个节点
{
"__type__": "cc.Scene",
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_parent": null,
"_children": [
{
"__id__": 2
}
],
},
在一个JSON配置文件里面,同时包含了节点的层级结构和样式,引擎拿到配置文件后递归生成节点树然后渲染即可。PixiJS虽然只是个渲染引擎,但同样可以和cocos2d一样做一个IDE去拖拽生成UI,然后写一个解析器,声称自己是PixiJS Creator😬。
这个方案很好,但缺点是每个引擎有一套自己的配置规则,没法做到通用化,而且在没有IDE的情况下,手写配置文件也会显得反人类,我们还需要更加通用一点的配置。
寻找更优方案
游戏引擎的配置方案如果要用起来主要有两个问题:
- 手写可读性差,特别是对于层级深的节点树;
- 样式和节点树没有分离,配置文件冗余;
- 配置不通用;
对于高可读性和样式分离,我们惊讶的发现,这不就是Web开发的套路么,编写HTML、CSS丢给浏览器,界面就出来了,省时省力。
如此优秀的使用姿势,我们要寻求方案在canvas里面实现一次!
实现分析
结果预览
在逐步分析实现方案之前,我们先抛个最终实现,编写XML和样式,就可以得到结果:
let template = `
<view id="container">
<text id="testText" class="redText" value="hello canvas"> </text>
</view>
`;
let style = {
container: {
width: 200,
height: 100,
backgroundColor: '#ffffff',
justContent: 'center',
alignItems: 'center',
},
testText: {
color: '#ff0000',
width: 200,
height: 100,
lineHeight: 100,
fontSize: 30,
textAlign: 'center',
}
}
// 初始化渲染引擎
Layout.init(template, style);
// 执行真正的渲染
Layout.layout(canvasContext);
方案总览
既然要参考浏览器的实现,我们不妨先看看浏览器是怎么做的:
如上图所示,浏览器从构建到渲染界面大致要经过下面几步:
- HTML 标记转换成文档对象模型 (DOM);CSS 标记转换成 CSS 对象模型 (CSSOM)
- DOM 树与 CSSOM 树合并后形成渲染树。
- 渲染树只包含渲染网页所需的节点。
- 布局计算每个对象的精确位置和大小。
- 最后一步是绘制,使用最终渲染树将像素渲染到屏幕上。
在canvas里面要实现将HTML+CSS绘制到canvas上面,上面的步骤缺一不可。
构建布局树和渲染树
上面的方案总览又分两大块,第一是渲染之前的各种解析计算,第二是渲染本身以及渲染之后的后续工作,先看看渲染之前需要做的事情。
解析XML和构建CSSOM
首先是将HTML(这里我们采用XML)字符串解析成节点树,等价于浏览器里面的“HTML 标记转换成文档对象模型 (DOM)”,在npm搜索xml parser,可以得到很多优秀的实现,这里我们只追求两点:
- 轻量:大部分库为了功能强大动辄几百k,而我们只需要最核心的xml解析成JSON对象;
- 高性能:在游戏里面不可避免有长列表滚动的场景,这时候XML会很大,要尽量控制XML解析时间;
综合以上考量,选择了fast-xml-parser,但是仍然做了一些阉割和改造,最终模板经过解析会得到下面的JSON对象
{
"name":"view",
"attr":{
"id":"container"
},
"children":[
{
"name":"text",
&#