MVVM 式的熱區元件開發

網易考拉前端團隊發表於2017-10-16

目錄

  1. 什麼是熱區
  2. 熱區功能介紹
  3. 實現手段與結構劃分
  4. 相關指令介紹
  5. 體驗增強
  6. 其它總結

1. 什麼是熱區

熱區,是指在一張圖片上選取一些區域,每個區域連結到指定的地址。

因此熱區元件的功能,就是在圖片上設定多個熱區區域並配置相應的資料。

2. 熱區功能介紹

熱區需要實現的功能為:

  1. 建立新熱區區域;
  2. 熱區大小可調整;
  3. 熱區位置可更改;
  4. 熱區資料可設定;
  5. 設定資料可顯示。

    • 試試實際效果: 戳我
    • 具體專案程式碼: 戳我

3. 實現手段與結構劃分

如果你在公司裡聽到有人在討論 MVVM,那麼話題一定離不開 RegularJS。

所以這裡以 RegularJS 為例對主題進行介紹(但若是用 Vue 在實現上區別其實也不大):

MVVM 講究以資料驅動檢視,然而熱區這類場景需要需要大量的 DOM 操作。

還好,AngularJS、VueJS 和 RegularJS 等框架都提供了自定義指令(directive,寫法及用法上大同小異),方便開發者對純 DOM 元素進行底層操作。

也避免了在節點上大量的掛載 on- 開頭的屬性:

<!-- not so good -->
<div on-keydown={this.handleKeyDown($event)}></div>

<!-- better -->
<div r-keydown></div>複製程式碼

由上可知,熱區元件重度依賴指令,則程式碼結構最終設定為:

src
 ├── assets
 │      ├── directive
 │      │     ├── addItem.js        新增熱區指令
 │      │     ├── changeSize.js     改變尺寸指令
 │      │     ├── dragItem.js       移動位置指令
 │      │     ├── resizeImg.js      圖片resize處理指令
 │      │     └── ...
 │      ├── constant.js             通用常量
 │      ├── filter.js               過濾器
 │      ├── operations.js           處理邏輯封裝
 │      └── util.js                 工具函式
 ├── components
 │      ├── modal                   資料設定模態窗元件
 │      └── zone                    熱區區域元件
 ├── mcss
 │      ├── _reset.mcss             限定作用範圍的樣式 reset
 │      └── index.mcss              基本樣式
 ├── index.js                       元件入口
 └── view.html                      元件模板複製程式碼

4. 相關指令介紹

指令一般需要返回一個函式用於指令銷燬工作。

Question: 為什麼要返回銷燬函式,而不是通過監聽 $destroy 事件來完成?

Answer: 因為指令的銷燬並不一定伴隨著元件銷燬,指令的生命週期更短,一些語法元素(if/list/include)會導致它在元件銷燬之前被重複建立和銷燬。

由於熱區元件使用了較多的事件監聽,基於上述考慮,熱區操作指令中都返回了用於 事件解綁 的函式。


新增熱區區域、拖拽熱區位置和調整熱區大小指令,都存在三個操作階段:

  1. mousedown: 滑鼠選中;
  2. mousemove: 移動滑鼠,此時通過 js 動態改變元素的樣式,但並不對真實資料做修改;
  3. mouseup: 釋放滑鼠,將改動儲存到真實資料中。
import { dom } from 'regularjs';

export default function (content) {

    dom.on(content, 'mousedown', handleMouseDown);

    function handleMouseDown(e) {
        // ...

        dom.on(window, 'mousemove', handleChange);
        dom.on(window, 'mouseup', handleMouseUp);

        function handleChange(e) {
            // ...
        };

        function handleMouseUp() {
            // ...

            dom.off(window, 'mousemove', handleChange);
            dom.off(window, 'mouseup', handleMouseUp);
        };
    }

    return () => {
        // 解綁 mousedown 事件
        dom.off(content, 'mousedown', handleMouseDown);
    };
}複製程式碼

