01Vue組件化實戰(zhàn)

01 組件化實戰(zhàn)

組件化

vue組件系統(tǒng)提供了?種抽象维咸,讓我們可以使用獨立可復用的組件來構建大型應用,任意類型的應用界面都可以抽象為?個組件樹。組件化能提高開發(fā)效率端辱,方便重復使用,簡化調試步驟虽画,提升項目可維護性舞蔽,便于多人協同開發(fā)。

image.png

組件通信常用方式

  • props

  • event

  • vuex

自定義事件

  1. 邊界情況

    • $parent

    • $children

    • $root

    • $refs

    • provide/inject

  2. 非prop特性

    • $attrs

    • $listeners

組件通信

props

父子傳值

// child
props: { msg: String }

// parent
<HelloWord msg="測試父子傳值" />

自定義事件

子父傳值

// child
this.$emit('sonToFather', 'son-->Father')

// parent
<Cart @sonToFather="testSonToFather($event)"></Cart>

事件總線

任意兩個組件之間傳值常用事件總線或 vuex 的方式码撰。

// Bus: 事件派發(fā)渗柿、監(jiān)聽和回調管理
class Bus {
  constructor() {
    this.event = {}
  }
  // 訂閱事件
  $on (eventName, callback) {
    if (!this.event[eventName]) {
      this.event[eventName] = []
    }
    this.event[eventName].push(callback)
  }
  // 觸發(fā)事件(發(fā)布事件)
  $emit (eventName, params) {
    let eventArr = this.event[eventName]
    if (eventArr) {
      eventArr.map(item => {
        item(params)
      })
    }
  }
  // 刪除訂閱事件
  $off (eventName, callback) {
    let arr = this.event[eventName]
    if (arr) {
      if (callback) {
        let index = arr.indexOf(callback)
        arr.splice(index, 1)
      } else {
        arr.length = 0
      }
    }
  }
}

// main.js
Vue.prototype.$bus = new Bus()

// child1
this.$bus.$on('testBus',handle)

// child2
this.$bus.$emit('testBus')

實踐中通常用 Vue 代替 Bus,因為 Vue 已經實現了相應接口

vuex

組件通信最佳實踐

創(chuàng)建唯?的全局數據管理者 store脖岛,通過它管理數據并通知組件狀態(tài)變更朵栖。

parent/root

兄弟組件之間通信可通過共同祖輩搭橋,parent 或root鸡岗。

// brother1
this.$parent.$on('testParent',handle)

// brother2
this.$parent.$emit('testParent')

$children

父組件可以通過 $children 訪問子組件實現父子通信混槐。

// parent
this.$children[0].xx = 'xxx'

注意:$children 不能保證子元素順序

和 $refs 有什么區(qū)別?

attrs/listeners

包含了父作用域中不作為 prop 被識別 (且獲取) 的特性綁定 ( class 和 style 除外)轩性。當?個組件沒有
聲明任何 prop 時声登,這里會包含所有父作用域的綁定 ( class 和 style 除外),并且可以通過 v-bind="$attrs" 傳入內部組件——在創(chuàng)建高級別的組件時非常有用揣苏。

// child:并未在props中聲明foo
<p>{{$attrs.foo}}</p>

// parent
<HelloWorld foo="foo"/>

文檔

refs

獲取子節(jié)點引用

// parent
<HelloWorld ref="testRef">

mounted() {
  this.$refs.testRef.xx='xxx'
}

provide/inject

能夠實現祖先和后代之間傳值

// ancestor
provide() {
  return {foo: 'foo'}
}

// descendant
inject: ['foo']

范例:組件通信

插槽

插槽語法是 Vue 實現的內容分發(fā) API悯嗓,用于復合組件開發(fā)。該技術在通用組件庫開發(fā)中有大量應用卸察。

匿名插槽

// comp1
<div>
  <slot></slot>
</div>

// parent
<Comp>testSlot</Comp>

