【译】Vue.js 3: 面向未来编程

3,541 阅读9分钟

基于函数的api是怎样来解决逻辑复用问题

如果你对Vue.js感兴趣,那么你应该知道Vue3马上就要发布了(如果你在将来来读我这篇文章,那我希望他仍然是有用的)。新版本仍在积极开发中,不过所有新功能都能在RFC仓库中找到。其中有一项是function-api,这将会较大地改变开发vue app的“姿势”。

阅读这篇文章的读者应该要有点javascript和vue经验

当前的api有些什么问题?

最好的方法是用一个例子来说明问题。比如我们现在需要实现一个组件,它能够获取数据,显示loading状态以及一个会根据页面滚动而变化的topbar。效果如下:

效果
效果

demo演示

一个比较好的做法是把公用的逻辑提取出来给别的组件重用。使用当前Vue2的API,比较常用的做法是:

  • Mixins (通过mixins选项)
  • Higher-order components (HOCs) 高阶组件

我们把跟踪滚动的逻辑放到mixin里,把获取数据的逻辑放到高阶组件里。一个典型的实现如下。

Scroll 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。

higher-order component:

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分别用来初始化loading状态以及posts数据。fetchPosts方法在组件创建后每次props.id变化的时候会被调用,以便获取新id的数据。

虽然这不算是个很完整的高阶组件,不过对于这个例子来说已经够用了。现在我们就来包装一个目标组件,并且传入这个目标组件的props。

目标组件是这样的:

// ...
<script>
export default {
    name'PostsPage',
    mixins: [scrollMixin],
    props: {
        idNumber,
        isLoadingBoolean,
        postsArray,
        countNumber
    }
}
</script>
// ...

然后需要通过刚才的HOC来包装一下,这样就能获取到需要的props:

PostsPageOptions: withPostsHOC(PostsPageOptions)

所有源代码能在这里找到,【译注】注意这里面是整个项目的源代码,还包括了后面改进版本的代码,这里的组件scrollMixin是在那个src/components/PostsPageOptions.vue里,HOC是在src/App.vue里的withPostsHOC,这里的目标组件就是PostsPageOptions

好了,我们刚刚使用mixin和HOC来实现了我们的任务,并且mixin和HOC还能够被别的组件通用。但是并不是一切都是那么美好,仍有一些问题在里面。

1.命名冲突

想象一下我们在目标组件里添加update方法的时候:
如果你再打开页面,并且滚动的时候,这个topbar不会再显示了。这是因为我们重写了mixin里的update方法(【译注】而且还不会有报错)。同样的事情也会在那个高阶组件里发生,如果你在data里把fetchedPosts改成posts:

。。。你将会得到这样的错误:

error
error

这是因为目标组件已经有posts了。

2.来源不明确

如果你以后在目标组件中想用另一个mixin的话:

// ...
export default {
    name'PostsPage',
    mixins: [scrollMixin, mouseMixin],
// ...

你能说清楚pageOffset这个属性是由哪个mixin带来的么?或者还有一个场景,例如两个mixins都能有yOffset这个属性或方法,那么后一个mixin将会覆盖掉前一个的yOffset。这样就不太好了,而且还会带来许多不可预料的bugs。

3.性能

高阶组件的另一个问题是,我们仅仅为了逻辑重用,就需要为每个目标组件来创建一个高阶组件实例,这样的创建会带来性能损失。

让我们“setup”

让我们看看下一代Vue能给我们提供什么样的替代方案,以及我们该如何使用function-based API来解决这个问题。

虽然Vue 3还没有发布,不过有个helper插件已经有了-vue-function-api。这样就能够在Vue2中使用Vue3的函数api,来开发下一代Vue应用。

第一步,我们需要装一下:

$ 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来设置我们组件的逻辑。来,让我们来实现一下刚刚那个能够按照滚动来变化的topbar。简单的组件例子如下:

// ...
<script>
export default {
  setup(props) {
    const pageOffset = 0
    return {
      pageOffset
    }
  }
}
</script>
// ...

注意:setup函数的第一个参数是解析过的并且是响应式的props对象。我们在这里还返回了包含pageOffset属性的对象,暴露给render的作用域使用(简单理解为可以在写dom时候当作绑定变量使用)。这个pageOffset属性也是响应式的,不过只在render的作用域内有效。我们就能像以前一样写模板:

<div class="topbar" :class="{ open: pageOffset > 120 }">...</div>

不过这个属性应该在每次页面滚动时都会变化。所以我们还需要在组件mounted的时候添加一个滚动事件监听,并且在unmounted的时候删除监听。而value,onMounted,onUnmounted这3个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>
// ...

其实可以注意到,2.x版本中的每个生命周期函数在setup()函数里都有一个onXXX与其对应。

你可能也注意到了pageOffset变量(【译注】被value api函数初始化后)仅有一个响应式属性:.value。因为像number和string这类基本类型在js中不是按照引用传递的,所以我们需要用这个value来包一下。这个value包装器能够为任何可变的类型提供响应式功能。(【译注】这里的响应式应该就是指一但值有变化,就能够做出响应,比如dom改变等)

这里是包装后的pageOffset的样子:

pageOffset
pageOffset

接下来实现一下数据获取。就像使用普通选项类api(option-based API)一样,你能够(在setup里)使用function-based API来申明computed values和watchers

// ...
<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>
// ...

这个computed value和2.x中的computed属性一样:只有在他依赖的值发生变化的时候才会重新计算。watch的第一个参数被称为"source",可以是以下这些值:

  • 一个getter函数
  • 一个value包装器
  • 一个包含以上两种类型的array(【译注】应该是用来监听多个值时使用的吧)
    第二个参数是一个回调函数,这个函数会在第一个参数里的值有变化后回调。

我们刚刚使用function-based API来实现了这个目标组件。接下来我们要将这个逻辑变的可重用。

拆分解耦

这里比较有意思,为了逻辑重用,我们可以把一些逻辑提取出来放进所谓的“composition function”并且返回可响应的state。

// ...
<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: {
        idNumber
    },
    setup(props) {
        const { isLoading, posts } = useFetchPosts(props)
        const count = computed(() => posts.value.length)
        return {
            ...useScroll(),
            isLoading,
            posts,
            count
        }
    }
}
</script>
/
/ ...

注意这里我们用了useFetchPosts and useScroll这两个函数并返回了可响应的属性。这两个函数能够独立保存成文件并且给其他组件使用。我们和之前option-based的方案对比一下:

  • 暴露给外面使用的属性具有明确的来源,因为它们是作为组合函数的返回值返回出来的。
  • 在组合函数里返回的值都可以随便命名,不会存在命名冲突。(【译注】从上面两个函数可以看出来,在函数里的命名如pageOffset不会对外面造成污染,而只要保证目标组件使用的时候没有冲突就行了)
  • 代码重用之后也不需要有new组件实例。(【译注】对应上面说的高阶组件的额外开销)

还有很多其他优点能在官方RFC找到

文章所有源代码在这里

demo演示地址在这里

总结

可以看到,Vue的function-based API可以干净、灵活地来开发组成组件之间或组件内部的逻辑,而不会有传统基于option-based API开发带来的副作用。想象一下,基于composition functions的这种开发是十分强大的,并且能够应对任何类型的项目——从小到大,组成复杂的web应用。

我希望这篇文章能够起到点作用。如果你有任何想法和疑问,请在下面留言!我很乐意回答。谢谢。

本文翻译自:blog.bitsrc.io/vue-js-3-fu…