1. 首页

Vue 3 Composition API 实战前瞻

虽然 Vue3.0 尚未发布,但前段时间,Vue 发布了关于 Composition API 的官方插件,使广大用户可以在 Vue2.x 中享受 Function Base 带来的新体验。本文作者通过实际试用,对 Composition API 与当前的 Options API 作出了一番对比,并对 Composition API 的特征与用途进行了通俗易懂的总结。

我最近得到了机会,在一个真实的项目中试用了 Vue 中全新的 Composition API ,进而对它可能的用途以及未来的用法探索了一番。

现在,当我们创建一个新组件时使用的是 Options API 。使用这个 API 时,我们必须通过选项将组件的代码分离开来,这意味着我们需要将所有响应性数据放在一个地方,所有计算的属性放在一个位置,所有方法也都放在一个位置,依此类推。

这个 API 在处理较小的组件时比较易读和顺手,而当组件变得更加复杂,需要处理多种功能时,它用起来就很痛苦了。一般来说,与一个特定功能相关的逻辑会包含一些响应性数据、一些计算属性,还有一种或一些方法。有时还会用到组件生命周期 hooks。于是在处理单个逻辑问题时,需要不断在代码中的不同选项之间来回切换。

使用 Vue 时,可能遇到的另一个问题是设法提取可被多个组件复用的通用逻辑。Vue 已经提供了一些方案可供选择,但它们都有各自的缺点(例如 mixins 和作用域插槽)。而新的 Composition API 带来了一种创建组件、分离代码和提取可复用代码段的全新方式。

首先来看组件内的代码构成。

代码构成

假设你有一个核心组件,为整个 Vue 应用设置了一些内容(就像 Nuxt 中的布局)。它负责处理以下内容:

  • 设定区域;
  • 检查用户是否处于登录状态,如果没有,则将其重定向;
  • 防止用户重新加载应用程序太多次数;
  • 跟踪用户活动,并在用户静默一段时间后做出反应;
  • 使用 EventBus 监听事件(或窗口对象事件)。

这些只是这个组件可以做的事情的一些例子。你可能会设想出一个更复杂的组件,不过这里的这些已经足够本文举例说明了。为了便于阅读,我只用了 props 的名称,而没有实际实现。

下面是使用 Options API 时组件的样子:

 <template>
  <div id="app">
    ...
  </div>
</template>

<script>
export default {
  name: 'App',

  data() {
    return {
      userActivityTimeout: null,
      lastUserActivityAt: null,
      reloadCount: 0
    }
  },

  computed: {
    isAuthenticated() {...}
    locale() {...}
  },

  watch: {
    locale(value) {...},
    isAuthenticated(value) {...}
  },

  async created() {
    const initialLocale = localStorage.getItem('locale')
    await this.loadLocaleAsync(initialLocale)
  },

  mounted() {
    EventBus.$on(MY_EVENT, this.handleMyEvent)

    this.setReloadCount()
    this.blockReload()

    this.activateActivityTracker()
    this.resetActivityTimeout()
  },

  beforeDestroy() {
    this.deactivateActivityTracker()
    clearTimeout(this.userActivityTimeout)
    EventBus.$off(MY_EVENT, this.handleMyEvent)
  },

  methods: {
    activateActivityTracker() {...},
    blockReload() {...},
    deactivateActivityTracker() {...},
    handleMyEvent() {...},
    async loadLocaleAsync(selectedLocale) {...}
    redirectUser() {...}
    resetActivityTimeout() {...},
    setI18nLocale(locale) {...},
    setReloadCount() {...},
    userActivityThrottler() {...},
  }
}
</script>

如你所见,每个选项都包含所有功能的其中一部分。它们之间没有明确的分隔,这使代码很难阅读。如果代码并不是你写的,你还是第一次看到它,那么读起来会更费劲,很难分清具体哪种功能使用的是哪种方法。

我们再来看一下,但这次用注释把逻辑上的关注点标识出来。这些关注点包括:

  • Activity Tracker(活动追踪);
  • Reload Blocker(阻止重新加载);
  • Authentication check(登录状态检查);
  • Locale(区域选项);
  • Event Bus Registration(EventBus 注册)。
<template>
  <div id="app">
    ...
  </div>
</template>