具名插槽

將內容分發(fā)到子組件指定位置

// comp2
<div>
  <slot></slot>
  <slot name="content"></slot>
</div>

// parent
<Comp2>
  <!-- 默認插槽用default做參數 -->
  <template v-slot:default>具名插槽</template>
  <!-- 具名插槽用插槽名做參數 -->
  <template v-slot:content>內容...</template>
</Comp2>

作用域插槽

分發(fā)內容要用到子組件中的數據

// comp3
<div>
  <slot :foo="foo"></slot>
</div>

// parent
<Comp3>
  <!-- 把v-slot的值指定為作用域上下文對象 -->
  <template v-slot:default="slotProps">來自子組件數據:{{slotProps.foo}}</template>
</Comp3>

范例:插槽

組件化實戰(zhàn)

通用表單組件

收集數據脯厨、校驗數據并提交。

需求分析

  1. 實現 KForm

    • 指定數據坑质、校驗規(guī)則
  2. KformItem

    • label 標簽添加

    • 執(zhí)行校驗

    • 顯示錯誤信息

  3. KInput

    • 維護數據

最終效果:Element 表單

范例代碼

KInput

創(chuàng)建 components/form/KInput.vue

<template>
  <div>
    <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)
      }
    }
  }
</script>

使用 KInput

創(chuàng)建 components/form/index.vue合武,添加如下代碼:

<template>
  <div>
    <h3>Form表單</h3>
    <hr>
    <k-input v-model="model.username"></k-input>
    <k-input type="password" v-model="model.password"></k-input>>
  </div>
</template>

<script>
import KInput from './KInput'

export default {
  components:{
    KInput
  },
  data(){
    return {
      model:{
        username:'tom',
        password:''
      }
    }
  }
}
</script>

實現 KFormItem

創(chuàng)建components/form/KFormItem.vue

<template>
  <div>
    <label v-if="label">{{label}}</label>
    <slot></slot>
    <p v-if="error">{{error}}</p>
  </div>
</template>

<script>
export default {
  props: {
    label:{ // 輸入項標簽
      type: String,
      default:''
    },
    prop:{ // 字段名
      type: String,
      default: ''
    }
  },
  data() {
    return {
      error: '' // 校驗錯誤
    }
  }
}
</script>

使用 KFormItem

components/form/index.vue,添加基礎代碼:

<template>
  <div>
    <h3>Form表單</h3>
    <hr>
    <k-form-item label="用戶名" prop="username">
      <k-input v-model="model.username"></k-input>
    </k-form-item>
    <k-form-item label="確認密碼" prop="password">
      <k-input type="password" v-model="model.password"></k-input>
    </k-form-item>
  </div>
</template>

實現 KForm

<template>
  <form>
    <slot></slot>
  </form>
</template>

<script>
export default {
  provide() {
    return {
      form: this // 將組件實例作為提供者涡扼,子代組件可方便獲取
    }
  },
  props:{
    model:{
      type: Object,
      required: true
    },
    rules:{
      type: Object
    }
  }
}
</script>

使用 KForm

components/form/index.vue稼跳,添加基礎代碼:

<template>
  <div>
    <h3>Form表單</h3>
    <hr/>
    <k-form :model="model" :rules="rules" ref="loginForm">
      ...
    </k-form>
  </div>
</template>

<script>
import KForm from './KForm'

export default {
  components: {
    KForm
  },
  data() {
    return {
      rules: {
        username: [{
          required: true,
          message: '請輸入用戶名'
        }],
        password: [{
          required: true,
          message: '請輸入密碼'
        }]
      }
    }
  },
  methods: {
    submitForm() {
      this.$refs['loginForm'].validate(valid => {
        if (valid) {
          alert('請求登錄')
        } else {
          alert('校驗失敗')
        }
      })
    }
  }
}
</script>

數據校驗

Input 通知校驗

onInput(e) {
  // ...
  // $parent指FormItem
  this.$parent.$emit('validate')
}

