如果你对 Vue.js 感兴趣,那很可能知道这个框架的第 3 个版本即将发布(如果你在未来读这篇文章,我希望本文还具有意义)。这个新版本正在积极开发中,但是所有可能的功能都可以在单独的 RFC(request for comments,即意见征求)处查看:https://github.com/vuejs/rfcs。其中一项特性function-api,将极大地改变开发 Vue 应用程序的方式。
本文主要面向具有基本的 JavaScript 和 Vue 背景知识的人。
开篇之前:使用 Bit 来封装 Vue 组件以及它们的依赖和配置。通过更好的代码复用、更简单的维护和更少的开销来构建真正的模块应用。(译者注,这里是对 Bit 平台的推广)
当前的 API 有什么问题?
最好的方法是在一个例子中展示所有问题。因此,我们可以想象,我们需要实现一个组件,这个组件应该获取某个用户的数据并根据滚动偏移显示加载状态和顶栏。下面是最终结果:
你还可以点击链接查看在线演示。
跨组件抽取一些逻辑进行复用是一种好习惯。使用 Vue 2.x 版本的当前 API,有许多常见的模式,最著名的是:
Mixins(通过 mixins 选项)
高阶组件(HOC)
因此,我们可以将滚动跟踪逻辑转移到一个 mixin,并将数据获取逻辑转移到一个高阶组件。典型的 Vue 实现如下。
滚动 mixin:
const scrollMixin = { data() { return { pageOffset: 0 } }, mounted() { window.addEventListener('scroll', this.update) }, destroyed() { window.removeEventListener('scroll', this.update) }, methods: { update() { this.pageOffset = window.pageYOffset } }}
复制代码
其中,我们增加了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分别针对加载状态和发布数据进行初始化。为了获取新id的数据,fetchPosts方法会在创建实例和props.id每次变化之后触发。
这并不是一个完整的高阶组件实现,但是针对这个例子,已经足够了。这里,我们只是包装了目标组件并传递原始属性以及数据请求相关的属性。
目标组件如下:
// ...<script>export default { name: 'PostsPage', mixins: [scrollMixin], props: { id: Number, isLoading: Boolean, posts: Array, count: Number }}</script>// ...
复制代码
为了获取指定 props,应该把它包装在创建的高阶组件中:
const PostsPage = withPostsHOC(PostsPage)
复制代码
包含模版和样式的完整组件链接在此。
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>// ...
复制代码
如果你重新打开页面并滚动,顶栏不会再显示。这都是由于 mixin 的update方法的重写。这对于高阶组件也适用。如果你将数据域从fetchedPosts改为posts:
const withPostsHOC = WrappedComponent => ({ props: WrappedComponent.props, // ['posts', ...] data() { return { postsIsLoading: false, posts: [] // fetchedPosts -> posts } }, // ...
复制代码
你将会得到如下报错:
报错的原因是封装组件已经用名字posts指定了组件。
2.来源不明
如果一段时间之后,你决定在组件中使用另一个 mixin:
// ...<script>export default { name: 'PostsPage', mixins: [scrollMixin, mouseMixin],// ...
复制代码
你能明确说明pageOffset属性来自哪个 mixin 吗?或者换个场景,两个 mixin 都可以有一个同名属性(比如说yOffset),后一个 mixin 的属性将覆盖前一个 mixin 的属性。这并不好,并且会导致许多不可预见的代码缺陷。
3.性能
高阶组件的问题是,我们需要仅仅因为逻辑复用而牺牲性能去创建单独的组件实例。
让我们来“setup”
让我们来看看下个 Vue.js 版本将提供什么可选方案,以及我们将如何适用基于函数的 API 解决同样的问题。
由于 Vue 3 还没有发布,所以创建了辅助插件——vue-function-api。这个插件提供 Vue 3.x 到 Vue 2.x 的版本的函数 API,用于创建下一代 Vue 应用程序。
首先,你需要进行安装:
$ npm install vue-function-api
复制代码
然后通过Vue.use()进行显式设置:
import Vue from 'vue'import { plugin } from 'vue-function-api'
Vue.use(plugin)
复制代码
基于函数的 API 主要新增了一个新的组件选项——setup()。顾名思义,这里是我们使用新的 API 的功能来设置我们的组件逻辑的地方。因此,让我们实现一个功能来根据滚动偏移显示顶栏,基本组件示例如下:
// ...<script>export default { setup(props) { const pageOffset = 0 return { pageOffset } }}</script>// ...
复制代码
注意,setup函数接收解析过的 props 对象作为它的首个参数,而且这个props对象是响应式的。我们也返回了一个包含pageOffset属性的对象来暴露给模版的渲染上下文。这个属性也变成响应式的,但是只关于渲染上下文。我们可以像往常一样在模版中使用它:
<div class="topbar" :class="{ open: pageOffset > 120 }">...</div>
复制代码
但是,这个属性在每次滚动事件中应该是变化的。为了实现这点,我们需要在这个组件被挂载时增加一个滚动事件监听器,而这个组件卸载时移除这个监听器。value、onMounted、onUnmounted API 函数就是为了实现这些目标而存在:
// ...<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>// ...
复制代码
注意,在 Vue 2.x 版本中,所有生命周期 hooks 都有一个可以在setup()中使用的等效的onXXX函数。
你可能也注意到pageOffset变量包含一个单个的响应式属性:.value。我们需要使用这个包装过的属性,因为在 JavaScript 中像 numbers 和 strings 这样的原始值不是引用传递。值包装器为任何值类型提供了一种方式来传递可变的响应式的引用。
下面是pageOffset对象:
下一步是实现用户数据获取。和使用基于选项的 API 时一样,你可以使用基于函数的 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 函数
一个值包装器
一个包含两个以上类型的数组
第二个参数是一个回调函数,只在从 getter 返回的值或值包装器改变时调用。
我们只是使用基于函数的 API 来实现目标组件。 下一步是使所有这些逻辑可复用。
解构
最有趣到部分是,为了复用部分逻辑的代码,我们只能将它抽取到一个组合函数并返回响应式状态:
// ...<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 基于函数的 API 展示了一个干净灵活的方式来组合组件内部以及组件之间的逻辑,而没有任何基于选项的 API 的缺陷。想象一下,对于任何类型的项目——从小型到大型再到复杂的 Web 应用程序,组合函数会多么有用。
作者介绍:
Taras Batenkov 主要关注 Web 前端和数据科学。
原文链接:
https://blog.bitsrc.io/vue-js-3-future-oriented-programming-54dee797988b
评论