Vue2.6之——組件化

這是我第20篇簡(jiǎn)書。

1、組件通信

(1)父子通信

① props-$emit

② $refs

短信驗(yàn)證碼、圖形驗(yàn)證碼組件我經(jīng)常用$refs

this.$refs.captcha= 'xxx'

③ 子組件$children[0]
并不保證順序,所以從來(lái)不用這個(gè)方法,除非只有一個(gè)子組件。

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

通過(guò)共同的祖輩組件搭橋,$parent$root。
$root$parent 都能夠?qū)崿F(xiàn)訪問(wèn)父組件的屬性和方法,兩者的區(qū)別在于,如果存在多級(jí)子組件,通過(guò)$parent訪問(wèn)得到的是它最近的一級(jí)的父組件,通過(guò)$root得到的是它的根父組件。

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

用于組件庫(kù)的開(kāi)發(fā),只能祖先給后代傳值.。
這時(shí)用props屬性就會(huì)嵌套太多props,不是很合適。

祖先: provide() {
        return {hi: 'hello 后代'}
      }
后代:inject:['hi'] 
-----------------------------------
可直接返回this,子組件直接拿祖先的數(shù)據(jù):
祖先: 
  provide() {
      return {hi: this}
  },
  data() {
    return {
        grandfa:'dxl'
    }
  }
后代:
  <p>{{hi.grandfa}}</p>
  inject:['hi'] 
(4)任意兩個(gè)組件之間通信:事件總線 或 vue

事件總線:創(chuàng)建一個(gè)Bus類負(fù)責(zé)事件派發(fā)、監(jiān)聽(tīng)和回調(diào)管理

// Bus:事件派發(fā)、監(jiān)聽(tīng)和回調(diào)管理 
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類實(shí)現(xiàn)觀察者模式
或者直接用vue實(shí)例即可。
Vue.prototype.$bus = new Vue()
// child1
this.$bus.$on('foo', handle) 
// child2
this.$bus.$emit('foo')

(5)$attrs$listeners(基本被Vuex替代了)
  • $listeners
    包含了父作用域中的 (不含 .native 修飾器的) v-on 事件監(jiān)聽(tīng)器。它可以通過(guò) v-on=”$listeners” 傳入內(nèi)部組件——在創(chuàng)建更高層次的組件時(shí)非常有用。
  • $attrs
    包含了父作用域中不被認(rèn)為 (且不預(yù)期為) props 的特性綁定 (class 和 style 除外)。當(dāng)一個(gè)組件沒(méi)有聲明任何 props 時(shí),這里會(huì)包含所有父作用域的綁定 (class 和 style 除外),并且可以通過(guò) v-bind=”$attrs” 傳入內(nèi)部組件——在創(chuàng)建更高層次的組件時(shí)非常有用。

舉例一:
想象一下,你打算封裝一個(gè)自定義input組件——MyInput,需要從父組件傳入type,placeholder,title等多個(gè)html元素的原生屬性。此時(shí)你的MyInput組件props如下:

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

很繁瑣不是嗎?$attrs專門為了解決這種問(wèn)題而誕生,這個(gè)屬性允許你在使用自定義組件時(shí)更像是使用原生html元素。比如:

// 父組件
<my-input placeholder="請(qǐng)輸入你的姓名" 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組件實(shí)現(xiàn)focus事件:

// 父組件
<my-input @focus="focus" placeholder="請(qǐng)輸入你的姓名" 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>


舉例二:
三個(gè)組件:Grandfa、Father、Son
Grandfa => Son: (爺爺給孫子傳值)
通過(guò)$attrs傳值

// Grandfa  傳了一個(gè)靜態(tài)placeholder值: 請(qǐng)輸入
<div id="app">
  {{value}}
  <wrapper v-on:focus="onFocus" v-bind:value="value" v-on:input="onFocus" placeholder="請(qǐng)輸入">
  </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、內(nèi)容分發(fā)slot插槽

