背景
很多渲染引擎或者可视化编辑器都会将视图保存成文件落地,很多都会选xml作为数据格式来存储,少数会使用json,然而笔者之前做的可视化编辑器,就是用json作为数据格式的,方便快捷,随取随存,甚是快活,直到。。。
直到脱离编辑器时,想手写视图文档的时候,才发现是那么的痛苦!而且,随着object和array的嵌套多起来,才是真的噩梦!后来沉思了许久,要不试试xml吧,以前在用白鹭的exml的时候感觉还行,几乎可以很直观地在脑中映射出视图来。
对于json和xml的对比,有人已经给出了详尽的结论:https://www.jianshu.com/p/1ff2bc9161f1
于是就增加了xml的文档解释器,然而痛苦确实减轻了许多,但是,使用xml并没有结合笔者的引擎的特殊性来考虑(笔者的引擎是基于E/C架构的实现),一个视图节点可以划分成4个部分:类名、节点属性、子节点、组件组。如果用xml的话,实现是可以实现的,只不过需要做很多特殊处理,比如组件组就会和子节点处于同一层,需要通过判断tagName来特殊处理;再比如节点属性里如果有object,那么就需要xml的子节点来存放,就又和子节点处于同一层了,需要通过首字母大小写来特殊处理。
上述的都是使用json和xml的痛点,直到想起同事曾今跟笔者说过的声明式编程。
声明式编程
那么啥是声明式编程,是否和函数式编程差不多的名词呢?
笔者经过查阅也是渐渐明白,现行的编程方式大概就3种:命令式编程、函数式编程和声明式编程。前两个就不多赘述了,搜索一下很多很多,唯独声明式编程,笔者才知道不久。
这样来理解:越接近自然语言,声明式编程的比重就越高,否则就是命令式编程的占比更高。
如果你还没理解,那么还是上个大开发论坛看看吧。
swiftUI
苹果新出的swift语言,就可以很自然地使用声明式编程,各种链式编程和流畅的语法,很似自然语言,所以官方给出了swiftUI的支持。
笔者也没用过,也不能多加描述,若是有兴趣可以去看看。https://developer.apple.com/xcode/swiftui/
简单实现
笔者是从事HTML5游戏开发的,自然会使用js,所以使用js来实现了相对可读性可编辑性更好的声明式编程实现。
看个示例:
const {Doc} = require('qunity');
const {Node, Rect, Text, StarBezier} = require('qunity-pixi');
return Doc({type: 'scene', name: 'main'}).kv({
root: Node({name: 'Scenes'}).c([
Node({name: 'MainScene', active: true}).c([
Rect({name: 'bg', shapeWidth: 750, shapeHeight: 1334, fillColor: 0xcc202e,}),
Node({name: 'slogan', y: 200, angle: "-3"}).c([
Text({
name: 'title1',
x: 150,
text: 'LEAP',
style: {fill: 0xFFFFFF, fontSize: 160, fontFamily: 'Arial Black'}
}),
Text({
name: 'title1',
x: 150,
y: 140,
text: 'ON!',
style: {fill: 0xFFFFFF, fontSize: 220, fontFamily: 'Arial Black'}
}),
]),
Node({name: 'InfoBoard', x: -100, y: 600, angle: -3,}).c([
Rect({name: 'bg', y: 70, shapeWidth: 1000, shapeHeight: 300, fillColor: 0x000, alpha: 0.7}),
Text({
name: 'title',
x: 375,
text: "最新数据",
fontWeight: 'bold',
alpha: 0.7,
style: {fill: 0x000, fontSize: 60, fontFamily: '方正粗圆_GBK'}
}),
]).s([
{script: "/scripts/InfoBoard"}
]),
]),
]),
asset: [
{
path: "images/1.png",
uuid: "dd22921b-e7ff-4fbc-9e79-ebfe517b9943",
url: "assets/images/1.png",
}
],
})
如上,就是视图文档解释出来的视图。
拆分
声明式编程什么?链式编程、相对丰富的方法库(可扩展),还有一个简易的解释器。
文档结构设计
看示例,导入几个方法,然后return整个文档实例,就是这么简单。
方法导入
笔者使用了commonjs的风格,用require
来导入方法。因为笔者的引擎是通过分模块的,所以做了两个导入来源。其中qunity
只提供了Doc方法,用于创建并返回文档实例,qunity-pixi
则提供了视图节点的创建方法,用于创建各种视图节点实例。(关于qunity
这个库,笔者这里就不打广告了,之后会开源,一个基于EC架构实现的HTML5游戏引擎,一笔带过)
文档导出
文档导出并没有使用export
,而是一个return
,原因下面会讲到。
解释器
解释器比较简单,主要是提供上述要导入的方法和接受返回的文档实例。
如果你熟悉commonjs工作原理,那可以跳过
Doc方法
let func = new Function('require', docSource);
let doc = func(requireMethod);
return doc;
很简单,通过一个Function来包裹执行(这就是为什么在视图文档里是直接用return
来返回整个文档实例,如果用commonjs的风格,则需要提供module
参数用来接收返回值),避免使用eval而外部入侵。传入一个require
参数,而这个require
方法则会接受一个id的参数。
function requireMethod(id) {
return requireContext[id];
}
id可能会是qunity
和qunity-pixi
,然后根据id返回内容。
那么requireContext
是一个怎么样的结构呢?
const requireContext = {
'qunity': {
Doc: function (props) {
let obj = {
kv,
p,
};
setTimeout(function () {
delete obj['kv'];
delete obj['p'];
});
return obj.p(props);
}
},
'qunity-pixi': pixiNodes,
};
看到了吗,就是一个map,分别是qunity
和qunity-pixi
的内容。
qunity里有一个Doc的方法,实例化一个object,然后存放两个方法,kv
和p
,最后执行obj.p(props)
并返回obj
。
那么kv
和p
这两个方法是什么呢?直接上代码:
function kv(props) {
for (let key in props) {
this[key] = props[key];
}
return this;
}
function p(props) {
injectProps(app, this, props);
if (props.active !== false && this.setActive) {
this.setActive(true);
}
return this;
}
代码比较简单,kv
是做键值对赋值的,p
是深度赋值用的(耦合度较高),因为笔者的引擎需要,Doc方法只给出了这两个方法。
至此,Doc方法完成了,在视图文档中只要调用Doc(...).kv(...).p(...)
可以生成一个文档的实例了。
节点方法
节点方法全部由qunity-pixi
导出。
const entityNames = Object.keys(app.entityDefs);
for (let entityName of entityNames) {
pixiNodes[entityName] = function (props) {
let entity = app.createEntity(entityName);
entity['kv'] = kv;
entity['p'] = p;
entity['c'] = c;
entity['s'] = s;
setTimeout(function () {
delete entity['kv'];
delete entity['p'];
delete entity['c'];
delete entity['s'];
});
return p.call(entity, props);
}
}
笔者的引擎里注册的节点的定义全部在app.entityDefs
上,然后遍历存放到pixiNodes
上,每个节点都有相同的链式调用方法,其中c和s方法看下面代码:
function c(children) {
for (let child of children) {
app.addDisplayNode(child, this);
}
return this;
}
function s(components) {
Object.defineProperty(this, '$componentConfigs', {
value: components,
writable: false,
enumerable: false,
});
return this;
}
c
方法是用来添加视图子节点的,s
方法是用来存放组件的配置信息的。
为了能达到链式编程的目的,每个方法都需要返回自身。
为了不污染原本的节点属性,需要把存放在节点上的kv/p/c/s
之类的方法都删除掉,但是也不能使用就删除,所以笔者想到的时候用setTimeout
,在回调里将他们全部删除。
小结
解释器主要是为视图文档的执行提供上下文,并能实行视图文档和接受视图文档。
其他顾虑
编辑器schema
笔者在手写视图文档的时候,发现智能提示功能并没效果,这是肯定的,因为并没有对这些上下文方法给出定义。
答案是直接使用typescript的声明文件来做,自己写d.ts文件或者用工具生成上下文的d.ts,这样,就能有很好的智能提示了。(注:目前idea的支持较好)
这样就可以很舒服地使用智能提示来手写视图文档了。
设计时映射到视图节点树
这个是什么应用场景呢?就是编辑器一般都会有一个视图节点树的组件来展示层级关系,那怎么在设计时从视图文档映射出这个视图节点树呢?
笔者想到的就是ast,用抽象语法树来处理,使用esprima
之类的库来静态生成ast,然后分析ast,得到最终的视图节点树。
其他顾虑暂未想到。。。
总结
目前,简单版本的解释器还在开发和使用中,会不断迭代更新,解耦,达到最终的方便快捷直观。
声明式编程对于视图节点文档实在是太友好了,但也不值局限于次,声明式编程可以用于更多的DSL中,结果必定超预期!