最近都是用element-ui 在协助本身的项目开发,用着用着就想看看饿了么是怎么实现组件的使用的,因而就想本身动手也来写,固然,我是要按部就班的,从最开始最简单的组件定义开始。总的写了三个小组件,我按照我本身以为难度等级,分别定义为基础版,打怪版,终极版。css
项目在线demo
项目github地址
项目github地址vue
嗯,在写以前,我先说一下,我会这篇文章中写下面三个小组件中的其中两个。分别以下所示:node
在编写组件的时候,复用组件是颇有好处的。可复用组件应该有一个清晰的公共接口。
Vue组件的API来自三个部分:props events slotsgit
那么,其实咱们能够用v-bind和v-on的简写语法,来使得模板清楚简洁:github
// 这是我写的backTop 组件的调用方法。
<back-top :scrollmyself = 'true'></back-top> // 用props将外部环境数据传递进去。复制代码
额,我以为仍是直接动手开始作吧,会比较实在一点,那就从最简单的backTop组件开始吧。element-ui
页面右下角就是本身封装的backtop组件。bash
<back-top :scrollmyself = 'true'></back-top> //这个scrollmyself是传进去组件的props值,复制代码
先说一下功能状况,这个backTop组件的做用就是,当页面存在滚动条,或者页面中某个局部有存在滚动条,当页面滚动到必定位置以后,页面就会出现点击返回顶部的按钮,点击以后就会返回顶部,此时返回顶部的icon消失。ide
首先看一下文件结构:函数
1 backtop内部的main.vue文件
-------- template模板
----------组件名: name: BackTop
----------props: 定义props数据格式,默认为false;true的时候当前发生滚动的对象就是内部引用该组件的父组件,为false的时候就是window对象。
<template>
<transition name='slide-fade'>
<div class='page-component-up' v-show='isShow' @click='getTop'>
<i class='tri'></i>
</div>
</transition>
</template>
<script>
export default {
name: 'BackTop', // 这个是export出去的组件名,我定义为BackTop
props: {
scrollmyself: {
type: Boolean, // 这是选择滚动对象的props值,若是滚动的对象是当前组件的父元素,就设置scrollObj为true.若是没有设置就默认为window对象
default: false
}
},
data () {
return {
isShow: false,
target: ''
}
},
methods: {
// 添加样式,鼠标hover上去,改变颜色
addhoverClass (e) {
if (e.type === 'mouseover') {
this.$el.classList.add('hover')
} else if (e.type === 'mouseout') {
this.$el.classList.remove('hover')
}
},
showIcon () {
// 根据scrollTop的值来判断是否显示返回顶部的icon
if (this.target.scrollTop > 100) {
this.isShow = true
this.$el.addEventListener('mouseover', this.addhoverClass)
this.$el.addEventListener('mouseout', this.addhoverClass)
} else if (this.target.scrollTop < 100) {
this.isShow = false
}
},
getTop () {
// 点击icon以后自动返回顶部的函数
let timer = setInterval(() => {
let top = this.target.scrollTop
let speed = Math.ceil(top / 5)
this.target.scrollTop = top - speed
if (top === 0) {
clearInterval(timer)
}
}, 20)
}
},
mounted () {
// 经过这个target来判断当前的滚动监听对象是谁
if (this.scrollmyself) {
this.target = this.$el.parentNode
} else {
this.target = document.body
}
this.target.addEventListener('scroll', this.showIcon)
},
beforeDestroy () {
// 组件销毁的时候,须要删除scroll的监听事件。
this.target.removeEventListener('scroll', this.showIcon)
}
}
</script>
// CSS部分:
<style lang="scss" rel="stylesheet/scss">
.slide-fade-enter-active {
transition: all .1s ease;
}
.slide-fade-leave-active {
transition: all .1s cubic-bezier(1.0, 0.3, 0.8, 1.0);
opacity: 0;
}
.slide-fade-enter, .slide-fade-leave-to
/* .slide-fade-leave-active 在低于 2.1.8 版本中 */ {
// transform: translateY(-20px);
opacity: 0;
}
.page-component-up {
background-color: #4eb1fb;
position: fixed;
right: 3rem;
bottom: 12rem;
width: 50px;
height: 50px;
border-radius: 25px;
cursor: pointer;
opacity: .4;
transition: .3s;
text-align: center;
z-index: 999;
}
.tri {
width: 0;
height: 0;
border: 12px solid transparent;
border-bottom-color: #dfe6ec;
text-align: center;
}
.hover {
background-color: red;
}
</style>复制代码
在咱们的component的内部的index.js文件中,咱们须要将组件引出;
import BackTop from './backtop/src/main';
/* istanbul ignore next */
BackTop.install = function(Vue) {
Vue.component(BackTop.name, BackTop);
};
export default BackTop;复制代码
####3 在main.js内部引用
import backTop from './myComponent/backtop'
Vue.use(backTop)复制代码
总结一下: 在上面这个backtop组件中,用props进行数据的传递,将数据传递给内部组件。
接下来这个侧边栏多级下拉导航侧边栏,实现的最终效果以下所示。
侧边栏组件能够实现多级下拉菜单,同时也能够实现路由的跳转,只要设置相应的route值就能够。
由于这个组件是侧边栏组件,有单个的子菜单,也有包含有下拉子菜单的菜单,同时,全部我分红三个小的组件来实现。
同时也会使用slot来进行内容的分发。
基本的结构以下所示:
其实这个组件对于我来讲,存在几个难点。
props myVisible
来控制导航侧边栏的出现与消失。同时你也会发现,经过点击蒙板(在组件内部定义)也能够实现侧边栏的消失,如何实现双向数据传递呢?待会我会提到这两个问题,不过咱们能够先来看一下这个组件引入(怎么引入我待会说,跟上面的同样)以后的使用范例:
<my-menu :my-visible.sync = "visible">
<!-- 这里的按钮能够本身去封装定义 -->
<!-- <p slot='toggleBtn'>点我点我</p> -->
<template slot="menu-title">个人我的助手小系统呀</template>
<menu-item route='/'><i slot='icon' class=' iconfont icon-403010'></i>首页</menu-item>
<menu-item route='/DatePlan'><i slot='icon' class=' iconfont icon-403010'></i>DatePlan</menu-item>
<menu-item route='/EatWhat'><i slot='icon' class=' iconfont icon-chi'></i>今天吃什么</menu-item>
<menu-item route='/memo'><i slot='icon' class=' iconfont icon-beiwanglu'></i>备忘录</menu-item>
<menu-item route='/when'><i slot='icon' class=' iconfont icon-fangjia'></i>何时放假</menu-item>
<menu-item route='/icon'><i slot='icon' class=' iconfont icon-pinrenpinkongxin'></i>抛硬币</menu-item>
<menu-item route='/mirror'><i slot='icon' class=' iconfont icon-jingzi'></i>照镜子</menu-item>
<my-submenu>
<i slot="icon" class=' iconfont icon-jizhang'></i><template slot="submenu-title"></i>记帐</template>
<menu-item route='/money'><i slot="icon" class=' fa fa-circle-o'></i>记帐首页</menu-item>
<menu-item route='/moneyRecord'><i slot="icon" class='fa fa-circle-o'></i>添加记帐</menu-item>
<my-submenu>
<i slot="icon" class='fa fa-circle-o'></i><template slot="submenu-title">这是有下拉菜单</template>
<menu-item><i slot="icon" class=' fa fa-circle-o'></i>我是第一个</menu-item>
<menu-item><i slot="icon" class=' fa fa-circle-o'></i>我是第二个</menu-item>
</my-submenu>
<my-submenu>
<i slot="icon" class='fa fa-circle-o'></i><template slot="submenu-title">这是有下拉菜单</template>
<menu-item><i slot="icon" class=' fa fa-circle-o'></i>我是第一个</menu-item>
<menu-item><i slot="icon" class='fa fa-circle-o'></i>我是第二个</menu-item>
<my-submenu>
<i slot="icon" class='fa fa-circle-o'></i><template slot="submenu-title">这是有下拉菜单</template>
<menu-item><i slot="icon" class=' fa fa-circle-o'></i>我是第一dddddd个</menu-item>
<menu-item><i slot="icon" class=' fa fa-circle-o'></i>我是第二ddddddddddddd个</menu-item>
</my-submenu>
</my-submenu>
</my-submenu>
</my-menu>复制代码
基本的使用结构,能够认真看例子代码。具体的细节我就不说啦。
参数
[my-menu 组件] myVisible : 默认为false,控制侧边栏的显示与消失。
[menu-item组件] route : 默认为空,控制路由的跳转。
slot
[my-menu 组件] menu-title: 控制菜单的标题显示
[menu-item组件] icon: icon图标的显示。
[my-submenu组件] submenu-title: 子级菜单的标题显示。icon: icon图标的显示。
难点实现:这个渲染以后是每个不含有子菜单的菜单,那么问题来了,当有绑定路由对象的时候,点击某个菜单的时候,侧边栏菜单是要消失的,那么如何,去告诉引用了menu-item组件的my-menu父组件去关闭呢?
解决方法:这里参考了饿了么组件的dispatch方法,(dispatch文件就不po出来了),向父组件传递事件。
引入了dispatch文件以后:
子组件中使用:this.dispatch('my-menu', 'closeByRoute')
监听的my-menu父组件:this.$on('closeByRoute', this.toggleShow)
<template>
<li >
<router-link href="#" style='color:white' :to='route' @click.native='handleRoute'>
<slot name='icon'></slot>
<span class='menutitle'><slot></slot></span></router-link>
</li>
</template>
<script>
import dispatch from '../../utils/dispatch'
export default {
name: 'menu-item',
mixins: [dispatch],
props: {
route: {
type: String,
default: ' '
}
},
methods: {
handleRoute () {
if (this.route) {
this.dispatch('my-menu', 'closeByRoute')
// 使用dispatch进行传递 this.dispatch(组件名, 触发的事件名)
}
}
}
}
</script>复制代码
前面提到,my-menu组件是用props myVisible来实现侧边栏的显示与消失。由于vue是不能够直接修改prop属性的,可是新版的vue可使用sync
来实现父子组件的双向数据绑定。
只要在调用的时候,使用 <my-menu :my-visible.sync = 'visible'></my-menu>
而在组件内部,想要变动props值的时候,只须要添加 this.$emit('update:myVisible', !this.myVisible)
来更新props属性值就能够。
www.cnblogs.com/penghuwan/p… 你们能够参考一下这篇文章。
这个组件的主要就是用props来控制显示和隐藏。最主要的是toggleShow方法,控制侧边栏的显示和隐藏,经过添加一个togglehide 的类来判断当前的侧边栏是不是显示的状态。
<template>
<div class='sideBar togglehide' ref='barPart'>
<div class='menuCover' @click='toggleMenu' ref='cover'></div>
<ul class='menu'>
<li class='list-title'><slot name="menu-title"></slot></li>
<slot></slot>
</ul>
</div>
</template>
<script>
export default {
name: 'my-menu',
props: {
myVisible: {
type: Boolean,
default: false
}
},
watch: {
myVisible () {
this.toggleShow()
}
},
methods: {
toggleMenu () {
this.$emit('update:myVisible', !this.myVisible)
},
toggleShow () {
let target = this.$refs.barPart
let test = target.classList.contains('togglehide')
if (!test) {
target.classList.add('togglehide')
this.$emit('closeBar') // 关闭导航标签的回调
let OpenMenu = target.querySelectorAll('.openMenu')
let OpenIcon = target.querySelectorAll('.openIcon')
this.$refs.barPart.style.left = -this.$refs.barPart.offsetWidth + 'px'
for (let i = 0; i < OpenMenu.length; i++) {
OpenMenu[i].classList.remove('openMenu')
OpenMenu[i].style.display = 'none'
}
for (let i = 0; i < OpenIcon.length; i++) {
OpenIcon[i].classList.remove('openIcon')
}
} else {
target.removeAttribute('style')
target.classList.remove('togglehide')
this.$emit('openBar') // 打开导航标签的回调
this.$refs.barPart.style.left = 0 + 'px'
}
}
},
mounted () {
this.$refs.barPart.style.left = -this.$refs.barPart.offsetWidth + 'px' //初始化经过left值来隐藏侧边栏组件
this.$on('closeByRoute', this.toggleShow)
}
}
</script>复制代码
含有子菜单的菜单引用,就须要引用my-submenu的组件。关于如何实现子菜单的下拉和收起的效果,这是这个组件的主要实现难点。
基本思路以下:
1 一开始先设置.treeview同级的treeview-menu菜单的display为none
2 当包含有子菜单的菜单即记帐标签被点击以后,设置treeview-menu的样式height为0,首先设置为display:block,并且over-flow为hidden。而后获取当前的子菜单下的li个数,便可以获取全部子元素的高度,而后再设置treeview-menu的高度为该高度。
结合transition就能够实现下拉效果了。具体能够看代码。
<template>
<div>
<li class='treeview' @click='toggleShowMenu'>
<a href="#" data-show = false style='color:white'>
<slot name='icon'></slot>
<span class='menutitle'><slot name="submenu-title"></slot></span>
<span class='pull-right-container'><i class='fa fa-angle-left pull-right' style='color:white'></i></span>
</a>
</li>
<ul class='treeview-menu' style='display:none'>
<slot></slot>
</ul>
</div>
</template>
<script>
export default {
name: 'my-submenu',
methods: {
toggleShowMenu (e) {
let setTarget = e.currentTarget.nextElementSibling
if (setTarget !== null) {
let showCon = setTarget.classList.contains('openMenu')
let childLi = setTarget.children
var totalHeight = 0
let h = e.currentTarget
var targetIcon = h.querySelectorAll('.pull-right')[0] // todo: h是当前的元素
let nodeListArr = Array.prototype.slice.call(childLi)
if (!showCon) {
setTarget.style.height = 0
setTarget.classList.add('openMenu')
targetIcon.classList.add('openIcon')
setTarget.style.overflow = 'hidden'
setTarget.style.display = 'block'
for (let i = 0; i < nodeListArr.length; i++) {
totalHeight = totalHeight + nodeListArr[i].offsetHeight
}
setTimeout(() => {
setTarget.style.height = totalHeight + 'px'
setTimeout(() => {
setTarget.removeAttribute('style')
setTarget.style.display = 'block'
}, 300)
}, 40)
} else {
targetIcon.classList.remove('openIcon')
setTarget.style.height = setTarget.offsetHeight + 'px'
setTarget.style.overflow = 'hidden'
setTarget.classList.remove('openMenu')
setTimeout(() => {
setTarget.style.height = 0 + 'px'
setTimeout(() => {
setTarget.removeAttribute('style')
setTarget.style.display = 'none'
}, 300)
}, 40)
}
}
}
}
}
</script>复制代码
import mymenu from './sidebar//src/my-menu.vue'
import menuitem from './sidebar/src/menu-item.vue'
import mysubmenu from './sidebar/src/my-submenu.vue'
import BackTop from './backtop/src/main'
/* istanbul ignore next */
const components = [
mymenu,
menuitem,
mysubmenu,
BackTop
]
const install = (Vue, OPts) => {
if (install.installed) {
return
}
components.map(component => {
Vue.component(component.name, component)
})
}
export default {
version: '0.0.1',
author: 'katherine',
install,
mymenu,
menuitem,
mysubmenu,
BackTop
}复制代码
main.js
import globalUI from './myComponent'
Vue.use(globalUI)复制代码
padding-left:20px;
的属性值。
而当有多级下拉菜单的时候,我在外部引用的时候都是这样子去引用调用的。
而含有多级子菜单的,我都是直接在my-submenu内部再去嵌套添加,细心的你会发现,个人子菜单list都是存在一个这样的ul里面。
因此,我就在CSS样式中设置以下,这个很关键,这样的话,只要你无论嵌套多少层下拉菜单,都会依次增长一个padding-left:20px;的属性值。
.treeview-menu {
.treeview-menu {
padding-left:20px;
}
}复制代码
呼呼,感受本身讲得太细节了,会不会反而会更混乱,可是这些是我在作的过程当中遇到的问题,毕竟本身是小白,以为不少东西若是不讲细节一点,一开始学确定以为很吃力,不知道要怎么深刻。因此我都会但愿本身可以详细地分享本身学习过程当中所遇到的问题。也但愿经过分享,本身也能从别人身上学到新的知识。