插槽語(yǔ)法是Vue實(shí)現(xiàn)的內(nèi)容分發(fā)API,用于復(fù)合組件開(kāi)發(fā),在通用組件庫(kù)開(kāi)發(fā)中大量應(yīng)用。
(注:Vue 2.6.0之后采用全新v-slot語(yǔ)法取代之前的slot、slot-scope

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

// parent
 <Comp1>hello</Comp1>

(2)具名插槽
// comp2 
<div>    
    <slot></slot>   
    <slot name="content"></slot> 
</div>
 
// parent 
<Comp2>    
    <!-- 默認(rèn)插槽用default做參數(shù) -->    
    <template v-slot:default>匿名插槽</template>    
    <!-- 具名插槽用插槽名做參數(shù) -->    
    <template v-slot:content>具名插槽的內(nèi)容...</template> 
</Comp2>

(3)作用域插槽

以上兩個(gè)子組件的插槽值只能由父組件決定安排,但是我們的實(shí)際業(yè)務(wù)中,往往是兒子安排老子...這時(shí)就要用到作用域插槽。

// 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的值指定為作用域上下文對(duì)象 -->    
   <template v-slot:default="slotProps">        
      來(lái)自子組件數(shù)據(jù):{{slotProps.foo}}   
   </template> 
  // slotProps這個(gè)命名可以隨便起,或者直接解構(gòu):
   <template v-slot:content="{foo}">        
      來(lái)自子組件數(shù)據(jù):{{foo}}   
   </template> 

  
</Comp3>

3、sync修飾符

sync修飾符添加于v2.4,類似于v-model,它能?于修改傳遞到?組件的屬性,可以簡(jiǎn)化子組件通知父元素更新傳入?yún)?shù)這個(gè)動(dòng)作的代碼邏輯。
場(chǎng)景:?組件傳遞的屬性?組件想修改
所以sync修飾符的控制能?都在?級(jí),事件名稱也相對(duì)固定update:xx。

// 父組件將value傳給子組件并使用.sync修飾符。
<Input :value.sync="model.username">
<!-- 等效于下?這?,那么和v-model的區(qū)別只有事件名稱的變化 -->
<Input :value="username" @update:value="username=$event">
<!-- 這?綁定屬性名稱更改,相應(yīng)的屬性名也會(huì)變化 -->
<Input :foo="username" @update:foo="username=$event">

 <!-- 綁定對(duì)象 -->
<my-com v-bind.sync="obj1"></my-com>

// 子組件觸發(fā)事件:
this.$emit('update:obj1', "it is new key by my-com");
 

4、實(shí)戰(zhàn)1:自定義表單組件

做幾個(gè)自定義組件來(lái)更好的鞏固知識(shí)。



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() {
      // 創(chuàng)建彈窗實(shí)例
      let notice;
      this.$refs.loginForm.validate(isValid => {
       <!--彈窗組件在第5點(diǎn)講解 -->
        notice = create(Notice, {
          title: "xxx",
          message: isValid ? "登錄成功!" : "有錯(cuò)??!",
          duration: 3000
        });

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



KInput.vue:

  • 重點(diǎn):v-bind="$attrs
    把父組件的 type="password"傳了過(guò)來(lái),但是此時(shí)會(huì)影響到div,這時(shí)就要用到 inheritAttrs,將其設(shè)為false避免頂層容器繼承屬性。
  • 實(shí)現(xiàn):
    雙向綁定::value@input
    派發(fā)校驗(yàn)事件
<template>
    <div>
        <!-- 自定義組件要實(shí)現(xiàn)v-model必須實(shí)現(xiàn):value, @input -->
        <!-- $attrs存儲(chǔ)的是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) {
                // 通知父組件數(shù)值變化
                this.$emit('input', e.target.value);

                // 通知FormItem校驗(yàn)
                // 此處用$parent派發(fā)事件不夠健壯,因?yàn)槿绻谇短滓粚訕?biāo)簽就派發(fā)不到了。
                // 可看下elementUI form表單的源碼,在下面
            
                this.$parent.$emit('validate');
            }
        },
    }
