基於vue的簡單流程圖開發

嚴重拖延症,一方面這項目模塊純屬我的娛樂。另外一方面,流程圖這塊涉及的東西仍是蠻多的,此次也只是介紹一些簡單的部分。拖了這麼久,如今終於要開始硬着頭皮寫一篇基於vue+svg的流程圖"僞教程"文章了。初次獻醜,還請輕噴。javascript

模塊簡介

項目地址css

圖片預覽

出於學習vue而非兼容的目的,本項目僅考慮現代瀏覽器( 谷歌 ),部分兼容問題還請見諒。html

本模塊的開發源於對流程圖的簡單需求( 純UI實現,暫不存在業務邏輯 ),這裏不贅訴vue-cli生成的目錄結構(能夠參考這篇或自行谷歌)。vue

項目實際用到的技術棧:SVG + vue + vuexjava

功能介紹:node

  • 畫布縮放
  • 節點( 開始,基礎,判斷等 )添加,刪除
  • 節點間連線( 直線/折線 )
  • 文本添加
  • 外部導入SVG圖形
  • 撤銷與重作

畫布縮放

考慮到畫布縮放後佈局需保持一致,這裏經過修改transform: scale(); transform-origin: ; 來實現,節點則相對父層定位。jquery

TODO: SVG最優縮放解決方案?css3

節點相關

下面我簡單說一下思路:git

因爲不存在業務邏輯,我把流程圖簡化爲 開始 基礎 判斷 3個基礎組件( 基於SVG )。github

如:

<template>
    <!-- 開始 -->
    <ellipse v-bind="style"></ellipse>
    <!-- 基礎 -->
    <rect v-bind="style"></rect>
    <!-- 判斷 -->
    <path v-bind="style"></path>
</template>

這裏說一下判斷這個組件( 後期可能出現複雜形狀均以path實現 ),通常由AI軟件直接導出相關形狀。

左邊工具欄跟畫布中的相同圖形源於同個組件,故設有兩個樣式,即 defaultStyledrawStyle。以前有考慮過,若是流程圖的圖形複雜多變的話,那這種模式豈不是每個組件都得人爲定義。一樣,採用導入SVG也有相似問題。由於若是圖形大小都不肯定的話,除了支持圖形修改大小,否者將致使畫布出現大小不一的圖形。( 很是遺憾這方面沒有作出突破,不過這將成爲將來改進的方向。)

最開始採用的解決方案是以scale的方式,也就是統一讓工具欄中的圖形跟拖入畫布中的圖造成等比縮放關係。不過該方式會形成stroke也同比縮放,並不是咱們想要的。

因此目前暫時採用寫死的方式。

注意: 在svg中 ellipse 定位相對於中心點,而rect定位是相對於左上角。

TODO是否有辦法將各組件定位源點設置爲組件中心點。

節點渲染

節點渲染方面,因爲以前是將圖形做爲組件,因而採用 component + is 的方式來渲染圖形。同時也是以數據驅動的方式來渲染,即數據決定視圖。

<component v-for="(item,index) in nodeData" :is="item.type" :id="item.id" v-node inDraw></component>

拖動節點涉及鏡像節點時:

<component :is="selNodeId" :transform="selNodeInfo.transform" v-if="isDragging" inDraw></component>

代碼直通車

新增節點

drag drop 的形式。採用該方式的好處是不須要模擬拖拽事件。也就是鏡像什麼的不須要本身作。( 畫布內節點拖拽則使用原生模擬 )

代碼直通車

對節點的操做均以指令( directives )的形式( 直接操做DOM )。這引起了我對該類項目是否適合用vue類框架來作的疑問,從開發效率方面,仍是首選vue,可是從性能方面,因爲沒有深刻研究,並無發言權。

TODO 場景模擬,假設咱們須要移動畫布內節點,經過directives的el來獲取節點,而後經過el.onmousemove來修改data中對應的translate來實現位置的更改。這裏修改data來驅動視圖是咱們經常使用的方式,可是我想不通的就是el.onmousemove來修改data實現的雙向數據綁定所帶來的性能在這裏是否有體現。

