vue组件开发

通用组件/事件/插槽/组件设计

Posted by SkioFox on March 5, 2018

几个vue的概念

  1. vue是单项数据流,不是双向数据流
  2. Vue的双向绑定仅仅是语法糖,实际是绑定数据和事件(v-model实际是:value和@input的结合)
  3. Object.defineProperty是用来做响应式更新的,于双向绑定无关

组件注册

  1. 全局注册
Vue.component('test-comp', { })
Vue.component('testComp', { })

// 使用
<div>
    <test-comp></test-comp>
    <test-comp></test-comp>
    <test-comp></test-comp>
</div>
  1. 局部
import testComp from 'xxx';
new Vue({
    el: '#app',
    components: {
        testComp;
    }   
})

父子传值:Props和$emit

  1. 通过v-bind:xxx来传递数据,组件内部通过props接收,同时可以验证(type/required/validator)。
  props: {
      // fooA只接受数值类型的参数
      fooA: Number,
      // fooB可以接受字符串和数值类型的参数
      fooB: [String, Number],
      // fooC可以接受字符串类型的参数,并且这个参数必须传入
      fooC: {
          type: String,
          required: true
     },
     // fooD接受数值类型的参数,如果不传入的话默认就是100
     fooD: {
         type: Number,
         default: 100
     },
     // fooE接受对象类型的参数
     fooE: {
         type: Object,
         // 当为对象类型设置默认值时必须使用函数返回
         default: function(){
             return { message: 'Hello, world' }
         }
     },
     // fooF使用一个自定义的验证器
     fooF: {
         validator: function(value){
             return value>=0 && value<=100;
         }
     }
 }
  1. props属于单项数据流,只能通过父级修改,组件不能自己修改父级传入的props。
  • 只能修改data定义内的值。如果需要修改需要通过自定义事件通知父级,由父级来修改。
// k-button
<template>
    <button @click="handleClick">test</button>
</template>
<script>
    export default {
        methods: {
            handleClick() {
                this.$emit('toParent', {msg:'hello'})
            }
        },
    }
</script>

// parents
<template>
    <div>
        <k-button @toParent="handleClick"></k-button>
    </div>
</template>

<script>

import KButton from './components/Button.vue';

export default {
  name: "app",
  components: {
    KButton,
  },
  methods: {
    handleClick(msg){
      console.log(msg);
    }
  }
};
</script>

v-model示例以及实现原理=>多v-model绑定实现(.sync)

// v-model是一个特殊的属性,相当于绑定了 :value和@input事件

<custom-input v-model="searchText"></custom-input>

<custom-input :value="searchText" @input="searchText = $event"></custom-input>

// PersonalInfo.vue 子组件

<template>
  <div>
    <select
      :value="phoneInfo.areaCode"
      placeholder="区号"
      @change="handleAreaCodeChange"
    >
      <option value="+86">+86</option>
      <option value="+60">+60</option>
    </select>
    <input
      :value="phoneInfo.phone"
      type="number"
      placeholder="手机号"
      @input="handlePhoneChange"
    />
    <input
      :value="zipCode"
      type="number"
      placeholder="邮编"
      @input="handleZipCodeChange"
    />
  </div>
</template>
<script>
export default {
  name: "PersonalInfo",
  model: {
    prop: "phoneInfo", // 默认 value
    event: "change" // 默认 input
  },
  props: {
    phoneInfo: Object,
    zipCode: String
  },
  methods: {
    handleAreaCodeChange(e) {
      this.$emit("change", {
        ...this.phoneInfo,
        areaCode: e.target.value
      });
    },
    handlePhoneChange(e) {
      this.$emit("change", {
        ...this.phoneInfo,
        phone: e.target.value
      });
    },
    handleZipCodeChange(e) {
      this.$emit("update:zipCode", e.target.value);
    }
  }
};
</script>

// index.vue 父组件

<template>
  <div>
    <PersonalInfo v-model="phoneInfo" :zip-code.sync="zipCode" />

    <PersonalInfo
      :phone-info="phoneInfo"
      :zip-code="zipCode"
      @change="val => (phoneInfo = val)"
      @update:zipCode="val => (zipCode = val)"
    />

    phoneInfo: 
    <br />
    zipCode: 
  </div>
