Vue2.6之——组件化

这是我第20篇简书。

1、组件通信

(1)父子通信

① props-$emit

② $refs

短信验证码、图形验证码组件我经常用$refs

this.$refs.captcha= 'xxx'

③ 子组件$children[0]
并不保证顺序,所以从来不用这个方法,除非只有一个子组件。

// parent 
this.$children[0].xx = 'xxx'
(2)兄弟通信

通过共同的祖辈组件搭桥,$parent$root
$root$parent 都能够实现访问父组件的属性和方法,两者的区别在于,如果存在多级子组件,通过$parent访问得到的是它最近的一级的父组件,通过$root得到的是它的根父组件。

brother1:
this.$parent.$emit('foo');
brother2:
this.$parent.$on('foo', handle) 
(3)祖先与后代通信

用于组件库的开发,只能祖先给后代传值.。
这时用props属性就会嵌套太多props,不是很合适。

祖先: provide() {
        return {hi: 'hello 后代'}
      }
后代:inject:['hi'] 
-----------------------------------
可直接返回this,子组件直接拿祖先的数据:
祖先: 
  provide() {
      return {hi: this}
  },
  data() {
    return {
        grandfa:'dxl'
    }
  }
后代:
  <p>{{hi.grandfa}}</p>
  inject:['hi'] 
(4)任意两个组件之间通信:事件总线 或 vue

事件总线:创建一个Bus类负责事件派发、监听和回调管理

// Bus:事件派发、监听和回调管理 
class Bus{ 
     constructor(){    
    // {    
    //   eventName1:[fn1,fn2],   
    //   eventName2:[fn3,fn4],   
    // }    
    this.callbacks = {}  
    } 
   $on(name, fn){    
      this.callbacks[name] = this.callbacks[name] || []    
      this.callbacks[name].push(fn) 
   } 
   $emit(name, args){   
     if(this.callbacks[name]){     
     this.callbacks[name].forEach(cb => cb(args))    
    }  
  } 
}
 
// main.js 
Vue.prototype.$bus = new Bus()
以上自定义bus类实现观察者模式
或者直接用vue实例即可。
Vue.prototype.$bus = new Vue()
// child1
this.$bus.$on('foo', handle) 
// child2
this.$bus.$emit('foo')

(5)$attrs$listeners(基本被Vuex替代了)
  • $listeners
    包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on=”$listeners” 传入内部组件——在创建更高层次的组件时非常有用。
  • $attrs
    包含了父作用域中不被认为 (且不预期为) props 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 props 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind=”$attrs” 传入内部组件——在创建更高层次的组件时非常有用。

举例一:
想象一下,你打算封装一个自定义input组件——MyInput,需要从父组件传入type,placeholder,title等多个html元素的原生属性。此时你的MyInput组件props如下:

props:['type','placeholder','title',...]

很繁琐不是吗?$attrs专门为了解决这种问题而诞生,这个属性允许你在使用自定义组件时更像是使用原生html元素。比如:

// 父组件
<my-input placeholder="请输入你的姓名" type="text" title="姓名" v-model="name"/>

// 子组件
<template>
  <div>
    <label>姓名:</label>
    <input v-bind="$attrs" :value="value" @input="$emit('input',$event.target.value)"/>
  </div>
</template>
<script>
export default {
  inheritAttrs:false,
  props:['value']
}
</script>

让MyInput组件实现focus事件:

// 父组件
<my-input @focus="focus" placeholder="请输入你的姓名" type="text" title="姓名" v-model="name"/>

// 子组件
<template>
  <div>
     <input v-bind="$attrsAll" v-on="$listenserAll"/>
  </div>
</template>
<script>
export default {
  inheritAttrs:false,
  props:['value'],
  computed:{
     $attrsAll() {
      return {
        value: this.value,
        ...this.$attrs
      }
    },
    $listenserAll(){
      return Object.assign({},
        this.$listeners,
        {input:(event) => 
           this.$emit('input',event.target.value)
        }
      )
    }
  }
}
</script>


举例二:
三个组件:Grandfa、Father、Son
Grandfa => Son: (爷爷给孙子传值)
通过$attrs传值

// Grandfa  传了一个静态placeholder值: 请输入
<div id="app">
  {{value}}
  <wrapper v-on:focus="onFocus" v-bind:value="value" v-on:input="onFocus" placeholder="请输入">
  </wrapper>
</div>

// Father
Vue.component("Wrapper",{
   template:`
    <div>
        <son v-bind="$attrs" v-on="$listeners"></son>
    </div>
   `
});

// Son
Vue.component("son",{
   template:`
    <div>
       <button @click="handleClick">sonbutton</button>
       <input type="text" v-bind="$attrs" v-on="rewriteListener">
    </div>
   `,
});

