七日打卡 | Vue 项目中组件的封装思想

1,460 阅读3分钟

前言

闲看掘金的时候发现社区推了七日打卡这个活动、本着重在参与的心态加入了打卡大军、接下来我将会连续七日分享 Vue 的高阶用法、让我们在编写组件的时候更具有灵活性、提高组件的可扩展、可拆分|组合的能力。

七日计划:

  • 七日打卡 | Vue 项目中组件的封装思想
  • 七日打卡 | Vue.mixin、Vue.exend 进阶用法
  • 七日打卡 | Vue.$watch、Vue.computed 组合用法
  • 七日打卡 | Vue 动态、递归、异步组件
  • 七日打卡 | Vue slots 内容分发
  • 七日打卡 | Vue JSX & Functional 编写高阶函数(组件)
  • 七日打卡 | Vue 高阶用法最佳实践

其实该系列的第一篇文章并不是这个,在10号晚上看到掘金的七日打卡活动,我打算先写篇相对比较基础但也相对比较重要的组件通信,可其实对于组件通信其实相关文章太多了,已至于我写完整篇文章想要发出来时我内心还是很忐忑的,一眼看上去都是文档上有的东西,所以就晚上下班之后修改了文章内容,本文会先讲下实际封装组件中不可或缺的组件通信方式,再结合真实项目讲上面的知识点在中的一些好的用法及可能会遇到的问题及解决方案,因为技术有限,如果文中有哪块写的不太对,欢迎大家指出、笔者也会立马响应

组件通信

对于组件之间通信有这么两个维度:

  • 组件之间有引用:父子组件
  • 组件之间没有引用:兄弟组件、隔代组件

所以组件之间的通信也就是上面三个角色之间的相互通信,可以实现的方式:

  • props/emitemit on | Event Bus
  • parent/parent/children/$root/refs
  • atttrs/atttrs/listeners
  • provied/inject
  • vuex/Storage
  • slots

parent/parent/children/$root/refs

  • $parent组件父实例
  • $children 实例子组件
  • $root 实例根组件,如果没有就是自身
  • $refs 如果引用的不是DOM元素就是组件实例

这几种方式都不是响应式的且真实项目都用的不多,也就$refs用的多一点,官方文档都有,这里不过多讲了

有引用关系之间的组件通信

props/$emit

父:props 向下传递数据
子:通过$emit向上抛事件,携带数据
父:再通过$on监听自定义事件,获取事件传递的数据

演示

A 组件

<template>
  <div class="A">
    我是A
    <B  :list="list" :name="name" @handlerClick="handlerClick" />
  </div>
</template>
<script>
import B from './B'
export default {
  name: 'A',
  components: { B },
  data() {
    return {
      name: '北歌',
      list: [{name:'Beige', ..xxxx}, {...}]
    }
  },
  methods: {
    // 通过监听自定义事件接受子组件抛出来的数据
    handlerClick({row, index}, callBack) { 
       callBack(this.list[index]) 
       // 做一系列的事情
    },
  }
}
</script>

B 组件

<template>
  <div class="B">
    我是{{name}}
    <div 
      class="title"
      @click="clickHandler(item, $i)"
      v-for="(item, $i) of list"
      :key="item.id"
    >
      {{item.name}}
    </div>
    <p>foo:{{ foo }}</p>
  </div>
</template>

<script>
export default {
  name: 'B',
  props: ['list', 'name'], // 接受父组件传递的数据
  methods: {
    clickHandler(item, $i) {
      // 向父组件传递数据
      this.$emit('handlerClick', {item, index: $i}, this.callBack) 
    },
    callBack(item) {
      console.log(item)
    }
  }
}
</script>

这种方式算是日常中用的比较多的,但是也会有遇到一些问题

  • $event 的不同表现
  • 对于基本数据类型: 如弹框显示/隐藏,能不能简单点?
  • 如果组件嵌套很深,我想获取最底层的数据,难道要一直向上抛事件吗?

$event 的不同表现

@click="clickHandler($event)"   clickHandler(e) {e => 事件对象}
@customClick="clickHandler($event)" clickHandler(...data) { data => 子组件抛出来的数据}

这个时候就会有一个问题,如果我监听自定义事件,不但需要子组件的数据、我还需要当前事件的事件对象、这个事件方法(clickHandler)我还需要传递其他参数进去该怎么做呢??

最典型的就是表格获取行数据的同时获取子组件抛出的数据,如图在我们新增一行的时候,这个弹框弹出来的就是我们引用的子组件,我们需要获取子组件的数据,也需要获取整行的数据

所以这个时候我们父组件A监听自定义事件还需要传递数据

<template #documentType="{row}">
 <!-- 监听弹框选中的事件 -->
	<B :foo="foo" @confirm="confirmHandler($event, row)" />
	<!-- 此时的$event就不是事件对象了,是子组件抛出的数据,如果没有抛出任何数据$event就是undefined -->
