[譯]Vue.js 3:面向將來編程(function-based API)

原文: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

當前 API 存在的問題

最好的學習方式就是看例子了,假設咱們須要實現以下頁面中的組件。這個組件根據當前的滾動偏移,拉取用戶數據、展現加載狀態、顯隱頂部的 topbar。git

線上 demo 地址看 這裏github

把出如今多個組件中的相同邏輯提煉出來是個不錯的實踐。若是使用 Vue 2.x API 的話,有兩個咱們可使用的模式:shell

  1. Mixin(經過 mixins 選項)🍹
  2. 高階組件(Higher-order components,即 HOC)🎢

接下來,咱們將追蹤滾動的邏輯封裝在 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
            }
        })
    }
})
複製代碼

這裏的 isLoadingposts 表示初始加載狀態和獲取到的文章列表。每當組件實例建立或 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 配合高階組件完成了咱們的任務。固然,它們還能夠在其餘組件裏使用。但並不是一切都那麼美好,仍是存在一些問題的。

1. 命名衝突⚔️

假設咱們組件裏新添了一個 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 了。

2. 來源不明📦

隨着業務邏輯的增長,組件中增長了一個 Mixin——mouseMixin

// ...
<script>
export default {
    name: 'PostsPage',
    mixins: [scrollMixin, mouseMixin],
// ...
複製代碼

如今你還能分得清楚 pageOffset 是來自哪一個 Mixin 嗎?或者換個場景,兩個 Mixin 中都有可能包含一個 yOffset。那麼後一個 Mixin 中定義的將會覆蓋前一個的。這不太行,並且還會觸發意料以外的 Bug。😕

3. 性能⏱

高級組件帶來的問題是,咱們爲了重用邏輯,多了一個額外包裝組件的開銷。

來,一塊兒「setup」!🏗

爲了解決上面的問題,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 爲咱們提供的 valueonMountedonMounted 函數:

// ...
<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 的第一個參數稱爲「源」,能夠爲如下類型之一:

  • 一個 getter 函數
  • 一個包裝對象
  • 一個包含以上兩種類型成員的數組

第二個參數是一個回調函數,當第一個參數返回值發生改變後,就會調用。

咱們如今使用 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>
// ...
複製代碼

如今咱們使用 useFetchPostsuseScroll 函數返回響應式屬性。這些函數能夠被存儲在單獨的文件中,能夠被任何須要的組件使用,與基於選項的方式比較發現:

  • 暴露給模板的屬性來源清晰,由於這些屬性都是從組合函數中返回的;
  • 組合函數裏的能夠任意命名,而不用擔憂會跟外部的命名衝突;
  • 爲了重用邏輯,咱們無需再額外提供一個組件實例了。

官方 RFC 頁面 咱們還能夠看到更多的關於使用此方案給咱們帶來的好處。

本篇文章的全部代碼 在這裏 能夠找到。

在線組件實例能夠 在這裏 找到。

總結

如你所見,Vue function-based API 相較於基於選項的 API,是一種更加乾淨和靈活的在組件內部和組件之間組合邏輯的方案。想象一下組合功能對於任何類型的項目——從小型到大型或複雜的 Web 應用程序,是真的強大。🚀

但願本篇文章能幫到你🎓,若是你有任何想法或問題,請在下方回覆和評論!我將樂意解答🙂,謝謝!

(完)

相關文章
相關標籤/搜索