我所設想的是,是否涉及多依賴的時候,diff帶來的性能提高才有價值。舉個例子,我有一個列表,存在於data中的listData,而後在view中有多處關聯listData。那此時操做listData比直接操做DOM來得更好些。

看過相關vitrualDOM的介紹,經過diff能夠只操做變化的DOM。

獲取SVG大小

獲取節點大小使用 getBoundingClientRect ,同時因爲前面作了縮放功能,這裏獲取節點大小時須要除以縮放比例來獲取正確值。

let obj = el.getElementsByTagName('g')[0]
let w = obj.getBoundingClientRect().width / _this.drawStyle.zoomRate
let h = obj.getBoundingClientRect().height / _this.drawStyle.zoomRate
let wh = {
    width: w,
    height: h
}

代碼直通車

節點操做總結

因爲節點的顯示是基於NodeData,因此增刪其實就是對NodeData的增刪。

主要代碼

連線相關

連線其實也只是用到了svg的linepolyline,這裏跟節點相似,均以組件的形式存在,並以lineData驅動連線視圖。因此最終連線的增刪也是對數據的操做。

鏈接點的顯示

<img src="https://leer0911.github.io/im... width="200">

首先是連接點的位置( 綠色遠點位置 ),以前基於jquery作的流程圖是用div佈局,如今用svg增長了難度,因爲svg不能使用position,因此沒法基於當前元素定位。採用的是土辦法,即用圖形大小+padding動態獲取4個點的位置。期間,因爲4個連線節點與圖形節點有空隙,當mouseover不處於圖形或節點時,事件沒法觸發。在此是模擬一個區域來解決的。因爲我的經驗問題,這部分代碼徹底就是命令式的風格。勿噴

代碼直通車
代碼直通車2

鏈接處理