</template>
<script>
import PersonalInfo from "./PersonalInfo";
export default {
  components: {
    PersonalInfo
  },
  data() {
    return {
      phoneInfo: {
        areaCode: "+86",
        phone: ""
      },
      zipCode: ""
    };
  }
};
</script>

provide and inject

  • 一般是上级组件provide,下级组件inject=>跨组件通信
  1. 主要用于高阶组件和组件库提供用例,不推荐在组件外使用
// componet A=>provide是提供数据
// 注意这里的form并不是响应式的
provide(){
    return {
        // 这里如果是form:this可以做到响应式,因为将当前组件的实例进行传递,子组件就可以获取到provide的内容
        form:"test"
    }
},
data () {
    return {
        form: "test"
    }
}
// inject注入数据 
// 子组件注入
var Child = {
    // 如果provide是this,这里获取到的就是form的整个实例
    inject: ['form'],
    created () {
        console.log(this.form) // => "test"
    }
    // ...
}
  1. vue中的this.$on监听当前实例上的自定义事件。事件可以由this.$emit触发

插槽

// index.vue

<a-tab-pane key="slot" tab="插槽">
    <h2>2.6 新语法</h2>
    <SlotDemo>
    // 普通插槽
        <p>default slot</p>
    // 具名插槽
        <template v-slot:title>
            <p>title slot1</p>
            <p>title slot2</p>
        </template>
    // 作用域插槽
        <template v-slot:item="props">
            <p>item slot-scope </p>
        </template>
    </SlotDemo> 
    <br />
    <h2>老语法</h2>
    <SlotDemo>
    // 普通插槽
        <p>default slot</p>
    // 具名插槽
        <p slot="title">title slot1</p>
        <p slot="title">title slot2</p>
    // 作用域插槽
        <p slot="item" slot-scope="props">item slot-scope </p>
    </SlotDemo>
</a-tab-pane>

// slot.vue

<template>
  <div>
    <slot />
    <slot name="title" />
    <slot name="item" v-bind="{ value: 'vue' }" />
  </div>
</template>

<script>
export default {
  name: "SlotDemo"
};
</script>

dispatch and $broadcast (vue 2.0已弃用,但是组件库开发中依然可用)

  • 原理就是可以通过this.$parent和this.​$children来获取父组件和子组件,递归一下
  • 缺点:和其他基于组件树结构的事件流方式一样,缺乏扩展性
  1. dispatch=>递归获取$parent,向上触发
<button @click="dispatch('dispatch','哈喽 我是GrandGrandChild1')">dispatch</button>
methods: {

    dispatch(eventName, data) {
      let parent = this.$parent
      // 查找父元素
      while (parent ) {
        if (parent) {
          // 父元素用$emit触发
          parent.$emit(eventName,data)
          // 递归查找父元素
          parent = parent.$parent
        }else{
          break
        }
      }
 
    }
  }

注意只向上传递了,并没有影响别的元素

  1. boardcast,递归获取$children 来向所有子元素广播
<button @click="$boardcast('boardcast','我是Child1')">广播子元素</button>
function boardcast(eventName, data){
  this.$children.forEach(child => {
    // 子元素触发$emit
    child.$emit(eventName, data)
    if(child.$children.length){
      // 递归调用,通过call修改this指向 child
      boardcast.call(child, eventName, data)
    }
  });
}
{
  methods: {

    $boardcast(eventName, data) {
      boardcast.call(this,eventName,data)
    }
  }
}
  1. 全局挂载dispatch和boardcast=>便于全局使用,将其挂载到Vue的原型链上
  • main.js
Vue.prototype.$dispatch =  function(eventName, data) {
  let parent = this.$parent
  // 查找父元素
  while (parent ) {
    if (parent) {
      // 父元素用$emit触发
      parent.$emit(eventName,data)
      // 递归查找父元素
      parent = parent.$parent
    }else{
      break
    }
  }
}

Vue.prototype.$boardcast = function(eventName, data){
  boardcast.call(this,eventName,data)
}
function boardcast(eventName, data){
  this.$children.forEach(child => {
    // 子元素触发$emit
    child.$emit(eventName, data)
    if(child.$children.length){
      // 递归调用,通过call修改this指向 child
      boardcast.call(child, eventName, data)
    }
  });
}