快應(yīng)用 組件

2020-08-08 15:23 更新
熟悉自定義組件的開發(fā),了解父子組件之間的通信方式,如:props,data,$dispatch(),$broadcast()

通過本節(jié),你將學(xué)會(huì):

組件自定義

開發(fā)頁面時(shí)開發(fā)者必須用到 Native 組件,如:?text?、?div?,這些組件是由各平臺 Native 底層渲染出來的;如果開發(fā)一個(gè)復(fù)雜的頁面,開發(fā)者把所有的 UI 部分寫在一個(gè)文件的?<template>?,那代碼的可維護(hù)性將會(huì)很低,并且模塊之間容易產(chǎn)生不必要的耦合關(guān)系

為了更好的組織邏輯與代碼,可以把頁面按照功能拆成多個(gè)模塊,每個(gè)模塊負(fù)責(zé)其中的一個(gè)功能部分,最后頁面將這些模塊引入管理起來,傳遞業(yè)務(wù)與配置數(shù)據(jù)完成代碼分離,那么這就是?自定義組件?的意義

自定義組件是一個(gè)開發(fā)者編寫的組件,使用起來和 Native 一樣,最終按照組件的?<template>?來渲染;同時(shí)開發(fā)起來又和頁面一樣,擁有 ViewModel 實(shí)現(xiàn)對數(shù)據(jù)、事件、方法的管理

這么來看,頁面也是一種特殊的自定義組件,無需引入即可使用,同時(shí)服務(wù)于整個(gè)頁面

示例如下:

<template>
  <div class="tutorial-page">
    <text class="tutorial-title">自定義組件:</text>
    <text>{{ say }}</text>
    <text>{{ obj.name }}</text>
  </div>
</template>

<style lang="less">
  .tutorial-page {
    flex-direction: column;
    padding-top: 20px;

    .tutorial-title {
      font-weight: bold;
    }
  }
</style>

<script>
  // 子組件
  export default {
    data: {
      say: 'hello',
      obj: {
        name: 'quickApp'
      }
    },
    /*
      data(){
      return {
          say:'hello',
          obj:{
            name:'quickApp'
          }
      }
      },
    */
    onInit() {
      console.log('我是子組件')
    }
  }
</script>

自定義組件中數(shù)據(jù)模型只能使用data 屬性,data 類型可以是 Object 或 Function。如果是函數(shù),返回結(jié)果必須是對象。

組件引入

快應(yīng)用中是通過?<import>?標(biāo)簽引入組件,如下面代碼所示

<import name="XXX" src="XXX"></import>

?<import>?標(biāo)簽中的的?src?屬性指定自定義組件的地址,?name?屬性指定在父組件中引用該組件時(shí)使用的標(biāo)簽名稱

示例如下:

<import name="comp-part1" src="./part1"></import>

<template>
  <div class="tutorial-page">
    <text class="tutorial-title">引入組件:</text>
    <comp-part1></comp-part1>
  </div>
</template>

<style lang="less">
  .tutorial-page {
    flex-direction: column;
    padding: 20px 10px;

    .tutorial-title {
      font-weight: bold;
    }
  }
</style>

<script>
  // 父組件
  export default {
    private: {},
    onInit() {
      this.$page.setTitleBar({ text: '引入組件' })
    }
  }
</script>

父子組件通信

父組件通過 Prop 向子組件傳遞數(shù)據(jù)

父組件向子組件傳遞數(shù)據(jù),通過在子組件的props屬性中聲明對外暴露的屬性名稱,然后在組件引用標(biāo)簽上聲明傳遞的父組件數(shù)據(jù),詳見Props

示例如下:

<!-- 子組件 -->
<template>
  <div class="child-demo">
    <text class="title">子組件:</text>
    <text>{{ say }}</text>
    <text>{{ propObject.name }}</text>
  </div>
</template>
<script>
  export default {
    props: ['say', 'propObject'],
    onInit() {
      console.info(`外部傳遞的數(shù)據(jù):`, this.say, this.propObject)
    }
  }
</script>
<!-- 父組件 -->
<import name="comp" src="./comp"></import>
<template>
  <div class="parent-demo">
    <comp say="{{say}}" prop-object="{{obj}}"></comp>
  </div>
</template>
<script>
  export default {
    private: {
      say:'hello',
      obj:{
        name:'child-demo'
      }
    }
  }
</script>

子組件對父組件通信

當(dāng)子組件對數(shù)據(jù)進(jìn)行改造后,把最終數(shù)據(jù)交給父組件甚至往上,往往有三種辦法

  • 父組件傳遞的數(shù)據(jù)本身就是對象,子組件直接修改對象中的屬性,父組件的值也會(huì)發(fā)生改變,不推薦這種;
  • 子組件通過$dispatch()觸發(fā)自定義事件,父組件通過$on()監(jiān)控自定義事件的觸發(fā),如:del;
  • 子組件通過$emit()觸發(fā)在節(jié)點(diǎn)上綁定的自定義事件來執(zhí)行父組件的方法,如:add;

