[IT 鐵人賽] Component 魔術方法 Day 5

上一篇提及了 importrequire 之後,我們現在把重心移到元件本身的一些屬性上面。本次內容將會圍繞在 :is 這一個元件屬性上面,我們來看看這個魔術方法到底可以做到哪些事情。

還有,可以做到什麼程度(燦笑)


:is 屬性

這個屬性相當特別,他的目的是,將使用這個屬性的元件替換掉,替換成你所 指定 的 Vue 元件。換句話說,你只要把你的 Vue 元件引入後,甚至可以不需要在父層元件設定 components,就可以使用。

<template>
  <section>
    <div :is="myComponent" />
  </section>
</template>

<script>
import HelloWorld from '@/components/HelloWorld.vue'

export default {
  name: 'App',
  data () {
    return {
      myComponent: HelloWorld
    }
  }
}
</script>

請注意,:is 後面必須要接受一個 Vue 元件實體,或者是一個非同步傳輸的 Vue 元件實體。不然他是無法動作的。

那麼,這件事情可以帶來的最大好處是什麼?

忘記寫 components 也沒關係(欸不對)!

由於它的彈性,所以我們可以做一些原本要放在樣板邏輯當中的事情,舉例來說:

<template>
  <section>
    <ul>
      <li @click="switchTab(1)">Tab 1</li>
      <li @click="switchTab(2)">Tab 2</li>
    </ul>
    
    <Tab1 class="tab tab-1" v-show="tab === 1" />
    <Tab2 class="tab tab-2" v-show="tab === 2" />
  </section>
</template>

<script>
import Tab1 from '@/components/tab1.vue'
import Tab2 from '@/components/tab2.vue'

export default {
  name: 'App',
  components: {
    Tab1,
    Tab2
  },
  data () {
    return {
      tab: 1
    }
  },
  methods: {
    switchTab(tab) {
      this.tab = tab
    }
  }
}
</script>

這是一個常見的例子,我們有分頁標籤,然後每個分頁標籤讀取了各自的分頁內容進來。如果在這邊使用 :is 可以怎麼做呢?

<template>
  <section>
    <ul>
      <li
        v-for="tab in tabs"
        :key="tab.id"
        v-text="tab.name"
        @click.prevent="switchTab(tab.id)"
      ></li>
    </ul>
    
    <div :is="activiteTab">
  </section>
</template>

<script>
import Tab1 from '@/components/tab1.vue'
import Tab2 from '@/components/tab2.vue'

export default {
  name: 'App',
  data () {
    return {
      activiteTab: Tab1,
      tabs: [
        {
          id: 1,
          name: 'Tab 1',
          context: Tab1
        },
        {
          id: 2,
          name: 'Tab 2',
          context: Tab2
        }
      ]
    }
  },
  methods: {
    switchTab(id) {
      let index = this.tabs.findIndex(t => t.id === id)
      if (index > -1) {
        this.activiteTab = this.tabs[index].context
      }
    }
  }
}
</script>

如果你有 10 個頁籤,你就不用寫十次 v-show 之類的動作。這樣的做法跟傳統使用 components 的做法,就差在於你載入的動作。不過,這個魔術方法還是有一點點不一樣,特別是在生命週期上面。


如果使用了 :is 這個方式,你必須特別留意一些元件實體上的差異:

  • $refs 如果使用 :is 的時候,對於 mounted 有些為差異:
    • 如果是先 import 再指定,你在 mounted 可以馬上取得該元件。
    • 如果是使用非同步載入,你在 mounted 必須等待 200ms 之後才能拿到。
  • 使用 :is 的元件,整個元件會被銷毀再重建。
  • 父元件 不保證 其使用 :is 的子元件是不是一定會在 DOM 結構樹當中。
const HelloWorld = () => ({
  component: import('@/components/HelloWorld.vue')
})

export default {
  name: 'App',
  data () {
    return {
      myComponent: HelloWorld
    }
  },
  created () {
    console.log('created, $refs count:', Object.keys(this.$refs).length)
  },
  mounted () {
    console.log('mounted, $refs count:', Object.keys(this.$refs).length)
    setTimeout(() => {
      console.log('mounted after 200ms, $refs count:', Object.keys(this.$refs).length)
    }, 200)
  }
}

