Lison《vue技术栈开发实战》(四)
从SplitPane组件谈Vue中“操作”DOM
在以往的前端开发中,我们习惯了使用jQuery来操作DOM,比如修改一个div的宽度,需要获取这个div的DOM,然后修改他的style;但是在Vue中,我们是不应该这样做的,而是要换一种思路,即“数据驱动视图”。
简单两列布局
将左边的元素宽度百分比进行设定,右边的元素绝对定位后设置left,就可以实现两个div
分别放置。
如何让两个div改变宽度
改变宽度只需要将上述的width
和left
变成计算属性即可。
鼠标拖动效果
<template>
<div class="split-pane-wrapper" ref="outer">
<div class="pane pane-left" :style="{ width: leftOffsetPercent, paddingRight: `${this.triggerWidth / 2}px` }">
<slot name="left"></slot>
</div>
<div class="pane-trigger-con" @mousedown="handleMousedown" :style="{ left: triggerLeft, width: `${triggerWidth}px` }"></div>
<div class="pane pane-right" :style="{ left: leftOffsetPercent, paddingLeft: `${this.triggerWidth / 2}px` }">
<slot name="right"></slot>
</div>
</div>
</template>
<script>
export default {
name: 'SplitPane',
props: {
value: {
type: Number,
default: 0.5
},
triggerWidth: {
type: Number,
default: 8
},
min: {
type: Number,
default: 0.1
},
max: {
type: Number,
default: 0.9
}
},
data () {
return {
// leftOffset: 0.3,
canMove: false,
initOffset: 0
}
},
computed: {
leftOffsetPercent () {
return `${this.value * 100}%`
},
triggerLeft () {
return `calc(${this.value * 100}% - ${this.triggerWidth / 2}px)`
}
},
methods: {
handleClick () {
this.leftOffset -= 0.02
},
handleMousedown (event) {
document.addEventListener('mousemove', this.handleMousemove)
document.addEventListener('mouseup', this.handleMouseup)
this.initOffset = event.pageX - event.srcElement.getBoundingClientRect().left
this.canMove = true
},
handleMousemove (event) {
if (!this.canMove) return
const outerRect = this.$refs.outer.getBoundingClientRect()
let offsetPercent = (event.pageX - this.initOffset + this.triggerWidth / 2 - outerRect.left) / outerRect.width
if (offsetPercent < this.min) offsetPercent = this.min
if (offsetPercent > this.max) offsetPercent = this.max
// this.$emit('input', offsetPercent)
this.$emit('update:value', offsetPercent)
},
handleMouseup () {
this.canMove = false
}
}
}
</script>
<style lang="less">
.split-pane-wrapper{
height: 100%;
width: 100%;
position: relative;
.pane{
position: absolute;
top: 0;
height: 100%;
&-left{
// width: 30%;
background: palevioletred;
}
&-right{
right: 0;
bottom: 0;
background: paleturquoise;
}
&-trigger-con{
height: 100%;
background: red;
position: absolute;
top: 0;
z-index: 10;
user-select: none;
cursor: col-resize;
}
}
}
</style>
看下几个注意点:
- handleMousedown的时候是给body绑定
mousemove
事件,以便鼠标在整个页面挪动的过程中完成监听 initOffset
的值是指点下去那一瞬间,鼠标相对于竖杠的偏移,需要进行初始化的记录user-select: none;
实现选中不显示,让页面展示的效果相对更加友好
v-model和.sync的用法
v-model
我们之前都用过,看下.sync
的用法:
//父组件给子组件传入一个函数
<MyFooter :age="age" @setAge="(res)=> age = res">
</MyFooter>
//子组件通过调用这个函数来实现修改父组件的状态。
mounted () {
console.log(this.$emit('setAge',1234567));
}
渲染函数和JSX快速掌握
render函数
看下render能够设置的部分函数值:
render: h => {
return h(CountTo, {
'class': [],
attrs: {},
style: {},
props: {
endVal: 100
},
// domProps: {
// innerHTML: '123'
// },
on: {
'on-animation-end': (val) => {
console.log('animation end!')
}
},
nativeOn: {
'click': () => {
console.log('click!')
}
},
directives: [],
scopedSlots: {},
slot: '',
key: '',
ref: ''
})
}
render: h => h('div', [
h('ul', {
on: {
'click': handleClick
}
}, getLiEleArr(h))
])
函数式组件
函数式组件是只传递给它数据,不监听传递给它的状态,没有生命周期和钩子函数,只是作为一个接收参数的函数。
<template>
<ul>
<li @mousemove="handleMove" v-for="(item, index) in list" :key="`item_${index}`">
<!-- <span v-if="!render">{{ item.number }}</span>
<render-dom v-else :render-func="render" :number="item.number"></render-dom> -->
<slot name="aa" :number="item.number"></slot>
<slot :number="item.number"></slot>
</li>
</ul>
</template>
<script>
import RenderDom from '_c/render-dom'
export default {
name: 'List',
components: {
RenderDom
},
props: {
list: {
type: Array,
default: () => []
},
render: {
type: Function,
default: () => {}
}
},
methods: {
handleMove (event) {
event.preventDefault()
}
}
}
</script>
renderPage
<template>
<div>
<list :list="list" :style="{color: 'red'}">
<count-to slot="aa" slot-scope="count" :end-val="count.number"></count-to>
</list>
</div>
</template>
<script>
import List from '_c/list'
import CountTo from '_c/count-to'
export default {
data () {
return {
list: [
{ number: 100 },
{ number: 45 }
]
}
},
components: {
List,
CountTo
},
methods: {
renderFunc (h, number) {
return (
<CountTo nativeOn-click={this.handleClick} on-on-animation-end={this.handleEnd} endVal={number} style={{color: 'pink'}}></CountTo>
)
},
handleClick (event) {
// console.log(event)
},
handleEnd () {
// console.log('end!')
}
}
}
</script>
JSX
看上述page页面的返回值
<CountTo nativeOn-click={this.handleClick} on-on-animation-end={this.handleEnd} endVal={number} style={{color: 'pink'}}></CountTo>
作用域插槽
我们在渲染list的时候都要进行function导入,很不直观,这里可以使用作用域插槽
看下作用域插槽的取值:
<slot :number="item.number"></slot>
<count-to slot="aa" slot-scope="count" :end-val="count.number"></count-to>
递归组件的使用
在我们日常开发中,常有一些用到嵌套使用的组件,而且嵌套的深度有时候是根据数据来定的,是在编写程序的时候未知的,所以这样在写页面的时候是没法固定写死的,而是需要通过像类似于递归函数那样,逐层遍历。递归组件和递归函数的使用思想相似,都需要有能够停止递归的条件,通过本节我们将详细学习如何使用递归组件。
封装简单Menu组件
<template>
<ul class="a-submenu">
<div class="a-submenu-title" @click="handleClick">
<slot name="title"></slot>
<span class="shrink-icon" :style="{ transform: `rotateZ(${showChild ? 0 : 180}deg)` }">^</span>
</div>
<div v-show="showChild" class="a-submenu-child-box">
<slot></slot>
</div>
</ul>
</template>
<script>
export default {
nmae: 'ASubmenu',
data () {
return {
showChild: false
}
},
methods: {
handleClick () {
this.showChild = !this.showChild
}
}
}
</script>
<style lang="less">
.a-submenu{
background: rgb(33, 35, 39);
&-title{
color: #fff;
position: relative;
.shrink-icon{
position: absolute;
top: 4px;
right: 10px;
}
}
&-child-box{
overflow: hidden;
padding-left: 20px;
}
li{
background: rgb(33, 35, 39);
}
}
</style>
递归组件
submenu.vue
<template>
<a-submenu>
<div slot="title">{{ parent.title }}</div>
<template v-for="(item, i) in parent.children">
<a-menu-item v-if="!item.children" :key="`menu_item_${index}_${i}`">{{ item.title }}</a-menu-item>
<re-submenu v-else :key="`menu_item_${index}_${i}`" :parent="item"></re-submenu>
</template>
</a-submenu>
</template>
<script>
import menuComponents from '_c/menu'
const { AMenuItem, ASubmenu } = menuComponents
export default {
name: 'ReSubmenu',
components: {
AMenuItem,
ASubmenu
},
props: {
parent: {
type: Object,
default: () => ({})
},
index: Number
}
}
</script>
memuPage
<template>
<div class="menu-box">
<!-- <a-menu>
<a-menu-item>1111</a-menu-item>
<a-menu-item>2222</a-menu-item>
<a-submenu>
<div slot="title">3333</div>
<a-menu-item>3333-11</a-menu-item>
<a-submenu>
<div slot="title">3333-22</div>
<a-menu-item>3333-22-11</a-menu-item>
<a-menu-item>3333-22-22</a-menu-item>
</a-submenu>
</a-submenu>
</a-menu> -->
<a-menu>
<template v-for="(item, index) in list">
<a-menu-item v-if="!item.children" :key="`menu_item_${index}`">{{ item.title }}</a-menu-item>
<re-submenu v-else :key="`menu_item_${index}`" :parent="item" :index="index"></re-submenu>
</template>
</a-menu>
</div>
</template>
<script>
import menuComponents from '_c/menu'
import ReSubmenu from './re-submenu.vue'
const { AMenu, AMenuItem, ASubmenu } = menuComponents
export default {
name: 'menu_page',
components: {
AMenu,
AMenuItem,
ASubmenu,
ReSubmenu
},
data () {
return {
list: [
{
title: '1111'
},
{
title: '2222'
},
{
title: '3333',
children: [
{
title: '3333-1'
},
{
title: '3333-2',
children: [
{
title: '3333-2-1'
},
{
title: '3333-2-2'
},
{
title: '3333-2-3',
children: [
{
title: '3333-2-3-1'
},
{
title: '3333-2-3-2'
}
]
}
]
}
]
}
]
}
}
}
</script>
<style lang="less">
.menu-box{
width: 300px;
height: 400px;
}
</style>
递归组件有两个注意点:
- 一定要写好终止条件
- 一定要进行命名,以便于继续调用