這是關於元件方法的最後一篇,實際上來說,Vue.options 並不能算是很魔術的方法。如果把他歸類為奇技淫巧可能比較適合(政治正確)。由於元件的註冊過程當中,會將相對應的實體放到這個屬性當中,所以,我們就可以利用這個地方來做一些事情。

身為邊緣人,我最喜歡這種邪門歪道了。


App 實體 a.k.k Vue 替身

我們在使用 Vue 的時候,在最後一定是使用 new Vue 之後再 $mount 到某個 DOM 底下。

import Vue from 'vue'
import App from './App.vue'

new Vue({
  render: h => h(App)
}).$mount('#app')

這一整個 Vue 實體會包含了你所使用到的相關資訊,你可以在 mounted 的時候將他印出來,底下這個例子就是在 App.vue 的地方,將 this 印出來的結果。

然後你會發現,奇怪了,這邊有一個叫做 $options 的東西,但是沒有叫做 options 的屬性啊?沒關係,我們繼續往下看,他其實是藏在 __proto__ 裡面的 constructor

我們把 Vue 這個原型展開之後,你就會看到裡面有一個 options,底下有包含了不少東西:

  • components 當前有幾個元件載入了。
  • name 當下的元件名稱。
  • data 當下元件所設定的預設資料集。
  • directives, filters 你在當下元件當中是否有設定過濾器或是自定義指令。
  • beforeCreate, beforeDestroy, mounted 你在當下元件中,所設定過的相對應方法會出現在這些地方。
  • render 你所使用的渲染函式。
  • staticRenderFns 這個很特別,是用來作一些渲染優化或是快取的。這個解釋起來可能可以在開個兩篇文章來討論,可以參考老爸的 Meetup 說明。

Special Vue.js Meetup - Even You

好的,那麼可能你會覺得,這個 options 好像哪裡怪怪的?是的,這裡的 options 其實只是 App 這個根元件的相關資訊。跟我們所提到的 Vue.options 到底,實際上的差異是什麼?

一言不合就 console.log

import Vue from 'vue'

export default {
  name: 'App',
  mounted () {
    console.log(Vue.options)
  }
}

你會在 components 的區塊裡面看到一些元件,這裡特別說明一下,那個 Helper 是我自己做的,至於為什麼會出現在這裡?而為何 App 又沒有出現在這裡?我們底下會繼續說明這個部分。


Vue 真正的實體

就如同我上面的例子,你在 console.log(Vue.options) 的時候,其實是把 Vue 這個根元件給印出來。但是,你可能會覺得奇怪的部分,在於:

為何這個元件沒有包含 App

首先,這個 Vue 其實是一個沒有被初始化的元件實體,你可以把他想成是 Vue 元件的地基,所有的 Vue 元件都是根據這個地基而來的。但,由於 App 是你使用 new Vue 所產生出來的,所以,在這個地方就不會包含 App 或是你往後所使用的任何元件。

那個 Helper 是怎麼放進去的?

還記得 Vue 有所謂 全域 的設定這件事情嗎?

Vue.component('my-component', { ... })

你覺得最前面的那個 Vue 會是誰呢?是的,他就是你在這整個應用程序中,每次在開頭所載入的 Vue 這個物件。

import Vue from 'vue' // 這裡就是所謂的「那個」Vue

export default {
  name: 'App'
}

所以說,這個 Vue 裡面,就會有 全域 的相關設定。那麼,我們就能夠從這邊,去拿到一些,使用全域設定所綁定進來的東西,就上面的結構來看,會有元件、自訂指令以及過濾器。那麼,利用這一點,我們就可以從 Vue.options 裡面的資料來做一些事情。


另類的動態載入

對啊,我們又回到動態載入這個議題了。只是這一次我們的對象換了,換成了對於 全域 物件的操作。這麼做會不會有什麼衍生問題?我這邊提出幾個點給大家思考一下。

  • 既然叫做 全域,會不會有人跟你用一樣的名字?
  • 如果我並不是使用 Webpack 或是相關封裝工具,Vue 是否會有其他風險?
  • 若是載入元件檔案,會不會因為檔案不存在而讓 App 整組壞掉?
  • 放在這裡的東西要怎麼跟別人溝通?

