写点什么

Vue.js 3:面向未来的编程

  • 2019-11-10
  • 本文字数:4612 字

    阅读完需:约 15 分钟

Vue.js 3:面向未来的编程

如果你对 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 } }) }})
复制代码


其中,isLoadingposts分别针对加载状态和发布数据进行初始化。为了获取新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>
复制代码


但是,这个属性在每次滚动事件中应该是变化的。为了实现这点,我们需要在这个组件被挂载时增加一个滚动事件监听器,而这个组件卸载时移除这个监听器。valueonMountedonUnmounted 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>// ...
复制代码


注意我们是如何使用useFetchPostsuseScroll函数来返回响应式属性的。这些函数可以被存储在单独的文件中,并且任何其它组件中使用。相较于基于选项的方案:


  • 暴露到模板的属性拥有清晰的来源,因为它们是组合函数返回的值;

  • 从组合函数返回的值是任意命名的,因此没有命名空间冲突;

  • 没有仅仅因为逻辑复用目的而创建的不必要的组件实例。


官方RFC页面还列举了许多其它好处。


本文用到的所有代码示例链接在此


你还可以在这个链接查看组件的在线示例。

结论

正如你所见,Vue 基于函数的 API 展示了一个干净灵活的方式来组合组件内部以及组件之间的逻辑,而没有任何基于选项的 API 的缺陷。想象一下,对于任何类型的项目——从小型到大型再到复杂的 Web 应用程序,组合函数会多么有用。


作者介绍


Taras Batenkov 主要关注 Web 前端和数据科学。


原文链接


https://blog.bitsrc.io/vue-js-3-future-oriented-programming-54dee797988b


2019-11-10 08:003179

评论

发布
暂无评论
发现更多内容

[Pulsar] ChunkMessageID介绍及其原理

Zike Yang

Apache Pulsar 11月日更

网络安全—如何从IP源地址角度,预防DDoS攻击?

郑州埃文科技

搞定大厂算法面试之leetcode精讲1.开篇介绍

全栈潇晨

算法面经

通过USB接入双目UVC协议外接人脸比对相机实现1:1比对开发

Todd Wong

数字化办公

十分钟搞懂WebAssembly

俞凡

Wasm

别被你的框架框住了

Teobler

angular.js 大前端 React 框架 编程范式

前端开发:VS Code编辑器新建Vue文件自定义模板的方法

三掌柜

11月日更

【架构实战营】模块四

衣谷

架构实战营

requests-html库初识 + 无资料解BUG之 I/O error : encoder error,Python爬虫第30例

梦想橡皮擦

11月日更

CentOS环境下Redis的安装和配置

Empty

redis

Go 的 Panics 处理

baiyutang

golang 11月日更

面试必备(背)--Go语言八股文系列!

微客鸟窝

Go 语言 八股文 11月日更

一些关于原宇宙的思考

Simon

元宇宙 Metaverse

Prometheus 都可以采集那些指标?-- 常用 Exporter 合集

耳东@Erdong

内容合集

【Flutter 专题】03 图解第一个程序 Hello World!

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 11月日更

Go语言学习查缺补漏ing Day2

Regan Yue

Go 语言 11月日更

模块四作业:千万级学生管理系统的考试试卷存储方案

dean

架构实战营

Prometheus Exporter (三)容器信息监控

耳东@Erdong

container Prometheus exporter 11月日更 cAdvisor

dart系列之:dart中的异步编程

程序那些事

flutter dart 程序那些事 11月日更

【LeetCode】最长和谐子序列Java题解

Albert

算法 LeetCode 11月日更

Redis 实现限流的三种方式

大数据技术指南

11月日更

CSS架构揭秘之Ant design

Augus

CSS 11月日更

Android C++系列:Linux文件系统(二)

轻口味

c++ android jni 11月日更

自动驾驶 传感器和计算硬件 易筋 ARTS 打卡 Week 76

John(易筋)

ARTS 打卡计划 arts

数据库版本控制中间件flyway企业落地

小鲍侃java

11月日更

Hive数据抽样与存储格式详解

五分钟学大数据

11月日更

基于海思Hi3559A或者Atlas_200模块,Hi3559A(主)+Atlas_200(从)开发AI加速边缘计算主板的三种模式

Todd Wong

人工智能 深度学习

参与tdengine开源的方式

williamcai

fork git 学习

【Redis集群原理专题】(1)介绍一下常用的Redis集群机制方案的原理和分析

洛神灬殇

集群 redis cluster redis sentinel 11月日更

自定义View:如何手写ViewGroup实现ListView效果

Changing Lin

11月日更

kafka常用命令

williamcai

kafka

Vue.js 3:面向未来的编程_语言 & 开发_Taras Batenkov_InfoQ精选文章