背景
很多渲染引擎或者可視化編輯器都會將視圖保存成文件落地裙椭,很多都會選xml作為數(shù)據(jù)格式來存儲躏哩,少數(shù)會使用json,然而筆者之前做的可視化編輯器揉燃,就是用json作為數(shù)據(jù)格式的扫尺,方便快捷,隨取隨存炊汤,甚是快活正驻,直到。婿崭。拨拓。
直到脫離編輯器時,想手寫視圖文檔的時候氓栈,才發(fā)現(xiàn)是那么的痛苦渣磷!而且,隨著object和array的嵌套多起來授瘦,才是真的噩夢醋界!后來沉思了許久,要不試試xml吧提完,以前在用白鷺的exml的時候感覺還行形纺,幾乎可以很直觀地在腦中映射出視圖來。
對于json和xml的對比徒欣,有人已經(jīng)給出了詳盡的結(jié)論:http://www.reibang.com/p/1ff2bc9161f1
于是就增加了xml的文檔解釋器逐样,然而痛苦確實減輕了許多,但是打肝,使用xml并沒有結(jié)合筆者的引擎的特殊性來考慮(筆者的引擎是基于E/C架構(gòu)的實現(xiàn))脂新,一個視圖節(jié)點可以劃分成4個部分:類名、節(jié)點屬性粗梭、子節(jié)點争便、組件組。如果用xml的話断医,實現(xiàn)是可以實現(xiàn)的滞乙,只不過需要做很多特殊處理,比如組件組就會和子節(jié)點處于同一層鉴嗤,需要通過判斷tagName來特殊處理斩启;再比如節(jié)點屬性里如果有object,那么就需要xml的子節(jié)點來存放醉锅,就又和子節(jié)點處于同一層了兔簇,需要通過首字母大小寫來特殊處理。
上述的都是使用json和xml的痛點,直到想起同事曾今跟筆者說過的聲明式編程男韧。
聲明式編程
那么啥是聲明式編程,是否和函數(shù)式編程差不多的名詞呢默垄?
筆者經(jīng)過查閱也是漸漸明白此虑,現(xiàn)行的編程方式大概就3種:命令式編程、函數(shù)式編程和聲明式編程口锭。前兩個就不多贅述了朦前,搜索一下很多很多,唯獨聲明式編程鹃操,筆者才知道不久韭寸。
這樣來理解:越接近自然語言,聲明式編程的比重就越高荆隘,否則就是命令式編程的占比更高恩伺。
如果你還沒理解,那么還是上個大開發(fā)論壇看看吧椰拒。
swiftUI
蘋果新出的swift語言晶渠,就可以很自然地使用聲明式編程,各種鏈式編程和流暢的語法燃观,很似自然語言褒脯,所以官方給出了swiftUI的支持。
筆者也沒用過缆毁,也不能多加描述番川,若是有興趣可以去看看。https://developer.apple.com/xcode/swiftui/
簡單實現(xiàn)
筆者是從事HTML5游戲開發(fā)的脊框,自然會使用js颁督,所以使用js來實現(xiàn)了相對可讀性可編輯性更好的聲明式編程實現(xiàn)。
看個示例:
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: "最新數(shù)據(jù)",
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",
}
],
})
如上缚陷,就是視圖文檔解釋出來的視圖适篙。
拆分
聲明式編程什么?鏈式編程箫爷、相對豐富的方法庫(可擴展)嚷节,還有一個簡易的解釋器。
文檔結(jié)構(gòu)設(shè)計
看示例虎锚,導(dǎo)入幾個方法硫痰,然后return整個文檔實例,就是這么簡單窜护。
方法導(dǎo)入
筆者使用了commonjs的風(fēng)格效斑,用require
來導(dǎo)入方法。因為筆者的引擎是通過分模塊的柱徙,所以做了兩個導(dǎo)入來源缓屠。其中qunity
只提供了Doc方法奇昙,用于創(chuàng)建并返回文檔實例,qunity-pixi
則提供了視圖節(jié)點的創(chuàng)建方法敌完,用于創(chuàng)建各種視圖節(jié)點實例储耐。(關(guān)于qunity
這個庫,筆者這里就不打廣告了滨溉,之后會開源什湘,一個基于EC架構(gòu)實現(xiàn)的HTML5游戲引擎,一筆帶過)
文檔導(dǎo)出
文檔導(dǎo)出并沒有使用export
晦攒,而是一個return
闽撤,原因下面會講到。
解釋器
解釋器比較簡單脯颜,主要是提供上述要導(dǎo)入的方法和接受返回的文檔實例哟旗。
如果你熟悉commonjs工作原理,那可以跳過
Doc方法
let func = new Function('require', docSource);
let doc = func(requireMethod);
return doc;
很簡單伐脖,通過一個Function來包裹執(zhí)行(這就是為什么在視圖文檔里是直接用return
來返回整個文檔實例热幔,如果用commonjs的風(fēng)格,則需要提供module
參數(shù)用來接收返回值)讼庇,避免使用eval而外部入侵绎巨。傳入一個require
參數(shù),而這個require
方法則會接受一個id的參數(shù)蠕啄。
function requireMethod(id) {
return requireContext[id];
}
id可能會是qunity
和qunity-pixi
场勤,然后根據(jù)id返回內(nèi)容。
那么requireContext
是一個怎么樣的結(jié)構(gòu)呢歼跟?
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
的內(nèi)容哈街。
qunity里有一個Doc的方法留瞳,實例化一個object,然后存放兩個方法骚秦,kv
和p
她倘,最后執(zhí)行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方法完成了跃巡,在視圖文檔中只要調(diào)用Doc(...).kv(...).p(...)
可以生成一個文檔的實例了危号。
節(jié)點方法
節(jié)點方法全部由qunity-pixi
導(dǎo)出。
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);
}
}
筆者的引擎里注冊的節(jié)點的定義全部在app.entityDefs
上素邪,然后遍歷存放到pixiNodes
上葱色,每個節(jié)點都有相同的鏈式調(diào)用方法,其中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
方法是用來添加視圖子節(jié)點的娘香,s
方法是用來存放組件的配置信息的。
為了能達到鏈式編程的目的办龄,每個方法都需要返回自身烘绽。
為了不污染原本的節(jié)點屬性,需要把存放在節(jié)點上的kv/p/c/s
之類的方法都刪除掉俐填,但是也不能使用就刪除安接,所以筆者想到的時候用setTimeout
,在回調(diào)里將他們?nèi)縿h除英融。
小結(jié)
解釋器主要是為視圖文檔的執(zhí)行提供上下文盏檐,并能實行視圖文檔和接受視圖文檔。
其他顧慮
編輯器schema
筆者在手寫視圖文檔的時候驶悟,發(fā)現(xiàn)智能提示功能并沒效果胡野,這是肯定的,因為并沒有對這些上下文方法給出定義痕鳍。
答案是直接使用typescript的聲明文件來做硫豆,自己寫d.ts文件或者用工具生成上下文的d.ts,這樣笼呆,就能有很好的智能提示了熊响。(注:目前idea的支持較好)
這樣就可以很舒服地使用智能提示來手寫視圖文檔了。
設(shè)計時映射到視圖節(jié)點樹
這個是什么應(yīng)用場景呢诗赌?就是編輯器一般都會有一個視圖節(jié)點樹的組件來展示層級關(guān)系汗茄,那怎么在設(shè)計時從視圖文檔映射出這個視圖節(jié)點樹呢?
筆者想到的就是ast铭若,用抽象語法樹來處理洪碳,使用esprima
之類的庫來靜態(tài)生成ast,然后分析ast奥喻,得到最終的視圖節(jié)點樹偶宫。
其他顧慮暫未想到。环鲤。纯趋。
總結(jié)
目前,簡單版本的解釋器還在開發(fā)和使用中,會不斷迭代更新吵冒,解耦纯命,達到最終的方便快捷直觀。
聲明式編程對于視圖節(jié)點文檔實在是太友好了痹栖,但也不值局限于次亿汞,聲明式編程可以用于更多的DSL中,結(jié)果必定超預(yù)期揪阿!