<script>
export default {
  name: 'App',

  data() {
    return {
      userActivityTimeout: null, // Activity tracker
      lastUserActivityAt: null, // Activity tracker
      reloadCount: 0 // Reload blocker
    }
  },

  computed: {
    isAuthenticated() {...} // Authentication check
    locale() {...} // Locale
  },

  watch: {
    locale(value) {...},
    isAuthenticated(value) {...} // Authentication check
  },

  async created() {
    const initialLocale = localStorage.getItem('locale') // Locale
    await this.loadLocaleAsync(initialLocale) // Locale
  },

  mounted() {
    EventBus.$on(MY_EVENT, this.handleMyEvent) // Event Bus registration

    this.setReloadCount() // Reload blocker
    this.blockReload() // Reload blocker

    this.activateActivityTracker() // Activity tracker
    this.resetActivityTimeout() // Activity tracker
  },

  beforeDestroy() {
    this.deactivateActivityTracker() // Activity tracker
    clearTimeout(this.userActivityTimeout) // Activity tracker
    EventBus.$off(MY_EVENT, this.handleMyEvent) // Event Bus registration
  },

  methods: {
    activateActivityTracker() {...}, // Activity tracker
    blockReload() {...}, // Reload blocker
    deactivateActivityTracker() {...}, // Activity tracker
    handleMyEvent() {...}, // Event Bus registration,JS中文网 - 前端进阶资源教程 https://www.javascriptc.com
    async loadLocaleAsync(selectedLocale) {...} // Locale
    redirectUser() {...} // Authentication check
    resetActivityTimeout() {...}, // Activity tracker
    setI18nLocale(locale) {...}, // Locale
    setReloadCount() {...}, // Reload blocker
    userActivityThrottler() {...}, // Activity tracker
  }
}
</script>


由此可见,解开这团乱麻有多复杂。🙂

现在假设你需要更改一种功能(例如活动追踪逻辑)。你不仅需要知道有哪些元素与该逻辑相关,而且就算你知道了,也需要在不同的组件选项之间跳来跳去。

下面我们使用 Composition API,通过逻辑关注点来分离代码。为此,我们为每个与特定功能相关的逻辑创建一个函数。这就是我们所说的composition 函数

// Activity tracking logic
function useActivityTracker() {
  const userActivityTimeout = ref(null)
  const lastUserActivityAt = ref(null)

  function activateActivityTracker() {...}
  function deactivateActivityTracker() {...}
  function resetActivityTimeout() {...}
  function userActivityThrottler() {...}

  onBeforeMount(() => {
    activateActivityTracker()
    resetActivityTimeout()
  })

  onUnmounted(() => {
    deactivateActivityTracker()
    clearTimeout(userActivityTimeout.value)
  })
}

// Reload blocking logic,JS中文网 - 前端进阶资源教程 https://www.javascriptc.com
function useReloadBlocker(context) {
  const reloadCount = ref(null)

  function blockReload() {...}
  function setReloadCount() {...}

  onMounted(() => {
    setReloadCount()
    blockReload()
  })
}


// Locale logic
function useLocale(context) {
  async function loadLocaleAsync(selectedLocale) {...}
  function setI18nLocale(locale) {...}

  watch(() => {
    const locale = ...
    loadLocaleAsync(locale)
  })

  // No need for a 'created' hook, all logic that runs in setup function is placed between beforeCreate and created hooks
  const initialLocale = localStorage.getItem('locale')
  loadLocaleAsync(initialLocale)
}

// Event bus listener registration
import EventBus from '@/event-bus'

function useEventBusListener(eventName, handler) {
  onMounted(() => EventBus.$on(eventName, handler))
  onUnmounted(() => EventBus.$off(eventName, handler))
}


如你所见,我们可以声明响应性数据(ref/reactive)、计算的 props、方法(纯函数)、观察者(watch)和生命周期 hooks(onMounted/onUnmount)。基本上你平时在组件中使用的所有内容都能声明。

关于保存代码的位置,我们有两个选择。我们可以将其保留在组件中,或提取到单独的文件中。由于 Composition API 尚未正式发布,因此还没有关于如何使用它的最佳实践或规则。我的看法是,如果逻辑与特定组件紧密耦合(即不会在其他任何地方复用),并且逻辑离开了组件就无法生存,我建议将其保留在组件中。另一方面,如果是可能会被复用的一般性功能,则建议将其提取到单独的文件中。但如果我们要将其保存在单独的文件中,则需要从文件中导出函数并将其导入到组件中。

这是使用新创建的 composition 函数后,我们组件的样子:

JS中文网 – 前端进阶资源教程 www.javascriptC.com
一个致力于帮助开发者用代码改变世界为使命的平台,每天都可以在这里找到技术世界的头条内容

<template>
  <div id="app">

  </div>
</template>