FormItem 監(jiān)聽校驗通知,獲取規(guī)則并執(zhí)行校驗

inject: ['form'], // 注入
mounted() { // 監(jiān)聽校驗事件
  this.$on('validate',() => {this.validate()})
},
methods:{
  validate() {
    // 獲取對應 FormItem 校驗規(guī)則
    console.log(this.form.rules[this.prop])
    // 獲取校驗值
    console.log(this.form.model[this.prop])
  }
}

安裝 async-validator:

npm i async-validator -S
import Schema from 'async-validator'

validate() {
  // 獲取對應 FormItem 校驗規(guī)則
  const rules = this.form.rules[this.prop]
  // 獲取校驗值
  const value = this.form.model[this.prop]
  // 校驗描述對象
  const descriptor = {[this.prop]:rules}
  // 創(chuàng)建校驗器
  const schema = new Schema(descriptor)
  // 返回 Promise吃沪,沒有觸發(fā) catch 就說明驗證通過
  return schema.validate({[this.prop]:value},errors=>{
    if (errors) {
      // 將錯誤信息顯示
      this.error = errors[0].message
    } else {
      // 校驗通過
      this.error = ''
    }
  })
}

表單全局驗證汤善,為 Form 提供 validate 方法

validate(cb){
  // 調用所有含有 prop 屬性的子組件的 validate 方法并得到 Promise 的值
  const tasks = this.$children
      .filter(item => item.prop)
      .map(item => item.validate())
  // 所有任務必須全部成功才算校驗通過魄眉,任一失敗則校驗失敗
  Promise.all(tasks)
      .then(() => cb(true))
      .catch(() => cb(false))
}

實現彈窗組件

彈窗這類組件的特點是它們在當前 vue 實例之外獨立存在逞姿,通常掛載于 body揭朝;它們是通過 JS 動態(tài)創(chuàng)建
的猪半,不需要在任何組件中聲明。常見使用姿勢:

this.$create(Notice, {
  title: '林慕-彈窗組件'
  message: '提示信息',
  duration: 1000
}).show()

create 函數

import Vue from 'vue'

// 創(chuàng)建函數接收要創(chuàng)建組件定義
function create(Component, props) {
  // 創(chuàng)建一個 Vue 實例
  const vm = new Vue({
    render(h) {
      // render 函數將傳入組件配置對象轉換為虛擬 dom
      console.log(h(Component,{props}))
      return h(Component, {props})
    }
  }).$mount() // 執(zhí)行掛載函數在旱,但未指定掛載目標摇零,表示只執(zhí)行初始化工作

  // 將生成 dom 元素追加至 body
  document.body.appendChild(vm.$el)
  // 給組件實例添加銷毀方法
  const comp = vm.$children[0]
  comp.remove = () => {
    document.body.removeChild(vm.$el)
    vm.$destroy()
  }
  return comp
}

// 暴露調用接口
export default create

另一種創(chuàng)建組件實例的方式: Vue.extend(Component)

通知組件

新建通知組件,Notice.vue

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

<script>
export default {
  props: {
    title: {
      type: String,
      default: ''
    },
    message: {
      type: String,
      default: ''
    },
    duration: {
      type: Number,
      default: 1000
    }
  },
  data() {
    return {
      isShow: false
    }
  },
  methods: {
    show() {
      this.isShow = truw
      setTimeout(this.hide, this.duration)
    },
    hide() {
      this.isShow = false
      this.remove()
    }
  }
}
</script>
<style>
.box {
  position: fixed;
  width: 100%;
  top: 16px;
  left: 0;
  text-align: center;
  pointer-events: none;
  background-color: #fff;
  border: grey 3px solid;
  box-sizing: border-box;
}
.box-content {
  width: 200px;
  margin: 10px auto;
  font-size: 14px;
  padding: 8px 16px;
  background: #fff;
  border-radius: 3px;
  margin-bottom: 8px;
}
</style>

