声明式编程风格的视图文档解决方案

背景

很多渲染引擎或者可视化编辑器都会将视图保存成文件落地,很多都会选xml作为数据格式来存储,少数会使用json,然而笔者之前做的可视化编辑器,就是用json作为数据格式的,方便快捷,随取随存,甚是快活,直到。。。

json vs xml(盗图)

直到脱离编辑器时,想手写视图文档的时候,才发现是那么的痛苦!而且,随着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",
        }
    ],
})
模仿Leap-on游戏的首页

如上,就是视图文档解释出来的视图。

拆分

声明式编程什么?链式编程、相对丰富的方法库(可扩展),还有一个简易的解释器。

文档结构设计

看示例,导入几个方法,然后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可能会是qunityqunity-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,分别是qunityqunity-pixi的内容。
qunity里有一个Doc的方法,实例化一个object,然后存放两个方法,kvp,最后执行obj.p(props)并返回obj
那么kvp这两个方法是什么呢?直接上代码:

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中,结果必定超预期!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352

推荐阅读更多精彩内容