原文: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 应用程序,是真的强大。🚀
但愿本篇文章能帮到你🎓,若是你有任何想法或问题,请在下方回复和评论!我将乐意解答🙂,谢谢!
(完)