原文:Vue.js 3: Future-Oriented Programming,by Taras Batenkovjavascript
—— function-based API 是如何解決邏輯重用問題的html
若是你在使用 Vue.js,那麼可能知道這個框架的第 3 版就要出來了(若是你是在本篇文章發佈後的一段時間看到這段話的話,我但願個人說法仍是中肯的😉)。新版本目前正在積極開發中,因此可能要加入的特性均可以在官方的 RFC(request for comments)倉庫中看到:github.com/vuejs/rfcs 。其中有一個特性 function-api,將會在很大程度上影響咱們將來 Vue 項目的編寫方式。vue
本篇文章旨在幫助那些有 JavaScript 和 Vue 編寫經驗的人們。java
最好的學習方式就是看例子了,假設咱們須要實現以下頁面中的組件。這個組件根據當前的滾動偏移,拉取用戶數據、展現加載狀態、顯隱頂部的 topbar。git
線上 demo 地址看 這裏。github
把出如今多個組件中的相同邏輯提煉出來是個不錯的實踐。若是使用 Vue 2.x API 的話,有兩個咱們可使用的模式:shell
接下來,咱們將追蹤滾動的邏輯封裝在 Mixin 中,獲取數據的邏輯封裝在高階組件中。下面是通用實現方式:npm
滾動 Mixin:api
const scrollMixin = {
data() {
return {
pageOffset: 0
}
},
mounted() {
window.addEventListener('scroll', this.update)
},
destroyed() {
window.removeEventListener('scroll', this.update)
},
methods: {
update() {
this.pageOffset = window.pageYOffset
}
}
}
複製代碼
此 Mixin 中註冊了 scroll
事件監聽器,追蹤當前頁面的滾動偏移,並將偏移值記錄在 pageOffset
中。數組
高階函數邏輯以下:
import { fetchUserPosts } from '@/api'
const withPostsHOC = WrappedComponent => ({
props: WrappedComponent.props,
data() {
return {
postsIsLoading: false,
fetchedPosts: []
}
},
watch: {
id: {
handler: 'fetchPosts',
immediate: true
}
},
methods: {
async fetchPosts() {
this.postsIsLoading = true
this.fetchedPosts = await fetchUserPosts(this.id)
this.postsIsLoading = false
}
},
computed: {
postsCount() {
return this.fetchedPosts.length
}
},
render(h) {
return h(WrappedComponent, {
props: {
...this.$props,
isLoading: this.postsIsLoading,
posts: this.fetchedPosts,
count: this.postsCount
}
})
}
})
複製代碼
這裏的 isLoading
和 posts
表示初始加載狀態和獲取到的文章列表。每當組件實例建立或 props.id
發生改變時,就會調用 fetchPosts
方法,來獲取新 id
對應的文章內容。
這並不是是一個完整 HOC 的例子,但對於本文說明,已經足夠了。這裏只是簡單地封裝了目標組件,並將獲取相關的 props
與原始的 props
一塊兒傳給了目標組件。
目標組件長這樣:
// ...
<script>
export default {
name: 'PostsPage',
mixins: [scrollMixin],
props: {
id: Number,
isLoading: Boolean,
posts: Array,
count: Number
}
}
</script>
// ...
複製代碼
爲了得到 props,咱們使用上面的高階組件進行封裝:
const PostsPage = withPostsHOC(PostsPage)
複製代碼
完整組件代碼 查看這裏。
OK!咱們已經用 Mixin 配合高階組件完成了咱們的任務。固然,它們還能夠在其餘組件裏使用。但並不是一切都那麼美好,仍是存在一些問題的。
假設咱們組件裏新添了一個 update
方法:
// ...
<script>
export default {
name: 'PostsPage',
mixins: [scrollMixin],
props: {
id: Number,
isLoading: Boolean,
posts: Array,
count: Number
},
methods: {
update() {
console.log('some update logic here')
}
}
}
</script>
// ...
複製代碼
如今打開頁面開始滾動,發現 topbar 不出現了。這是由於 Mixin 中的 update
被組件的同名方法覆蓋了。一樣的問題在高階組件中也存在。好比,咱們把 fetchedPosts
改爲 posts
就有問題:
const withPostsHOC = WrappedComponent => ({
props: WrappedComponent.props, // ['posts', ...]
data() {
return {
postsIsLoading: false,
posts: [] // fetchedPosts -> posts
}
},
// ...
複製代碼
會報錯:
錯誤緣由是目標組件中已經聲明瞭一個叫 posts
的 prop 了。
隨着業務邏輯的增長,組件中增長了一個 Mixin——mouseMixin
:
// ...
<script>
export default {
name: 'PostsPage',
mixins: [scrollMixin, mouseMixin],
// ...
複製代碼
如今你還能分得清楚 pageOffset
是來自哪一個 Mixin 嗎?或者換個場景,兩個 Mixin 中都有可能包含一個 yOffset
。那麼後一個 Mixin 中定義的將會覆蓋前一個的。這不太行,並且還會觸發意料以外的 Bug。😕
高級組件帶來的問題是,咱們爲了重用邏輯,多了一個額外包裝組件的開銷。
爲了解決上面的問題,Vue.js 3 引入了一個可選方案 function-based API。
Vue.js 3 還沒有發佈,不過如今能夠以插件的形式引入此功能——vue-function-api。此插件讓 Vue2.x 能提早使用下一代方案解決問題。
首先安裝:
$ npm install vue-function-api
複製代碼
使用 Vue.use()
顯式安裝此插件:
import Vue from 'vue'
import { plugin } from 'vue-function-api'
Vue.use(plugin)
複製代碼
引入的 function-based API 主要新添加了一個組件選項——setup()
。顧名思義,在這裏可使用新的 API 功能 setup 咱們的組件邏輯。如今,讓咱們實現一個根據滾動偏移量顯示 topbar 的功能吧。
// ...
<script>
export default {
setup(props) {
const pageOffset = 0
return {
pageOffset
}
}
}
</script>
// ...
複製代碼
注意,setup
函數接收一個解析 props 對象做爲參數,而且是響應式的。咱們返回了一個包含 pageOffset
屬性的對象,暴露給模板渲染上下文,這個屬性也是響應式的,但僅在渲染上下文中是這樣。咱們能夠這樣使用:
<div class="topbar" :class="{ open: pageOffset > 120 }">...</div>
複製代碼
可是這個屬性應該在每一次發生 scroll
事件的時候被修改。爲了實現這個效果,咱們須要在組件掛載的時候添加 scroll
事件監聽器,而在卸載的時候,移除此事件監聽器。爲了達到這些目的,須要用到 function-based API 爲咱們提供的 value
、onMounted
和 onMounted
函數:
// ...
<script>
import { value, onMounted, onUnmounted } from 'vue-function-api'
export default {
setup(props) {
const pageOffset = value(0)
const update = () => {
pageOffset.value = window.pageYOffset
}
onMounted(() => window.addEventListener('scroll', update))
onUnmounted(() => window.removeEventListener('scroll', update))
return {
pageOffset
}
}
}
</script>
// ...
複製代碼
須要注意的是,2.x 版本中提供的全部生命週期鉤子函數,在這裏都有 onXXX
函數與之對應,並能夠在 setup()
中使用。
你可能注意到了,變量 pageOffset
變量包含一個響應式屬性 .value
。之因此使用包裝對象(Value wrappers),是由於 JavaScript 中像數值、字符串這樣的基本類型值不是經過引用傳遞的,而是經過值賦值的方式。而包裝對象就提供了爲任何類型值提供可修改和可響應的方案。
pageOffset
對象看起來是這樣的:
下一步是實現獲取用戶數據。一樣,與基於選項的 API 相似,function-based API 一樣提供了聲明計算屬性和 Watcher 的 API:
// ...
<script>
import {
value,
watch,
computed,
onMounted,
onUnmounted
} from 'vue-function-api'
import { fetchUserPosts } from '@/api'
export default {
setup(props) {
const pageOffset = value(0)
const isLoading = value(false)
const posts = value([])
const count = computed(() => posts.value.length)
const update = () => {
pageOffset.value = window.pageYOffset
}
onMounted(() => window.addEventListener('scroll', update))
onUnmounted(() => window.removeEventListener('scroll', update))
watch(
() => props.id,
async id => {
isLoading.value = true
posts.value = await fetchUserPosts(id)
isLoading.value = false
}
)
return {
isLoading,
pageOffset,
posts,
count
}
}
}
</script>
// ...
複製代碼
此處的計算屬性與 2.x 中的提供的計算屬性行爲相似:會跟蹤依賴,若是依賴的值發生改變,就會從新計算。傳遞給 watch
的第一個參數稱爲「源」,能夠爲如下類型之一:
第二個參數是一個回調函數,當第一個參數返回值發生改變後,就會調用。
咱們如今使用 function-based API 實現了目標組件。下一步再來看如何使邏輯可以重用吧。
這一部分很是有趣,將與某塊邏輯相關的代碼提煉出來並重用,須要用到「組合函數」(composition function),咱們在函數中返回響應式狀態變量。
// ...
<script>
import {
value,
watch,
computed,
onMounted,
onUnmounted
} from 'vue-function-api'
import { fetchUserPosts } from '@/api'
function useScroll() {
const pageOffset = value(0)
const update = () => {
pageOffset.value = window.pageYOffset
}
onMounted(() => window.addEventListener('scroll', update))
onUnmounted(() => window.removeEventListener('scroll', update))
return { pageOffset }
}
function useFetchPosts(props) {
const isLoading = value(false)
const posts = value([])
watch(
() => props.id,
async id => {
isLoading.value = true
posts.value = await fetchUserPosts(id)
isLoading.value = false
}
)
return { isLoading, posts }
}
export default {
props: {
id: Number
},
setup(props) {
const { isLoading, posts } = useFetchPosts(props)
const count = computed(() => posts.value.length)
return {
...useScroll(),
isLoading,
posts,
count
}
}
}
</script>
// ...
複製代碼
如今咱們使用 useFetchPosts
和 useScroll
函數返回響應式屬性。這些函數能夠被存儲在單獨的文件中,能夠被任何須要的組件使用,與基於選項的方式比較發現:
在 官方 RFC 頁面 咱們還能夠看到更多的關於使用此方案給咱們帶來的好處。
本篇文章的全部代碼 在這裏 能夠找到。
在線組件實例能夠 在這裏 找到。
如你所見,Vue function-based API 相較於基於選項的 API,是一種更加乾淨和靈活的在組件內部和組件之間組合邏輯的方案。想象一下組合功能對於任何類型的項目——從小型到大型或複雜的 Web 應用程序,是真的強大。🚀
但願本篇文章能幫到你🎓,若是你有任何想法或問題,請在下方回覆和評論!我將樂意解答🙂,謝謝!
(完)