介紹

求關注,求收藏,謝謝!
之前負責了我司的幾個后臺管理型的專案的迭代和新需求開發,第一次以前端負責人的身份參與專案,從技術方案的選型到需求的迭代,再到作業包的分配,組件的拆分,代碼的審核,最后的提測、改BUG,雖然有點累,但確實接觸到了很多以前不曾接觸到的東西,漲了很多見識,當然也見識到了一些嘆為觀止的騷操作…
今天要說的是組件開發中的一項,我司的專案技術堆疊主要是圍繞著Vue的體系開發的,UI框架基本上不是IView就是Element,且這兩者都有其自帶的基礎組件:彈窗組件,但是從需求的角度出發,這兩者的彈窗組件其實都不能滿足業務的開發,比如,我司的需求中有一項比較常用但是組件又沒有的功能,拖動,(IView可以拖動,但是設定拖動屬性會強制將遮罩移除,這個問題直到4.6.0版本才改為不強制,而我司的iView又比較早,升級是不可能升級了);
?
考慮到其他模塊中也有使用到了拖動這個功能,且在Vue中DOM的操作能和自定義指令非常完美契合,最終決定使用自定義指令實作該功能,并且該功能會被作為一個基礎組件放入專案;
效果圖
話不多說,直接先看最終的效果圖,后臺模版借用的是IVew的,這個無所謂,主要是彈窗:

大致效果其實就是這樣,挺簡單的,這部分的用法就是我們指定了只有按住title部分才可以觸發拖動,且拖動的時候是整個彈窗都會被拖動;
?
下載
其實我們已經積累了一部分常用的自定義指令作為后臺模版的基礎建設,比如v-loading,以及這個v-move之類的,如果有需要的話,可以看看后面怎么辦,這里先暫時上傳了v-move的相關開發版的代碼,有需要的小伙伴可以下載看看:v-move自定義指令,如果不能下載請及時留言告知,謝謝;
?
流程圖
既然是一個自定義的指令,并且該指令具有一定的通用性,那么我們就必須要進行一些簡單的設計,畢竟在開發前我們就想的很周到的話,那么不是就可以輕松很多,并且后期有人接手維護也能有一定的參考,不是啥資料都沒有,大致流程圖如下:

代碼實作
其實整個代碼的實作并不復雜,拖動相關的功能說到底就是一個跟蹤滑鼠移動實時改變被拖DOM的x軸和y軸坐標的程序,坐標改變了,那么就相當于被拖的DOM拖動了
?
基礎知識
閱讀一下功能模塊需要對vue的自定義指令有一些基礎認知,具體可以直接看官網的說明:Vue自定義指令,簡單的說自定義指令就是Vue允許通過自定義指令來對DOM元素進行一些相關操作;
接下來,就按照流程圖的順序,逐一開發相關代碼
合法引數驗證
在這個階段,我們需要驗證自定義指令的系結物件,先直接看代碼:
/**
* 初始化階段
* @param {HTMLElement} el 待待系結的元素
* @param {Object} binding Vue binding集合
*/
inserted(el, binding) {
// 容錯判斷,排除非DOM
if (!(el instanceof HTMLElement)) {
console.error("型別錯誤,系結物件必須是HTMLElement");
return false;
}
},
我們知道,自定義指令也是有鉤子函式的,示例中使用的是**inserted()**這個鉤子函式,這個函式表示:被系結元素插入父節點時呼叫 (僅保證父節點存在,但不一定已被插入檔案中),并且這個函式存在兩個引數,這里我們用到了第一個:
- el:代表當前被系結的DOM元素;
?
既然是驗證,那么我們肯定要驗證一下這個el是不是HTMLElement型別,只要是這個型別,那么就代表這個自定義指令系結的物件是DOM元素;
?
合并動態引數
在這一步,我們需要將用戶自定義的動態引數和v-move中預設好的默認引數進行合并,合并的規則是:如果用戶設定了引數那么用戶設定的優先,如果沒有設定那么就啟用默認引數,比如,我們默認拖動的DOM和實際移動的DOM是同一個,但彈窗就相對比較特殊,彈窗是拖動的是title部分,移動的確實整個彈窗整體;
/**
* 處理動態引數
* @param {String | Obejct} params 動態引數原始資料
* @returns {Obejct} 期望的動態引數物件
*/
export function handleParams(params) {
const data = {};
Object.assign(data, DEFAULT_CONFIG);
// 動態引數型別
switch (commom.getType(params)) {
// 字串
case "[object String]":
data.move = params;
data.click = params;
break;
// 物件
case "[object Object]":
for (let item in data) {
if (!Object.prototype.hasOwnProperty.call(data, item)) continue;
// 容錯,判斷是否為undefined
data[item] = commom.isUndefined(params[item])
? data[item]
: params[item];
}
break;
default:
// 啟用默認引數
break;
}
return data;
}
在這里,我們允許用戶輸入的動態引數型別有兩種,一種是字串型別,一種是物件型別,除此之外的所有型別都會被認為是非法型別:
- 字串:那么我們就認為拖動和實際移動的DOM是同一種;
- 物件:根據key進行一次遍歷,并逐一對key對應的值進行覆寫;
?
最后會得到一個全新的合法的動態引數,并回傳;
?
合并修飾符
動態引數合并完了,那么緊接著就是合并修飾符,和動態引數一樣的規則:用戶設定優先,如果沒有設定,啟動默認修飾符,這一塊主要合并的是一些比如是否可以超屏拖動的引數,比如設定:
// 允許超屏拖動
<div v-move.all></div>
開啟超屏拖動后,那么被拖動的DOM邊界可以超出螢屏之外,這個功能是什么用處呢?具體舉例一下,比如:實際專案中圖片預覽功能,某張圖片被放大后,因為視角不方便,需要將圖片中待閱讀的部分拖動到螢屏中央,因為圖片放大后肯定有部分會被移出螢屏,因此就需要超屏拖動了,如果不開啟,那么被拖動的DOM永遠只能在螢屏內部拖動;
具體引數合并代碼如下:
/**
* 處理修飾符
* @param {Object} modifiers 修飾符原始資料
* @returns {Object} 期望的修飾符物件
*/
export function handleModifiers(modifiers) {
// 容錯判斷,排除非物件
if (!commom.isObj(modifiers)) {
console.error("型別錯誤,修飾符必須是物件型別");
return false;
}
return {
dirPosition: ABSOLUTE,
dirAll: modifiers.all ? ALL : null,
};
}
挺簡單的代碼,首先是判斷了修飾符引數是否是一個物件,做了一個簡單的容錯,其實這里不對,如果修飾符如果設定例外,那么回傳的應該是默認引數,而不是false,這里有問題;
?
拖動驗證
肯定有小伙伴覺得奇怪,為什么會有一個拖動驗證,很簡單,有時候這個拖動功能也是需要開啟的,比如,有時候我們希望這個DOM默認是不可以拖動的,只有當開啟了某個開關后才能拖動,用法示例:
// isMove判斷當前拖動是否生效,true-可拖動,false-不可拖動
<div v-move.all="isMove"></div>
代碼上的話,我們只需要對這個value值做一個判斷即可,如果vlaue值是false,直接回傳,不給走代碼就行了
// false-不可拖動
if (commom.isBol(binding.value) && !binding.value) {
return false;
}
初始化坐標
這一步中,主要的作用就是初始化DOM的坐標,一是我們得獲取到當前DOM現有的坐標,二是初始化一些下面步驟中需要使用到的變數
// 坐標,這個是初始化下面步驟需要的變數
let x = 0,
y = 0;
let offsetLeft = 0,
offsetRight = 0;
具體初始化的時機就在于我們滑鼠按下的那一刻,在那一刻,我們可以獲取到當前被拖動DOM的初始偏移量,我們需要將這些偏移量保存下來做計算:
// 觸發滑鼠左擊
dom.click.onmousedown = function(event) {
// 阻止默認事件和冒泡
event.preventDefault();
event.stopPropagation();
// 設定初始坐標
x = event.pageX;
y = event.pageY;
// 設定初始偏移位置
offsetLeft = dom.move.offsetLeft;
offsetRight = dom.move.offsetTop;
onMove = true;
};
?
計算坐標
在計算坐標這一部分,一共有四個坐標需要計算:
- x軸上的最小坐標,最大坐標
- y軸上的最小坐標,最大坐標
當我們拖動DOM的時候,需要實時計算出對應的坐標
最小值計算
/**
* 計算在X軸/Y軸可拖動的最小值
* @param {Obejct} modifiers 修飾符
* @param {String} position X軸/Y軸
* @param {HTMLElement} el
* @returns {Number} 可拖動的最小距離值
*/
export function computedMin(modifiers, position, el) {
// 容錯判斷,排除非物件的修飾符
if (!commom.isObj(modifiers)) {
console.error("型別錯誤,修飾符必須是物件型別");
return false;
}
// 容錯判斷,排除非DOM
if (!(el instanceof HTMLElement)) {
console.error("型別錯誤,系結物件必須是HTMLElement");
return false;
}
// 獲得x軸最小值
const minXDistance = parseInt(window.getComputedStyle(el).width);
// 獲得Y軸最小值
const minYDistance = parseInt(window.getComputedStyle(el).height);
// 超屏拖動
if (modifiers.dirAll === ALL) {
return position === "width" ? -minXDistance : -minYDistance;
}
// 非超屏拖動
else {
return position === "width" ? 0 : 0;
}
}
因為x軸和y軸上都存在最小值的計算,因此就通用了一下
?
最大值計算
/**
* 計算在X軸/Y軸可拖動的最大值
* @param {Object} modifiers 修飾符
* @param {String} position X軸-width,Y軸-height
* @param {HTMLElement} el 系結的元素
* @returns {Number} 可拖動的最大距離值
*/
export function computedMax(modifiers, position, el) {
// 容錯判斷,排除非物件
if (!commom.isObj(modifiers)) {
console.error("型別錯誤,修飾符必須是物件型別");
return false;
}
// 容錯判斷,排除非DOM
if (!(el instanceof HTMLElement)) {
console.error("型別錯誤,系結物件必須是HTMLElement");
return false;
}
// 無上級元素
if (!el.parentNode) {
return 0;
}
// 獲得X軸最大值
const maxXDistance = parseInt(window.getComputedStyle(el).width);
// 獲得Y軸最大值
const maxYDistance = parseInt(window.getComputedStyle(el).height);
// 超屏拖動
if (modifiers.dirAll === ALL) {
return position === "width"
? el.parentNode.clientWidth + maxXDistance
: el.parentNode.clientHeight + maxYDistance;
}
// 非超屏拖動
else {
return position === "width"
? el.parentNode.clientWidth - maxXDistance
: el.parentNode.clientHeight - maxYDistance;
}
}
同理,最大值的計算也存在x軸和y軸上的,所以也通用了一下
?
這樣在這一步中,我們就得到了四個坐標,分別是:x軸的最小值和最大值,y軸上的最小值和最大值;
?
設定坐標
在這一步就是對DOM進行坐標設定了,我們需要實時修改DOM的left和top的值,達到DOM的位置的變化,當然,這部分代碼需要寫在mousemove中,以及滑鼠松開時,移除拖動事件
// 觸發滑鼠拖動
document.onmousemove = function(e) {
// 判斷控制閥確認是否可拖動
if (!onMove) return false;
// 初始化位置坐標
let initMouseX = e.pageX;
let initMouseY = e.pageY;
// 初始化偏移坐標
let offsetX = initMouseX - (x - offsetLeft);
let offsetY = initMouseY - (y - offsetRight);
// 獲得X軸最大值
let maxX = computedMax(modifiers, "width", dom.move);
// 獲得X軸最小值
let minX = computedMin(modifiers, "width", dom.move);
// 獲得Y軸最大值
let maxY = computedMax(modifiers, "height", dom.move);
// 獲得Y軸最小值
let minY = computedMin(modifiers, "height", dom.move);
// 拖動后坐標
offsetX = offsetX > maxX ? maxX : offsetX < minX ? minX : offsetX;
offsetY = offsetY > maxY ? maxY : offsetY < minY ? minY : offsetY;
// 設定坐標
dom.move.style.left = offsetX + "px";
dom.move.style.top = offsetY + "px";
};
// 觸發滑鼠左擊釋放
document.onmouseup = function() {
// 關閉控制閥
onMove = false;
// 清空狀態
document.onmousemove = document.onmouseup = null;
// 非函式時回傳,函式時觸發函式
if (!commom.isFunction(binding.value)) {
return false;
}
binding.value.call(this);
};
當然,這里還有一個小細節,就是如果我們的value值是一個函式的時候,那么松開滑鼠的時候需要觸發函式
?
代碼
以下為v-move的主體代碼,有興趣的小伙伴可以試下
/**
* @Description 本指令應用于設定DOM元素為可拖動狀態,具體用法見檔案
*/
// 引入工具函式
import commom from "../../Utils/common";
// dom的position值
const ABSOLUTE = "absolute";
// 是否可以超屏拖動
const ALL = "all";
/**
* 處理修飾符
* @param {Object} modifiers 修飾符原始資料
* @returns {Object} 期望的修飾符物件
*/
export function handleModifiers(modifiers) {
// 容錯判斷,排除非物件
if (!commom.isObj(modifiers)) {
console.error("型別錯誤,修飾符必須是物件型別");
return false;
}
return {
dirPosition: ABSOLUTE,
dirAll: modifiers.all ? ALL : null,
};
}
// 默認引數
const DEFAULT_CONFIG = {
move: "",
click: "",
};
/**
* 處理動態引數
* @param {String | Obejct} params 動態引數原始資料
* @returns {Obejct} 期望的動態引數物件
*/
export function handleParams(params) {
const data = {};
Object.assign(data, DEFAULT_CONFIG);
// 動態引數型別
switch (commom.getType(params)) {
// 字串
case "[object String]":
data.move = params;
data.click = params;
break;
// 物件
case "[object Object]":
for (let item in data) {
if (!Object.prototype.hasOwnProperty.call(data, item)) continue;
// 容錯,判斷是否為undefined
data[item] = commom.isUndefined(params[item])
? data[item]
: params[item];
}
break;
default:
// 啟用默認引數
break;
}
return data;
}
/**
* 計算在X軸/Y軸可拖動的最大值
* @param {Object} modifiers 修飾符
* @param {String} position X軸-width,Y軸-height
* @param {HTMLElement} el 系結的元素
* @returns {Number} 可拖動的最大距離值
*/
export function computedMax(modifiers, position, el) {
// 容錯判斷,排除非物件
if (!commom.isObj(modifiers)) {
console.error("型別錯誤,修飾符必須是物件型別");
return false;
}
// 容錯判斷,排除非DOM
if (!(el instanceof HTMLElement)) {
console.error("型別錯誤,系結物件必須是HTMLElement");
return false;
}
// 無上級元素
if (!el.parentNode) {
return 0;
}
// 獲得X軸最大值
const maxXDistance = parseInt(window.getComputedStyle(el).width);
// 獲得Y軸最大值
const maxYDistance = parseInt(window.getComputedStyle(el).height);
// 超屏拖動
if (modifiers.dirAll === ALL) {
return position === "width"
? el.parentNode.clientWidth + maxXDistance
: el.parentNode.clientHeight + maxYDistance;
}
// 非超屏拖動
else {
return position === "width"
? el.parentNode.clientWidth - maxXDistance
: el.parentNode.clientHeight - maxYDistance;
}
}
/**
* 計算在X軸/Y軸可拖動的最小值
* @param {Obejct} modifiers 修飾符
* @param {String} position X軸/Y軸
* @param {HTMLElement} el
* @returns {Number} 可拖動的最小距離值
*/
export function computedMin(modifiers, position, el) {
// 容錯判斷,排除非物件的修飾符
if (!commom.isObj(modifiers)) {
console.error("型別錯誤,修飾符必須是物件型別");
return false;
}
// 容錯判斷,排除非DOM
if (!(el instanceof HTMLElement)) {
console.error("型別錯誤,系結物件必須是HTMLElement");
return false;
}
// 獲得x軸最小值
const minXDistance = parseInt(window.getComputedStyle(el).width);
// 獲得Y軸最小值
const minYDistance = parseInt(window.getComputedStyle(el).height);
// 超屏拖動
if (modifiers.dirAll === ALL) {
return position === "width" ? -minXDistance : -minYDistance;
}
// 非超屏拖動
else {
return position === "width" ? 0 : 0;
}
}
/**
* 獲得DOM
* @param {String} className dom的類名
* @param {HTMLElement} el DOM
* @returns {HTMLElement} 獲得的DOM
*/
export function getDOM(className, el) {
// 容錯判斷,排除非DOM
if (!(el instanceof HTMLElement)) {
console.error("型別錯誤,系結物件必須是HTMLElement");
return false;
}
// 容錯判斷,排除非字串
if (!commom.isString(className)) {
return el;
}
// 存在重復命名DOM時,僅取第一個
const domArr = el.getElementsByClassName(`${className}`);
switch (domArr.length) {
case 1:
return domArr[0];
default:
return el;
}
}
/**
* 執行拖動事件
* @param {HTMLElement} el 可拖動的DOM
* @param {Object} binding 引數集合
* @returns {Boolean} true-成功 false-失敗
*/
export function handleMove(el, binding) {
// 容錯判斷,排除非DOM
if (!(el instanceof HTMLElement)) {
console.error("型別錯誤,系結物件必須是HTMLElement");
return false;
}
// 獲得動態引數
const arg = handleParams(binding.arg);
// 可操作物件操作
const dom = {
// 拖動元素
move: getDOM(arg.move, el),
// 點擊元素
click: getDOM(arg.click, el),
};
// 獲得修飾符
const modifiers = handleModifiers(binding.modifiers);
// 坐標
let x = 0,
y = 0;
let offsetLeft = 0,
offsetRight = 0;
// 控制閥
let onMove = false;
// 初始化事件
document.onmousemove = document.onmouseup = dom.click.onmousedown = null;
// 設定滑鼠狀態
dom.click.style.cursor = "default";
// false-不可拖動
if (commom.isBol(binding.value) && !binding.value) {
return false;
}
// 初始化拖動物件的滑鼠狀態及position型別
dom.click.style.cursor = "move";
dom.move.style.position = modifiers.dirPosition;
// 觸發滑鼠左擊
dom.click.onmousedown = function(event) {
// 阻止默認事件和冒泡
event.preventDefault();
event.stopPropagation();
// 設定初始坐標
x = event.pageX;
y = event.pageY;
// 設定初始偏移位置
offsetLeft = dom.move.offsetLeft;
offsetRight = dom.move.offsetTop;
onMove = true;
// 觸發滑鼠拖動
document.onmousemove = function(e) {
// 判斷控制閥確認是否可拖動
if (!onMove) return false;
// 初始化位置坐標
let initMouseX = e.pageX;
let initMouseY = e.pageY;
// 初始化偏移坐標
let offsetX = initMouseX - (x - offsetLeft);
let offsetY = initMouseY - (y - offsetRight);
// 獲得X軸最大值
let maxX = computedMax(modifiers, "width", dom.move);
// 獲得X軸最小值
let minX = computedMin(modifiers, "width", dom.move);
// 獲得Y軸最大值
let maxY = computedMax(modifiers, "height", dom.move);
// 獲得Y軸最小值
let minY = computedMin(modifiers, "height", dom.move);
// 拖動后坐標
offsetX = offsetX > maxX ? maxX : offsetX < minX ? minX : offsetX;
offsetY = offsetY > maxY ? maxY : offsetY < minY ? minY : offsetY;
// 設定坐標
dom.move.style.left = offsetX + "px";
dom.move.style.top = offsetY + "px";
};
// 觸發滑鼠左擊釋放
document.onmouseup = function() {
// 關閉控制閥
onMove = false;
// 清空狀態
document.onmousemove = document.onmouseup = null;
// 非函式時回傳,函式時觸發函式
if (!commom.isFunction(binding.value)) {
return false;
}
binding.value.call(this);
};
};
return true;
}
export default {
// 指令名稱
name: "tc-move",
// 指令
directive: {
/**
* 初始化階段
* @param {HTMLElement} el 待待系結的元素
* @param {Object} binding Vue binding集合
* @returns {Boolean} true-成功 false-失敗
*/
inserted(el, binding) {
// 容錯判斷,排除非DOM
if (!(el instanceof HTMLElement)) {
console.error("型別錯誤,系結物件必須是HTMLElement");
return false;
}
return handleMove(el, binding);
},
/**
* 更新階段
* @param {HTMLElement} el 待待系結的元素
* @param {Object} binding Vue binding集合
* @returns {Boolean} true-成功 false-失敗
*/
update(el, binding) {
// 容錯判斷,排除非DOM
if (!(el instanceof HTMLElement)) {
console.error("型別錯誤,系結物件必須是HTMLElement");
return false;
}
return handleMove(el, binding);
},
},
};
小結
本文簡單講述了一個拖動自定義指令的實作,步驟上主要分為:動態引數,修飾符的合并,合并規則是優先用戶設定,如果用戶沒有設定引數那么就啟用默認引數,之后就是實時計算拖動后的坐標,并修改DOM的left和top的值以達到拖動的效果;
?
至此簡單的v-move就實作了,之后在專案中注冊指令就可以直接使用了;
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/395360.html
標籤:其他
上一篇:成功解決AttributeError: ‘DataFrame‘ object has no attribute ‘tolist‘