</script>

<style lang="scss" scoped>

</style>

elementUI form表單input部分源碼:

  // 派發(fā),就是子組件向父組件派發(fā)事件
    dispatch (componentName, eventName, params) {
      // 獲取當(dāng)前組件的父組件
      var parent = this.$parent || this.$root
      // 拿到父組件名稱
      var name = parent.$options.componentName
      // 通過(guò)循環(huán)的方式不斷向父組件查找目標(biāo)組件
      while (parent && (!name || name !== componentName)) {
        parent = parent.$parent
        if (parent) {
          name = parent.$options.componentName
        }
      }
      // 當(dāng)循環(huán)結(jié)束,證明目標(biāo)父組件已找到(如果存在),就通知父組件觸發(fā)相應(yīng)事件
      if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params))
      }
    },

KCheckBox.vue:

還可以通過(guò)設(shè)置model選項(xiàng)修改默認(rèn)行為:

<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:

  • 實(shí)現(xiàn):
    給Input預(yù)留插槽 - slot
    能夠展示label和校驗(yàn)信息
    能夠進(jìn)行校驗(yàn)
<template>
  <div>
    <label v-if="label">{{label}}</label>
    <slot></slot>
    <!-- 校驗(yàn)信息 -->
    <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() {
    // 監(jiān)聽(tīng)校驗(yàn)事件、并執(zhí)行監(jiān)聽(tīng)
    this.$on("validate", () => {
      this.validate();
    });
  },
  methods: {
    validate() {
      // 執(zhí)行組件校驗(yàn)
      // 1.獲取校驗(yàn)規(guī)則
      const rules = this.form.rules[this.prop];

      // 2.獲取數(shù)據(jù)
      const value = this.form.model[this.prop];

      // 3.執(zhí)行校驗(yàn)
      const desc = {
        [this.prop]: rules
      };
      const schema = new Schema(desc);
      //   參數(shù)1是值,參數(shù)2是校驗(yàn)錯(cuò)誤對(duì)象數(shù)組
    //   返回的Promise<boolean>
      return schema.validate({ [this.prop]: value }, errors => {
        if (errors) {
          // 有錯(cuò)
          this.errorMessage = errors[0].message;
        } else {
          // 沒(méi)錯(cuò),清除錯(cuò)誤信息
          this.errorMessage = "";
        }
      });
    }
  }
};
</script>

5、實(shí)戰(zhàn)2:彈窗類組件

彈窗類組件的特點(diǎn):

  • 在當(dāng)前vue實(shí)例之外獨(dú)立存在,通常掛載與body
  • 通過(guò)js動(dòng)態(tài)創(chuàng)建,不需要在任何組件中聲明

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

import Vue from 'vue'
// 創(chuàng)建指定組件實(shí)例并掛載于body上
/**
* Component 組件
* props 屬性值
*/
export default function create(Component, props) {
   // 0. 先創(chuàng)建vue實(shí)例
   const vm = new Vue({
     // render方法提供給我們一個(gè)h函數(shù),它可以渲染VNode
     render(h) {
         return h(Component, {props})
     }
   }).$mount(); // 更新操作
   // 1. 上面vm幫我們創(chuàng)建組件實(shí)例
   // 2. 通過(guò)$children獲取該組件實(shí)例
   cosole.log(vm.$root);
   const comp = vm.$children[0];
   // 3.追加至body
   document.body.appendChild(vm.$el);
   // 4.清理函數(shù)
   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();
      (對(duì)應(yīng)create.js里的remove清理函數(shù))
    }
  }
};
</script>

未完待續(xù) 。。。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容