以上問題我都不會回答,你可以等到踩到雷的時候再來問我(燦笑)。

請注意!使用這種動態載入的方式,並不是什麼正規技巧,繼續往下看之前,請先詳閱公開說明書。


首先,我先給大家一個比較完整的範例,來示範一下這個奇技淫巧的正確姿勢。

import HelloKitty from '@/components/HelloKitty.vue'
import HelloWorld from '@/components/HelloWorld.vue'

Vue.component('HelloKitty', HelloKitty)
Vue.component('HelloWorld', HelloWorld)

上面這個檔案內容,我們假設叫做 myComponent.js,並且把他放在 extends 目錄底下。然後我們的 App.vue 裡面的 <script>,就換成這種寫法。

import Vue from 'vue'

export default {
  name: 'App',
  data () {
    return {
      myComponent: 'HelloKitty',
      loadedComponent: null
    }
  },
  created () {
    if (typeof Vue.options.components[this.myComponent] !== 'undefined') {
      this.loadedComponent = Vue.options.components[this.myComponent]
    }
  }
}

接著,我們的應用程式入口 main.js 那邊需要調整一下。

import Vue from 'vue'
import App from './App.vue'

require('./extends/myComponent.js')

new Vue({
  render: h => h(App)
}).$mount('#app')

最後執行結果:


我這邊總結一下我們到底做了什麼樣的事情:

  1. 我們把 components 都拿掉了。
  2. 我們把元件都搬去了 Vue.component 那邊去做。
  3. 透過 main.js 把元件 require 進來。
  4. App.vue 透過 :is 的方法來做到元件替換。

或許多數人會覺得,那跟我在 App.vue 裡面把元件都 import 進來到底有什麼差異?而且把他放到 extends 裡面不就更難維護了嗎?

對!

我這個人憨慢講話,但是我金實(機)(歪)

這邊如果在這種小型專案中,確實是看不出什麼好處。那麼,我現在把這個專案結構 放大 10 倍 來看看。

  • 網站有 10 種樣版。
  • 每一種樣版有獨立的 header, mainfooter

好的,當你的 PM 不能叫客戶回家吃自己的時候,那我離職總可以吧!

不行!

所以,如果依照正規的作法,我們應該會這樣做。

<template>
  <div id="#app">
    <header :is="headerComponent"></header>
    <main :is="mainComponent"></main>
    <footer :is="footerComponent"></footer>
  </div>
</template>

然後我們的 <script> 部分,就放了很多元件載入的動作。這邊絕對不是要充字數,是的話我就會把 mainfooter 一樣都打 10 次了(欸)。

import Header0 from '@/components/header0.vue'
import Header1 from '@/components/header1.vue'
import Header2 from '@/components/header2.vue'
import Header3 from '@/components/header3.vue'
import Header4 from '@/components/header4.vue'
import Header5 from '@/components/header5.vue'
import Header6 from '@/components/header6.vue'
import Header7 from '@/components/header7.vue'
import Header8 from '@/components/header8.vue'
import Header9 from '@/components/header9.vue'

import Main0 from '@/components/main0.vue'
// 以下類推

import Footer0 from '@/components/footer0.vue'
// 以下類推

export default {
  name: 'App',
  data () {
    return {
      headerComponent: null,
      footerComponent: null,
      mainComponent: null
    }
  },
  created () {
    this.headerComponent = Header0
    this.footerComponent = Footer0
    this.mainComponent = Main0
  }
}

然後,你每次要修改,或是新增,或是做什麼其他的事情,你就是得改這個 App.vue 才行。然後,每次打包出來的就是這麼一大包。像是我自己的 2012 MacBook Pro,每次要打包這麼多檔案,看著看著我自己都 褲底一包


小結

上面那個例子,放到 main.js 使用 require 是一種解法。但是,你想想每次再開發的時候,都要載入這麼多檔案不覺得煩悶?當然,如果你是 2019 的 Mac 可能就不覺得,

有錢人的生活往往就是這麼樸實無華,且枯燥。

還是有更取巧的作法,我們後面再慢慢聊。

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