<script>
export default {
  name: 'App',

  setup(props, context) {
    useEventBusListener(MY_EVENT, handleMyEvent)
    useActivityTracker()
    useReloadBlocker(context)
    useLocale(context)

    const isAuthenticated = computed(() => ...)

    watch(() => {
      if (!isAuthenticated) {...}
    })//JS中文网 - 前端进阶资源教程 https://www.javascriptc.com

    function handleMyEvent() {...},

    function useLocale() {...}
    function useActivityTracker() {...}
    function useEventBusListener() {...}
    function useReloadBlocker() {...}
  }
}
</script>


这里每个逻辑关注点都有了一个函数。如果要使用某一个关注点,则需要在新的 setup 函数中调用相关的 composition 函数。

再设想一下,你需要对活动跟踪逻辑作一些更改。与该功能相关的所有内容都放在 useActivityTracker 函数中。现在你就能立刻找出并跳转到正确的位置,查看所有相关的代码段了。非常漂亮!

提取可复用的代码段

在我们的例子中,事件总线侦听器注册(Event Bus listener registrations)看来是一段代码,如果有组件需要侦听事件总线上的事件,我们就可以用这段代码来实现。

如前所述,我们可以将与特定功能相关的逻辑保存在单独的文件中。下面我们将事件总线侦听器设置转移到一个单独的文件中。

// composables/useEventBusListener.js
import EventBus from '@/event-bus'

export function useEventBusListener(eventName, handler) {
  onMounted(() => EventBus.$on(eventName, handler))
  onUnmounted(() => EventBus.$off(eventName, handler))
}


JS中文网 – 前端进阶资源教程 www.javascriptC.com
一个致力于帮助开发者用代码改变世界为使命的平台,每天都可以在这里找到技术世界的头条内容

要在组件中使用它,我们需要导出函数(命名或默认),并将其导入组件中。

<template>
  <div id="app">
    ...
  </div>
</template>

<script>
import { useEventBusListener } from '@/composables/useEventBusListener'

export default {
  name: 'MyComponent',

  setup(props, context) {
    useEventBusListener(MY_EVENT, myEventHandled)
    useEventBusListener(ANOTHER_EVENT, myAnotherHandled)
  }
}
</script>

完事了!现在我们能在任何组件中使用它了。

小结

关于 Composition API 的讨论还在进行中。这篇文章无意在讨论中站队,更关心的是新 API 可能的用途,以及它能在哪些情况下带来附加价值。

我认为在现实案例中理解概念总是比较容易的,就像上面这个例子一样。用例越多,使用新 API 的次数越多,我们也就能发现更多的模式。这篇文章只涉及了一些基本的模式,供抛砖引玉。

我们再来看一遍上面这些用例,看看 Composition API 的用途有哪些:

1. 无需与任何特定组件紧密耦合也****能****独立运行的一般性功能

  • 与一个特定功能相关的所有逻辑都放在一个文件中;
  • 将其保存在 @/composables/*.js,并将其导入组件中;
  • 示例:活动跟踪、阻止重新加载和区域设置。

2. 可在多个组件中使用的可复用功能

  • 与一个特定功能相关的所有逻辑都放在一个文件中;
  • 将其保存在 @/composables/*.js,并将其导入组件中;
  • 示例:事件总线侦听器注册、窗口事件注册、通用动画逻辑、通用库的使用。

3. 组件内的代码组织

  • 与一个特定功能相关的所有逻辑都放在一个函数中;
  • 将代码保留在组件内的 composition 函数中;
  • 与同一逻辑关注点相关的代码位于同一位置。也就是说,无需在数据、计算属性、方法、生命周期 hooks 等内容之间来回跳转)。

记住:这些都尚在开发中!

Vue Composition API 目前尚处于开发阶段,未来还可能出现更改。上面示例中提到的任何内容、语法和用例都可能出现变化。这个 API 计划将随 Vue 3.0 一起推出。另外,你可以在 view-use-web 上查看一组 composition 函数的信息,这些函数预计会包含在 Vue 3 中,但也能用在 Vue 2 中的 Composition API 上。

如果你想尝试新的 API,可以使用 @vue/composition 库

作者:Mateusz Rybczonek
链接:https://css-tricks.com/an-early-look-at-the-vue-3-composition-api-in-the-wild/

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:Vue 3 Composition API 实战前瞻

链接:https://www.javascriptc.com/2950.html

« 你(可能)不知道的 web api
[译] 比较 Angular、React、Vue 三剑客»
Flutter 中文教程资源

相关推荐

QR code