</template>

这个时候就遇到了一个问题,我需要当前自定义事件的事件对象,子组件没抛出来我是获取不到的,所以我们在封装一些特殊的组件(如 DragDialog)的时候一定要记得抛出事件对象

clickHandler(item, $i, e) {
	// 向父组件传递数据
	this.$emit('handlerClick', { item, index: $i, event: e }) 
},

使用.sync简化自定义事件

封装弹框组件的时候,弹框的显示和隐藏一般都是传递一个变量、子组件抛出事件,父组件改变变量,通过.sync语法糖可以简化此类操作

常规操作

Vue.component('BaseDialog', {
    template: '<div @close="handleClose">content...</div>',
  	props: {
      show: {
      	type: Boolean, // 是否弹出窗口
      	required: true
    	}
    }
    data() {
      return {dialogShow: this.show}
    },
    methods: {
      handleClose() {
        this.dialogShow = false
        this.$emit('close', this.dialogShow);
      }
    }
});

<BaseDialog :show="isShow" @close="isShow = false" />

简化操作

Vue.component('BaseDialog', {
    template: '<div @close="dialogShow = false">content...</div>',
  	props: {
      show: {
      	type: Boolean, // 是否弹出窗口
      	required: true
    	}
    }
    watch: {
    	show(val) {
      	this.dialogShow = val
    	},
    	dialogShow(val) {
      	this.$emit('update:show', val)
    	}
   	}
});

<BaseDialog :show.sync="isShow" /> // => 等同于下面
<BaseDialog :show="isShow" @update:show="(show) => (isShow = show)" />

provied/inject

真实项目中,组件的引用关系相对没有那么的简单,组件可能会深层次的嵌套,这个想必大家都知道,所以provied/inject这种方式也就出来了

  • provide: 通过这种方式注入数据,且一直向下穿透
  • inject:只有和组件有引用关系就可以获取上面组件注入的数据

用起来非常简单

var app=new Vue({
    el:'#app',
    template:`
        <div>
            <parent></parent>
        </div>
    `
})

Vue.component('A',{
        template:`
            <div>
                <p>A组件</p>
                <B/>
            </div>
        `,
        provide(){ // 祖先组件向下注入数据
        	return {FormModel: this.form}
        },
        data(){
            return {
                form:{表单对象数据}
            }
        }
})


Vue.component('B',{
inject: { // 子组件获取“上面”注入下来的数据
    FormModel: { default: {}} // 如果没有默认空对象,防止程序出错
},
// => 不做默认处理 inject: ['editTable'],
data(){
    return {
        FormModel:this.form
    }
  },
  template:`
    <DragDialog>
       <p>B组件</p>
        <input type="tet" v-model="form"> 
    </DragDialog>
)

Vue.component('C',{
inject: { // 只有有引用关系的都可以获取注入的数据
    FormModel: { default: {}}
  },
  data(){
    return {
        FormModel:this.form
    }
  },
  template:`
    <div>
       <p>C组件</p>
        <input type="tet" v-model="form"> 
    </div>
)

上面这种方式最常见的就是Form里面打开一个Dialog,Dialog里面又有一个Form,这种表单套娃的场景了,很多时候我们调后端的API接口其实要传递的就一个对象数据给它,我们可以将所有表单都引用formModel这个对象,省去很多对象合并一系列的数据操作。

就如图所示,从进详情页面开始所有看到的表单、包括Dialog的大表单,点击Input弹框弹出的表单,都可以放在一个Form对象里面,这个时候我们将外层的Form注入进去,里面如果需要外层数据的时候就非常容易了。

注意:provide注入的对象并不能响应式,所以我们在注入之前需要先自己做响应式处理

A组件

<template>
  <p>A 组件</p>
   <button @click="changeForm(val)">改变表单对象</button>
   <B/>
   <C/>
</template>


<script>
export default {
  data() {
    return {
      form: {
        a: '1',
        b: {b1: 1, b2: [{xxx}]}
      }
    };
  },
  provide() {
    return {
      formModel: this.form 注入的数据并不是响应式
    } 
  },
  methods: {
    changeColor(color) {
      if (color) {
        this.color = color;
      } else {
        this.color = this.color === "blue" ? "red" : "blue";
      }
    }
  }
  methods: {
    changeColor(val) {
      // 更改了form表单数据,但是B组件引用的表单对象还是最开始注入的表单对象
      form.b.b2.push(val)  
    }
  }
}
</script>

所以A组件在注入之前我们可以对注入进去的对象做响应式处理

provide() {
 // 使用observable来返回响应式式的侦听对象
 this.formModel = Vue.observable(this.form);
 return {
   formModel: this.formModel
 };
},

。。。未完,时间来不急,先发出去