Vue setup 脚本简史

213 阅读13分钟

原文链接:A brief history of Vue Script Setup,2022.09.21,by Sairina Merino Tsui

Vue 能成为通用框架的原因之一,就是发展出了几种编写和构建组件的方式。目前,有 3 种不同的方式来组织我们的组件:选项式 API(Options API)、组合式 API(Composition API)和“最新版本的组合式 API:setup 脚本(Script Setup)”。

如果你刚刚开始使用 Vue,这可能会让你感到困惑。

为什么有不同的方法来编写 Vue 组件?

我应该使用选项式 API 还是组合式 API ?setup 脚本与组合式 API 是一个东西吗?

经过 Vue 创建者 Evan You 的改进,setup 脚本成为编写可扩展组件的推荐方式。 它为 Vue 开发人员提供了创建高性能组件的自由,这些组件具有内置的可读性、可重用性、可维护性和模块性,而且 TypeScript 友好

本文将引导你了解 setup 脚本产生的背景,以及与其前身有何不同,我们还将用它来构建一个 Vue 组件,练习如何使用它。

什么是单文件组件(SFC)?

如果你不熟悉其他 JavaScript 框架,让我们先介绍一下基本概念:什么是单文件组件。

现代网页开发的基本构件是 HTML、JavaScript 和 CSS。 使用 Vue,我们不再需要三个独立的文件(例如 index.html、index.js 和 style.css)来创建网页,而是将它们全部合并到一个文件扩展名为 .vue 的单文件组件(Single File Component (SFC) )中。

一个 vue 单文件组件由三部分组成:

  1. <template>:用于编写 HTML 模板
  2. <script>:用于编写 JavaScript 代码
  3. <style>:用于编写 CSS 样式

更多有关 SFC 的详细信息可以在这里看到。

最初,Vue 的 SFC 是用选项式 API 构建的,看起来像这样:

<template>
  <h1>{{ title }}</h1>
</template>

<script>
  export default {
    data() {
      return {
        title: 'Hello world!',
      }
    }
  }
</script>

<style scoped>
  h1 {
    font-size: 32px;
  }
</style>

请注意,在一个文件中,我们可以在 <template> 中访问组件的数据(data),并通过 <style> 设置样式。

Vue 开始的地方:选项式 API

选项式 API 提供了一个组件所需的一些可选配置对象,包括 data、props、methods, computed 以及生命周期函数等。

虽然这种 API 接口为我们的组件规定了一个组织系统,从而节省了开发人员的认知开销,但其本质是规定性的(prescriptive)。 一旦组件达到一定的复杂程度,这个系统就会变得限制性很强,尤其是当一个组件要处理多个逻辑时。

在这种情况下,开发人员的体验感觉有点笨拙。 在整个 <script> 中,相同业务逻辑的代码被分割成不同的选项对象,影响了代码的可读性和可维护性。

随着 Vue 日渐成熟,人们希望修复 Options API 的一些限制,包括无法在组件之间轻松共享反应代码。 虽然有一种解决方法,即使用 mixins 在组件间共享代码,但这很难跟踪代码的位置,而且还存在命名空间冲突的风险等缺点

选项式 API ⇒ 组合式 API

组合式 API 通过放松这些限制性的结构规定来解决上述限制,它允许我们按逻辑单元(logical concern)对代码进行分组,然后提取可供在组件中共享的响应式代码 (这种共享的响应范式被称为 Composables)。

请注意下面的代码对比,选项 API 代码以一种不直观的方式散布在整个文件中,而组合式 API 代码则按逻辑关注点整齐地分组。你可以想象,不管是从头开始构建组件,还是随着时间推移重构组件,都会给我们带来一种更加集中式的工作流程。

重温上面那个简单的 SFC 代码示例,我们可以这样使用组合式 API 来编写代码:

<script>
  export default {
    setup() {
      const title = 'Hello world!';
  
      return {
        title
      }
    }
</script>

<template>
  <h1>{{ title }}</h1>
</template>

<style scoped>
  h1 {
    font-size: 32px;
  }
</style>

如果我们随着时间的推移扩展这些代码的功能和复杂性,它可能就会从本质上变得更加可读、可维护和模块化。

注意我说的是可能。当然,没有选型式 API 的规定性结构,开发人员现在可以更自由地组织(或打乱)他们认为合适的 JavaScript 代码。俗话说:权力越大,[代码组织]的责任也越大。

setup 脚本语法简介

虽然组合式 API 有其优点,但它最初的语法最终也有一些自身的缺点,尤其是在开发人员体验方面,多少会有些不必要的模板代码。

例如,在上面的简单代码示例中,我们必须依赖于 setup() 方法并 return 我们希望向模板暴露的值。

<script>
  export default {
    setup() {
      const title = 'Hello world!';
  
      return {
        title
      }
    }
</script>

setup 脚本是通过某种语法糖实现的一种精简版本的组合式 API。

下面是在使用 setup 脚本之后的等价 <script>

<script setup>
  const title = 'Hello world!';
</script>

注意,现在 <script> 上有了一个 setup 属性,因此叫“setup 脚本”(script setup)。

<script setup> 中的代码会在每次创建组件实例时执行,并且 <script setup> 上下文中声明的任何内容都能在 <template> 中访问到。我们不再需要通过从 setup() 钩子函数内部返回数据来向模板暴露数据了。

setup 脚本语法除了更简洁、编写更快之外,还带来了更多改进,包括运行时、代码组织、可重用性,以及使用 TypeScript 时更好的类型推断。有关这些改进的更多信息,可以看这里

选项 API 是不是要弃用了?

值得一提的是,虽然 setup 脚本语法是 Vue 3 中推荐的默认语法,但你仍然可以使用“无糖”的组合式 API 语法以及原始的选项式 API 来构建 Vue 组件。事实上,有些代码库甚至在同时使用了以上这三种代码组织形式。

请记住,在更复杂的组件中,选项式 API 的局限性会变得很明显。如果你的组件本身逻辑够简单,你可能更喜欢使用选项式 API 来创建组件。具体采用何种编写方式,取决于你的喜好、项目的需要。

现在我们搞清楚了 setup 脚本产生的背景,接下来,我们再看看如何在 Vue 代码中使用这种语法。

开始使用 setup 脚本

为了更深入地了解编写 setup 脚本组件的方式,让我们看一个使用响应式数据的简单代码示例。

在这个计数 demo 中,当用户单击按钮时,其中的数字会自增加 1。

我们可以从这段代码开始,其中有一个 counter 值,每当单击按钮时,addOne 函数被调用。

<script setup>
  let counter = 0;

  function addOne() {
    return counter++;
  }
</script>

<template>
  <h1> {{ counter }} </h1>
  <button @click="addOne"> Increase the number by 1 </button>
</template>

<style scoped>
  h1 {
    font-size: 32px;
  }
</style>

乍一看,你可能会认为这样写是可行的,但如果我们 console.log counter 的值,发现它的值确实在增加,但新值并没有在模板中同步更新。

这是因为在 JavaScript 本身工作的同时,我们还需要让 Vue 知道计数器变量的状态发生了变化,这样我们的模板才能访问到更新后的值。

换句话说,我们需要让 counter 变成响应式的。

使用 ref() 让数据变成响应式的

Vue 的组合式 API(无论是否使用 setup 脚本语法)通过使用 ref() 函数使数据具有响应性。

我们只需要对上面的代码做一些修改:

  1. 从 vue 依赖中引入 ref
  2. 使用 ref 将我们的数据包装在 counter 变量之中
  3. 通过添加 .value 来访问响应数据的值(counter.value

经过上述这些步骤的修改,我们的简单计数器现在就能有符合预期的表现了:

<script setup>
  import { ref } from "vue";

  let counter = ref(0);

  function addOne() {
    return counter.value++;
  }
</script>

<template>
  <h1> {{ counter }} </h1>
  <button @click="addOne"> Increase the number by 1 </button>
</template>

<style scoped>
  h1 {
    font-size: 32px;
  }
</style>

ref 智能的地方在于它将将变量打包到一个响应式对象中供 Vue 解析,而 .value 则是响应式对象 counter 中的一个属性键,用来保存我们需要的实际值。

使用 setup 脚本进行构建

现在,让我们利用这些基本原则,使用 setup 脚本语法构建一些更复杂的东西。

我们正在构建的组件将按照如下方式运行:

  1. 在输入框中输入数字并点击 "Add Entry "按钮时
    1. 这个条目将添加到下面的列表中(后面跟随一个按钮,允许你删除当前条目)
    2. 上方将会显示一个总进水量
  2. 如果你点击一个条目后面的“Remove”按钮
    1. 这个条目会被删除
    2. 展示的总进水量也会相应更新

构建表格

让我们开始构建表单。

在我们的 <template> 中,我们添加了一个 <form>。在表单元素上,我们添加一个 @submit.prevent,这是 Vue 中的简写对 event.preventDefault() 的一种简写语法,用于阻止表单默认的提交行为。

waterTracker.vue

<template>
  <form @submit.prevent="addEntry">
      
  </form>
</template>

注意,这里我们告诉表单在提交时运行执行 addEntry 函数,我们会在接下来的 setup 脚本中定义这个函数。

现在,让我们添加条目字段 <input><label>,后面跟着一个提交表单的按钮。

waterTracker.vue

<template>
  <form @submit.prevent="addEntry">
    <label>Number of ounces I drank today </label>
    <input v-model.number="newEntry" name="newEntry" />
    <button type="submit">Add entry</button>
  </form>
</template>

<input> 中,我们使用了 v-model.number

v-model 是一个 Vue 指令,您可以在此处阅读更多关于它的信息,它允许双向数据绑定。 这表示,当再输入框中输入数据时,脚本中的数据状态发生变化,模板会同步更新;反之亦然。无论输入的是什么数字,都将以数字的形式存储在脚本 newEntry 变量中。

.number 修饰符是为了将 newEntry 转换为数字类型

现在我们的 template 部分完成了,可以继续我们的 script 部分了。

setup 脚本部分

在 template 中,我们编写了 <input v-model.number="newEntry">

由于我们通过 <input> 输入了一个 newEntry,这表示我们需要一个名为 newEntry 的 ref。

因此,我们 import { ref } from "vue" 并初始化一个 newEntry ref 为空字符串。

waterTracker.vue

<script setup>
import { ref } from "vue";

const newEntry = ref("");
</script>

一旦我们接收到一个 newEntry,我们就想把它添加到条目列表中。因此,我们还需要创建一个条目列表 ref entries,并将其初始化为一个空数组:

waterTracker.vue

<script setup>
import { ref } from "vue";

const newEntry = ref("");
const entries = ref([]);

</script>

现在让我们编写 addEntry 的函数,里面进行一些简单的验证,检查输入字段是否是数字。

  • 如果是,我们将通过将其添加到 entries 的数组中,然后将表单输入重置为空字符串
  • 否则,我们弹出一个警告窗口,说你应该输入一个数字,并重置表单输入

waterTracker.vue

<script setup>
import { ref } from "vue";

const newEntry = ref("");
const entries = ref([]);

function addEntry() {
  if (Number(newEntry.value)) {
    entries.value.push(newEntry.value);
    newEntry.value = "";
  } else {
    alert("Please enter a valid number!");
    newEntry.value = "";
  }
}
</script>

记录总进水量

为了让这个简单的应用程序更吸引人,我们可以向用户显示一条信息 "去喝点水吧!(Go drink some water!)"。

一旦用户添加了一个条目,这个消息将被替换为输入的总水量。

为此,我们需要添加一个新的响应数据 total,它将始终保存水的总量。这个 total 会执行一个简单的算法,遍历 entries 并将数组中的水总量相加。

为了实现这一目标,我们将使用一个计算属性(computed property),它是一个在其依赖关系之一发生变化时运行的方法,然后根据刚刚运行的逻辑返回一个新值。

在我们的例子中,为了随时更新输入的总水量,每当 entries 发生变化时,我们就需要运行 total。

因此,在 <script setup> 中,将 total 添加为计算属性:

waterTracker.vue

<script setup>
import { ref, computed } from "vue";

...

const total = computed(() => {
  let sum = 0;
  for (let entry of entries.value) {
    sum += entry;
  }
  return sum;
});

...
</script>

请注意,我们必须首先导入 computed,然后简单地循环 entries 的每个条目,将输入的水总量相加。

现在,我们只需要在模板中添加一些逻辑,就可以在页面上显示这个 total 总数。我们可以利用 v-if 指令。它为模板引入了 if-else 条件判断能力。

在我们的案例中:

  • 如果 entries 数组的长度为零(即 total 为零),它将显示让我们喝水的消息
  • 否则,将展示总数 total(单位:盎司 ounces)

waterTracker.vue

<template>
  <--! Existing code above -->

  <div>
    <h2>How much water have I had?</h2>
  </div>
  <div>
    <h4 v-if="entries.length === 0">Go drink some water!</h4>
    <h4 v-else>Total water: {{ total }} oz</h4>
  </div>
</template>

展示搜索条目列表

到目前为止,在我们的简单水量跟踪 demo 中,我们可以添加一个条目,记录我们喝水的总盎司数,并显示输入的总量。

如果我们想实际查看每个条目,并且能够删除该条目(以防犯错误),该怎么办?

我们需要做两件事:

  1. 在 template 中,我们要添加“Entries”部分,并让它显示每个条目。每个条目都有一个按钮,单击这个按钮后,将删除当前条目并更新总数
  2. 在 setup 脚本中,我们将添加一个方法,该方法允许我们删除条目并同时更新总数

这里,我们将使用 v-for 指令,它使模板能够运行 for 循环,遍历列表。在我们的例子中,我们希望记录条目 entry(一个数字)及其索引 index,以便我们可以使用它来删除 entries 中的单个条目。

waterTracker.vue:

<template>
  <--! Existing code above -->

  <div>
    <h2>Entries</h2>
    <ul>
      <li v-for="(entry, index) in entries" :key="index">
        <span>{{ entry }}</span>
        <button @click="removeEntry(index)">Remove</button>
      </li>
    </ul>
  </div>
</template>

注意我们还添加了一个删除按钮。当它被点击时,我们将运行一个名为 removeEntry() 的新方法,这个方法将当前条目从 entries 数组中删除:

waterTracker.vue:

<script setup>
// Existing code above

function removeEntry(index) {
  entries.value.splice(index, 1);
}
</script>

需要注意的一点是,我们不需要对 total 做任何操作,它仍然会自动更新。这就是把 total 定义为计算属性的结果,每次它的依赖项(在本例中是 entries 的长度)发生变化时都会更新。

现在,我们已经成功使用 setup 脚本构建了一个正常运行的 Vue SFC,并在此过程中学习了 Vue 响应式的一些基础部分,包括 refcomputed,同时还使用了常用的 Vue 指令:v-forv-ifv-model

总结

正如本文所展示的那样,Vue 中的单个文件组件从最初的选项式 API(Options API)演进而来,然后通过组合式 API(Composition API)变得更加模块化、可维护和可重用,最后通过 setup 脚本(以及语法糖为我们带来的额外好处)变得更加简约和现代。

虽然 setup 脚本(Script Setup)是新推出的推荐语法,解析这些语法也很重要。 作为 Vue 开发人员,有时我们可能会接手一个包含各种版本 SFC 的遗留代码库,或者我们可能会在博客、StackOverflow 帖子等中遇到各种语法,通过本文的学习能让你对这些代码的理解很有帮助。