使用 create api

測試颈渊,components/form/index.vue

<script>
import create from "@/utils/create"
import Notice from "@/components/Notice"

export default {
  methods: {
    submitForm(form) {
      this.$refs[form].validate(valid => {
        const notice = create(Notice, {
          title: '林慕-create',
          message: valid ? '請求登錄' : '校驗失敗',
          duration: 1000
        })
        notice.show()
      })
    }
  }
}
</script>

遞歸組件

// TODO

拓展

  1. 使用 Vue.extend 方式實現 create 方法
  • 方法一:和第一個 create 方法類似
export function create2 (Component, props) {
  let VueMessage = Vue.extend({
    render(h) {
      return h(Component, {props})
    }
  })
  let newMessage = new VueMessage()
  let vm = newMessage.$mount()
  let el = vm.$el
  document.body.appendChild(el) 
  const comp = vm.$children[0]
  comp.remove = () => {
    document.body.removeChild(vm.$el)
    vm.$destroy()
  }
  return comp
}
  • 方法二:利用 propsData 屬性
export function create3 (Component, props) {
  // 組件構造函數如何獲人焓颉终佛?
  // 1. Vue.extend()
  const Ctor = Vue.extend(Component)
  // 創(chuàng)建組件實例
  const comp = new Ctor({ propsData: props })
  comp.$mount()
  document.body.appendChild(comp.$el)
  comp.remove = function () {
    document.body.removeChild(comp.$el)
    comp.$destroy()
  }
  return comp
}

方法三:使用插件進一步封裝便于使用俊嗽,create.js

import Notice from '@/components/Notice.vue'
// ...
export default {
  install(Vue) {
    Vue.prototype.$notice = function (options) {
      return create(Notice, options)
    }
  }
}
// 使用
this.$notice({title: 'xxx'})
  1. 修正 input 中 $parent 寫法的問題
  • mixin emitter

  • 聲明 componentName

  • dispatch()

  1. 學習 Element 源碼
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市铃彰,隨后出現的幾起案子绍豁,更是在濱河造成了極大的恐慌,老刑警劉巖牙捉,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件竹揍,死亡現場離奇詭異,居然都是意外死亡邪铲,警方通過查閱死者的電腦和手機芬位,發(fā)現死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來带到,“玉大人昧碉,你說我怎么就攤上這事±咳牵” “怎么了被饿?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長搪搏。 經常有香客問我狭握,道長,這世上最難降的妖魔是什么疯溺? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任论颅,我火速辦了婚禮,結果婚禮上囱嫩,老公的妹妹穿的比我還像新娘恃疯。我一直安慰自己,他們只是感情好挠说,可當我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布澡谭。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蛙奖。 梳的紋絲不亂的頭發(fā)上潘酗,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天,我揣著相機與錄音雁仲,去河邊找鬼仔夺。 笑死,一個胖子當著我的面吹牛攒砖,可吹牛的內容都是我干的缸兔。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼吹艇,長吁一口氣:“原來是場噩夢啊……” “哼惰蜜!你這毒婦竟也來了?” 一聲冷哼從身側響起受神,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤抛猖,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后鼻听,有當地人在樹林里發(fā)現了一具尸體财著,經...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年撑碴,在試婚紗的時候發(fā)現自己被綠了撑教。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡醉拓,死狀恐怖伟姐,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情廉嚼,我是刑警寧澤玫镐,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站怠噪,受9級特大地震影響恐似,放射性物質發(fā)生泄漏。R本人自食惡果不足惜傍念,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一矫夷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧憋槐,春花似錦双藕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春嘶摊,著一層夾襖步出監(jiān)牢的瞬間延蟹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工叶堆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留阱飘,地道東北人。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓虱颗,卻偏偏與公主長得像沥匈,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子忘渔,可洞房花燭夜當晚...
    茶點故事閱讀 44,781評論 2 354