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

我們講完元件的應用之後,本次篇章我們會來說明一下,關於 importrequire 在元件當中的應用。這兩個方法對於現今的 JavaScript 來說,並不是什麼新奇的東西。而搭配 Vue 的元件又能玩出什麼新的把戲?讓我們繼續看下去。


require 方法

這個方法在一般的 JavaScript 當中很常見,主要目的就是為了將檔案讀取進來。而在 Vue 裡面,他不僅僅是可以引入 JavaScript 檔案,連同圖片檔案也可以利用這種方式來載入。只是圖片檔案利用 require 載入後,會變成以 base64 的方式存在於元件中。

當然,以目前的潮流來說,使用 import 是比較常見的作法,但也不是說 require 這個方法不行。只是要記得,如果是使用 require 的話,最後面必須要加上 .default 才能讓元件可以使用。

那麼,在這邊使用 require 的方法有什麼好處嗎?

沒有(欸)。

有一些比較詭譎的作法,或許你會需要使用 require 的方法把 .vue 檔案給載入,例如說,我如果依照環境變數,來載入元件的時候,那麼使用 require 是個不錯的選擇。

let component

if (window.env.component !== '') {
  component = require(`@/component/${window.env.component}.vue`)
}

export default {
  name: 'App',
  components: {
    MyComponent: component
  }
}

我們底下先假裝一下我們有設定 winow.env.component 這件事 XD

至於你說 import 不行嗎?不好意思,這種方法還真的是不行。對於 import 來說,後面是不能使用 `${...}` 這種魔術方法的。

所以說,當你忘記設定 window.env 這個全域變數的時候,你的 App 就會壞掉囉~

所以,總結來說,我們使用會 require 這個方法,有個考量,就是需要依照某些邏輯條件,來載入不同的元件的時候,我們就能利用 require 的方法來達成。而除了放在元件外面,你也可以放在 components 屬性裡面來載入,記得要給他一個名字。

使用的方式跟你放在外面差不多,不過這樣的方式你就不能做其他的判斷了。


import 方法

就如同剛剛提到的,import 應該是目前比較流行的方式。當然,他跟 require 不一樣的地方,除了不能使用魔術方法之外。另外,你也不能將 import 放在其他的 JavaScript 裡面,編譯過程中會報錯。

撇除掉這件事情之外,importrequire 不一樣的地方在於,當你在 components 裡面使用的時候,import 可以應用懶加載的方式來達成。也就是說 components 裡面可以利用函式的方式來載入元件。

export default {
  name: 'App',
  components: {
    HelloWorld: () => import('@/components/HelloWorld.vue')
  }
}

這一點是 require 所無法辦到的。有鑑於此,我們就可以在這邊多動一些手腳。

export default {
  name: 'App',
  components: {
    HelloWorld: () => {
      if (conditions) {
        return import('@/components/HelloWorld.vue')
      } else {
        return import('@/components/HelloKitty.vue')
      }
    }
  }
}

這樣的方式就很類似我們在 <script> 標籤開始的地方,使用邏輯判斷搭配 require 來載入元件。不過,基本上 import 依舊無法使用 `${...}` 這種魔術方法。


其他用途

題外話,剛剛提到了 require 可以載入圖片,那麼 import 當然也可以用來做其他的事情。例如說,你可以使用這兩個方法來載入 Mixins,

import MyMixins from '@/mixins/MyMixins'
let OtherMixins = require('@/mixins/OtherMixins').default

export default {
  name: 'MyComponent',
  mixins: [ MyMixins, OtherMixins ],
  data () {
    return {
      avatar: require('@/assets/images/avatar.jpg')
    }
  }
}

當然這好像只是 JavaScript 基本用法,讓我們回歸到元件本身。一般使用的情境下,其實對於所謂的 動態 元件載入這件事情,需求並不高。而之所以會想這麼做,除了專案架構本身可能有所需求之外,另一點大概就是 開發人員覺得爽 所以這麼做。


非同步載入

前面提到了 import 可以懶加載這件事,所以相對應的,我們就可以思考一下非同步載入的可行性。所以,我們這邊先 假設 有一個需求,讓我們來寫一點點 User Story 來描述一下:

  1. 小明第一次來到這個網站。
  2. 網站的上方有一個註冊跟登入的按鈕。
  3. 小明點了登入之後,出現了登入畫面。
  4. 但是小明想想好像沒有註冊過帳號,所以又點了註冊的按鈕。
  5. 出現了註冊的畫面,然後小明又按了登入按鈕。
  6. 他想起了他的帳號密碼登入了,然後網站上方出現了他的帳號資訊。
  7. 然後小明就關掉視窗了。