示例如下:

 <!-- 父組件 -->
<import name="comp1" src="./comp1.ux"></import>
<import name="comp2" src="./comp2.ux"></import>
<import name="comp3" src="./comp3.ux"></import>
<template>
  <div class="parent-demo">
    <text>我是父組件count:{{count}}</text>
    <comp1 count="{{count}}" onemit-evt="emitEvt"></comp1>

    <text>我是父組件num:{{num}}</text>
    <comp2 num="{{num}}"></comp2>

    <text>我是父組件age:{{age}}</text>
    <input type="button" onclick="evtTypeEmit" value="觸發(fā)$broadcast()"></input>
    <comp3></comp3>
  </div>
</template>

<script>
  export default {
    private:{
        count:20,
        num:20,
        age:18
    },
    onInit(){
      this.$on('dispathEvt',this.dispathEvt)
    },
    emitEvt(evt){
        this.count = evt.detail.count
    },
    dispathEvt(evt){
        this.num = evt.detail.num
    },
    evtTypeEmit(){
       this.$broadcast('broadevt',{
         age:19
       })
    },
  }
</script>
<!-- comp1 -->
<template>
  <div class="child-demo">
    <text>我是子組件一count:{{compCount}}</text>
    <input type="button" onclick='addHandler' value='add'></input>
  </div>
</template>
<script>
  export default {
    props: ['count'],
    data(){
        return{
            compCount:this.count
        }
    },
    addHandler(){
        this.compCount ++
        this.$emit('emitEvt',{
            count:this.compCount
        })
    },
  }
</script>
<!-- comp2 -->
<template>
  <div class="child-demo">
    <text>我是子組件二num:{{compNum}}</text>
    <input type="button" onclick='delHandler' value='del'></input>
  </div>
</template>
<script>
  export default {
    props: ['num'],
    data(){
        return{
            compNum:this.num
        }
    },
    delHandler(){
        this.compNum --
        this.$dispatch('dispathEvt',{
            num:this.compNum
        })
    },
  }
</script>
<!-- comp3 -->
<template>
  <div class="child-demo">
    <text>我是子組件三age:{{compAge}}</text>
  </div>
</template>
<script>
  export default {
    props:[],
    data(){
        return{
            compAge:null
        }
    },
    onInit(){
      this.$on('broadevt',this.broadevt)
    },
    broadevt(evt){
      this.compAge = evt.detail.age
    }
  }
</script>

所以,框架向開發(fā)者提供了雙向的事件傳遞

  • 向下傳遞:父組件觸發(fā),子組件響應(yīng);調(diào)用parentVm.$broadcast()完成向下傳遞,如:broadevt
  • 向上傳遞:子組件觸發(fā),父組件響應(yīng);調(diào)用childVm.$dispath()完成向上傳遞,如:evtType2

提示:

  • 觸發(fā)時(shí)傳遞參數(shù),再接收時(shí)使用evt.detail來獲取參數(shù)
  • 當(dāng)傳遞結(jié)束后,可以調(diào)用evt.stop()來結(jié)束傳遞,否則會(huì)一直傳遞下去

兄弟/跨級組件通信

傳統(tǒng)的兄弟組件通信、跨父子組件通信,可以通過 Publish/Subscribe 模型來完成,這里提供兩種實(shí)現(xiàn)思路:

1) 開發(fā)者實(shí)現(xiàn)一個(gè) Pub/Sub 模型完成通信:

開發(fā)者單獨(dú)寫一個(gè) JS 文件,提供發(fā)布訂閱的能力;然后各個(gè) ViewModel 引入這個(gè)JS文件;或者將其掛載在頁面級別的 ViewModel,子組件通過 $root 引用到頁面級別的 ViewModel

/**
 * @file pubsub.js 提供發(fā)布訂閱的能力
 */

/**
 * 提供Publish-Subscribe模型
 */
export default class Pubsub {
  constructor(name) {
    this.name = name
    this.eventMap = {}
  }

  /**
   * 訂閱事件
   * @param type {string} 事件名稱
   * @param fn {function} 響應(yīng)函數(shù)
   * @param options {object} 暫時(shí)保留
   * @return {*}
   */
  subscribe(type, fn, options) {
    if (options && options.once) {
      const fnOnce = args => {
        fn(args)
        this.remove(type, fnOnce)
      }
      return this.subscribe(type, fnOnce)
    }

    this.eventMap[type] = this.eventMap[type] || []

    if (typeof fn === 'function') {
      const list = this.eventMap[type]
      if (list.indexOf(fn) === -1) {
        list.push(fn)
      }
    }
  }

  /**
   * 發(fā)布事件
   * @param type {string} 事件名稱
   * @param args {array} 事件觸發(fā)時(shí)的參數(shù)
   * @return {*}
   */
  publish(type, args) {
    let lastRet = null
    const list = this.eventMap[type] || []
    for (let i = 0, len = list.length; i < len; i++) {
      lastRet = list[i](args, lastRet)
    }
    return lastRet
  }