節點間連線作了兩種狀況:(這裏不講訴從mousedown至mouseup具體細節,能夠看這裏

其實不少人說,算法能夠解決不少垃圾代碼。惋惜我還沒掌握它的真諦,好比以前的圖形組件,以及接下來的不一樣線條。其實均可以經過必定的算法得出來。我這裏只講講最笨的方法,待我成長到能用算法來講話的時候,在回來好好理下這篇文章。

  • line直線

直線無外乎就是兩個點座標,經過svg中的line來顯示。這時候就得看項目的需求,咱們假設最簡單的狀況,就是上面講到過的4個鏈接點最爲連線的起始或結束點。
下面是計算圖形中4個點的座標位置

computeLine(direction, obj) { // low不止一點點
    let { top, left, width, height } = obj
    let w = width / 2
    let h = height / 2
    switch (direction) {
    case 't':
        top = top - h
        break
    case 'b':
        top = top + h
        break
    case 'l':
        left = left - w
        break
    case 'r':
        left = left + w
        break
    default:
        break
    }
    return { top, left }
}
  • polyline折線

折線考慮的狀況相對比較多一點,這邊因爲使用的是polyline,它的點位設置長這樣子points="125,96 183.5,96 183.5,399 242,399"

這個時候通常會把字符轉化爲較爲好操做的數組或對象。折線涉及的開始點跟結束點跟上面介紹直線的點位同樣,不一樣的是中間線的位置,若是不考慮複雜的狀況,

通常能夠分爲兩種,上下,左右。經過獲取開始與結束點的中點位置來肯定中線便可以獲得想要的折線。代碼以下:(都是用簡單粗暴的方式。)

computePolyLine(start, end, direction) {
    let startPoint = {
    x: +(start.split(',')[0]),
    y: +(start.split(',')[1])
    }
    let endPoint = {
    x: +(end.split(',')[0]),
    y: +(end.split(',')[1])
    }
    let m1, m2
    switch (direction) {
    case 't':
    case 'b':
        let mY = startPoint.y + (endPoint.y - startPoint.y) / 2
        m1 = {
        x: startPoint.x,
        y: mY
        }
        m2 = {
        x: endPoint.x,
        y: mY
        }
        break
    case 'l':
    case 'r':
        let mX = startPoint.x + (endPoint.x - startPoint.x) / 2
        m1 = {
        x: mX,
        y: startPoint.y
        }
        m2 = {
        x: mX,
        y: endPoint.y
        }
        break
    default:
        break
    }
    return `${startPoint.x},${startPoint.y} ${m1.x},${m1.y} ${m2.x},${m2.y} ${endPoint.x},${endPoint.y}`
}

連線總結

節點跟連線在渲染以及操做的處理上大同小異,這裏不肯定是否爲最佳實踐的有兩個地方,一是採用component+is的形式來渲染組件,二是採用 diretives的方式來操做DOM。連線的計算形式也略顯簡單,這確實是須要必定時間來成長的。扯偏了,在這簡單總結一下,不管是哪一種連線方式,咱們須要作的就是正確獲取對應點的位置,而後修改數據來驅動視圖。不過能在各類複雜的狀況下總結出算法,也是一種跨越,加油吧。

節點及連線的文本添加

節點及連線的文本添加原理都同樣,這裏採用的是設置 contenteditable 當contenteditable爲true時,html結構自動添加文本節點而且可編輯。更多細節能夠參考張鑫旭的這篇

順道講一下pointer-events本模塊有兩個地方用到該css屬性。一個是文本添加這塊,以及頭部工具欄部分。

CSS屬性pointer-events容許做者控制特定的圖形元素在什麼時候成爲鼠標事件的target。當未指定該屬性時,SVG內容表現如同visiblePainted。

除了指定元素不成爲鼠標事件的目標,none值還指示鼠標事件穿過該元素,並指向位於元素下面的元素。

更多細節關於pointer-events

張鑫旭
MDN

TODO 文本編輯雖已實現功能,但這塊BUG較多,還未完善。

外部導入SVG

這邊也是用到了HTML5的Drop功能,顯示則是用到了svg的images。拖拽實現比較簡單:

dropHandle (e) {
    let reader = new FileReader()
    let file = e.dataTransfer.files[0]
    reader.onload = (e) => {
        this.userImages.push(e.target.result)
    }
    reader.readAsDataURL(file)
},
dragoverHandle () {
},
dragstart (imgSrc) {
    event.dataTransfer.setData('URL', imgSrc)
}

這邊須要注意的是@drop.stop.prevent="dropHandle" @dragover.stop.prevent="dragoverHandle"要阻止冒泡以及阻止瀏覽器默認行爲。

還有一個要注意的是dataTransfer.getData()在dragover,dragenter,dragleave中沒法獲取數據的問題

根據W3C標準,drag data store有三種模式,Read/write mode, Read-only mode跟Protected mode。細節

Read/write mode
讀/寫模式,在dragstart事件中使用,能夠添加新數據到drag data store中。

Read-only mode
只讀模式,在drop事件中使用,能夠讀取被拖拽數據,不可添加新數據。

Protected mode
保護模式,在全部其餘的事件中使用,數據的列表能夠被枚舉,可是數據自己不可用且不能添加新數據。

深刻

撤銷與重作

這一功能本質上是沒有完成的,由於採用了一種偷懶的方式,vuex 生成 State 快照,生產環境不建議使用。

基本原理就是經過vuex提交更高(mutation)來觸發回調。以此來記錄state 快照

代碼直通車

總結

本項目屬於入門級的vue+vuex,可是並無講如何使用vue或者vuex,由於這些在官方文檔其實都已經講的很是清楚了。該項目也只是簡單使用瞭如vue的自定義指令,MiXin等經常使用方法。諸如vue Render函數組件,不在本文談論範圍,這裏簡單講下使用體驗,render組件比較適合高自定義的組件(變化邏輯比較複雜)。由於一些簡單組件其實更適合用tempalte的形式,雖然使用Render能夠提升必定的性能( 減小了從tempalte到render這一步 ),可是不少現有的如sync,是render組件所不具有的( 需本身實現 )。vuex的使用,則須要注意的是object引用地址的問題。也就是說,要避免數據間的潛在影響。(雖然vuex自身也有規避這個問題)能夠了解一下immutable

本教程主要講述一個基於vue如何實現一個簡單的流程圖,更多引起的思考是,什麼項目更適合使用這種MVVM模式的框架,以及如何發揮VitrualDOM的價值。其實上面幾個章節的點隨便拿個出來均可以深刻探討出不少技術問題,之後有機會再陸續深刻。

相關文章
相關標籤/搜索