這裡可以做些優化:

  1. 在 changeSize 時對可拖拽點的事件監聽上,利用「事件委託」+「自定義屬性」減少事件繫結,並統一處理不同方位的拖拽點:

    dragpoint
    dragpoint
    如圖所示,熱區區域周圍的八個小方塊就是「拖拽點」。

    <ul r-changeSize>
        <li class="hz-u-square hz-u-square-tl" data-pointer="dealTL"></li>
        <li class="hz-u-square hz-u-square-tc" data-pointer="dealTC"></li>
        <li class="hz-u-square hz-u-square-tr" data-pointer="dealTR"></li>
        <li class="hz-u-square hz-u-square-cl" data-pointer="dealCL"></li>
        <li class="hz-u-square hz-u-square-cr" data-pointer="dealCR"></li>
        <li class="hz-u-square hz-u-square-bl" data-pointer="dealBL"></li>
        <li class="hz-u-square hz-u-square-bc" data-pointer="dealBC"></li>
        <li class="hz-u-square hz-u-square-br" data-pointer="dealBR"></li>
    </ul>複製程式碼
    function handleMouseDown(e) {
        // 獲取選中節點的自定義屬性值
        let pointer = e.target.dataset.pointer;
        if(!pointer) {
            return;
        }
        e && e.stopPropagation();
    
        dom.on(window, 'mousemove', handleChange);
        dom.on(window, 'mouseup', handleMouseUp);
    
        function handleChange(e) {
            e && e.preventDefault();
    
            // 處理選中不同拖拽點時的情況
            let styleInfo = operations[pointer](itemInfo, moveX, moveY);
    
            // 邊界值處理
            itemInfo = operations.dealEdgeValue(itemInfo, styleInfo, container);
    
            // ...
        }
        function handleMouseUp() {
            // ...
    
            dom.off(window, 'mousemove', handleChange);
            dom.off(window, 'mouseup', handleMouseUp);
        }
    };複製程式碼

    統一封裝 operations 處理邏輯:

    export default {
        /**
         * 改變熱區大小時的邊界情況處理
         * @param {Object} itemInfo   實際使用的熱區模組資料 
         * @param {Object} styleInfo  操作中的熱區模組資料
         * @param {Object} container  圖片區域的寬高資料
         */
        dealEdgeValue(itemInfo, styleInfo, container) {},
        /**
         * 處理不同的拖拽點,大寫字母表示含義:T-top,L-left,C-center,R-right,B-bottom
         * @param  {Object} itemInfo 
         * @param  {Number} moveX 
         * @param  {Number} moveY
         * @return {Object} 對過程資料進行處理
         */
        dealTL(itemInfo, moveX, moveY, minLimit = MIN_LIMIT) {},
        dealTC(itemInfo, moveX, moveY, minLimit = MIN_LIMIT) {},
        dealTR(itemInfo, moveX, moveY, minLimit = MIN_LIMIT) {},
        dealCL(itemInfo, moveX, moveY, minLimit = MIN_LIMIT) {},
        dealCR(itemInfo, moveX, moveY, minLimit = MIN_LIMIT) {},
        dealBL(itemInfo, moveX, moveY, minLimit = MIN_LIMIT) {},
        dealBC(itemInfo, moveX, moveY, minLimit = MIN_LIMIT) {},
        dealBR(itemInfo, moveX, moveY, minLimit = MIN_LIMIT) {}
    };複製程式碼
  2. 在拖拽移動熱區位置時,使用 translate 替代直接改變 top && left 值避免重繪,實現更流暢的拖拽效果。

    // bad
    dom.css(elem, {
        top: `${moveY}px`,
        left: `${moveX}px` 
    });
    
    // better
    dom.css(elem, {
        transform: `translate(${moveX}px, ${moveY}px)` 
    });複製程式碼
  3. 熱區尺寸單位用 % 取代 px,不同螢幕尺寸的使用者都獲得更好的熱區操作體驗。

    但熱區區域存在最小尺寸限制,需要利用 element-resize-detector 對圖片進行監聽,在圖片尺寸變化時對邊界區域做相容。

    import elementResizeDetectorMaker from 'element-resize-detector';
    const erd = elementResizeDetectorMaker();
    
    export default function resizeImg(elem) {
        // ...
    
        const resize = _.debounce(() => {
            // ...
        }, 500);
    
        erd.listenTo(elem, resize);
    
        return () => {
            erd.removeListener(elem, resize);
        };
    };複製程式碼
  4. 通過計算屬性動態修改設定資料的 hover 位置,確保不超過圖片範圍,以確保資訊的正確顯示。

    <ul r-style={{top: infoTop, bottom: infoBottom, left: infoLeft, right: infoRight, transform: infoTransform}}></ul>複製程式碼

5. 體驗增強

  1. 使用透明小方塊增強滑鼠從熱區區域移動到設定資訊區域的體驗:

    hideblock
    hideblock
    (這裡用灰色標識一下小方塊)

  2. 雙擊熱區區域彈出資訊設定模態框時,利用 r-autofocus 指令自動聚焦:

    modal
    modal

    同時繫結鍵盤監聽事件,監聽 Enter 鍵和 Esc 鍵,增強「確認」和「取消」體驗。

6. 其他總結

  1. 無論 reset(容易遺漏)或基礎樣式,都需要限制 scope,避免名稱空間汙染;
  2. 統一控制 Constant 全域性常量,如最小熱區尺寸限制。

相關文章