  /**
   * 刪除事件訂閱
   * @param type {string} 事件名稱
   * @param fn {function} 響應(yīng)函數(shù)
   */
  remove(type, fn) {
    if (!this.eventMap[type]) return
    const list = this.eventMap[type]
    const index = list.indexOf(fn)
    if (index > -1) {
      list.splice(index, 1)
    }
  }
}

// 實(shí)例緩存
const modelCache = {}

/**
 * 用于創(chuàng)建或獲取一個(gè)指定名稱的Pubsub模型的實(shí)例
 * @param name {string} 通過名稱創(chuàng)建不同的實(shí)例
 * @return {*}
 */
export function createOrRetrieveInst (name) {
  if (!modelCache[name]) {
    modelCache[name] = new Pubsub(name)
  }

  return modelCache[name]
}

接著,UX文件中引入進(jìn)來,綁定在ViewModel上面,例如:

<script>
import {
  createOrRetrieveInst
} from './pubsub.js'

export default {
  onReady () {
    // 1. 實(shí)例化:并綁定在VM上
    this.pubsubModel = createOrRetrieveInst()

    // 2. 訂閱:其它VM也可以調(diào)用
    this.pubsubModel.subscribe('count-add', function (vArg0, vArg1){ ... })

    // 3. 發(fā)布:其它VM也可以調(diào)用
    this.pubsubModel.publish('count-add', ['arg0', 'arg1'])
  }
}
</script>

子組件通過 $root 引用,例如:

<script>
export default {
  onReady () {
    // 1. 訂閱
    this.$root().pubsubModel.subscribe('count-add', function (vArg0, vArg1){ ... })

    // 2. 發(fā)布
    this.$root().pubsubModel.publish('count-add', ['arg0', 'arg1'])
  }
}
</script>

開發(fā)者通過上面的方式完成解耦,示例代碼僅供參考;

2) 利用框架本身提供的事件綁定接口:

在業(yè)務(wù)邏輯相對簡單的情況下,也可以使用ViewModel本身的事件綁定來處理:$on(),$emit()。

示例如下:

子組件定義了 Sub 端的邏輯處理,有processMessage()、customEventInVm2(),后者同使用$on效果一致

<template>
  <div class="tutorial-page">
    <text class="tutorial-title">自定義組件2:</text>
    <text>處理消息:{{msg}}</text>
    <text>事件內(nèi)容:{{eventDetail}}</text>
  </div>
</template>

<style lang="less"></style>

<script>
  // 子組件: part2
  export default {
    props: [],
    data() {
      return {
        msg: null,
        eventDetail: null
      }
    },
    processMessage(msg) {
      const now = new Date().toISOString()
      this.msg = `${now}: ${msg}`
    },
    /**
     * 通過events對象:綁定事件
     */
    events: {
      customEventInVm2(evt) {
        const now = new Date().toISOString()
        this.eventDetail = `${now}: ${evt.detail}`
      }
    }
  }
</script>

另外一個(gè)兄弟組件可以通過父組件中建立相互引用達(dá)到相互持有ViewModel的目的,通過在生命周期onReady()執(zhí)行establishRef()實(shí)現(xiàn),如下代碼所示:

<!-- 父組件 -->
<import name="comp-part2" src="./part2"></import>
<import name="comp-part3" src="./part3"></import>

<template>
  <div class="tutorial-page">
    <!-- 兄弟VM通信 -->
    <comp-part2 id="sibling1"></comp-part2>
    <comp-part3 id="sibling2"></comp-part3>
  </div>
</template>

<style lang="less"></style>

<script>
  export default {
    onReady() {
      this.establishRef()
    },
    /**
     * 建立相互VM的引用
     */
    establishRef() {
      const siblingVm1 = this.$vm('sibling1')
      const siblingVm2 = this.$vm('sibling2')

      siblingVm1.parentVm = this
      siblingVm1.nextVm = siblingVm2
      siblingVm2.parentVm = this
      siblingVm2.previousVm = siblingVm1
    }
  }
</script>

那么另外一個(gè)子組件的 Pub 端定義就很簡單了,執(zhí)行sendMesssage()即可完成觸發(fā),如下代碼所示:

<template>
  <div class="tutorial-page">
    <text class="tutorial-title">自定義組件3:</text>
    <text onclick="sendMesssage">點(diǎn)擊發(fā)送消息</text>
  </div>
</template>

<style lang="less"></style>

<script>
  // 子組件: part3
  export default {
    sendMesssage() {
      if (this.previousVm) {
        // Way1. 調(diào)用方法
        this.previousVm.processMessage('兄弟之間通信的消息內(nèi)容')
        // Way2. 觸發(fā)事件
        this.previousVm.$emit('customEventInVm2', '兄弟之間通信的消息內(nèi)容')
      }
    }
  }
</script>

總結(jié)

自定義組件有助于更好的組織代碼邏輯,結(jié)構(gòu)更清晰;了解父子組件之間的數(shù)據(jù)通信,是開發(fā)自定義組件的必備技能


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號