[译]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 应用程序,是真的强大。🚀

但愿本篇文章能帮到你🎓,若是你有任何想法或问题,请在下方回复和评论!我将乐意解答🙂,谢谢!

(完)

相关文章
相关标签/搜索