Son => Grandfa: (孙子通知爷爷)


 computed: {
      rewriteListener() {
          const vm = this;
          return Object.assign({},
              this.$listeners,
              {
                input: (event) =>
                vm.$emit("input", event.target.value)
              }
          )
      }
  }

2、内容分发slot插槽

插槽语法是Vue实现的内容分发API,用于复合组件开发,在通用组件库开发中大量应用。
(注:Vue 2.6.0之后采用全新v-slot语法取代之前的slotslot-scope

(1)匿名插槽
// comp1
 <div>   
   <slot></slot>
 </div>

// parent
 <Comp1>hello</Comp1>

(2)具名插槽
// comp2 
<div>    
    <slot></slot>   
    <slot name="content"></slot> 
</div>
 
// parent 
<Comp2>    
    <!-- 默认插槽用default做参数 -->    
    <template v-slot:default>匿名插槽</template>    
    <!-- 具名插槽用插槽名做参数 -->    
    <template v-slot:content>具名插槽的内容...</template> 
</Comp2>

(3)作用域插槽

以上两个子组件的插槽值只能由父组件决定安排,但是我们的实际业务中,往往是儿子安排老子...这时就要用到作用域插槽。

// comp3 
 // 子组件决定值是'your name is XXX'
<div>    
    <slot :foo="your name is defaultFoo"></slot> 
    <slot name="content" :foo="your name is contentFoo"></slot> 
</div>
 
// parent 
<Comp3>    
    <!-- 把v-slot的值指定为作用域上下文对象 -->    
   <template v-slot:default="slotProps">        
      来自子组件数据:{{slotProps.foo}}   
   </template> 
  // slotProps这个命名可以随便起,或者直接解构:
   <template v-slot:content="{foo}">        
      来自子组件数据:{{foo}}   
   </template> 

  
</Comp3>

3、sync修饰符

sync修饰符添加于v2.4,类似于v-model,它能⽤于修改传递到⼦组件的属性,可以简化子组件通知父元素更新传入参数这个动作的代码逻辑。
场景:⽗组件传递的属性⼦组件想修改
所以sync修饰符的控制能⼒都在⽗级,事件名称也相对固定update:xx

// 父组件将value传给子组件并使用.sync修饰符。
<Input :value.sync="model.username">
<!-- 等效于下⾯这⾏,那么和v-model的区别只有事件名称的变化 -->
<Input :value="username" @update:value="username=$event">
<!-- 这⾥绑定属性名称更改,相应的属性名也会变化 -->
<Input :foo="username" @update:foo="username=$event">

 <!-- 绑定对象 -->
<my-com v-bind.sync="obj1"></my-com>

// 子组件触发事件:
this.$emit('update:obj1', "it is new key by my-com");
 

4、实战1:自定义表单组件

做几个自定义组件来更好的巩固知识。



index.vue:

<template>
  <div>
    <KForm :model="model" :rules="rules" ref="loginForm">
      <KFormItem label="用户名" prop="username">
        <KInput v-model="model.username"></KInput>
      </KFormItem>
      <KFormItem label="密码" prop="password">
        <KInput v-model="model.password" type="password"></KInput>
      </KFormItem>
      <KFormItem label="记住密码" prop="password">
        <KCheckBox v-model="model.remember"></KCheckBox>
        <KCheckBox :checked="model.remember" @change="model.remember = $event"></KCheckBox>
      </KFormItem>
      <KFormItem>
        <button @click="onLogin">登录</button>
      </KFormItem>
    </KForm>
    {{model}}
  </div>
</template>

<script>
import KInput from "./KInput.vue";
import KCheckBox from "./KCheckBox.vue";
import KFormItem from "./KFormItem.vue";
import KForm from "./KForm.vue";
import Notice from "../Notice";
import create from "@/utils/create";

export default {
  components: {
    KInput,
    KFormItem,
    KForm,
    KCheckBox
  },
  data() {
    return {
      model: {
        username: "tom",
        password: "",
        remember: false
      },
      rules: {
        username: [{ required: true, message: "用户名必填" }],
        password: [{ required: true, message: "密码必填" }]
      }
    };
  },
  methods: {
    onLogin() {
      // 创建弹窗实例
      let notice;
      this.$refs.loginForm.validate(isValid => {
       <!--弹窗组件在第5点讲解 -->
        notice = create(Notice, {
          title: "xxx",
          message: isValid ? "登录成功!" : "有错!!",
          duration: 3000
        });

        notice.show();
      });
    }
  }
};
</script>



KInput.vue:

  • 重点:v-bind="$attrs
    把父组件的 type="password"传了过来,但是此时会影响到div,这时就要用到 inheritAttrs,将其设为false避免顶层容器继承属性。
  • 实现:
    双向绑定::value@input
    派发校验事件
<template>
    <div>
        <!-- 自定义组件要实现v-model必须实现:value, @input -->
        <!-- $attrs存储的是props之外的部分:------{type:'password'} -->
        <input :value="value" @input="onInput" v-bind="$attrs">
    </div>
</template>

<script>
    export default {
        inheritAttrs: false, // 避免顶层容器继承属性
        props: {
            value: {
                type: String,
                default: ''
            }
        },
        methods: {
            onInput(e) {
                // 通知父组件数值变化
                this.$emit('input', e.target.value);

                // 通知FormItem校验
                // 此处用$parent派发事件不够健壮,因为如果在嵌套一层标签就派发不到了。
                // 可看下elementUI form表单的源码,在下面
            
                this.$parent.$emit('validate');
            }
        },
    }
</script>

<style lang="scss" scoped>

</style>

elementUI form表单input部分源码:

  // 派发,就是子组件向父组件派发事件
    dispatch (componentName, eventName, params) {
      // 获取当前组件的父组件
      var parent = this.$parent || this.$root
      // 拿到父组件名称
      var name = parent.$options.componentName
      // 通过循环的方式不断向父组件查找目标组件
      while (parent && (!name || name !== componentName)) {
        parent = parent.$parent
        if (parent) {
          name = parent.$options.componentName
        }
      }
      // 当循环结束,证明目标父组件已找到(如果存在),就通知父组件触发相应事件
      if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params))
      }
    },

