在很久很久以前,我夢到了一個朋友那邊來的請求,是想幫忙調整一個外包使用 Vue 開發的前端頁面。然後我收到朋友給的 Gitlab 之後,在我 Clone 下來,並且嘗試用 Chrome 跑跑看的時候,我看到了一個叫做 <iframe href="首頁.html"...
的東西,然後我夢就醒了。
關於 Component 這件事
我之前(這裡 跟 這裡)也寫過一點關於元件的事情,不過,不知道為何總是會看到一些奇怪的用法。或者是說,其實只是想 new Vue()
這樣看起來比較潮(?),然後後面就把每一個 Vue 當作是單一元件在使用,換句話說,就是一個 html 一個 Vue,這樣看起來就比較厲害。
接踵而至的問題就在於,每個元件失去了溝通方式。如果都只是在同一個 html 底下還好,頂多就是使用 EventBus 的方式,雖然比較麻煩,但是也不失為各個元件互相溝通的一種方法。關於元件互相溝通這件事情,這裡有一位 老司機 的文章可以參考一下(上車囉)。
動態載入
如果你不想將所有的東西都包在同一個 App 裡面運作,每次修改就是一大包的話,動態載入也是一種可行的方式,我之前聊過了,今天就來聊一點別的東西。
元件被切開所衍生的問題其實不少,
- 元件之間的溝通。
- 每個元件都有屬於自己的
scope
。 - 每一個 Vue Instance 都算是獨立個體。
$mount
造成的差異(Kuro 有說過這件事)。
這裡所謂的『切開』,不是你在 App 裡面使用 Vue.component(...)
這種方法,這樣理論上還是屬於同一個 App,並不能算是切開的動作。所謂的『切開』是指,你在每一次驅動 App 的時候,都呼叫了 $mount
來將 App 綁定到頁面上。換句話說,你若是每一個 html 檔案,都初始化了一個 Vue,或是說,你在同一個 Vue 檔案,同時初始化了多個 Vue,然後綁定($mount
)在不同的 DOM 上面,這是這裡所謂的『切開』的意思。
所以說,不同的 $mount
造成的差異是什麼呢?
- 每個個體都有各自的
scope
。 - 你的
rootScope
不是你的rootScope
。 - 每個元件的 Life-cycle 跟你載入的順序,或者是
$mount
順序無關。
最終,你是不是終究要面對這種差異,還是說全部包在同一個 App 就好了?
不使用 Webpack 包裝的範例
由於我不會用 Webpack,所以這邊就只聊一下不使用 Webpack 來封裝的一些元件使用範例。依據 Vue 官方的新手使用手冊,我們使用 Vue 的方式就是載入 vue.js
。這是我們的 index.html
所需要的內容,那麼,如果我們還需要做簡易的 Router 的話,就接著載入來用就好了。
<body>
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue-router.min.js"></script>
<script src="./main.js"></script>
</body>
接著我們來做 main.js
基本的部分,
import Vue from 'vue';
import VueRouter from 'vue-router';
let router = new VueRouter({
routes: []
});
new Vue({
router,
el: '#app'
});
然後,你打開瀏覽器發現什麼都沒有,這是正確的喔!因為你的 Router 裡面本來什麼都沒有。接著,我們想要將首頁叫做 homepage
元件,所以說,我們就在路由表上加入一個 homepage
元件。
let router = new VueRouter({
routes: [
{
path: '/',
name: 'homepage',
component: { template: '<my-homepage></my-homepage>' },
props: true,
meta: {
name: '首頁'
}
}
]
});
然後你再次打開瀏覽器,發現還是什麼都沒有。這是因為 <my-homepage>
這個元件並不存在,所以畫面上也沒辦法染出什麼東西。 !!注意!! 上述原始碼當中的 component
寫法,請參照官方的 文件說明,在這邊就不贅述了。
接著我們得思考,到底要怎麼把 <my-homepage>
給載入呢?
- 直接在
component
實例化一個 Vue。 - 使用
Vue.component
。
衍生問題,
- 因為沒有 webpack,所以沒辦法用
.vue
來做。 - 因為沒有 webpack,所以你的 html 要想辦法拋給
template
。 - 你一直
new Vue()
你有想過瀏覽器的感受嗎?
這邊提供一個解決方式,
- 使用 xhr 將樣版檔案載入。
- 樣版檔案載入之後,再去初始化 Component。
- 最後將初始化的結果拋給 Router。
實際上的操作方式,以下提供一點範例,
!!注意!! 範例程式碼若是運行中有蟲不賠,複製貼上前請詳閱公開說明書。
// 接續著上面的 Router,我們針對 beforeEach 動手腳。
router.beforeEach(function(to, from, next) {
let template = '';
if (typeof to.name !== 'undefined' && to.name !== null) {
template = to.name;
}
return generateTemplateLoader(template).then(function() {
next();
});
});
上述的意思是說,每次 Router 進來之前,我們就先拿到你 想去的地方 叫什麼名字,然後把你的名字餵給 template
,請一個叫做 generateTemplateLoader
的人,幫我們把樣版給拿回來,拿回來之後再去運作 next()
。具體 Router 的操作方法,請參閱 官方手冊。
那麼 generateTemplateLoader
會幫我們做些什麼事情?
- 他是一個
Promise
。 - 他會先看看這個
template
有沒有載入過? - 如果有,直接
resolve
。 - 如果沒有,就把那個
template
載入。
那麼,所謂的 template
具體來說是什麼?
- 他是一個 JavaScript 檔案。
- 你的
template
叫什麼名字,JavaScript 檔案就叫做什麼名字。 - 以上述的例子來說,你會有一支叫做
homepage.js
的檔案。
所以 generateTemplateLoader
大概會是這樣,
let generateTemplateLoader = function(template) {
// 注意,我們這邊不處理 reject 的情況,請自行解決。
return new Promise(function(resolve) {
let exists = document.getElementById('my-' + template);
if (exists) {
resolve(true);
} else {
let ps = document.getElementsByTagName('script')[0];
let s = document.createElement('script');
s.src = './js/' + template + '.js';
s.type = 'text/javascript';
s.id = 'my-' + template;
s.onload = function() {
resolve(true);
};
ps.parentNode.insertBefore(s, ps);
}
});
};
上述就是利用插入 <script>
的方式,將我們的 homepage.js
載入。然後,你再次的打開瀏覽器,然後發現還是空的。歐,因為你還沒寫 homepage.js
的內容啊。這個時候你可能猜得到,既然 App 缺少了 <my-homepage>
這個元件,那麼我的 homepage.js
就是要補上這個元件對吧。
然後你的 homepage.js
可能就這麼開始寫了,
Vue.component('my-homepage', {
template: '<h1>Hello World!</h1>'
});
接著你回到瀏覽器,發現好像出現了 Hello World!
了耶,如果你沒有出現的話,應該是哪裡做錯了,這一點都不甘我的事情喔。
!!注意!! 範例程式碼若是運行中有蟲不賠,複製貼上前請詳閱公開說明書。
載入元件所需要的樣版
接續上面的範例,我們在製作 homepage.js
的時候,裡面的 template
總不可能每次都寫一大堆 HTML 然後拼成字串吧?如果你要這麼做的話,其實我也是不反對的。所以,我們可以借用一下 XHR 來幫我們拿到比較不一樣的東西,例如說,一個叫做 homepage.html
的檔案。
那麼,因為我不會寫 XHR,所以就先用 axios 來幫我們做掉吧。首先,在 index.html
需要加入一點東西,
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
然後,修改一下 homepage.js
的地方,
axios({
method: 'get',
url: '../templates/homepage.html',
responseType: 'text'
}).then(function(res) {
Vue.component('my-homepage', {
template: res.data
});
});
然後我們在 templates/homepage.html
裡面就寫,
<div>
<h1>Hello World!</h1>
</div>
不要問我為何需要用 <div>
包起來,請參考 官方說明。
接著打開瀏覽器,發現,什麼都沒有。
!!注意!! 範例程式碼若是運行中有蟲不賠,複製貼上前請詳閱公開說明書。
以上的範例有幾個問題,
- axios 本身是
Promise
。 generateTemplateLoader
的onload
僅代表homepage.js
被 讀取 完了。- 承上,不代表
Promise
已經被resolve
了。 - 所以當
onload
觸發時,Router 運行next()
後,會發現其實元件還沒準備好。
那麼,這個時候需要來一點 Event 互相溝通,最簡單的方式,就是 new Vue()
來當作 EventBus 使用,
// ... 前略
window.EventBus = new Vue();
// ... 中略
if (exists) {
resolve(true);
} else {
// 加入一個 EventBus 事件監聽
window.EventBus.$off('templateLoaded');
window.EventBus.$on('templateLoaded', function() {
resolve(true);
});
let ps = document.getElementsByTagName('script')[0];
let s = document.createElement('script');
s.src = './js/' + template + '.js';
s.type = 'text/javascript';
s.id = 'my-' + template;
// 原本的 onload 不需要了。
ps.parentNode.insertBefore(s, ps);
// ... 後略
接著我們的 homepage.js
做一點事件發送的動作,
axios({
method: 'get',
url: '../templates/homepage.html',
responseType: 'text'
}).then(function(res) {
Vue.component('my-homepage', {
template: res.data
});
// 發送一個事件,就說這個樣版已經讀取好了。
window.EventBus.$emit('templateLoaded');
});
這樣做的話,你的 homepage.js
就會動態載入到頁面上,然後你的 templates/homepage.html
會被 homepage.js
載入到 Vue 的元件裡面,當作是樣版來使用。當然,如果要在 homepage.html
當中,使用 Vue 的相關動作也是可以的。
<div>
<h1>Hello {{ yourName }}!</h1>
</div>
樣版有幾個必須留意的地方,
- 如果有使用到其他元件,請確認元件已經被載入。
- 樣版的絕對路徑與
index.html
相同。 - 承上,如果你有圖片資料夾
images
,樣版裡要寫./images/
,即便你的樣版檔案放在./templates/homepage.html
裡面。 - 如果你沒有使用 Vuex,那麼元件溝通多半得用 EventBus,或是你狂用 $refs 也可以。
- 如果你有使用
:is
這個 Vue 元件的魔術方法,請確認元件已經被載入。
文末
如果你不想使用 Webpack,這裡是一個蠻輕便的解法。至於 <iframe
,我想應該是前陣子比較晚下班,遇到什麼髒東西的緣故吧(欸)。
!!注意!! 範例程式碼若是運行中有蟲不賠,複製貼上前請詳閱公開說明書。