大家一定會覺得這個標題很奇怪,不就都是 new Vue 嗎?是有什麼差別?對啊,其實都是 Vue 沒錯,但是就像是斯斯有兩種,Vue 也是有好多種不同面向。

是說現在斯斯好像不只兩種了。


new Vue

通常,我們在實作一個 App 的時候,最終我們使用了 new Vue 來將整個 App 綁定在某一個 DOM 的元件上。如果,你綁定一次不夠,想要綁定兩次呢?也不是不可以,

import Vue from 'vue'
import App1 from './App1.vue'
import App2 from './App2.vue'

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

new Vue({
  render: h => h(App2)
}).$mount("#app2')

雖然不是很好的例子,但實際上也是可以這樣操作的。但是,基於什麼樣的理由需要這樣操作呢?這樣操作的情況下,又會衍生出什麼狀況?

  • 當你的區塊很邊緣的時候。
  • 這兩個 Vue App 有各自獨立的生態。
  • 這兩個 Vue App 建議透過 EventBus 來溝通。
  • 如果共用 Store 或是 Router 最好有互相干擾的心理準備。
  • 原本使用 全域 的設定,兩個 App 都可以看到。

倘若,你將這兩個 App 分成兩個入口,也就是 main1.jsmain2.js 的話,還會有更多問題:

  • 這兩個 Vue App 是互相獨立的個體。
  • 這兩個 Vue App 並無有效溝通方式。
  • 你的 全域 不一定是我的 全域
  • 如果要互相溝通,需要把層級再往上提升,甚至放到 window 這一層。

現在會有多少人這樣做呢?

沒有(吧)?


在沒有 Webpack 工具下

主要的命題在此。我們不一定每次經手一個專案,都要做一大堆前置作業。那麼,在沒有這些工具的情況下,我們是不是就沒辦法寫 Vue 了?

對啊(欸等等)!

最簡單的方式,我們這樣就可以開始使用 Vue 了,

<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>

然後你的寫法就會變成,

new Vue({
  template: '<div>{{ msg }}</div>',
  data: function () {
    return {
      msg: 'HelloWorld'
    }
  }
})

然後就覺得,我靠!這樣超難寫的!我要回去用 Vue CLI 建立環境了(住手!) 我們這邊的大前提就是,在 沒有 Webpack 這一類的工具下,所以神說沒有 Webpack 就是沒有 Webpack,沒得含扣。


所以,在這種 極端 環境底下,我們到底要怎麼製作 Vue App?我們該怎麼製作我們的元件?

放棄不可恥,但是有用!

首先,你必須要知道幾件事情:

  • 你的 Vue 不能用 Runtime-only 的套件。
  • 第三方套件不一定可以直接使用。
  • 你可以使用 EventBus 來溝通。

我們可以直接用例子來看,

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>ITHome 2019</title>
  </head>
  <body>
    <div id="app">
      <h1>{{ msg }}</h1>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
    <script>
      (function() {
        new Vue({
          data: function () {
            return {
              msg: 'Hello World'
            }
          }
        }).$mount('#app');
      })();
    </script>
  </body>
</html>

這樣看起來好像沒什麼大問題,如果我們有其他的元件要載入使用呢?

Vue.component(
    'MyComponent',
    {
        name: 'MyComponent',
        template: '<div class="my-component">{{ msg }}</div>',
        data: function () {
            return  {
            msg: 'Hello My Component'
            }
        }
    }
)

然後我們就能在 HTML 裡面使用這個 <my-component></my-component> 元件了。


元件載入問題

我們又要來聊元件載入的問題了。上述的例子你會發現 template 的地方是直接寫在 <script> 裡面,那麼,我們每次要更新的時候,是不是就變得相當麻煩。那麼我們可以怎麼做呢?

  • 把元件獨立到一個 JavaScript 檔案內。
  • template 的部分分開來寫,寫到一個 .html 檔案裡面。
  • 利用 XHR 的方式讀取 .html 的資料,把讀取的資料放到 template 裡面。

具體的方式怎麼做呢?首先,我們可以拿 axios 這個工具來用,

axios({
  method: 'get',
  url: './templates/mycomponent.html',
  responseType: 'text'
}).then(function (res) {
  Vue.component(
    'MyComponent',
    {
      template: res.data,
      data: function () {
        return {
          msg: 'Hello My Component'
        }
      }
    }
  )
})

然後這個檔案我們把他儲存成 ./js/mycomponent.js 之後,再把他放到 index.html 裡面。然後原本的 template 的部分,將他放到 ./templates/mycomponent.html,這樣我們就不需要改 JavaScript 的檔案,直接改樣版檔案即可。

有沒有 MVC 的錯覺。

所以,我們一開始可能會是這樣,

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>ITHome 2019</title>
  </head>
  <body>
    <div id="app">
      <h1>{{ msg }}</h1>
      <my-component></my-component>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.0/axios.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
    <script src="./js/mycomponent.js"></script>
    <script>
      (function () {
        new Vue({
          data: function () {
            return {
              msg: 'Hello World'
            }
          }
        }).$mount('#app');
      })();
    </script>
  </body>
</html>

這個時候你再回到瀏覽器,發現 my-component 怎麼不會出現?原因在於你的 mycomponent.js 當中,是使用 XHR 的方式來讀取樣版檔案。所以,當你的 App 在啟動的時候,你的 XHR 可能還在讀取,所以這個時候,你的 my-component 其實並沒有在 Vue 的全域元件當中。

這樣一來,你就必須要先確保樣版檔案已經讀取進來了,才能將你的 App 啟動。這個時候,我們可以利用 Vue 自身,製作一個 EventBus 來當作監聽工具。所以,我們這個監聽工具需要做哪些事情呢?

  1. 打開一個監聽接口,我們叫做 componentLoaded 代表元件被讀取進來了。
  2. 當監聽接口被觸發之後,我們這個時候才將 Vue App 做初始化啟動。
window.EventBus = new Vue();
window.EventBus.$on(
  'componentLoaded',
  function () {
    new Vue({
      data: function () {
        return {
          msg: 'Hello World'
        }
      }
    }).$mount('#app');
  }
);

然後我們的 mycomponent.js 在讀取完樣版之後,要觸發 我已經讀取完畢 的動作。

// ...前略
window.EventBus.$emit('componentLoaded');

這麼一來,我們的元件看起來就很正常顯示了。

那麼,如果我們有很多元件,巢狀元件的時候該怎麼辦?

Promise.all 是你的好伙伴。

也許你們會覺得很奇怪,為何會有這種奇怪的需求?就如同我剛剛的大前提,當你沒有 Webpack 的時候,這就是一種比較詭異,但是還算是合理的解決方案。只是說,如果我是開發完之後,把 dist 給客戶就好,那麼,好像也不必真的那麼麻煩。


其實在這個系列文後面還是有提到動態載入的事情,只是最近這幾篇文章好像都快講完了?

事情當然不是你們所想的那麼簡單

雖然說我後面會提到 Vue-Router 的部分,但是,這邊先偷偷給大家看一個,搭配 Router 動態載入的實例:

let router = new VueRouter({
    routes: [
        {
            path: '/',
            name: 'homepage',
            component: { template: '<router-homepage></router-homepage>' },
            props: true,
            meta: {
                name: '首頁'
            }
        }
    ]
});

router.beforeEach(function(to, from, next) {
    let template = 'not-found';
    if (typeof to.name !== 'undefined' && to.name !== null) {
        template = to.name;
    }
    return generateTemplateLoader(template).then(function() {
        next();
    });
});

至於那個 generateTemplateLoader 的地方呢?我們這邊就賣個關子吧。等到大概第 21, 22 篇的時候我們再來聊聊。


小結

或許你會覺得,怎麼一直都在講動態載入。我只能說,一旦遇上了你還是得想出各種花招來滿足一些需求。然後,這些所謂 動態載入 都是從奇怪的需求衍生而來的。

可以的話,我也想每次都 VUE CLI 來做啊(苦笑)。

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