如何開發一款可視化編程工具(1)
可視化編程的好與壞已經爭論了幾十年,前赴後繼參與到可視化編程開發中的人也不少,各種工具也很多。我很小就對計算機編程感興趣,我的第一門語言是Visual Basic,它幫我打開了計算機世界的大門,因此我對可視化編程的實現倍感好奇,直到後來大學系統學習了數據結構才知道其中奧秘。
十幾歲的我既嘗試過黑底白字的C語言,也嘗試過eclipse複雜的GUI操作,可是都沒有可視化吸引我。拖一拖就出來了界面,不用設計噁心的top、left,雙擊控制項即可寫事件,讓我成就感滿滿。
利用VB,讓我在十幾歲的時候就可以做出漂亮、複雜的計算機應用程序。我當年也嘗試過去學習MFC、QT、WPF,但這對一個十幾歲的少年來說是一個天塹。可以說,如果沒有可視化編程,我可能會拒計算機於門外,我也就感受不到這個世界有多麼美好了。
借用知乎常用的口氣來說,談論編程的時候我們到底在討論什麼?答案是,如何更好的結構化數據以及如何更好的對結構化數據進行增刪查改。
有一種觀點,搜索引擎的搜索技術用到了幾乎所有計算機科學中的知識,這話一點也不假。
在經歷過無數次資料庫的增刪查改之後,我總結出開發可視化編程工具的一些經驗,在這裡分享給大家,不敢說一定是最優方案,但是我目前所知的最優方案。
註:本文中所有的代碼都由Vuejs(1.x)和jQuery完成,因此需要讀者有一定的MVVM編程思想和DOM知識。
另:本系列文章只會給出大體的實現方案,很多工程細節不會悉數羅列。
理清業務需求
假設我們現在的需求是這樣:
在頁面左側有兩個控制項(一個按鈕<button>,一個段落<p>),通過拖拽之後在右側的iframe中生成相應的控制項
這個需求非常簡單,分析一下,我們要處理以下幾件事:
1、讀取控制項
2、實現控制項的拖拽功能
3、生成控制項
只要解決了這三個問題,那麼我們就邁出了第一步。
我們在編寫Web應用程序時,第一件事是根據業務需求設計資料庫,資料庫在硬碟中,最終會被轉換為存儲在內存中的數據結構進行使用。而因為開發可視化編程是在內存中操作,所以下一步就變成了設計數據結構。
設計數據結構
通過設計數據結構,我們可以達到對生成的代碼的粒子級控制,還可以做成一個擁有很強健壯性、可以靈活變動的可視化開發工具。現在市場上很多可視化工具生成的代碼質量低,可讀性差,幾乎不能二次維護,就是因為沒有做到粒子級控制代碼。
先看我們的需求:
在頁面左側有兩個控制項(一個按鈕<button>,一個段落<p>
根據這個需求,我們知道,左側的控制項列表是一個數組對象,我們可以寫成下面這樣的數據結構:
controls: [{ name: "按鈕", //顯示給用戶看的控制項名稱 tag: "button", //真實的標籤名稱 attrs: [], //控制項的DOM屬性 _id: "", //控制項在整個系統中的唯一標識符,區別於html中的id}, { name: "段落", tag: "p", attrs: [], _id: ""}]
目前這個結構還很簡單,name是呈現給用戶看的控制項名稱,tag是元素的真實標籤,attrs是控制項的HTML屬性列表,attrs這個屬性是整個可視化編程中的核心,非常有用,後面會用到。
編寫界面
可視化編程工具最重要的其界面,所以數據結構完成之後就該到界面的編寫了。
下面需要編寫HTML和邏輯代碼將控制項顯示出來(本系列文章主要講解可視化開發的邏輯實現,故不考慮界面美觀度):
主要代碼如下(Hello.vue):
<template> <div class="grid"> <div class="grid-cell u-1of4" style="border-right: 1px solid #f7f7f7;text-align: center;height:100vh"> <ul class="control-list" v-for="control in controls"> <li>{{control.name}}</li> </ul> </div> <div class="grid-cell"> <iframe class="ifr" src="static/designer.html"></iframe> </div> </div></template><script>export default { name: "hello", data () { return { controls: [{ name: "按鈕", //顯示給用戶看的控制項名稱 tag: "button", //真實的標籤名稱 attrs: [], //控制項的DOM屬性 _id: "", //控制項在整個系統中的唯一標識符,區別於html中的id }, { name: "段落", tag: "p", attrs: [], _id: "", }] } }}
其中iframe中的地址static/designer.html就是要將控制項拖拽的HTML容器,其代碼結構如下:
<!DOCTYPE html><html><head> <title>designer</title> <style type="text/css"> body { background: #ececec; } </style></head><body id="container"> <script src="//cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script> <script type="text/javascript" src="./designer.js"></script></body></html>
其中的designer.js是在iframe中處理拖拽進入的核心腳本。
最終的界面效果如下:

到此為止,我們完成了第一步,即顯示控制項,那麼下面我們就要實現控制項的拖拽功能。
處理拖拽
處理拖拽主要使用HTML5的Drag & Drop API,代碼寫在designer.js,其實現代碼如下:
$(function() { function DndInitialization() { var self = this; this.containerSelector = "#container"; this.inter = 0; this.makeParentComponentsDraggable(); $(self.containerSelector).on("drop", function (e) { self.onDrop(e); }) $(self.containerSelector).on("dragover", function (e) { self.onOver(e); }) $(self.containerSelector).on("dragenter", function (e) { self.onEnter(e); }) } DndInitialization.prototype = { onDrop: function() { console.log("onDrop"); }, onOver: function() { console.log("onOver"); }, onEnter: function() { console.log("onEnter"); }, makeParentComponentsDraggable: function() { var self = this; var components = $(window.parent.document, window.parent.document).find(".control-elem"); components.each(function(n) { $(this).attr("draggable", true); $(this).on("dragstart", function (e) { e.stopPropagation(); }); $(this).on("dragend", function (e) { e.preventDefault(); }); }); } } setTimeout(function() { new DndInitialization(); }, 2000);});
代碼中定義了DndInitialization類,這個類會讓父級的控制項列表可以被拖動,並且會處理拖拽開始、拖拽結束、拖拽進入、拖拽放置事件,且會定義一個合法的拖拽放置容器,即containerSelector。
makeParentComponentsDraggable是核心方法,該方法將遍歷所有的控制項並加上一個draggable屬性,這個時候左側的所有控制項就可以進行拖拽了。
現在我們完成了第二步了嗎?沒有,僅僅完成了一小部分,重要的是如何處理拖拽過程,這一過程將和控制項的生成一起完善。
注意最後我們用了一個setTimeout,是為了等待父級DOM載入完成,當然這種寫法不太規範,我們後面會對這段代碼進行修正。
生成控制項
生成控制項,主要是根據我們定義好的數據結構編程。對,別忘了,根據數據編程,編程主要是處理數據,那麼我們的iframe怎麼跨dom知道當前被拖拽的是哪個控制項呢?
這裡提供有兩種方法:
1、使用window對象傳輸,window在同源下的iframe中是可以共享的
2、使用postMessage
根據生成控制項的需求可知,當前被拖拽的控制項有且僅有一個,所以用window對象傳輸是最節省成本的。
生成控制項應該是在onDrop事件下,所以生成控制項的方法要寫在onDrop事件下。
Hello.vue,使用window對象綁定當前被拖拽控制項的數據結構:
<li v-on:mousedown="mousedown(control)" class="control-elem">{{control.name}}</li>
在控制項列表上監聽mousedown事件:
mousedown: function(control) { window.dndData = control;}
這樣就可以在designer.js中直接獲得當前被拖拽控制項的數據結構
在designer.js中監聽dragend事件:
$(this).on("dragend", function (e) { e.preventDefault(); console.log("onDrop", window.parent.dndData);});
拖拽按鈕後,此處輸出:
{ name: "按鈕", tag: "button", attrs: [], _id: ""}
好了,我們拿到數據了,下面我們就要根據這些數據生成相關的HTML代碼了:
在designer.js中定義了一個新的類:ElemGenerator,這個類將會根據相應的數據結構生成控制項,每一個ElemGenerator都是一個控制項。
function ElemGenerator(controller) { this.controller = controller; } ElemGenerator.prototype = { createElement: function() { var docCtrl = $("[id=""+ this.controller._id + ""]");//尋找DOM中是否已經存在元素 this.elem = docCtrl.length > 0 ? docCtrl : $(document.createElement(this.controller.tag));//虛擬出一個元素 return this.elem; } }
ondragend事件要做出如下更改:
$(this).on("dragend", function (e) { e.preventDefault(); var elems = new ElemGenerator(window.parent.dndData).createElement(); $(self.containerSelector).append(elems);});
其調用了ElemGenerator類的createElement方法並append進了設計區域中。
打開瀏覽器調試,可以發現我們的拖拽功能和控制項生成可以使用了:

這個時候,上文說到的attrs屬性就有用了,我們要將數據改成如下結構:
controls: [{ name: "按鈕", //顯示給用戶看的控制項名稱 tag: "button", //真實的標籤名稱 attrs: [{ name: "value", //屬性的英文名稱 title: "按鈕名稱", //屬性的中文名稱 isHTML: true, //是否為HTML屬性 value: "新的按鈕" //值 }], //控制項的DOM屬性 _id: "", //控制項在整個系統中的唯一標識符,區別於html中的id}, { name: "段落", tag: "p", attrs: [], _id: ""}]
根據數據結構編程(劃重點),修改ElemGenerator的createElement方法:
function ElemGenerator(controller) { this.controller = controller;}ElemGenerator.prototype = { setAttribute: function(attr) { console.log(attr); if(attr.isHTML) { this.elem.html(attr.value); } }, createElement: function() { var docCtrl = $("[id=""+ this.controller._id + ""]");//尋找DOM中是否已經存在元素 this.elem = docCtrl.length > 0 ? docCtrl : $(document.createElement(this.controller.tag));//虛擬出一個元素 for (var i = 0; i < this.controller.attrs.length; i++) { this.setAttribute(this.controller.attrs[i]); }; return this.elem; }}
在createElement中將循環遍歷控制項的屬性並調用setAttribute方法為控制項設置屬性。
HTML元素的屬性包括兩大類:
1、在HTML標籤上的
2、在HTML標籤裡面的
setAttribute將主要根據這兩種不同的屬性類型執行不同的DOM操作,在實際工程中,其邏輯代碼可達上千行。
再次拖拽,發現已經成功生成一個正常的按鈕了。


在頁面左側有兩個控制項(一個按鈕<button>,一個段落<p>),通過拖拽之後在右側的iframe中生成相應的控制項
並且主要做了三件事:
1、讀取控制項
2、實現控制項的拖拽功能
3、生成控制項
當然,這份代碼僅僅能做演示之用,工業級別的可視化編程步驟遠比這複雜,在拖拽-放置的過程中,其處理代碼可能多達萬行。
在下一篇中,將介紹如何生成更複雜的控制項、如何實現可視化配置HTML屬性以及如何達到粒子級別的代碼控制。
代碼已在Github中,歡迎關注:)
leinue/vd
推薦閱讀:
※一個數字鍵盤引發的血案——移動端H5輸入框、游標、數字鍵盤全假套件實現
※opengl/webgl 可以部分重繪嗎?
※推動HTML5生態發展,Gospel還能做什麼?
※手把手教你擼一個跑男動畫 順便抽絲剝繭CSS3動畫奧秘
※如何解剖一個網站?
TAG:HTML5 | 前端开发 | JavaScript |
