Vue 3 中那些激動人心的新功能
- 2019 年 11 月 7 日
- 筆記
讓我們先從大多數人可能都聽說過的 API 開始……
合成 API
合成(Composition)API 是 Vue 的新大版本中討論最多的特色語法。這是一種全新的邏輯復用和程式碼組織方法。
目前,我們使用所謂 Options API 構建組件。要將邏輯添加到 Vue 組件中,我們要填充(option)諸如 data、methods、computed 之類的屬性。這種方法的最大缺點是它本身並非有效的 JavaScript 程式碼。你需要準確了解模板中可以訪問哪些屬性,以及 this 關鍵字的行為。在後台,Vue 編譯器需要將屬性轉換為可用的程式碼。因此我們無法享受自動提示或類型檢查功能的幫助。
合成 API 會將組件屬性中當前可用的機制暴露為 JavaScript 函數,從而解決這個問題。Vue 核心團隊將合成 API 定義為「一組基於函數的附加 API,可以靈活地組合組件邏輯」。用合成 API 編寫的程式碼更具可讀性,也沒有在幕後隱藏什麼魔法,這使它更易於閱讀和學習。
下面來看一個使用了新的合成 API 的組件示例,從中了解其工作機制。
<template> <button @click="increment"> Count is: {{ count }}, double is {{ double }}, click to increment. </button> </template> <script> import { ref, computed, onMounted } from \'vue\' export default { setup() { const count = ref(0) const double = computed(() => count.value * 2) function increment() { count.value++ } onMounted(() => console.log(\'component mounted!\')) return { count, double, increment } } } </script>
現在我們將這段程式碼分解為幾部分,看看到底發生了什麼事情:
import { ref, computed, onMounted } from \'vue\'
如前所述,合成 API 將組件屬性暴露為函數,因此第一步是導入所需的函數。在我們的示例中,我們需要用 ref 創建響應性引用、用 computed 創建計算屬性,並用 onMounted 訪問掛載的生命周期 hook。
現在你可能想知道神秘的 setup 方法是做什麼的?
export default { setup() {
簡單說,它只是一個將屬性和函數返回到模板的函數,僅此而已。我們在這裡聲明所有響應屬性、計算屬性、觀察者和生命周期 hooks,然後返回它們,以便在模板中使用。
我們沒有從 setup 函數返回的內容將無法在模板中使用。
const count = ref(0)
根據上述內容,我們使用 ref 函數聲明了稱為 count 的響應屬性。它可以包裝任何原語或對象,並返回其響應性引用。傳遞的元素的值將保留在所創建引用的 value 屬性中。例如,如果要訪問 count 引用的值,則需要顯式請求 count.-value。
const double = computed(() => count.value * 2) function increment() { count.value++ }
這正是我們在聲明計算屬性 double 和 increment 函數時所做的事情。
onMounted(() => console.log(\'component mounted!\'))
我們使用 onMounted hook 在組件掛載時記錄了一些消息,這裡只是告訴你可以這樣做?
return { count, double, increment }
最後,我們使用 increment 方法返回 count 和 double 屬性,以使其在模板中可用。
<template> <button @click="increment"> Count is: {{ count }}, double is {{ double }}. Click to increment. </button> </template>
好了!現在,我們可以訪問模板中 setup 方法返回的屬性和函數,就像通過舊的 Options API 聲明它們一樣。
這是一個簡單的示例,也可以通過 Options API 輕鬆實現。新的合成 API 的真正好處不僅僅是以不同的方式編碼,在復用我們的程式碼 / 邏輯時,其優勢就能顯現出來。
使用合成 API 復用程式碼
新的合成 API 有更多優勢。想想程式碼復用,這就是它的一大用武之地。目前,如果我們要在其他組件之間共享一些程式碼,則有兩個選擇可用——分別是 mixins 和作用域插槽。兩者都有自己的缺點。
假設我們要提取 counter 功能並在其他組件中復用。下面的程式碼中你可以看到如何分別使用現有的 API 和新的合成 API 做到這一點:
先從 mixins 開始:
import CounterMixin from \'./mixins/counter\' export default { mixins: [CounterMixin] }
mixins 的最大缺點是我們對它實際上添加到組件中的內容一無所知。這不僅讓程式碼很難理解,而且還可能導致命名與已有的屬性和函數發生衝突。
下面是作用域插槽。
<template> <Counter v-slot="{ count, increment }"> {{ count }} <button @click="increment">Increment</button> </Counter> </template>
使用作用域插槽時,我們確切地知道可以通過 v-slot 屬性訪問哪些屬性,因此程式碼更容易理解。這種方法的缺點是我們只能在模板中訪問它,並且只能在 Counter 組件作用域中使用。
現在是時候使用合成 API 了:
function useCounter() { const count = ref(0) function increment () { count.value++ } return { count, incrememt } } export default { setup () { const { count, increment } = useCounter() return { count, increment } } }
是不是更優雅?我們不受模板和組件作用域的限制,並且確切地知道可以從 counter 訪問哪些屬性。此外,由於 useCounter 只是返回某些屬性的函數,因此我們可以獲得編輯器中程式碼自動完成的幫助。沒有隱藏在幕後的魔法,因此編輯器可以幫助我們檢查類型並給出建議。
使用第三方庫也能變得更優雅。例如,如果我們要使用 Vuex,則可以顯式使用 useStore 函數,而不用污染 Vue 原型(this.$store),這種方法也消除了 Vue 插件的幕後魔法。
const { commit, dispatch } = useStore()
如果你想了解有關合成 API 及其用例的更多資訊,我強烈建議你閱讀 Vue 團隊寫的這篇文檔。這份文檔解釋了新 API 背後的理念,並給出了最佳用例的建議。Vue 核心團隊的 ThorstenLünborg 還提供了一個很棒的存儲庫,其中包含合成 API 的使用示例。
全局掛載 / 配置 API 更改
在實例化和配置應用程式的方式方面,還有一項重大變化。現在我們是這樣做的:
import Vue from \'vue\' import App from \'./App.vue\' Vue.config.ignoredElements = [/^app-/] Vue.use(/* ... */) Vue.mixin(/* ... */) Vue.component(/* ... */) Vue.directive(/* ... */) new Vue({ render: h => h(App) }).$mount(\'#app\')
目前我們使用全局 Vue 對象來提供配置並創建新的 Vue 實例。對 Vue 對象所做的任何更改都會影響每個 Vue 實例和組件。
下面我們看看 Vue 3 中是怎麼做的:
import { createApp } from \'vue\' import App from \'./App.vue\' const app = createApp(App) app.config.ignoredElements = [/^app-/] app.use(/* ... */) app.mixin(/* ... */) app.component(/* ... */) app.directive(/* ... */) app.mount(\'#app\')
你可能已經注意到,每個配置都局限於使用 createApp 定義的某個 Vue 應用程式。
它可以讓你的程式碼更容易理解,並且不容易出現由第三方插件引起的意外問題。當前,如果某些第三方解決方案正在修改 Vue 對象,則可能會以意想不到的方式(尤其是全局 mixins)影響你的應用程式,而 Vue 3 則不會出現這種情況。
這一 API 更改現在正在這個 RFC 中討論,意味著將來它可能還會繼續變動。
片段
Vue 3 中值得期待的另一個激動人心的新功能是片段(Fragments)。
你可能會問什麼是片段?嗯,如果你創建一個 Vue 組件,則它只能有一個根節點。這意味著無法創建這樣的組件:
<template> <div>Hello</div> <div>World</div> </template>
原因是代表任何 Vue 組件的 Vue 實例都需要綁定到單個 DOM 元素中。想要創建具有多個 DOM 節點的組件,唯一的方法是創建一個沒有基礎 Vue 實例的功能組件。
事實證明,React 社區也有同樣的問題。他們提出的解決方案是一個名為片段(Fragment)的虛擬元素。它看起來差不多是這個樣子:
class Columns extends React.Component { render() { return ( <React.Fragment> <td>Hello</td> <td>World</td> </React.Fragment> ); } }
儘管片段看起來像是普通的 DOM 元素,但它是虛擬的,根本不會在 DOM 樹中渲染。這樣我們可以將組件功能綁定到單個元素中,而無需創建多餘的 DOM 節點。
目前,你可以在 Vue 2 中使用 vue-fragments 庫來應用片段,而在 Vue 3 中它是開箱即用的!
Suspense
React 生態系統中還有一個好主意也將在 Vue 3 中採用,就是 Suspense 組件。
Suspense 會暫停你的組件渲染,並渲染回退組件,直到滿足一個條件為止。在 Vue London Evan 期間,Vue 團隊簡單介紹了這個主題,並向我們展示了值得期待的 API。到頭來 Suspense 只是一個具有插槽的組件:
<Suspense> <template > <Suspended-component /> </template> <template #fallback> Loading... </template> </Suspense>
在 Suspended-component 完全渲染之前將顯示回退組件。Suspense 可以等待組件下載完畢(如果該組件是非同步的),或者在 setup 函數中執行一些非同步操作。
多個 v-model
V-model 是一種指令,可用於在給定組件上實現雙向綁定。我們可以傳遞響應性屬性,並從組件內部對其進行修改。
從表單元素可以很好地了解 v-model:
<input v-bind="property />
但是你知道可以對所有組件使用 v-model 嗎?在後台,v-model 只是傳遞 value 屬性和偵聽 input 事件的捷徑。將上面的示例重寫為以下語法會有完全相同的效果:
<input v-bind:value="property" v-on:input="property = $event.target.value" />
我們甚至可以使用組件 model 屬性更改默認屬性和事件的名稱:
model: { prop: \'checked\', event: \'change\' }
如你所見,如果我們希望在組件中進行雙向綁定,則 v-model 指令可能是一個非常有用的語法糖。不幸的是,每個組件只能有一個 v-model。
還好在 Vue 3 中不會有這個問題!你將能夠給 v-model 賦予屬性名稱,並根據需要擁有儘可能多的 v-model。下面這個示例中,你可以在一個表單組件中找到兩個 v-model:
<InviteeForm v-model:name="inviteeName" v-model:email="inviteeEmail" />
這一 API 更改現在正在這個 RFC 中討論,這意味著將來可能會有新的變化。
Portals
Portals 是特殊的組件,用來在當前組件之外渲染某些內容。這也是 React 原生實現的功能之一。React 文檔關於 portals 是這樣介紹的:
「Portals 提供了一種一流的方式來將子級渲染到父組件的 DOM 層級之外的 DOM 節點中。」
這是一種處理模態、彈出窗口以及頁面頂部組件的非常好用的方法。通過 portals,你可以確保沒有任何主機組件 CSS 規則會影響你要顯示的組件,也無需使用 z-index 搞些小動作了。
對於每個 portal,我們需要指定其目標位置,在其中渲染 portal 內容。下面是 portal-vue 庫的實現,它為 Vue 2 添加了這一功能:
<portal to="destination"> <p>This slot content will be rendered wherever thportal-target with name \'destination\' is located.</p> </portal> <portal-target name="destination"> <!-- 這個組件可以放在你應用中的任何位置上 上面 portal 組件的插槽內容會在這裡渲染。 --> </portal-target>
Vue 3 將提供對 portals 的開箱即用支援!
新的自定義指令 API
自定義指令 API 在 Vue 3 中將略有變化,以更好地與組件生命周期保持一致。這項更改會讓 API 更加直觀,從而幫助新手更容易地理解和學習 API。
這是當前的自定義指令 API:
const MyDirective = { bind(el, binding, vnode, prevVnode) {}, inserted() {}, update() {}, componentUpdated() {}, unbind() {} }
下面是 Vue 3 中的樣子:
const MyDirective = { beforeMount(el, binding, vnode, prevVnode) {}, mounted() {}, beforeUpdate() {}, updated() {}, beforeUnmount() {}, // new unmounted() {} }
雖然這是一項重大更改,使用 Vue 兼容構建也應該能輕鬆覆蓋。
這一 API 更改現在正在這個 RFC 中討論,意味著將來它可能還會繼續變動。
小結
除了合成 API(它是 Vue 3 中最重要的新 API)之外,我們還能在新版中找到很多較小的改進。我們可以看到,Vue 正在向更好的開發人員體驗和更簡單、更直觀的 API 的目標前進。我們也很高興看到 Vue 團隊決定採用目前由第三方庫帶來的許多理念,將它們引入核心框架。
上面的內容只介紹了主要的 API 更改和改進。如果你對其他內容感到好奇,請務必查看 Vue RFCs 存儲庫。