[TOC]
>[success] # 从SplitPane组件谈Vue中如何【操作】DOM
**标题** 中指的 **操作DOM** 实际上不是真正的 **操作DOM** , 接下来我们通过 **封装** 一个 **SplitPane组件** 来讲解一下 **在Vue中如何对属性和样式进行修改** , 在传统的 **JS** 开发中一般使用 **Javascript** 或者 **JQuery** ,如果需要修改一个 **div** 的样式,我们是 **需要获取到这个DOM中Style对象的属性来进行修改** ,在 **Vue** 中我们 **提倡的是数据驱动视图** ,也就是说 **视图的改变是通过数据的改变而变化** , 也就是在 **Vue** 中我们是 **不可以像 Javascript 或者 JQuery 一样操作DOM**修改样式的,这样会 **导致视图跟数据不同步不匹配**,接下来我们会了解到 **在Vue中如何修改样式** 。
>[success] ## 组件封装
[SplitPane组件效果](https://www.iviewui.com/components/split)
**组件功能介绍** : 可将一片区域,分割为可以拖拽调整宽度或高度的两部分区域。
1. **父组件**
首先在 **路由列表** 的 **路由对象** 中添加新创建的 **split-pane** 页面配置路由,**这个页面名字跟上面的组件名重名,但是无关系,这个文件只是用来展示的页面,不是组件,注意一下就好**
**src/router/router.js**
~~~
export default [
{
path: '/split-pane',
name: '/split_pane',
component: () => import('@/views/split-pane')
}
]
~~~
然后在 **src/views/split-pane.vue** 页面 **引入split-pane组件**
**src/views/split-pane.vue**
~~~
<template>
<div class="split-pane-con">
<split-pane :value.sync="offset">
<div slot="left">left</div>
<div slot="right">right</div>
</split-pane>
</div>
</template>
<script>
import SplitPane from '_c/split-pane'
export default{
components: {
SplitPane
},
data(){
return{
offset: 0.8
}
},
methods: {
handleInput(value){
this.offset = value
}
}
}
</script>
<style lang="scss">
.split-pane-con{
width: 400px;
height: 200px;
background: papayawhip;
}
</style>
~~~
2. **子组件**
首先我们在 **src/components** 文件夹中创建 **split-pane** 文件夹,跟 **split-pane组件有关的文件都放在这个文件夹中** 。 接下来创建一个 **split-pane.vue** 文件,该文件 **作为组件文件**
**src/components/split-pane/split-pane.vue**
~~~
<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{
canMove: false, // 是否可以移动
initOffset: 0 // 初始偏移量
}
},
computed: {
leftOffsetPercent(){
return `${ this.value * 100 }%`
},
triggerLeft(){
return `calc(${ this.value * 100 }% - ${ this.triggerWidth / 2 }px)`
}
},
methods: {
/**
* 鼠标按下事件
* 【鼠标按下】中间拖拽条时触发【document的鼠标移动事件】
*/
handleMousedown(event){
// 鼠标移动事件
document.addEventListener('mousemove', this.handleMousemove)
// 鼠标抬起事件
document.addEventListener('mouseup', this.handleMouseup)
this.initOffset = event.pageX - event.srcElement.getBoundingClientRect().left
this.canMove = true
},
/**
* 鼠标移动事件
* @param { event } 原生event对象
*/
//
handleMousemove(event){
/**
* getBoundingClientRect() 方法回返回指定元素的dom元素的位置信息如下:
* {
* bottom: 208
* height: 200
* left: 8
* right: 408
* top: 8
* width: 400
* x: 8
* y: 8
* }
*
*/
if(!this.canMove) return
const outerRect = this.$refs.outer.getBoundingClientRect()
// offset是偏移的像素数
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('update:value', offsetPercent)
},
/**
* 鼠标抬起事件
*/
handleMouseup(){
this.canMove = false
}
}
}
</script>
<style lang="scss">
.split-pane-wrapper{
height: 100%;
width: 100%;
position: relative;
.pane{
position: absolute;
top: 0;
height: 100%;
&-left{ // .pane-left
// width: 30%;
background: palevioletred;
}
&-right{
right: 0;
bottom: 0;
// left: 30%;
background: paleturquoise;
}
&-trigger-con{
height: 100%;
background:red;
position: absolute;
top: 0;
z-index: 10;
user-select: none;
cursor: col-resize;
}
}
}
</style>
~~~
再创建一个**index.js**,在**index.js**中引入**split-pane.vue组件**并**导出(export default)** 。
**src/components/split-pane/index.js**
~~~
import SplitPane from './split-pane'
export default SplitPane
~~~
>[success] ## 简单两列布局
因为要2个盒子 **拖拽的效果** ,所以要做一个盒子的雏形,如下:
效果图:
![](https://img.kancloud.cn/c1/20/c1209c095b84d1e9879a26087148cb9f_413x221.png)
代码如下:
**demo.vue**
~~~
<template>
<div class="split-pane-wrapper">
<div class="pane pane-left"></div>
<div></div>
<div class="pane pane-right"></div>
</div>
</template>
<script>
export default{
name: 'SplitPane'
}
</script>
<style lang="scss">
.split-pane-wrapper{
height: 100%;
width: 100%;
position: relative;
.pane{
position: absolute;
top: 0;
height: 100%;
&-left{ // .pane-left
width: 30%;
background: palevioletred;
}
&-right{
right: 0;
bottom: 0;
left: 30%;
background: paleturquoise;
}
}
}
</style>
~~~
>[success] ## 如何让两个div改变宽度
上面的列子中演示了简单的 **两列布局** ,那么如何做到 **改变2个div盒子的宽度** 呢, **原理** :**左面盒子位置不变,只改变宽度,右边盒子只改变位置** ,所以只需要 **动态修改 ** **.pane-left** 类名的 **width** 和 **.pane-right** 类名的 **left** 值即可。
>[success] ## 鼠标拖动效果
鼠标的拖动这里主要运用到了 **3个事件** , **2个方法**,并且 **根据返回的坐标信息进行计算**:
**3个事件** :
1. 鼠标按下:**mousedown**
2. 鼠标抬起:**mouseup**
3. 鼠标移动: **mousemove**
**2个方法** :
1. **getBoundingClientRect()** 方法可以返回指定的 **DOM** 元素的坐标等信息,需要注意的是 **x、y轴信息在一些浏览器里不支持,其他属性在ie9下也是支持的**
使用方法:
~~~
this.$refs.outer.getBoundingClientRect().left
// 或者可以根据event获取标签元素本身的 DOM 信息
event.srcElement.getBoundingClientRect().left
~~~
2. **event.srcElement** 可以获取该 **事件本身的标签元素**
>[success] ## v-model和.sync的用法
下面的案例中直讲述了 **如何通过v-model** 修改单个值的情况,[v-model修改多个值的写法](https://www.kancloud.cn/wangjiachong/vue_notes/2141975) 看我的另外一篇文章。
一般 **子组件想修改父组件的值** 通常需要在 **子组件中通过this.$emit传递给父组件** ,然后在 **父组件自定义事件中改变值** ,有 **3种方法** ,我会在下面的代码中例举出来:
>[success] ### :value + @input方式
这种方法就是 **最普通的方式**, **子组件中通过this.$emit传递给父组件**,然后在 **父组件自定义事件中改变值**
父组件:
~~~
<template>
<div class="split-pane-con">
<split-pane :value="offset" @input="handleInput"></split-pane>
</div>
</template>
<script>
import SplitPane from '_c/split-pane'
export default{
components: {
SplitPane
},
data(){
return{
offset: 0.8
}
},
methods: {
handleInput(value){ // 监听子组件值变化后在父组件中改变值
this.offset = value
}
}
}
</script>
~~~
子组件:
~~~
<template>
<div>
<p>{{ value }}</p>
<button @click="handleClick">点击</button>
</div>
</template>
<script>
export default {
props: {
value: {
type: Number,
default: 0.5
}
},
methods: {
handleClick(){
this.$emit('input', 1)
}
}
}
</script>
~~~
>[success] ### v-model 方式
**v-model** 这种写法就是 **:value + @input** 的语法糖
父组件:
~~~
<template>
<div class="split-pane-con">
<split-pane v-model="offset"></split-pane>
</div>
</template>
<script>
import SplitPane from '_c/split-pane'
export default{
components: {
SplitPane
},
data(){
return{
offset: 0.8
}
}
}
</script>
~~~
子组件:
~~~
<template>
<div>
<p>{{ value }}</p>
<button @click="handleClick">点击</button>
</div>
</template>
<script>
export default {
props: {
value: {
type: Number,
default: 0.5
}
},
methods: {
handleClick(){
this.$emit('input', 1)
}
}
}
</script>
~~~
>[success] ### .sync 方式
这种方式需要 **在父组件中子组件标签上,需要子组件修改的属性上添加 .sync**,然后 **子组件中修改值时需要这样写 this.$emit('update:属性名', 更新的值)**
父组件:
~~~
<template>
<div class="split-pane-con">
<split-pane :value.sync="offset"></split-pane>
</div>
</template>
<script>
import SplitPane from '_c/split-pane'
export default{
components: {
SplitPane
},
data(){
return{
offset: 0.8
}
}
}
</script>
~~~
子组件:
~~~
<template>
<div>
<p>{{ value }}</p>
<button @click="handleClick">点击</button>
</div>
</template>
<script>
export default {
props: {
value: {
type: Number,
default: 0.5
}
},
methods: {
handleClick(){
// update: 属性名
this.$emit('update:value', 1)
}
}
}
</script>
~~~
>[success] ## 封装组件技巧
1. [css-cursor鼠标划入的样式网站](http://css-cursor.techstream.org/)
2. 鼠标拖动运用到的3个鼠标事件: **mousedown** 、**mouseup** 、 **mousemove**
3. .sync的可以做到子组件内修改父组件的值,如果 **props** 传入的是 **引用类型(Object、Array)** 数据,**不需要使用.sync** ,可以直接修改数据。
- vue 26课
- Vue-cli3.0项目搭建
- Vue-ui 创建cli3.0项目
- Vue-ui 界面详解
- 项目目录详解
- public文件夹
- favicon.ico
- index.html
- src文件夹
- api文件夹
- assets文件夹
- components文件夹
- config文件夹
- directive文件夹
- lib文件夹
- mock文件夹
- mock简明文档
- router文件夹
- store文件夹
- views文件夹
- App.vue
- main.js
- .browserslistrc
- .editorconfig
- .eslintrc.js
- .gitignore
- babel.config.js
- package-lock.json
- package.json
- postcss.config.js
- README.en.md
- README.md
- vue.config.js
- Vue Router
- 路由详解(一)----基础篇
- 路由详解(二)----进阶篇
- Vuex
- Bus
- Vuex-基础-state&getter
- Vuex-基础-mutation&action/module
- Vuex-进阶
- Ajax请求
- 解决跨域问题
- 封装axios
- Mock.js模拟Ajax响应
- 组件封装
- 从数字渐变组件谈第三方JS库使用
- 从SplitPane组件谈Vue中如何【操作】DOM
- 渲染函数和JSX快速掌握
- 递归组件的使用
- 登陆/登出以及JWT认证
- 响应式布局
- 可收缩多级菜单的实现
- vue杂项
- vue递归组件
- vue-cli3.0多环境打包配置
- Vue+Canvas实现图片剪切
- vue3系统入门与项目实战
- Vue语法初探
- 初学编写 HelloWorld 和 Counter
- 编写字符串反转和内容隐藏功能
- 编写TodoList功能了解循环与双向绑定
- 组件概念初探,对 TodoList 进行组件代码拆分
- Vue基础语法
- Vue 中应用和组件的基础概念
- 理解 Vue 中的生命周期函数
- 常用模版语法讲解
- 数据,方法,计算属性和侦听器
- 样式绑定语法
- 条件渲染
- 列表循环渲染
- 事件绑定
- 表单中双向绑定指令的使用
- 探索组件的理念
- 组件的定义及复用性,局部组件和全局组件
- 组件间传值及传值校验
- 单向数据流的理解
- Non-Props 属性是什么
- 父子组件间如何通过事件进行通信
- 组件间双向绑定高级内容
- 使用匿名插槽和具名插槽解决组件内容传递问题
- 作用域插槽
- 动态组件和异步组件
- 基础语法知识点查缺补漏
- Vue 中的动画
- 使用 Vue 实现基础的 CSS 过渡与动画效果
- 使用 transition 标签实现单元素组件的过渡和动画效果
- 组件和元素切换动画的实现
- 列表动画
- 状态动画
- Vue 中的高级语法
- Mixin 混入的基础语法
- 开发实现 Vue 中的自定义指令
- Teleport 传送门功能
- 更加底层的 render 函数
- 插件的定义和使用
- 数据校验插件开发实例
- Composition API
- Setup 函数的使用
- ref,reactive 响应式引用的用法和原理
- toRef 以及 context 参数
- 使用 Composition API 开发TodoList
- computed方法生成计算属性
- watch 和 watchEffect 的使用和差异性
- 生命周期函数的新写法
- Provide,Inject,模版 Ref 的用法
- Vue 项目开发配套工具讲解
- VueCLI 的使用和单文件组件
- 使用单文件组件编写 TodoList
- Vue-Router 路由的理解和使用
- VueX 的语法详解
- CompositionAPI 中如何使用 VueX
- 使用 axios 发送ajax 请求
- Vue3.0(正式版) + TS
- 你好 Typescript: 进入类型的世界
- 什么是 Typescript
- 为什么要学习 Typescript
- 安装 Typescript
- 原始数据类型和 Any 类型
- 数组和元组
- Interface- 接口初探
- 函数
- 类型推论 联合类型和 类型断言
- class - 类 初次见面
- 类和接口 - 完美搭档
- 枚举(Enum)
- 泛型(Generics) 第一部分
- 泛型(Generics) 第二部分 - 约束泛型
- 泛型第三部分 - 泛型在类和接口中的使用
- 类型别名,字面量 和 交叉类型
- 声明文件
- 内置类型
- 总结