為何是 200ms

我們在使用非同步載入元件時,他有一個固定的結構:

const HelloWorld = () => ({
  // 你需要一個返回 Promise 的 Vue 元件實體
  component: import('@/components/HelloWorld.vue'),
  // 這裡可以指定在讀取時,使用什麼 Vue 元件來呈現
  // 這個元件不可以是非同步載入
  loading: LoadingComponent,
  // 這裡可以指定在讀取錯誤時(包含超時),使用什麼 Vue 元件來呈現
  // 這個元件不可以是非同步載入
  error: ErrorComponent,
  // 預設非同步元件延遲時間,這就是 200ms 由來(預設值)
  delay: 200,
  // 定義錯誤元件何時會顯示,預設是 Infinity
  timeout: 3000
})

所以說,如果你的 delay 設定成 3000 的話,那麼,你的 mounted 就必須要等待 3 秒之後,才能從 $refs 當中拿到你的子元件實體。

這一招如果寫在第三方套件裡面,超糟糕的(好孩子不要學)。


再看非同步載入

其實你會發現,我大多都圍繞在非同步載入這件事情。實際上,由於自己的開發專案需要,所以我使用了非常多奇怪的方法。之後會慢慢的讓大家看看(燦笑)。我們繼續看 import:is 之間能做些什麼事情。

上面有提到了,非同步載入元件需要一個 Promise不是鑽石恆久遠,一顆就破產的那種。所以,我們圍繞著 Promise 來看看。

上面這一顆 55 分,D Color 兩個 Excellent(等等離題了)

還記得 import 本身也是一種 Promise 嗎?如果忘記了請回去看 Component 魔術方法 Day 4

所以如果我們需要一大堆動態樣式的時候,可以怎麼做?如果我們有很多 .vue 檔案,而且你可以確認那些檔案不會因為 突然 git push -f 或是 rm -rf / 被砍掉,那麼你可以利用 computed 這件事來幫忙。

<template>
  <section>
    <div :is="loadedComponent"></div>
  </section>
</template>

<script>
export default {
  name: 'App',
  data () {
    return {
      activedComponent: 'Tab1'
    }
  },
  computed: {
    loadedComponent () {
      return ((component => {
        return () => ({
          component: import('@/components/' + component + '.vue')
        })
      })(this.activedComponent)
    }
  }
}
</script>

實際上操作結果:

記得我剛剛說的,每次被 :is 載入的元件,都是被消滅然後重新建立。所以,你不管呼叫幾次元件,他的 created 都會被呼叫出來。也就是說,每次的生命週期都回重新走過一遍。

所以,當你在這些生命週期,甚至是你的元件當中,有將任何動作 綁定 到全域變數上面的,請記得把他取消,例如:

window.addEventListener('click', function () { ... }, false)

我們來直接實作一個例子,我們在元件的 created 監聽某個事件,

export default {
  name: 'HelloWorld',
  created () {
    window.addEventListener('click', function () {
      console.log('HelloWorld Component click!')
    }, false)
  }
}

然後我們的順序是這樣,

  1. 先載入 HelloWorld 元件,這個時候他綁定了一個 click 事件。
  2. 過了 2 秒鐘後,載入 HelloKitty 這個元件。
  3. 最後再等 2 秒鐘,重複載入 HelloWorld 元件。

我們先前提過了,他會將元件銷毀之後再次重建,所以你剛剛的 綁定 動作,就會被再次執行一次。

一次綁定一次爽,一直綁定一直爽。

所以,當你使用 :is 來操作元件時,無論你是否用了非同步載入,

請務必確認綁定的動作有確實被移除。

其實 :is 應用範圍多數是在切換不同元件,但是,單純拿來切換元件實在是太浪費了。搭配 import 看起來多美妙你說是不是。當然,這邊還是得小心 $refs 的坑。


小結

其實你用 require 也能做到類似效果,我就不贅述了。

ITHome 鐵人賽同步刊登 Component 魔術方法 Day 5

Hina Chen
偏執與強迫症的患者,算不上是無可救藥,只是我已經遇上我的良醫了。
Taipei