目錄
- 什麼是熱區
- 熱區功能介紹
- 實現手段與結構劃分
- 相關指令介紹
- 體驗增強
- 其它總結
1. 什麼是熱區
熱區,是指在一張圖片上選取一些區域,每個區域連結到指定的地址。
因此熱區元件的功能,就是在圖片上設定多個熱區區域並配置相應的資料。
2. 熱區功能介紹
熱區需要實現的功能為:
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
)會導致它在元件銷燬之前被重複建立和銷燬。
由於熱區元件使用了較多的事件監聽,基於上述考慮,熱區操作指令中都返回了用於 事件解綁 的函式。
新增熱區區域、拖拽熱區位置和調整熱區大小指令,都存在三個操作階段:
- mousedown: 滑鼠選中;
- mousemove: 移動滑鼠,此時通過 js 動態改變元素的樣式,但並不對真實資料做修改;
- 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);
};
}複製程式碼
這裡可以做些優化:
在 changeSize 時對可拖拽點的事件監聽上,利用「事件委託」+「自定義屬性」減少事件繫結,並統一處理不同方位的拖拽點:
如圖所示,熱區區域周圍的八個小方塊就是「拖拽點」。<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) {} };複製程式碼
在拖拽移動熱區位置時,使用 translate 替代直接改變 top && left 值避免重繪,實現更流暢的拖拽效果。
// bad dom.css(elem, { top: `${moveY}px`, left: `${moveX}px` }); // better dom.css(elem, { transform: `translate(${moveX}px, ${moveY}px)` });複製程式碼
熱區尺寸單位用 % 取代 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); }; };複製程式碼
通過計算屬性動態修改設定資料的 hover 位置,確保不超過圖片範圍,以確保資訊的正確顯示。
<ul r-style={{top: infoTop, bottom: infoBottom, left: infoLeft, right: infoRight, transform: infoTransform}}></ul>複製程式碼
5. 體驗增強
使用透明小方塊增強滑鼠從熱區區域移動到設定資訊區域的體驗:
雙擊熱區區域彈出資訊設定模態框時,利用 r-autofocus 指令自動聚焦:
同時繫結鍵盤監聽事件,監聽 Enter 鍵和 Esc 鍵,增強「確認」和「取消」體驗。
6. 其他總結
- 無論 reset(容易遺漏)或基礎樣式,都需要限制 scope,避免名稱空間汙染;
- 統一控制 Constant 全域性常量,如最小熱區尺寸限制。