你看,多麼清晰明瞭毫無目的與建樹的使用者故事。這個故事告訴了我們以下的需求(才沒有!

  1. 沒登入的人,網站上方有「登入」以及「註冊」的按鈕。
  2. 登入之後,網站上方會出現「帳號資訊」的區塊。
  3. 先不要管「帳號資訊」是什麼,先做一版出來看看。

就這樣,我們得出了一個 相當明確 的需求量表,接著我們要進入 Sprint Plaining。

好了!這裡沒有 Sprint!

先試想一下我們可以怎麼做,關於「未登入」以及「已登入」這兩件事情。

  • 做一個「未登入」的元件 A。
  • 做一個「已登入」的元件 B。
  • 然後判斷登入狀態,如果登入就顯示 A,否則就顯示 B。

好的,那我們先把他寫在同一個元件內試試看,

<template>
  <header>
    <div class="user" v-if="isLogin">
      <img class="avatar" :src="userAvatar" :alt="userName"/>
      <p v-text="userName"></p>
    </div>
    <div class="not-login" v-else>
      <button type="button" @click="signin">登入</button>
      <button type="button" @click="signup">註冊</button>
    </div>
  </header>
</template>

<script>
export default {
  name: 'App',
  data () {
    return {
      isLogin: false
    },
    user: {
      name: ''
      avatar: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='
    }
  },
  computed: {
    userAvatar () {
      if (this.isLogin) {
        return this.user.avatar
      }
      return 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='
    },
    userName () {
      if (this.isLogin) {
        return this.user.name
      }
      return ''
    }
  },
  methods: {
    signin () {
      // 這裡執行一個登入的動作
      setTimeout(() => {
        this.isLogin = true
      }, 3000)
    },
    signup () {
      // 這裡執行一個註冊的動作
      setTimeout(() => {
        this.isLogin = true
      }, 3000)
    }
  }
}
</script>

這樣寫下來其實也沒什麼毛病。然後,我們把「未登入」以及「已登入」這兩件事情拆成兩個元件呢,他可能就會變成這個樣子:

<template>
  <header>
    <LoginUser v-if="isLogin" />
    <NotLoginUser v-else />
  </header>
</template>

<script>
import LoginUser from '@/components/LoginUser.vue'
import NotLoginUser from '@/components/NotLoginUser.vue'

export default {
  name: 'App',
  components: {
    LoginUser,
    NotLoginUser
  },
  data () {
    return {
      isLogin: false
    }
  }
}
</script>

這個時候你會發現,你的子元件 <LoginUser><NotLoginUser> 必須要想辦法跟父元件溝通,這樣才能去觸發 isLogin 來改變元件顯示的狀況。而元件溝通這件事情,我們後續還會有章節討論。假設,今天我們連元件溝通這件事情都不想做呢?

那麼,我們就找一個同事來幫我們做元件溝通就好了。

這裡用上非同步讀取的方式,其實有點過於獵奇。不過,為了衝一下字數,我們試著來實作看看,

<template>
  <header>
    <User />
  </header>
</template>

<script>
export default {
  name: 'App',
  components: {
    User: () => {
      if (this.isLogin) {
        return import('@/components/User.vue')
      } else {
        return import('@/components/Geusts.vue')
      }
    }
  },
  data () {
    return {
      isLogin: false
    }
  }
}
</script>

然後你會發現爆炸了!

是的,你以為使用了 => 就覺得 this 一定會指向你的元件實體,在 components 裡面是不正確的!那麼你覺得這一個 this 會指向誰呢?我們把他 console.log 出來就會發現!

這個 this 什麼都不是!

所以我們就無法採用動態載入了嗎?當然不是,除了 this 之外,你還可以放一個變數到外面去。當然,放到外面去有一個但書,你得確保你這個元件,這個變數,僅只有單一目的、單一功能,否則放在外面的變數,會因為封裝問題,造成元件重用時的污染。關於這個污染我們後續會聊到,簡單來說是個小坑。

<template>
  <header>
    <User />
  </header>
</template>

<script>
let userIsLogin = false

export default {
  name: 'App',
  components: {
    User: () => {
      if (userIsLogin) {
        return import('@/components/User.vue')
      } else {
        return import('@/components/Geusts.vue')
      }
    }
  },
  data () {
    return {
      isLogin: userIsLogin
    }
  }
}
</script>

雖然你拿到外面去了,但是,你還是得告訴父元件,到底這個使用者登入了沒。所以,元件之間的溝通還是勢必要存在。當然,你可以再往外拋給 window 這個全域變數,不過這樣做有點太過於沒節操了點。


說完一大堆 import 的部分,最後提一下關於他自身的事情。import 本身其實是可以支援 Promise 的操作的,也就是說,這兩種寫法是可以被接受的,

import('./a.js').then(...)

/* OR */

let a = await import('./a.js')

那麼有趣的事情來了,我可不可以在 components 裡面這樣做呢?答案是可以的!但是記得要把 .then 傳回來的東西 return 回去,不然你的元件會報錯。

export default {
  name: 'App',
  components: {
    User: () => {
      return import('@/components/User.vue').then(module => {
        return module
      })
    }
  }
}

當你把那個 module 印出來,你會發現他其實是你的子元件 Vue 的實例。

至於這樣可以做什麼事情,就留給大家一個想像的空間啦!

小結

動態載入的地方還有很多可以聊聊,就我們後續搭配到 Vue-Router 的時候再來談。

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

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