Vue2.6之——組件化

這是我第20篇簡書。

1、組件通信

(1)父子通信

① props-$emit

② $refs

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

this.$refs.captcha= 'xxx'

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

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

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

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

用于組件庫的開發(fā),只能祖先給后代傳值.。
這時用props屬性就會嵌套太多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)任意兩個組件之間通信:事件總線 或 vue

事件總線:創(chuàng)建一個Bus類負責事件派發(fā)、監(jiān)聽和回調管理

// Bus:事件派發(fā)、監(jiān)聽和回調管理 
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類實現(xiàn)觀察者模式
或者直接用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 事件監(jiān)聽器。它可以通過 v-on=”$listeners” 傳入內部組件——在創(chuàng)建更高層次的組件時非常有用。
  • $attrs
    包含了父作用域中不被認為 (且不預期為) props 的特性綁定 (class 和 style 除外)。當一個組件沒有聲明任何 props 時,這里會包含所有父作用域的綁定 (class 和 style 除外),并且可以通過 v-bind=”$attrs” 傳入內部組件——在創(chuàng)建更高層次的組件時非常有用。

舉例一:
想象一下,你打算封裝一個自定義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組件實現(xiàn)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  傳了一個靜態(tài)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、內容分發(fā)slot插槽

插槽語法是Vue實現(xiàn)的內容分發(fā)API,用于復合組件開發(fā),在通用組件庫開發(fā)中大量應用。
(注:Vue 2.6.0之后采用全新v-slot語法取代之前的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>    
    <!-- 默認插槽用default做參數(shù) -->    
    <template v-slot:default>匿名插槽</template>    
    <!-- 具名插槽用插槽名做參數(shù) -->    
    <template v-slot:content>具名插槽的內容...</template> 
</Comp2>

(3)作用域插槽

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

// 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">        
      來自子組件數(shù)據(jù):{{slotProps.foo}}   
   </template> 
  // slotProps這個命名可以隨便起,或者直接解構:
   <template v-slot:content="{foo}">        
      來自子組件數(shù)據(jù):{{foo}}   
   </template> 

  
</Comp3>

3、sync修飾符

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

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

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

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

4、實戰(zhàn)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() {
      // 創(chuàng)建彈窗實例
      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避免頂層容器繼承屬性。
  • 實現(xiàn):
    雙向綁定::value 、@input
    派發(fā)校驗事件
<template>
    <div>
        <!-- 自定義組件要實現(xiàn)v-model必須實現(xiàn):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) {
                // 通知父組件數(shù)值變化
                this.$emit('input', e.target.value);

                // 通知FormItem校驗
                // 此處用$parent派發(fā)事件不夠健壯,因為如果在嵌套一層標簽就派發(fā)不到了。
                // 可看下elementUI form表單的源碼,在下面
            
                this.$parent.$emit('validate');
            }
        },
    }
</script>

<style lang="scss" scoped>

</style>

elementUI form表單input部分源碼:

  // 派發(fā),就是子組件向父組件派發(fā)事件
    dispatch (componentName, eventName, params) {
      // 獲取當前組件的父組件
      var parent = this.$parent || this.$root
      // 拿到父組件名稱
      var name = parent.$options.componentName
      // 通過循環(huán)的方式不斷向父組件查找目標組件
      while (parent && (!name || name !== componentName)) {
        parent = parent.$parent
        if (parent) {
          name = parent.$options.componentName
        }
      }
      // 當循環(huán)結束,證明目標父組件已找到(如果存在),就通知父組件觸發(fā)相應事件
      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:

  • 實現(xiàn):
    給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() {
    // 監(jiān)聽校驗事件、并執(zhí)行監(jiān)聽
    this.$on("validate", () => {
      this.validate();
    });
  },
  methods: {
    validate() {
      // 執(zhí)行組件校驗
      // 1.獲取校驗規(guī)則
      const rules = this.form.rules[this.prop];

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

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

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

彈窗類組件的特點:

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

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

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

未完待續(xù) 。。。

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

友情鏈接更多精彩內容