使用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,欢迎围观
以上就是我的总结啦~~