<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          可視化拖拽組件庫一些技術(shù)要點(diǎn)原理分析

          共 12418字,需瀏覽 25分鐘

           ·

          2021-01-18 22:16

          • 本文已獲得原作者的獨(dú)家授權(quán),有想轉(zhuǎn)載的朋友們可以在后臺(tái)聯(lián)系我申請(qǐng)開白哦!
          • PS:歡迎掘友們向我投稿哦,被采用的文章還可以送你掘金精美周邊!

          本文主要對(duì)以下技術(shù)要點(diǎn)進(jìn)行分析:

          1. 編輯器
          2. 自定義組件
          3. 拖拽
          4. 刪除組件、調(diào)整圖層層級(jí)
          5. 放大縮小
          6. 撤消、重做
          7. 組件屬性設(shè)置
          8. 吸附
          9. 預(yù)覽、保存代碼
          10. 綁定事件
          11. 綁定動(dòng)畫
          12. 導(dǎo)入 PSD
          13. 手機(jī)模式

          為了讓本文更加容易理解,我將以上技術(shù)要點(diǎn)結(jié)合在一起寫了一個(gè)可視化拖拽組件庫 DEMO:

          • github 項(xiàng)目地址
          • 在線預(yù)覽

          建議結(jié)合源碼一起閱讀,效果更好(這個(gè) DEMO 使用的是 Vue 技術(shù)棧)。

          1.編輯器


          先來看一下頁面的整體結(jié)構(gòu)。

          這一節(jié)要講的編輯器其實(shí)就是中間的畫布。它的作用是:當(dāng)從左邊組件列表拖拽出一個(gè)組件放到畫布中時(shí),畫布要把這個(gè)組件渲染出來。

          這個(gè)編輯器的實(shí)現(xiàn)思路是:

          1. 用一個(gè)數(shù)組?componentData?維護(hù)編輯器中的數(shù)據(jù)。
          2. 把組件拖拽到畫布中時(shí),使用?push()?方法將新的組件數(shù)據(jù)添加到?componentData
          3. 編輯器使用?v-for?指令遍歷?componentData,將每個(gè)組件逐個(gè)渲染到畫布(也可以使用 JSX 語法結(jié)合?render()?方法代替)。

          編輯器渲染的核心代碼如下所示:

          ??v-for="item?in?componentData"
          ??:key="item.id"
          ??:is="item.component"
          ??:style="item.style"
          ??:propValue="item.propValue"
          />

          每個(gè)組件數(shù)據(jù)大概是這樣:

          {
          ????component:?'v-text',?
          ????label:?'文字',?
          ????propValue:?'文字',?
          ????icon:?'el-icon-edit',?
          ????animations:?[],?
          ????events:?{},?
          ????style:?{?
          ????????width:?200,
          ????????height:?33,
          ????????fontSize:?14,
          ????????fontWeight:?500,
          ????????lineHeight:?'',
          ????????letterSpacing:?0,
          ????????textAlign:?'',
          ????????color:?'',
          ????},
          }

          在遍歷?componentData?組件數(shù)據(jù)時(shí),主要靠?is?屬性來識(shí)別出真正要渲染的是哪個(gè)組件。

          例如要渲染的組件數(shù)據(jù)是?{ component: 'v-text' },則??會(huì)被轉(zhuǎn)換為?。當(dāng)然,你這個(gè)組件也要提前注冊(cè)到 Vue 中。

          如果你想了解更多?is?屬性的資料,請(qǐng)查看官方文檔。

          2. 自定義組件


          原則上使用第三方組件也是可以的,但建議你最好封裝一下。不管是第三方組件還是自定義組件,每個(gè)組件所需的屬性可能都不一樣,所以每個(gè)組件數(shù)據(jù)可以暴露出一個(gè)屬性?propValue?用于傳遞值。

          例如 a 組件只需要一個(gè)屬性,你的?propValue?可以這樣寫:propValue: 'aaa'。如果需要多個(gè)屬性,propValue?則可以是一個(gè)對(duì)象:

          propValue:?{
          ??a:?1,
          ??b:?'text'
          }

          在這個(gè) DEMO 組件庫中我定義了三個(gè)組件。

          圖片組件?Picture




          按鈕組件?VButton:




          文本組件?VText:




          3. 拖拽


          從組件列表到畫布

          一個(gè)元素如果要設(shè)為可拖拽,必須給它添加一個(gè)?draggable?屬性。另外,在將組件列表中的組件拖拽到畫布中,還有兩個(gè)事件是起到關(guān)鍵作用的:

          1. dragstart?事件,在拖拽剛開始時(shí)觸發(fā)。它主要用于將拖拽的組件信息傳遞給畫布。
          2. drop?事件,在拖拽結(jié)束時(shí)觸發(fā)。主要用于接收拖拽的組件信息。

          先來看一下左側(cè)組件列表的代碼:

          "handleDragStart">
          ????"(item,?index)?in?componentList"?:key="index"?draggable?:data-index="index">
          ????????"item.icon">
          ????????{{?item.label?}}
          ????


          handleDragStart(e)?{
          ????e.dataTransfer.setData('index',?e.target.dataset.index)
          }

          可以看到給列表中的每一個(gè)組件都設(shè)置了?draggable?屬性。另外,在觸發(fā)?dragstart?事件時(shí),使用?dataTransfer.setData()?傳輸數(shù)據(jù)。再來看一下接收數(shù)據(jù)的代碼:

          "handleDrop"?@dragover="handleDragOver"?@click="deselectCurComponent">
          ????

          handleDrop(e)?{
          ????e.preventDefault()
          ????e.stopPropagation()
          ????const?component?=?deepCopy(componentList[e.dataTransfer.getData('index')])
          ????this.$store.commit('addComponent',?component)
          }

          觸發(fā)?drop?事件時(shí),使用?dataTransfer.getData()?接收傳輸過來的索引數(shù)據(jù),然后根據(jù)索引找到對(duì)應(yīng)的組件數(shù)據(jù),再添加到畫布,從而渲染組件。

          組件在畫布中移動(dòng)

          首先需要將畫布設(shè)為相對(duì)定位?position: relative,然后將每個(gè)組件設(shè)為絕對(duì)定位?position: absolute。除了這一點(diǎn)外,還要通過監(jiān)聽三個(gè)事件來進(jìn)行移動(dòng):

          1. mousedown?事件,在組件上按下鼠標(biāo)時(shí),記錄組件當(dāng)前的位置,即 xy 坐標(biāo)(為了方便講解,這里使用的坐標(biāo)軸,實(shí)際上 xy 對(duì)應(yīng)的是 css 中的?left?和?top
          2. mousemove?事件,每次鼠標(biāo)移動(dòng)時(shí),都用當(dāng)前最新的 xy 坐標(biāo)減去最開始的 xy 坐標(biāo),從而計(jì)算出移動(dòng)距離,再改變組件位置。
          3. mouseup?事件,鼠標(biāo)抬起時(shí)結(jié)束移動(dòng)。
          handleMouseDown(e)?{
          ????e.stopPropagation()
          ????this.$store.commit('setCurComponent',?{?component:?this.element,?zIndex:?this.zIndex?})

          ????const?pos?=?{?...this.defaultStyle?}
          ????const?startY?=?e.clientY
          ????const?startX?=?e.clientX
          ????
          ????const?startTop?=?Number(pos.top)
          ????const?startLeft?=?Number(pos.left)

          ????const?move?=?(moveEvent)?=>?{
          ????????const?currX?=?moveEvent.clientX
          ????????const?currY?=?moveEvent.clientY
          ????????pos.top?=?currY?-?startY?+?startTop
          ????????pos.left?=?currX?-?startX?+?startLeft
          ????????
          ????????this.$store.commit('setShapeStyle',?pos)
          ????}

          ????const?up?=?()?=>?{
          ????????document.removeEventListener('mousemove',?move)
          ????????document.removeEventListener('mouseup',?up)
          ????}

          ????document.addEventListener('mousemove',?move)
          ????document.addEventListener('mouseup',?up)
          }

          4. 刪除組件、調(diào)整圖層層級(jí)


          改變圖層層級(jí)

          由于拖拽組件到畫布中是有先后順序的,所以可以按照數(shù)據(jù)順序來分配圖層層級(jí)。

          例如畫布新增了五個(gè)組件 abcde,那它們?cè)诋嫴紨?shù)據(jù)中的順序?yàn)?[a, b, c, d, e],圖層層級(jí)和索引一一對(duì)應(yīng),即它們的?z-index?屬性值是 01234(后來居上)。用代碼表示如下:

          "(item,?index)?in?componentData"?:zIndex="index">

          如果不了解?z-index?屬性的,請(qǐng)看一下 MDN 文檔。

          理解了這一點(diǎn)之后,改變圖層層級(jí)就很容易做到了。改變圖層層級(jí),即是改變組件數(shù)據(jù)在?componentData?數(shù)組中的順序。例如有?[a, b, c]?三個(gè)組件,它們的圖層層級(jí)從低到高順序?yàn)?abc(索引越大,層級(jí)越高)。

          如果要將 b 組件上移,只需將它和 c 調(diào)換順序即可:

          const?temp?=?componentData[1]
          componentData[1]?=?componentData[2]
          componentData[2]?=?temp

          同理,置頂置底也是一樣,例如我要將 a 組件置頂,只需將 a 和最后一個(gè)組件調(diào)換順序即可:

          const?temp?=?componentData[0]
          componentData[0]?=?componentData[componentData.lenght?-?1]
          componentData[componentData.lenght?-?1]?=?temp

          刪除組件

          刪除組件非常簡(jiǎn)單,一行代碼搞定:componentData.splice(index, 1)

          5. 放大縮小


          細(xì)心的網(wǎng)友可能會(huì)發(fā)現(xiàn),點(diǎn)擊畫布上的組件時(shí),組件上會(huì)出現(xiàn) 8 個(gè)小圓點(diǎn)。這 8 個(gè)小圓點(diǎn)就是用來放大縮小用的。實(shí)現(xiàn)原理如下:

          1. 在每個(gè)組件外面包一層?Shape?組件,Shape?組件里包含 8 個(gè)小圓點(diǎn)和一個(gè)??插槽,用于放置組件。

          "(item,?index)?in?componentData"
          ????:defaultStyle="item.style"
          ????:style="getShapeStyle(item.style,?index)"
          ????:key="item.id"
          ????:active="item?===?curComponent"
          ????:element="item"
          ????:zIndex="index"
          >
          ???????????
          ????????:is="item.component"
          ????????:style="getComponentStyle(item.style)"
          ????????:propValue="item.propValue"
          ????/>

          Shape?組件內(nèi)部結(jié)構(gòu):


          2. 點(diǎn)擊組件時(shí),將 8 個(gè)小圓點(diǎn)顯示出來。

          起作用的是這行代碼?:active="item === curComponent"

          3. 計(jì)算每個(gè)小圓點(diǎn)的位置。

          先來看一下計(jì)算小圓點(diǎn)位置的代碼:

          const?pointList?=?['t',?'r',?'b',?'l',?'lt',?'rt',?'lb',?'rb']

          getPointStyle(point)?{
          ????const?{?width,?height?}?=?this.defaultStyle
          ????const?hasT?=?/t/.test(point)
          ????const?hasB?=?/b/.test(point)
          ????const?hasL?=?/l/.test(point)
          ????const?hasR?=?/r/.test(point)
          ????let?newLeft?=?0
          ????let?newTop?=?0

          ????
          ????if?(point.length?===?2)?{
          ????????newLeft?=?hasL??0?:?width
          ????????newTop?=?hasT??0?:?height
          ????}?else?{
          ????????
          ????????if?(hasT?||?hasB)?{
          ????????????newLeft?=?width?/?2
          ????????????newTop?=?hasT??0?:?height
          ????????}

          ????????
          ????????if?(hasL?||?hasR)?{
          ????????????newLeft?=?hasL??0?:?width
          ????????????newTop?=?Math.floor(height?/?2)
          ????????}
          ????}

          ????const?style?=?{
          ????????marginLeft:?hasR??'-4px'?:?'-3px',
          ????????marginTop:?'-3px',
          ????????left:?`${newLeft}px`,
          ????????top:?`${newTop}px`,
          ????????cursor:?point.split('').reverse().map(m?=>?this.directionKey[m]).join('')?+?'-resize',
          ????}

          ????return?style
          }

          計(jì)算小圓點(diǎn)的位置需要獲取一些信息:

          • 組件的高度?height、寬度?width

          注意,小圓點(diǎn)也是絕對(duì)定位的,相對(duì)于?Shape?組件。所以有四個(gè)小圓點(diǎn)的位置很好確定:

          1. 左上角的小圓點(diǎn),坐標(biāo)?left: 0, top: 0
          2. 右上角的小圓點(diǎn),坐標(biāo)?left: width, top: 0
          3. 左下角的小圓點(diǎn),坐標(biāo)?left: 0, top: height
          4. 右下角的小圓點(diǎn),坐標(biāo)?left: width, top: height

          另外的四個(gè)小圓點(diǎn)需要通過計(jì)算間接算出來。例如左邊中間的小圓點(diǎn),計(jì)算公式為?left: 0, top: height / 2,其他小圓點(diǎn)同理。

          4. 點(diǎn)擊小圓點(diǎn)時(shí),可以進(jìn)行放大縮小操作。

          handleMouseDownOnPoint(point)?{
          ????const?downEvent?=?window.event
          ????downEvent.stopPropagation()
          ????downEvent.preventDefault()

          ????const?pos?=?{?...this.defaultStyle?}
          ????const?height?=?Number(pos.height)
          ????const?width?=?Number(pos.width)
          ????const?top?=?Number(pos.top)
          ????const?left?=?Number(pos.left)
          ????const?startX?=?downEvent.clientX
          ????const?startY?=?downEvent.clientY

          ????
          ????let?needSave?=?false
          ????const?move?=?(moveEvent)?=>?{
          ????????needSave?=?true
          ????????const?currX?=?moveEvent.clientX
          ????????const?currY?=?moveEvent.clientY
          ????????const?disY?=?currY?-?startY
          ????????const?disX?=?currX?-?startX
          ????????const?hasT?=?/t/.test(point)
          ????????const?hasB?=?/b/.test(point)
          ????????const?hasL?=?/l/.test(point)
          ????????const?hasR?=?/r/.test(point)
          ????????const?newHeight?=?height?+?(hasT??-disY?:?hasB??disY?:?0)
          ????????const?newWidth?=?width?+?(hasL??-disX?:?hasR??disX?:?0)
          ????????pos.height?=?newHeight?>?0??newHeight?:?0
          ????????pos.width?=?newWidth?>?0??newWidth?:?0
          ????????pos.left?=?left?+?(hasL??disX?:?0)
          ????????pos.top?=?top?+?(hasT??disY?:?0)
          ????????this.$store.commit('setShapeStyle',?pos)
          ????}

          ????const?up?=?()?=>?{
          ????????document.removeEventListener('mousemove',?move)
          ????????document.removeEventListener('mouseup',?up)
          ????????needSave?&&?this.$store.commit('recordSnapshot')
          ????}

          ????document.addEventListener('mousemove',?move)
          ????document.addEventListener('mouseup',?up)
          }

          它的原理是這樣的:

          1. 點(diǎn)擊小圓點(diǎn)時(shí),記錄點(diǎn)擊的坐標(biāo) xy。
          2. 假設(shè)我們現(xiàn)在向下拖動(dòng),那么 y 坐標(biāo)就會(huì)增大。
          3. 用新的 y 坐標(biāo)減去原來的 y 坐標(biāo),就可以知道在縱軸方向的移動(dòng)距離是多少。
          4. 最后再將移動(dòng)距離加上原來組件的高度,就可以得出新的組件高度。
          5. 如果是正數(shù),說明是往下拉,組件的高度在增加。如果是負(fù)數(shù),說明是往上拉,組件的高度在減少。

          6. 撤消、重做


          撤銷重做的實(shí)現(xiàn)原理其實(shí)挺簡(jiǎn)單的,先看一下代碼:

          snapshotData:?[],?
          snapshotIndex:?-1,?
          ????????
          undo(state)?{
          ????if?(state.snapshotIndex?>=?0)?{
          ????????state.snapshotIndex--
          ????????store.commit('setComponentData',?deepCopy(state.snapshotData[state.snapshotIndex]))
          ????}
          },

          redo(state)?{
          ????if?(state.snapshotIndex?????????state.snapshotIndex++
          ????????store.commit('setComponentData',?deepCopy(state.snapshotData[state.snapshotIndex]))
          ????}
          },

          setComponentData(state,?componentData?=?[])?{
          ????Vue.set(state,?'componentData',?componentData)
          },

          recordSnapshot(state)?{
          ????
          ????state.snapshotData[++state.snapshotIndex]?=?deepCopy(state.componentData)
          ????
          ????if?(state.snapshotIndex?????????state.snapshotData?=?state.snapshotData.slice(0,?state.snapshotIndex?+?1)
          ????}
          },

          用一個(gè)數(shù)組來保存編輯器的快照數(shù)據(jù)。保存快照就是不停地執(zhí)行?push()?操作,將當(dāng)前的編輯器數(shù)據(jù)推入?snapshotData?數(shù)組,并增加快照索引?snapshotIndex。目前以下幾個(gè)動(dòng)作會(huì)觸發(fā)保存快照操作:

          • 新增組件
          • 刪除組件
          • 改變圖層層級(jí)
          • 拖動(dòng)組件結(jié)束時(shí)

          ...

          撤銷

          假設(shè)現(xiàn)在?snapshotData?保存了 4 個(gè)快照。即?[a, b, c, d],對(duì)應(yīng)的快照索引為 3。如果這時(shí)進(jìn)行了撤銷操作,我們需要將快照索引減 1,然后將對(duì)應(yīng)的快照數(shù)據(jù)賦值給畫布。

          例如當(dāng)前畫布數(shù)據(jù)是 d,進(jìn)行撤銷后,索引 -1,現(xiàn)在畫布的數(shù)據(jù)是 c。

          重做

          明白了撤銷,那重做就很好理解了,就是將快照索引加 1,然后將對(duì)應(yīng)的快照數(shù)據(jù)賦值給畫布。

          不過還有一點(diǎn)要注意,就是在撤銷操作中進(jìn)行了新的操作,要怎么辦呢?有兩種解決方案:

          1. 新操作替換當(dāng)前快照索引后面所有的數(shù)據(jù)。還是用剛才的數(shù)據(jù)?[a, b, c, d]?舉例,假設(shè)現(xiàn)在進(jìn)行了兩次撤銷操作,快照索引變?yōu)?1,對(duì)應(yīng)的快照數(shù)據(jù)為 b,如果這時(shí)進(jìn)行了新的操作,對(duì)應(yīng)的快照數(shù)據(jù)為 e。那 e 會(huì)把 cd 頂?shù)簦F(xiàn)在的快照數(shù)據(jù)為?[a, b, e]
          2. 不頂?shù)魯?shù)據(jù),在原來的快照中新增一條記錄。用剛才的例子舉例,e 不會(huì)把 cd 頂?shù)簦窃?cd 之前插入,即快照數(shù)據(jù)變?yōu)?[a, b, e, c, d]

          我采用的是第一種方案。

          7. 吸附


          什么是吸附?就是在拖拽組件時(shí),如果它和另一個(gè)組件的距離比較接近,就會(huì)自動(dòng)吸附在一起。

          吸附的代碼大概在 300 行左右,建議自己打開源碼文件看(文件路徑:src\components\Editor\MarkLine.vue)。這里不貼代碼了,主要說說原理是怎么實(shí)現(xiàn)的。

          標(biāo)線

          在頁面上創(chuàng)建 6 條線,分別是三橫三豎。這 6 條線的作用是對(duì)齊,它們什么時(shí)候會(huì)出現(xiàn)呢?

          1. 上下方向的兩個(gè)組件左邊、中間、右邊對(duì)齊時(shí)會(huì)出現(xiàn)豎線
          2. 左右方向的兩個(gè)組件上邊、中間、下邊對(duì)齊時(shí)會(huì)出現(xiàn)橫線

          具體的計(jì)算公式主要是根據(jù)每個(gè)組件的 xy 坐標(biāo)和寬度高度進(jìn)行計(jì)算的。例如要判斷 ab 兩個(gè)組件的左邊是否對(duì)齊,則要知道它們每個(gè)組件的 x 坐標(biāo);如果要知道它們右邊是否對(duì)齊,除了要知道 x 坐標(biāo),還要知道它們各自的寬度。

          a.x?==?b.x


          a.x?+?a.width?==?b.x?+?b.width

          在對(duì)齊的時(shí)候,顯示標(biāo)線。

          另外還要判斷 ab 兩個(gè)組件是否 “足夠” 近。如果足夠近,就吸附在一起。是否足夠近要靠一個(gè)變量來判斷:

          diff:?3,?

          小于等于?diff?像素則自動(dòng)吸附。

          吸附

          吸附效果是怎么實(shí)現(xiàn)的呢?

          假設(shè)現(xiàn)在有 ab 組件,a 組件坐標(biāo) xy 都是 0,寬高都是 100。現(xiàn)在假設(shè) a 組件不動(dòng),我們正在拖拽 b 組件。當(dāng)把 b 組件拖到坐標(biāo)為?x: 0, y: 103?時(shí),由于?103 - 100 <= 3(diff),所以可以判定它們已經(jīng)接近得足夠近。這時(shí)需要手動(dòng)將 b 組件的 y 坐標(biāo)值設(shè)為 100,這樣就將 ab 組件吸附在一起了。

          優(yōu)化

          在拖拽時(shí)如果 6 條標(biāo)線都顯示出來會(huì)不太美觀。所以我們可以做一下優(yōu)化,在縱橫方向上最多只同時(shí)顯示一條線。實(shí)現(xiàn)原理如下:

          1. a 組件在左邊不動(dòng),我們拖著 b 組件往 a 組件靠近。
          2. 這時(shí)它們最先對(duì)齊的是 a 的右邊和 b 的左邊,所以只需要一條線就夠了。
          3. 如果 ab 組件已經(jīng)靠近,并且 b 組件繼續(xù)往左邊移動(dòng),這時(shí)就要判斷它們倆的中間是否對(duì)齊。
          4. b 組件繼續(xù)拖動(dòng),這時(shí)需要判斷 a 組件的左邊和 b 組件的右邊是否對(duì)齊,也是只需要一條線。

          可以發(fā)現(xiàn),關(guān)鍵的地方是我們要知道兩個(gè)組件的方向。即 ab 兩個(gè)組件靠近,我們要知道到底 b 是在 a 的左邊還是右邊。

          這一點(diǎn)可以通過鼠標(biāo)移動(dòng)事件來判斷,之前在講解拖拽的時(shí)候說過,mousedown?事件觸發(fā)時(shí)會(huì)記錄起點(diǎn)坐標(biāo)。所以每次觸發(fā)?mousemove?事件時(shí),用當(dāng)前坐標(biāo)減去原來的坐標(biāo),就可以判斷組件方向。例如 x 方向上,如果?b.x - a.x?的差值為正,說明是 b 在 a 右邊,否則為左邊。

          eventBus.$emit('move',?this.$el,?currY?-?startY?>?0,?currX?-?startX?>?0)

          8. 組件屬性設(shè)置


          每個(gè)組件都有一些通用屬性和獨(dú)有的屬性,我們需要提供一個(gè)能顯示和修改屬性的地方。

          {
          ????component:?'v-text',?
          ????label:?'文字',?
          ????propValue:?'文字',?
          ????icon:?'el-icon-edit',?
          ????animations:?[],?
          ????events:?{},?
          ????style:?{?
          ????????width:?200,
          ????????height:?33,
          ????????fontSize:?14,
          ????????fontWeight:?500,
          ????????lineHeight:?'',
          ????????letterSpacing:?0,
          ????????textAlign:?'',
          ????????color:?'',
          ????},
          }

          我定義了一個(gè)?AttrList?組件,用于顯示每個(gè)組件的屬性。


          代碼邏輯很簡(jiǎn)單,就是遍歷組件的?style?對(duì)象,將每一個(gè)屬性遍歷出來。并且需要根據(jù)具體的屬性用不同的組件顯示出來,例如顏色屬性,需要用顏色選擇器顯示;數(shù)值類的屬性需要用?type=number?的 input 組件顯示等等。

          為了方便用戶修改屬性值,我使用?v-model?將組件和值綁定在一起。

          9. 預(yù)覽、保存代碼


          預(yù)覽和編輯的渲染原理是一樣的,區(qū)別是不需要編輯功能。所以只需要將原先渲染組件的代碼稍微改一下就可以了。

          "(item,?index)?in?componentData"
          ????:defaultStyle="item.style"
          ????:style="getShapeStyle(item.style,?index)"
          ????:key="item.id"
          ????:active="item?===?curComponent"
          ????:element="item"
          ????:zIndex="index"
          >
          ???????????
          ????????:is="item.component"
          ????????:style="getComponentStyle(item.style)"
          ????????:propValue="item.propValue"
          ????/>

          經(jīng)過剛才的介紹,我們知道?Shape?組件具備了拖拽、放大縮小的功能。現(xiàn)在只需要將?Shape?組件去掉,外面改成套一個(gè)普通的 DIV 就可以了(其實(shí)不用這個(gè) DIV 也行,但為了綁定事件這個(gè)功能,所以需要加上)。

          "(item,?index)?in?componentData"?:key="item.id">
          ???????????
          ????????:is="item.component"
          ????????:style="getComponentStyle(item.style)"
          ????????:propValue="item.propValue"
          ????/>

          保存代碼的功能也特別簡(jiǎn)單,只需要保存畫布上的數(shù)據(jù)?componentData?即可。保存有兩種選擇:

          1. 保存到服務(wù)器
          2. 本地保存

          在 DEMO 上我使用的?localStorage?保存在本地。

          10. 綁定事件


          每個(gè)組件有一個(gè)?events?對(duì)象,用于存儲(chǔ)綁定的事件。目前我只定義了兩個(gè)事件:

          const?events?=?{
          ????redirect(url)?{
          ????????if?(url)?{
          ????????????window.location.href?=?url
          ????????}
          ????},

          ????alert(msg)?{
          ????????if?(msg)?{
          ????????????alert(msg)
          ????????}
          ????},
          }

          const?mixins?=?{
          ????methods:?events,
          }

          const?eventList?=?[
          ????{
          ????????key:?'redirect',
          ????????label:?'跳轉(zhuǎn)事件',
          ????????event:?events.redirect,
          ????????param:?'',
          ????},
          ????{
          ????????key:?'alert',
          ????????label:?'alert?事件',
          ????????event:?events.alert,
          ????????param:?'',
          ????},
          ]

          export?{
          ????mixins,
          ????events,
          ????eventList,
          }

          不過不能在編輯的時(shí)候觸發(fā),可以在預(yù)覽的時(shí)候觸發(fā)。

          添加事件

          通過?v-for?指令將事件列表渲染出來:

          "eventActiveName">
          ????"item?in?eventList"?:key="item.key"?:label="item.label"?:>
          ????????"item.key?==?'redirect'"?v-model="item.param"?type="textarea"?placeholder="請(qǐng)輸入完整的?URL"?/>
          ????????"item.key?==?'alert'"?v-model="item.param"?type="textarea"?placeholder="請(qǐng)輸入要?alert?的內(nèi)容"?/>
          ????????"addEvent(item.key,?item.param)">確定
          ????

          選中事件時(shí)將事件添加到組件的?events?對(duì)象。

          觸發(fā)事件

          預(yù)覽或真正渲染頁面時(shí),也需要在每個(gè)組件外面套一層 DIV,這樣就可以在 DIV 上綁定一個(gè)點(diǎn)擊事件,點(diǎn)擊時(shí)觸發(fā)我們剛才添加的事件。


          handleClick()?{
          ????const?events?=?this.config.events
          ????
          ????Object.keys(events).forEach(event?=>?{
          ????????this[event](events[event])
          ????})
          }

          11. 綁定動(dòng)畫


          動(dòng)畫和事件的原理是一樣的,先將所有的動(dòng)畫通過?v-for?指令渲染出來,然后點(diǎn)擊動(dòng)畫將對(duì)應(yīng)的動(dòng)畫添加到組件的?animations?數(shù)組里。同事件一樣,執(zhí)行的時(shí)候也是遍歷組件所有的動(dòng)畫并執(zhí)行。

          為了方便,我們使用了 animate.css 動(dòng)畫庫。

          import?'@/styles/animate.css'

          現(xiàn)在我們提前定義好所有的動(dòng)畫數(shù)據(jù):

          export?default?[
          ????{
          ????????label:?'進(jìn)入',
          ????????children:?[
          ????????????{?label:?'漸顯',?value:?'fadeIn'?},
          ????????????{?label:?'向右進(jìn)入',?value:?'fadeInLeft'?},
          ????????????{?label:?'向左進(jìn)入',?value:?'fadeInRight'?},
          ????????????{?label:?'向上進(jìn)入',?value:?'fadeInUp'?},
          ????????????{?label:?'向下進(jìn)入',?value:?'fadeInDown'?},
          ????????????{?label:?'向右長(zhǎng)距進(jìn)入',?value:?'fadeInLeftBig'?},
          ????????????{?label:?'向左長(zhǎng)距進(jìn)入',?value:?'fadeInRightBig'?},
          ????????????{?label:?'向上長(zhǎng)距進(jìn)入',?value:?'fadeInUpBig'?},
          ????????????{?label:?'向下長(zhǎng)距進(jìn)入',?value:?'fadeInDownBig'?},
          ????????????{?label:?'旋轉(zhuǎn)進(jìn)入',?value:?'rotateIn'?},
          ????????????{?label:?'左順時(shí)針旋轉(zhuǎn)',?value:?'rotateInDownLeft'?},
          ????????????{?label:?'右逆時(shí)針旋轉(zhuǎn)',?value:?'rotateInDownRight'?},
          ????????????{?label:?'左逆時(shí)針旋轉(zhuǎn)',?value:?'rotateInUpLeft'?},
          ????????????{?label:?'右逆時(shí)針旋轉(zhuǎn)',?value:?'rotateInUpRight'?},
          ????????????{?label:?'彈入',?value:?'bounceIn'?},
          ????????????{?label:?'向右彈入',?value:?'bounceInLeft'?},
          ????????????{?label:?'向左彈入',?value:?'bounceInRight'?},
          ????????????{?label:?'向上彈入',?value:?'bounceInUp'?},
          ????????????{?label:?'向下彈入',?value:?'bounceInDown'?},
          ????????????{?label:?'光速從右進(jìn)入',?value:?'lightSpeedInRight'?},
          ????????????{?label:?'光速從左進(jìn)入',?value:?'lightSpeedInLeft'?},
          ????????????{?label:?'光速從右退出',?value:?'lightSpeedOutRight'?},
          ????????????{?label:?'光速從左退出',?value:?'lightSpeedOutLeft'?},
          ????????????{?label:?'Y軸旋轉(zhuǎn)',?value:?'flip'?},
          ????????????{?label:?'中心X軸旋轉(zhuǎn)',?value:?'flipInX'?},
          ????????????{?label:?'中心Y軸旋轉(zhuǎn)',?value:?'flipInY'?},
          ????????????{?label:?'左長(zhǎng)半徑旋轉(zhuǎn)',?value:?'rollIn'?},
          ????????????{?label:?'由小變大進(jìn)入',?value:?'zoomIn'?},
          ????????????{?label:?'左變大進(jìn)入',?value:?'zoomInLeft'?},
          ????????????{?label:?'右變大進(jìn)入',?value:?'zoomInRight'?},
          ????????????{?label:?'向上變大進(jìn)入',?value:?'zoomInUp'?},
          ????????????{?label:?'向下變大進(jìn)入',?value:?'zoomInDown'?},
          ????????????{?label:?'向右滑動(dòng)展開',?value:?'slideInLeft'?},
          ????????????{?label:?'向左滑動(dòng)展開',?value:?'slideInRight'?},
          ????????????{?label:?'向上滑動(dòng)展開',?value:?'slideInUp'?},
          ????????????{?label:?'向下滑動(dòng)展開',?value:?'slideInDown'?},
          ????????],
          ????},
          ????{
          ????????label:?'強(qiáng)調(diào)',
          ????????children:?[
          ????????????{?label:?'彈跳',?value:?'bounce'?},
          ????????????{?label:?'閃爍',?value:?'flash'?},
          ????????????{?label:?'放大縮小',?value:?'pulse'?},
          ????????????{?label:?'放大縮小彈簧',?value:?'rubberBand'?},
          ????????????{?label:?'左右晃動(dòng)',?value:?'headShake'?},
          ????????????{?label:?'左右扇形搖擺',?value:?'swing'?},
          ????????????{?label:?'放大晃動(dòng)縮小',?value:?'tada'?},
          ????????????{?label:?'扇形搖擺',?value:?'wobble'?},
          ????????????{?label:?'左右上下晃動(dòng)',?value:?'jello'?},
          ????????????{?label:?'Y軸旋轉(zhuǎn)',?value:?'flip'?},
          ????????],
          ????},
          ????{
          ????????label:?'退出',
          ????????children:?[
          ????????????{?label:?'漸隱',?value:?'fadeOut'?},
          ????????????{?label:?'向左退出',?value:?'fadeOutLeft'?},
          ????????????{?label:?'向右退出',?value:?'fadeOutRight'?},
          ????????????{?label:?'向上退出',?value:?'fadeOutUp'?},
          ????????????{?label:?'向下退出',?value:?'fadeOutDown'?},
          ????????????{?label:?'向左長(zhǎng)距退出',?value:?'fadeOutLeftBig'?},
          ????????????{?label:?'向右長(zhǎng)距退出',?value:?'fadeOutRightBig'?},
          ????????????{?label:?'向上長(zhǎng)距退出',?value:?'fadeOutUpBig'?},
          ????????????{?label:?'向下長(zhǎng)距退出',?value:?'fadeOutDownBig'?},
          ????????????{?label:?'旋轉(zhuǎn)退出',?value:?'rotateOut'?},
          ????????????{?label:?'左順時(shí)針旋轉(zhuǎn)',?value:?'rotateOutDownLeft'?},
          ????????????{?label:?'右逆時(shí)針旋轉(zhuǎn)',?value:?'rotateOutDownRight'?},
          ????????????{?label:?'左逆時(shí)針旋轉(zhuǎn)',?value:?'rotateOutUpLeft'?},
          ????????????{?label:?'右逆時(shí)針旋轉(zhuǎn)',?value:?'rotateOutUpRight'?},
          ????????????{?label:?'彈出',?value:?'bounceOut'?},
          ????????????{?label:?'向左彈出',?value:?'bounceOutLeft'?},
          ????????????{?label:?'向右彈出',?value:?'bounceOutRight'?},
          ????????????{?label:?'向上彈出',?value:?'bounceOutUp'?},
          ????????????{?label:?'向下彈出',?value:?'bounceOutDown'?},
          ????????????{?label:?'中心X軸旋轉(zhuǎn)',?value:?'flipOutX'?},
          ????????????{?label:?'中心Y軸旋轉(zhuǎn)',?value:?'flipOutY'?},
          ????????????{?label:?'左長(zhǎng)半徑旋轉(zhuǎn)',?value:?'rollOut'?},
          ????????????{?label:?'由小變大退出',?value:?'zoomOut'?},
          ????????????{?label:?'左變大退出',?value:?'zoomOutLeft'?},
          ????????????{?label:?'右變大退出',?value:?'zoomOutRight'?},
          ????????????{?label:?'向上變大退出',?value:?'zoomOutUp'?},
          ????????????{?label:?'向下變大退出',?value:?'zoomOutDown'?},
          ????????????{?label:?'向左滑動(dòng)收起',?value:?'slideOutLeft'?},
          ????????????{?label:?'向右滑動(dòng)收起',?value:?'slideOutRight'?},
          ????????????{?label:?'向上滑動(dòng)收起',?value:?'slideOutUp'?},
          ????????????{?label:?'向下滑動(dòng)收起',?value:?'slideOutDown'?},
          ????????],
          ????},
          ]

          然后用?v-for?指令渲染出來動(dòng)畫列表。

          添加動(dòng)畫

          "animationActiveName">
          ????"item?in?animationClassData"?:key="item.label"?:label="item.label"?:>
          ????????
          ???????????????????????????
          ????????????????v-for="(animate,?index)?in?item.children"
          ????????????????:key="index"
          ????????????????@mouseover="hoverPreviewAnimate?=?animate.value"
          ????????????????@click="addAnimation(animate)"
          ????????????>
          ????????????????"[hoverPreviewAnimate?===?animate.value?&&?animate.value?+?'?animated']">
          ????????????????????{{?animate.label?}}
          ????????????????
          ????????????
          ????????

          ????

          點(diǎn)擊動(dòng)畫將調(diào)用?addAnimation(animate)?將動(dòng)畫添加到組件的?animations?數(shù)組。

          觸發(fā)動(dòng)畫

          運(yùn)行動(dòng)畫的代碼:

          export?default?async?function?runAnimation($el,?animations?=?[])?{
          ????const?play?=?(animation)?=>?new?Promise(resolve?=>?{
          ????????$el.classList.add(animation.value,?'animated')
          ????????const?removeAnimation?=?()?=>?{
          ????????????$el.removeEventListener('animationend',?removeAnimation)
          ????????????$el.removeEventListener('animationcancel',?removeAnimation)
          ????????????$el.classList.remove(animation.value,?'animated')
          ????????????resolve()
          ????????}
          ????????????
          ????????$el.addEventListener('animationend',?removeAnimation)
          ????????$el.addEventListener('animationcancel',?removeAnimation)
          ????})

          ????for?(let?i?=?0,?len?=?animations.length;?i?????????await?play(animations[i])
          ????}
          }

          運(yùn)行動(dòng)畫需要兩個(gè)參數(shù):組件對(duì)應(yīng)的 DOM 元素(在組件使用?this.$el?獲取)和它的動(dòng)畫數(shù)據(jù)?animations。并且需要監(jiān)聽?animationend?事件和?animationcancel?事件:一個(gè)是動(dòng)畫結(jié)束時(shí)觸發(fā),一個(gè)是動(dòng)畫意外終止時(shí)觸發(fā)。

          利用這一點(diǎn)再配合?Promise?一起使用,就可以逐個(gè)運(yùn)行組件的每個(gè)動(dòng)畫了。

          12. 導(dǎo)入 PSD


          由于時(shí)間關(guān)系,這個(gè)功能我還沒做。現(xiàn)在簡(jiǎn)單的描述一下怎么做這個(gè)功能。那就是使用 psd.js 庫,它可以解析 PSD 文件。

          使用?psd?庫解析 PSD 文件得出的數(shù)據(jù)如下:

          {?children:?
          ???[?{?type:?'group',
          ???????visible:?false,
          ???????opacity:?1,
          ???????blendingMode:?'normal',
          ???????name:?'Version?D',
          ???????left:?0,
          ???????right:?900,
          ???????top:?0,
          ???????bottom:?600,
          ???????height:?600,
          ???????width:?900,
          ???????children:?
          ????????[?{?type:?'layer',
          ????????????visible:?true,
          ????????????opacity:?1,
          ????????????blendingMode:?'normal',
          ????????????name:?'Make?a?change?and?save.',
          ????????????left:?275,
          ????????????right:?636,
          ????????????top:?435,
          ????????????bottom:?466,
          ????????????height:?31,
          ????????????width:?361,
          ????????????mask:?{},
          ????????????text:?
          ?????????????{?value:?'Make?a?change?and?save.',
          ???????????????font:?
          ????????????????{?name:?'HelveticaNeue-Light',
          ??????????????????sizes:?[?33?],
          ??????????????????colors:?[?[?85,?96,?110,?255?]?],
          ??????????????????alignment:?[?'center'?]?},
          ???????????????left:?0,
          ???????????????top:?0,
          ???????????????right:?0,
          ???????????????bottom:?0,
          ???????????????transform:?{?xx:?1,?xy:?0,?yx:?0,?yy:?1,?tx:?456,?ty:?459?}?},
          ????????????image:?{}?}?]?}?],
          ????document:?
          ???????{?width:?900,
          ?????????height:?600,
          ?????????resources:?
          ??????????{?layerComps:?
          ?????????????[?{?id:?692243163,?name:?'Version?A',?capturedInfo:?1?},
          ???????????????{?id:?725235304,?name:?'Version?B',?capturedInfo:?1?},
          ???????????????{?id:?730932877,?name:?'Version?C',?capturedInfo:?1?}?],
          ????????????guides:?[],
          ????????????slices:?[]?}?}?}

          從以上代碼可以發(fā)現(xiàn),這些數(shù)據(jù)和 css 非常像。根據(jù)這一點(diǎn),只需要寫一個(gè)轉(zhuǎn)換函數(shù),將這些數(shù)據(jù)轉(zhuǎn)換成我們組件所需的數(shù)據(jù),就能實(shí)現(xiàn) PSD 文件轉(zhuǎn)成渲染組件的功能。目前 quark-h5 和 luban-h5 都是這樣實(shí)現(xiàn)的 PSD 轉(zhuǎn)換功能。

          13. 手機(jī)模式


          由于畫布是可以調(diào)整大小的,我們可以使用 iphone6 的分辨率來開發(fā)手機(jī)頁面。

          這樣開發(fā)出來的頁面也可以在手機(jī)下正常瀏覽,但可能會(huì)有樣式偏差。因?yàn)槲易远x的三個(gè)組件是沒有做適配的,如果你需要開發(fā)手機(jī)頁面,那自定義組件必須使用移動(dòng)端的 UI 組件庫。或者自己開發(fā)移動(dòng)端專用的自定義組件。

          總結(jié)


          由于 DEMO 的代碼比較多,所以在講解每一個(gè)功能點(diǎn)時(shí),我只把關(guān)鍵代碼貼上來。所以大家會(huì)發(fā)現(xiàn) DEMO 的源碼和我貼上來的代碼會(huì)有些區(qū)別,請(qǐng)不必在意。

          另外,DEMO 的樣式也比較簡(jiǎn)陋,主要是最近事情比較多,沒太多時(shí)間寫好看點(diǎn),請(qǐng)見諒。

          參考資料


          最后



          如果你覺得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我三個(gè)小忙:

          1. 點(diǎn)個(gè)「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)

          2. 歡迎加我微信「huab119」拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...

          3. 關(guān)注公眾號(hào)「前端勸退師」,持續(xù)為你推送精選好文,也可以加我為好友,隨時(shí)聊騷。


          點(diǎn)個(gè)在看支持我吧,轉(zhuǎn)發(fā)就更好了
          瀏覽 21
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  www夜片内射视频日韩精品成人 | 一级性爱视频中文字幕 | 日韩爱爱网址 | 大香蕉色色AV | v天堂最新资源在线网 |