使用vue開發過幾個專案了,對vue感覺很好,不愧為開源世界華人的驕傲(順便膜拜下尤大~~ )
Vue是一個資料驅動頁面的一個框架,他的雙向繫結原理使我們開發頁面更簡單
總結起來的幾大特點:1.簡潔2.輕量3.快速4.資料驅動5.模組友好6.元件化
在這整理一篇vue的醜陋版的下拉框元件(因為還沒加更加詳細的功能),用作學習vue的筆記
開發元件的設計原則:(摘自elementUI)
– 一致性 Consistency
與現實生活一致:與現實生活的流程、邏輯保持一致,遵循使用者習慣的語言和概念;
在介面中一致:所有的元素和結構需保持一致,比如:設計樣式、圖示和文字、元素的位置等。
– 反饋 Feedback
控制反饋:通過介面樣式和互動動效讓使用者可以清晰的感知自己的操作;
頁面反饋:操作後,通過頁面元素的變化清晰地展現當前狀態。
– 效率 Efficiency
簡化流程:設計簡潔直觀的操作流程;
清晰明確:語言表達清晰且表意明確,讓使用者快速理解進而作出決策;
幫助使用者識別:介面簡單直白,讓使用者快速識別而非回憶,減少使用者記憶負擔。
– 可控 Controllability
使用者決策:根據場景可給予使用者操作建議或安全提示,但不能代替使用者進行決策;
結果可控:使用者可以自由的進行操作,包括撤銷、回退和終止當前操作等。
不僅要遵循以上原則,還必須要禁用vue全家桶,元件要遵循低耦合性
以下為發開下拉框的幾個步驟:
1. 安裝vue腳手架,官網上有詳細的說明,略過…
我沒有用單元測試,直接將元件上到app.vue中了
2.按照elementui的元件寫法寫好APP.vue等待開發
<template>
<div id="app">
<div class="main">
<my-dropdown @command="handleCommand">
<my-button>
{{smyectedValue}}<i class="my-icon-arrow-down"></i>
</my-button>
<my-dropdown-menu slot="dropdown">
<my-dropdown-item v-for="(item,i) in dropdownData" :key="i" :command="item.id">{{item.value}}</my-dropdown-item>
</my-dropdown-menu>
</my-dropdown>
</div>
</div>
</template>
<script>
import MyButton from "./components/button";
import MyDropdown from "./components/dropdown";
import MyDropdownMenu from "./components/dropdown-menu";
import myDropdownItem from "./components/dropdown-item";
export default {
name: "App",
components: { MyButton, MyDropdown, MyDropdownMenu, myDropdownItem },
data() {
return {
smyectedValue: "請選擇",
dropdownData: [
{ id: 1000, value: "我是選項一" },
{ id: 1001, value: "我是選項二" },
{ id: 1002, value: "我是選項三" },
{ id: 1003, value: "我是選項四" },
{ id: 1004, value: "我是選項五" }
]
};
},
methods:{
handleCommand(command){
console.log(`我被點選了,command為${command}`);
this.dropdownData.every(ele=>{
if(command == ele.id){
this.smyectedValue = ele.value
return false
}
return true
})
}
}
};
</script>
<style>
#app {
font-family: "Avenir", Hmyvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
.main {}
</style>
複製程式碼
3.寫入button.vue、dropdown.vue、dropdown-ment.vue、dropdown-item.vue初始元素架構,準備開發了
因檔案太多,就不貼程式碼了,放上GitHub的開發日誌連結
button.vue
dropdown-item.vue
dropdown-menu.vue
dropdown.vue
現在基本已經有一個大致的內容了
4.在dropdown.vue中先給按鈕加上點選事件,可以正確列印出顯示隱藏的狀態
init() {
this.buttonEl = this.$slots.default[0].elm; //元件按鈕
this.dropdownEl = this.$slots.dropdown[0].elm; //元件下拉框
},
initEvent() {
let { buttonEl, dropdownEl } = this;
buttonEl.addEventListener("click", this.handleClick); //設定按鈕點選顯示隱藏
},
handleClick() {
this.visible ? this.hide() : this.show();
},
hide() {
console.log("hide");
this.visible = false;
},
show() {
console.log("show");
this.visible = true;
}
複製程式碼
5.通過dropdown.vue將button與dropdown-item.vue連通起來,實現元件間的通訊
具體做法為
1.在dropdown中為button繫結click事件,設定下拉元件的visible為true和false,通過Vue的watch鉤子來監聽visible的變化來向子元件dropdown-menu.vue來emit一個事件,在dropdown-menu.vue生命週期中只要事先繫結好了這個事件,就會接收到事件反饋和引數傳遞,通過引數的值來判斷是否顯示和隱藏menu,方法見(broadcast與dispatch)
//這是dropdown.vue
methods: {
//...
/**
* @description 遞迴遍歷當前元件下面所有與元件名稱相匹配的元件,並觸發目標事件並傳參
* @param {Component} target 當前元件
* @param {String} componentName 元件名稱
* @param {String} eventName 需要觸發的事件名稱
* @param {any[]} params 需要傳遞的引數
* @return {void}
*/
broadcast(children, componentName, eventName, params) {
let me = this;
children.forEach(function(child) {
var name = child.$options.componentName;
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
me.broadcast(child.$children, componentName, eventName, params);
}
});
}
},
watch: {
visible(val) {
console.log(`即將為${val ? "顯示" : "隱藏"}狀態`);
this.broadcast(this.$children, "MyDropdownMenu", "visible", val);
}
},
//這是dropdown-menu.vue
methods:{
//...
initEvent(){
this.$on(`visible`,val=>{
console.log(`現在為${val?`顯示`:`隱藏`}狀態`)
this.showPopper = val
})
}
}
複製程式碼
2.在dropdown.vue中繫結一個item中的自定義點選事件,當觸發該方法是隱藏整個menu
這兩個方法的關鍵點在父子元件之間的通訊),整個下拉元件四個子元件的通訊實現
//dropdown-item.vue
methods: {
handleClick() {
this.dispatch(this.$parent, "MyDropdown", "menu-item-click", [
this.command,
this
]);
},
/**
* @description 逆向尋找當前元件的父元件,然後emit事件並傳遞引數
* @param {Component} target 當前元件
* @param {String} componentName 元件名稱
* @param {String} eventName 需要觸發的事件名稱
* @param {any[]} params 需要傳遞的引數
* @return {void}
*/
dispatch(target, componentName, eventName, params) {
let name;
if( !(name = parent.$options.componentName))return
if (name === componentName) {
target.$emit.apply(target, [eventName].concat(params));
} else {
this.dispatch(target.$parent, componentName, eventName, params);
}
}
//dropdown.vue
methods: {
//...
initEvent() {
//...
this.$on(`menu-item-click`, this.handleMenuItemClick); //註冊下拉選單點選事件
},
},
複製程式碼
6.因為下拉框受父元素的高度和overflow影響,不能作為button的兄弟元件存在,現在將menu渲染到body中,並給出美觀的css(copy自elementUI),然後根據button的位置絕對定位到具體位置
具體做法為
1.設定menu的position:absolute;
2.獲取button的位置資訊–使用getBoundingClientRect方法可以獲取元素在視窗的相對位置和元素的寬高(但不是在文件流中的位置,所以絕對定位時要加上window的scrollTop和scrollLeft)
3.如果元素的display為none時,getBoundingClientRect方法是無法獲取物理尺寸的,所以,實現menu與button的右對齊還需要算出menu的寬度,所以用jQuery的方法為將元素設定visible為hidden,然後將其脫離文件流獲取尺寸後在還原回去
//dropdown-menu.vue
methods: {
init() {
this.buttonEl = this.$parent.$children[0].$el; //獲取按鈕
this.dropdownEl = this.$el; //下拉框元件
document.body.appendChild(this.$el); //將元件掛載到body中去
},
initEvent() {
this.$on("visible", val => {
console.log(`現在為${val ? "顯示" : "隱藏"}狀態`);
this.showPopper = val;
val && this.$emit("update", val);
});
this.$on("update", val => {
val && this.update(this.buttonEl, this.dropdownEl);
});
},
/**
* @description 根據目標元素更新掛載元素的位置
* @param {Element} $el 目標元素
* @param {Element} $target 掛載元素
* @return {Void}
*/
update($el, $target) {
if (!$el || !$target) return;
let {
bottom: $elBottom,
height: $elHeight,
left: $elLeft,
right: $elRight,
top: $elTop,
width: $elWidth
} = this.getBoundingClientRect($el);
let {
bottom: $targetBottom,
height: $targetHeight,
left: $targetLeft,
right: $targetRight,
top: $targetTop,
width: $targetWidth
} = this.getBoundingClientRect($target);
let scrollTop = window.scrollY;
let scrollLeft = window.scrollX;
$target.style.top = `${$elTop + $elHeight + scrollTop}px`;
$target.style.left = `${$elRight - $targetWidth + scrollLeft}px`;
},
/**
* @description 獲取元素的bottom、height、left、right、top、width屬性
* @param {Element} $el 目標元素
* @return {ClientRect}
*/
getBoundingClientRect($el) {
let style = $el.style;
if (style.display === "none") {
let _addCss = {
display: "",
position: "absolute",
visibility: "hidden"
};
let _oldCss = {};
for (let i in _addCss) {
_oldCss[i] = style[i];
style[i] = _addCss[i];
}
let clientRect = $el.getBoundingClientRect();
for (let i in _oldCss) {
style[i] = _oldCss[i];
}
return clientRect;
}
return $el.getBoundingClientRect();
}
}
複製程式碼
第七步現在大致的定位和樣式以及互動都完成了,現在如果button的父元件或祖父元件可以拉滾動條的話,在滾動時需要實時跟新menu的位置
具體做法為:
由button向上遞迴獲取父元素,併為他們繫結滾動監聽,如果下拉框是顯示的狀態。則需要不斷的更新下拉框的位置
dropdown-menu.vue
//dropdown-menu.vue
methods: {
//...
initEvent() {
//...
//實現滑動滾動條時,下拉框能跟隨按鈕一起滑動定位
this.bindUpdate(this.buttonEl, () => {
this.showPopper && this.update(this.buttonEl, this.dropdownEl);
});
},
/**
* @description 逆向尋找當前元件的父元件,然後emit事件並傳遞引數
* @param {Component} $el 當前元件
* @param {Component} $target 元件名稱
* @param {($el,$target)=>void} callback 回撥函式
* @return {void}
*/
bindUpdate($el, callback) {
let target;
if ((target = $el.parentElement)) {
target.addEventListener("scroll", callback);
this.bindUpdate(target, callback);
}
}
}
複製程式碼
第八步為將一些可以重用的方法提出到util中,作為元件的混合使用
第九步新增一些tab鍵聚焦和按鍵操作,整個流程走完了,元件基本功能算是完善了
dropdown.vue
methods: {
init() {
this.buttonEl = this.$slots.default[0].elm; //元件按鈕
this.dropdownEl = this.$slots.dropdown[0].elm; //元件下拉框
this.dropdownItem = this.dropdownEl.querySelectorAll("li")
},
initEvent() {
let {
buttonEl,
dropdownEl,
dropdownItem,
hide,
handleClick,
handleKeyDown,
handleMenuKeyDown
} = this
buttonEl.addEventListener("click", handleClick); //按鈕點選設定顯示隱藏
buttonEl.addEventListener("keydown", handleKeyDown); //按鈕鍵盤事件監聽
dropdownEl.addEventListener("keydown", handleMenuKeyDown); //下拉選單鍵盤事件監聽
document.addEventListener("click", event => {
if (event.target != buttonEl) {
this.visible && hide()
}
}) //點選整個視窗下拉框消失
this.$on("menu-item-click", this.handleMenuItemClick) //註冊下拉選單點選事件
},
handleClick() {
this.visible ? this.hide() : this.show()
},
handleKeyDown(event) {
let keyCode = event.keyCode
if (keyCode == 38 || keyCode == 40) {
// up|down
this.resetIndex(0)
this.dropdownItem[0].focus()
event.preventDefault()
event.stopPropagation()
}
return;
},
handleMenuKeyDown(event) {
let keyCode = event.keyCode
let currentIndex = [].indexOf.call(this.dropdownItem, event.target)
let max = this.dropdownItem.length - 1
let nextIndex
if (keyCode == 38 || keyCode == 40) {
// up|down
if (keyCode === 38) {
// up
nextIndex = currentIndex !== 0 ? currentIndex - 1 : 0
} else {
// down
nextIndex = currentIndex < max ? currentIndex + 1 : max
}
this.removeIndex()
this.resetIndex(nextIndex)
this.dropdownItem[nextIndex].focus()
event.preventDefault()
event.stopPropagation()
} else if (keyCode === 13) {
//enter選中
event.target.click()
}
return;
},
removeIndex() {
this.dropdownItem.forEach(ele => {
ele.setAttribute("tabindex", "-1")
})
},
resetIndex(index) {
this.dropdownItem[index].setAttribute("tabindex", "0")
},
hide() {
console.log("hide")
this.visible = false
this.removeIndex()
},
show() {
console.log("show")
this.visible = true
},
handleMenuItemClick(command, instance) {
this.hide()
this.$emit("command", command, instance)
}
},
複製程式碼
將下拉框外掛化,併發布到npm上
第一步構建這樣的目錄結構,根目錄的index.js為元件的入口檔案
第二步現在將四個元件通過index.js檔案export出去,因為vue.use方法會呼叫元件export中暴露的install方法,所以在index.js中新增install方法,用來將外掛元件化
import MyButton from `./packages/button`
import MyDropdownItem from `./packages/dropdown-item`
import MyDropdownMenu from `./packages/dropdown-menu`
import MyDropdown from `./packages/dropdown`
const components = [
MyButton, MyDropdownItem, MyDropdownMenu, MyDropdown
]
const install = function(Vue, opts = {}) {
components.map(component => {
Vue.component(component.name, component)
})
}
if (typeof window !== `undefined` && window.Vue) {
install(window.Vue)
}
export default {
MyButton,
MyDropdownItem,
MyDropdownMenu,
MyDropdown,
install
}
複製程式碼
第三步可以將這個外掛釋出到npm上去啦 (暫時還沒有加types~~)
具體做法為在元件元件根目錄執行$ npm init
後執行$ npm publish
專案安裝:
$ npm install my-dropdown --save-dev
複製程式碼
專案GitHub地址:vue-dropdown,歡迎圍觀
以上就是我的總結啦~~