ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[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盒子的宽度** 呢, **原理** :**左面盒子位置不变,只改变宽度,右边盒子只改变位置** ,所以只需要 **动态修改&nbsp;** **.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** ,可以直接修改数据。