KCheckBox.vue:

还可以通过设置model选项修改默认行为:

<template>
  <div>
    <input type="checkbox" :checked="checked" @change="onChange"/>
  </div>
</template>

<script>
export default {
  props: {
    checked: {
      type: Boolean,
      default: false
    }
  },
  model: {
    prop: "checked",
    event: "change"
  },
  methods: {
      onChange(e) {          
          this.$emit('change', e.target.checked)
      }
  },
};
</script>

KFormItem.vue:

  • 实现:
    给Input预留插槽 - slot
    能够展示label和校验信息
    能够进行校验
<template>
  <div>
    <label v-if="label">{{label}}</label>
    <slot></slot>
    <!-- 校验信息 -->
    <p v-if="errorMessage">{{errorMessage}}</p>
  </div>
</template>

<script>
import Schema from "async-validator";

export default {
  data() {
    return {
      errorMessage: ""
    };
  },
  inject: ["form"],
  props: {
    label: {
      type: String,
      default: ""
    },
    prop: String
  },
  mounted() {
    // 监听校验事件、并执行监听
    this.$on("validate", () => {
      this.validate();
    });
  },
  methods: {
    validate() {
      // 执行组件校验
      // 1.获取校验规则
      const rules = this.form.rules[this.prop];

      // 2.获取数据
      const value = this.form.model[this.prop];

      // 3.执行校验
      const desc = {
        [this.prop]: rules
      };
      const schema = new Schema(desc);
      //   参数1是值,参数2是校验错误对象数组
    //   返回的Promise<boolean>
      return schema.validate({ [this.prop]: value }, errors => {
        if (errors) {
          // 有错
          this.errorMessage = errors[0].message;
        } else {
          // 没错,清除错误信息
          this.errorMessage = "";
        }
      });
    }
  }
};
</script>

5、实战2:弹窗类组件

弹窗类组件的特点:

  • 在当前vue实例之外独立存在,通常挂载与body
  • 通过js动态创建,不需要在任何组件中声明

----------- create.js:--------------

import Vue from 'vue'
// 创建指定组件实例并挂载于body上
/**
* Component 组件
* props 属性值
*/
export default function create(Component, props) {
   // 0. 先创建vue实例
   const vm = new Vue({
     // render方法提供给我们一个h函数,它可以渲染VNode
     render(h) {
         return h(Component, {props})
     }
   }).$mount(); // 更新操作
   // 1. 上面vm帮我们创建组件实例
   // 2. 通过$children获取该组件实例
   cosole.log(vm.$root);
   const comp = vm.$children[0];
   // 3.追加至body
   document.body.appendChild(vm.$el);
   // 4.清理函数
   comp.remove = () => {
     document.body.removeChild(vm.$el);
     vm.$destroy();
   }
   return comp;
}

----------- notice.vue:--------------

<template>
  <div v-if="isShow">
    <h3>{{title}}</h3>
    <p>{{message}}</p>
  </div>
</template>

<script>
export default {
  props: {
    title: {
      type: String,
      default: ""
    },
    message: {
      type: String,
      default: ""
    },
    duration: {
      type: Number,
      default: ""
    }
  },
  data() {
    return {
      isShow: false
    };
  },
  methods: {
    show() {
      this.isShow = true;
      setTimeout(() => {
          this.hide()
      }, this.duration);
    },
    hide() {
      this.isShow = false;
      this.remove();
      (对应create.js里的remove清理函数)
    }
  }
};
</script>

未完待续 。。。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260