上一回有提到動態載入的事情,既然提到了動態的元件載入,就不得不提一下 functional component 這個東西。
是說,也沒有太多神奇的地方,只是借用了 React 的東西拿來班門弄斧一下而已。
Functional Component
官方文件 有提到詳細的功能,有興趣的人可以先去看看。基本上他就是一個無狀態、不屬於任何生命週期的一種元件。
用這種元件的好處是什麼?
- 預先載入其他的元件
- 預先處理邏輯
官方的範例當中,就是提及使用他依據不同的條件,來載入不同的元件。這一點很有趣,因為我們可以做在 Vue-Route 裡面的事情,就能搬到函式化的元件裡面來做。當然,這麼做也不是沒有缺點,因為對於整個 Router 來說,你相對應的 Router Name 就會一樣,這一點可能在操作 <route-link>
的時候會造成困擾。
搭配 Vue-Route
剛剛提及了,原本在 Router 裡面做的事情,可以用函式化元件取代。舉一個簡單的例子來說,
// 原本我們的 Routes 是這個樣子
routes: [
{
path: '/hello/world',
name: 'World',
component: World
}
]
如果我們用函式化元件來取代的話,可能會變這個樣子,
// 把原本的 World 用函式化元件取代
routes: [
{
path: '/:path1?/:path2?',
name: 'FunctionalComponent',
component: FunctionalComponent,
// 請務必確認有加這一行,用來將路徑資料傳入元件中
props: true
}
]
然後你的函式化元件基本結構會長這樣,
export default {
name: 'container',
functional: true,
render (createElement, context) {
// ... 中略
},
props: {
path1: {
type: String,
default: ''
},
path2: {
type: String,
default: ''
}
}
}
接著我們看 render
那邊要做什麼事情,
render (createElement, context) {
// 簡單來說就是直接把 ./path1/path2.vue 檔案拉進來用
return createElement(
require(`./${context.props.path1}/${context.props.path2}.vue`).default,
context.data,
context.children
)
}
當然,上述例子如果找不到檔案就會噴錯誤,所以例外還是要處理一下。例如說,沒有檔案的話就做一個 404 的元件給他顯示之類的。
壞處
這樣你的 Routes 可能永遠都只有一條,然後剛剛提到了,你的 Router Name 就只會是 FunctionalComponent
而已,其實對於之後元件內部要使用 <route-link>
的時候會是一個麻煩的地方。
邪魔歪道的實作
由於你不知道真正的路徑到底會有多少,所以你的 Routes 可能會變成這樣,
// 偷偷用 MCV 的路徑命名規則
routes: [
{
path: '/:component?/:action?/:params(.*)?',
name: 'FunctionalComponent',
component: FunctionalComponent,
props: true
}
]
然後你在函式化元件當中就會有 component
, action
與 params
三種數值可以使用,而這三個可以分別代表你要載入的元件、執行的方法以及其他參數。
實際上的作法大概會是,
// NotFound 是外部引入的,當作是找不到畫面的元件
// Homepage 也是外部引入,當作首頁
render (createElement, context) {
const component = context.props.component
const action = context.props.action
const renderComponent () => {
if (component === '') {
return Homepage
} else {
try {
if (action === '') {
return require(`./${component}.vue`).default
} else {
return require(`./${component}/${action}.vue`).default
}
} catch (e) {
return NotFound
}
}
}
return createElement(
renderComponent(),
context.data,
context.children
)
}
然後,在你的 ${component}.vue
或是 ${action}.vue
都可以拿到 props
的數值。請注意最後一個 params
的地方,由於他的正規表示式是 (.*)?
,所以你會拿到類似這樣的東西,
/category/1/page/2
當然這對我們來說很難使用,所以可以用 reduce
處理一下,讓他變成陣列比較好取用。所以我們可以在元件內做一些處理,例如這樣,
export default {
name: 'hello',
props: {
params: {
type: String,
default: ''
}
},
beforeMount () {
// 用來處理傳入的 params
if (this.params !== '') {
// 把他變成 { key: 0, value: 'category' } 這種格式
this.parameters = this.params.split('/')
.reduce((carry, item, index) => {
carry = carry.concat([
{
key: index,
value: item
}
])
return carry
}, [])
}
},
data () {
return {
parameters: []
}
}
}
但是這裡有一件事情要處理,由於 beforeMount
只會發生在元件還沒被綁到 DOM 上面之前,所以,如果你的元件是可覆用的,那麼 beforeMount
就有機會不會被觸發。
所以,需要處理 沒有被觸發 的情形。
搭配 :is
這個就更邪魔了,其實不建議這樣用!但是我還是實作出了一個範例(抹臉
上一回有提到 :is
這個東西,也是可以動態更換元件。那麼,如果說我們在 ${action}.vue
裡面,將第一個 parameters 當作更細的子元件來操作,那麼我們就能用 :is
來進行。
先說缺點:
:is
要呼叫方法,但是這個方法可能不會被觸發- 可以再用一次
require
來把元件載入,但要處理元件失效的問題
所以如果要用 :is
的話該怎麼做?首先,我們需要一個元件,
<component :is="myComponent"></component>
接著,只要這個 ${action}.vue
被載入,然後如果第一個參數存在,就可以指定成元件,
import MyComponent from './MyComponent.vue'
export default {
name: 'b',
components: {
helloWorld: MyComponent
},
// ... 中略
beforeMount () {
// ... 處理 parameters
if (this.parameters.length > 0) {
if (this.parameters[0] !== '') {
// 把第一個數值指定成元件的名字
this.myComponent = this.parameters[0]
}
}
}
}
所以這樣,當你訪問 /a/b/hello-world
的時候,就會動態載入 MyComponent.vue
了。
問題在於,如果你訪問其他路徑,再次回到 /a/b/hello-world
的時候,你會發現畫面不會更新。原因就在於 beforeMount
並不會重新被呼叫一次,也就是上一點說的 沒有被觸發 的情形。
為何 beforeMount
不會被觸發?
原因在於,你的 hello-world
元件是在 b
元件內被載入,而 b
元件本來就已經載入了,所以當你切換路徑的時候,本來就不會觸發 beforeMount
,這是合理的。
所以,你可以去偷聽 $route
來做到一些改變。
export default {
name: 'b',
// ... 中略
watch: {
$route (route) {
if (typeof route.params.params !== 'undefined') {
// 這邊要重新處理一次 parameters
//
// 最後把 parameters 第一個數值取出來
this.myComponent = this.parameters[0]
}
}
}
}
但是每次都 watch
其實也不太好,最好是 beforeDestroy
的時候 unwatch 比較好。
// 所以可以在 created 的時候綁定 $watch
created () {
// 把 $watch 指定給 this.routerWatcher
// 這樣我們在元件消滅之前可以先 unwatch
this.routerWatcher = this.$watch(() => this.$route, route => {
// 剛剛上面的東西放到這裡面來
}
},
// ... 中略
beforeDestroy () {
// unwatch 我們剛剛的東西,或許可以省一點資源?
this.routerWatcher()
}
結論
其實沒有比較好用,都只是一些奇技淫巧,小朋友不要學(欸