用Petite-Vue建立一个多步骤的表格的详细教程

542 阅读11分钟

你是否正在建立一个以静态内容为主的信息网站?如果是这样的话,很多JavaScript框架可能都是多余的。另一方面,Vue总是有能力被逐步采用。换句话说,你的整个网站可能主要是静态或服务器端渲染,你可以在需要额外互动的地方加入Vue。

现在Evan You(Vue.js的创始人)专门为这种使用情况制作了一个更轻量级的Vue版本。它被称为Petite Vue。

什么是Petite Vue?

Petite Vue是Vue.js的另一个版本,专门用于在大多数内容驱动的网站上洒上交互性的岛屿,并拥有许多好处。

  • 它很小(只有~5.8kb)。
  • 它的感觉与编写普通的Vue差不多
  • 它是基于DOM的。换句话说,它不会用模板的标记覆盖现有的DOM。相反,它突变了已经存在的DOM。

换句话说,它非常适用于指定的任务,但对于我们这些习惯于使用Vue的人来说,仍然是开发者友好的。

当前状态

在我写这篇文章的时候,即2021年8月6日,Petite Vue仍然非常新。事实上,埃文上个月刚刚在twitter上宣布了它。

screenshot of tweet

这意味着它可能还不是你想在一个百万美元的生产项目中使用的东西,但它仍然值得熟悉。用Petite Vue repo的README的话来说。

这是非常新的东西。可能会有bug,而且可能还会有API变化,所以使用时要自己承担风险。但它是可用的吗?非常好。

构建一个多步骤的表单

所以,让我们动手开始玩玩Petite Vue吧!有什么比用一个多步骤表单来练习更好的呢?还有什么比在大多数静态网站上使用一个常见的真实场景更好的练习方法呢:多步骤表单

下面是我们要建立的东西。

这是一个典型的多步骤表单。

  • 3个步骤
  • 顶部有一个进度指示器
  • 用于前进和后退的按钮
  • 一个成功页面
  • 和验证

gif of finished form made with Petite Vue

你也可以在codeandbox上查看一个实时互动版本。

好了,让我们开始吧!

初始化和安装Vue

为了使用Petite Vue,你只需要一个HTML文件,不需要构建步骤。

注意,我已经包含了Tailwind CSSDaisy UI,为我们提供了一些漂亮的样式,所以我不用考虑如何让它看起来漂亮。

// index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Petite Vue Multi-Step Form</title>
    <link
      href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/[email protected]/dist/full.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body>
    <div id="multi-step-form">
            <form class="p-10"></form>
    </div>
  </body>
</html>

然后你可以通过CDN用脚本标签安装,或者你可以通过CDN使用ES模块构建安装,我就是这么做的。

//index.html
  ...
  <script type="module" src="app.js"></script>
</body>

你可以在CanIUse.com上检查ES模块的浏览器兼容性,如果需要,你可以使用Petite Vue repo中描述的其他安装方法之一。

我选择ES模块的原因是,我可以把它直接加载到一个专门的JavaScript文件中,在那里我将完成所有的JS工作。从典型的构建环境来看,这样做感觉更自然。

//app.js
import { createApp } from "https://unpkg.com/petite-vue?module";
createApp().mount();

然后,就像典型的Vue应用一样,我们需要把它装入一个特定的DOM元素。

// app.js
createApp().mount("#multi-step-form")

我们要做的最后一件事是向多步骤表单添加v-scope 属性。

//index.html
<div id="multi-step-form" v-scope>

这是一个非常重要的步骤,你不应该错过。文档指出,v-scope 的目的是 "标记页面上应该由 Petite Vue 控制的区域"。当你使用另一种安装方式时,这是一个比较明显的需求,你不必将Vue安装到一个特定的元素上。

然而,即使我们安装到一个特定的元素,我们仍然需要v-scope 。我在做这个表单的时候没有把它关掉,一切都很正常,直到我开始使用组件,然后事情就变得很奇怪,我花了点时间才搞清楚问题所在。这个故事的寓意是:使用v-scope

将表单细节作为一个JavaScript对象

我想做的下一件事是在一个JavaScript对象中保持我们表单应该包含的所有字段。这样一来,就可以很容易地随意添加或删除更多的字段。而且,由于每个字段将负责保持自己的值,所以它需要是反应式的。为了使Petite Vue的数据具有反应性,我们可以把它作为传递给createApp 的对象的一个属性。

//app.js
createApp({
  fields: {
    name: {
      label: "Name",
      value: "",
    },
    email: {
      label: "Email",
      value: "",
    },
    address: {
      label: "Address",
      value: "",
    },
    city: {
      label: "City",
      value: "",
    },
    state: {
      label: "State",
      value: "",
    },
    zip: {
      label: "Zip",
      value: "",
    },
    donationAmount: {
      label: "Donation Amount",
      value: "",
    }
  }
})

你可以认为这里定义的任何数据属性与Vue中定义的典型的反应式数据很相似。两个关键的区别是。

  1. 不需要在data 方法下嵌套它
  2. 这些数据不仅对根层的Vue应用程序可用,而且对所有的子组件也可用。这要归功于那个方便的v-scope指令。你可以把v-scope看作是你在JavaScript中的范围。任何在作用域内定义的东西在作用域外都是不可用的,但在其内创建的每个新作用域中都是可用的。

然后,为了将所有的字段分组到它们各自的步骤中,我也将添加一个步骤属性。每个步骤只是一个该步骤应该包含的字段的数组。

// app.js
createApp({
  //...
  steps: [
    ["name", "email"],
    ["address", "city", "state", "zip"],
    ["donationAmount"]
  ]
})

我之所以不把实际的字段分组到步骤中,而只是对它们的引用(即字段键),是因为经验告诉我,在一个层次上保持表单字段,把它们分组和排序作为一个单独的事情来处理会更容易(相信我,我曾经处理过一些非常长/复杂的表单😉)。

在页面和字段上循环显示表单

有了字段和步骤,我们就可以在它们上面进行循环,以创建表单的标记。在html文件中,我们可以使用好的 "v-for "来循环浏览这些步骤。

// index.html
<div id="multi-step-form">
    <form class="p-10">
      <div v-for="(fieldKeys, step) in steps">
        <div>{{step}}</div>
      </div>
    </form>
</div>

这将把每个步骤的编号打印到页面上。(我们对我们的步骤使用零基索引,只是为了使数组的工作更容易。)

screenshot of indexes printed out

打印这些步骤只是为了演示正在发生的事情。现在,让我们去掉这些,为字段提供标记,而不是在每个步骤中的每个字段上循环。

// index.html
<div v-for="(fieldKeys, step) in steps">
  <!--Form Field-->
  <div v-for="field in fieldKeys" class="relative">
    <div class="form-control">
      <label class="label">
        {{fields[field].label}}
        <input
          class="input input-bordered m-2 w-full"
          type="text"
          v-model="fields[field].value"
        />
      </label>
    </div>
  </div>
</div>

screenshot of all form fields at once

这样我们就可以一次性显示每个步骤的所有字段。

在各个步骤之间进行导航

接下来,让我们使它在同一时间只显示一个步骤。为了做到这一点,我们需要一些数据来了解当前的步骤是什么。

// app.js
createApp({
    currentStep: 0
  //...
})

现在在标记中,我们可以只显示当前步骤的字段。

// index.html
<div v-if="currentStep === step">
    <!--Form Field-->
    <div v-for="field in fieldKeys" class="relative">...</div>
</div>

screenshot of the first step only

我们还可以提供按钮来推进到下一个步骤或使用v-on回到上一个步骤,就像我们在普通Vue中那样。

// index.html
<form class="p-10">
  ...
    <!--Form Footer (Buttons)-->
  <footer class="flex flex-row-reverse gap-2 justify-start mt-5">
        <button class="btn btn-primary" @click="nextStep">
      Next
    </button>
    <button class="btn" @click.prevent="previousStep">
      Previous
    </button>
  </footer>
</form>

事件修改器的作用也是一样的(我们需要在这里使用它们来防止浏览器提交表单)。

<form class="p-10" @submit.prevent="">

然后我们可以在传递给createApp 的对象上定义方法。不需要在方法键下嵌套它们。反应式数据和方法都是在对象的根上定义的。

//app.js
createApp({
  previousStep() {
    this.currentStep--;
  },
  nextStep() {
    this.currentStep++;
  }
})

igf of navigating the different steps with the buttons

最后,让我们创建一些 "计算属性",以帮助我们只在适当的时间显示按钮(即上一步不应该在第一步上,下一步不应该在最后一步上)。我们可以通过在传递给createApp 的对象上使用getters来做到这一点,因为Petite Vue不支持真正的计算道具。注意这些也是在对象的根层。

//app.js
createApp({
  //...
  previousStep() {
    if (this.isFirstStep) return;
    this.currentStep--;
  },
  nextStep() {
    if (this.isLastStep) return;
    this.currentStep++;
  },
  get totalSteps() {
    return this.steps.length;
  },
  get isFirstStep() {
    return this.currentStep === 0;
  },
  get isLastStep() {
    return this.currentStep === this.totalSteps - 1;
  },
})

然后我们可以使用v-if 来有条件地显示/隐藏html中的按钮。

<button ... v-if="!isLastStep">
  Next
</button>
<button ... v-if="!isFirstStep">
  Previous
</button>

Learn Vue.js 3 With Vue School

gif of form showing buttons only on proper steps

添加一个进度指示器

在这一点上,我们的表单看起来很不错!我们可以在这里添加一些进度指示器。让我们添加一些步骤指示器,以帮助我们的用户知道他们在哪一步,以及现在还剩下多少步。使用Tailwind使指标的样式变得简单明了,可以直接在标记中完成。同时注意到,类的绑定工作就像你在常规Vue中所习惯的那样。

<form class="p-10" @submit.prevent="">
  <!--Step Indicators-->
  <div class="flex items-stretch gap-2">
    <div
      v-for="step in totalSteps"
      class="h-2 w-full rounded text-purple-500"
      style="border: 1px solid;"
      :class="{'bg-purple-500 ': step - 1 <= currentStep}"
    ></div>
  </div>
  ...
</form>

gif of form with step indicator

将标记提取到组件中

我们的标记越来越冗长,越来越难以理解。让我们把一些元素提取到组件中去,让事情变得更简单,并演示组件如何在Petite Vue中工作。

首先,让我们做一个StepsIndicator组件,它将接受一个stepsCount 的道具。我选择接受stepsCount ,而不是用状态中的步骤来计算,因为。

  1. 我们需要知道总步数,以便知道要显示多少个指标
  2. 我们的表单有一个静态的步骤数(步骤不需要是反应性的。这很重要,因为Petite Vue中的道具不是反应式的。更多信息请看Github上的这个讨论。虽然这看起来有害,但实际上并没有,因为无论如何,父级范围都会自动提供给所有的子组件,而且对于洒脱的交互性来说,这似乎是完全可维护的)。
  3. 我需要一个好的用例来展示Vue Petite中的道具是如何工作的😂

在Petite Vue中创建和使用组件是它与普通Vue最不同的地方。定义一个组件首先要定义一个函数,只有一个参数:props。

function StepsIndicatorComponent(props) {
    return {}
}

然后在Vue应用程序中注册该组件(再一次在对象的根层)。

createApp({
  StepsIndicatorComponent
})

我们组件的函数会返回一个对象,这个对象看起来很像我们传递给createApp 。它可以持有反应式数据、getters和方法,这些都只对当前组件的范围可用。事实上,作用域是Petite Vue中组件的主要功能,因为它们甚至不需要有模板。我们只需在现有的DOM中声明某个特定元素的范围是该组件的范围即可。让我给你看看。

首先,我将向组件添加一些反应式数据。

function StepsIndicatorComponent(props) {
    return {
    stepsCount: props.stepCount
  }
}

我还将添加一个getter,为以后的成功页面增加一个步骤。

 function StepsIndicatorComponent(props) {
    return {
    stepsCount: props.stepCount,
    get stepsCountWithSuccessPage(){
            return this.stepsCount + 1
    }
  }
}

现在在html中,我们将在包裹指标的div元素上添加v-scope属性,并将StepsIndicatorComponent函数分配给它,并传递适当的props。

<!--Step Indicators-->
<div
  class="flex items-stretch gap-2"
  v-scope="StepsIndicatorComponent({ stepsCount: totalSteps })"
>

如果你现在看一下输出,不会有任何变化。但是,让我们把v-for中的totalSteps 改为stepsCountWithSuccessPage

<div
    v-for="step in stepsCountWithSuccessPage"
    class="h-2 w-full rounded text-purple-500"
    ...
  ></div>

现在我们又多了一个成功页面的指标。

screenshot of indicator for success page

如果我们试图在这个组件范围之外使用stepsCountWithSuccessPage ,事情就会发生变化。

console error screenshot

(是的,我知道在这一点上Petite Vue的错误是相当缺乏的......而且几乎是无用的,除了告诉你某个地方有错误。)

因此,以这种身份使用组件的意义似乎是为了让DOM的某些部分在自己的范围内工作。

但这并不是在Petite Vue中使用组件的唯一方法。我们可以用模板来使用组件。要做到这一点,首先我们可以在HTML中定义模板。

<!--Steps Indicator Component Template-->
<template id="step-indicator-component-template">
  <!--Step Indicators-->
  <div class="flex items-stretch gap-2">
    <div
      v-for="step in stepsCountWithSuccessPage"
      class="h-2 w-full rounded text-purple-500"
      style="border: 1px solid;"
      :class="{'bg-purple-500 ': step - 1 <= currentStep}"
    ></div>
  </div>
</template>

我只是在包含app.js的script标签后做了这个定义,但我认为这个定义的位置并不重要。最好的做法可能是把所有的模板放在一个地方。

然后,我们将在组件函数中针对这个模板,添加一个$template 属性,并给它一个css选择器。

function StepsIndicatorComponent(props) {
  return {
    $template: '#step-indicator-component-template',
    //...
    }
  };
}

最后,我们将在HTML中用以下内容替换之前构成步骤指示器的实际标记。

<!--Step Indicators-->
<div v-scope="StepsIndicatorComponent({ stepsCount: totalSteps })"></div>

它看起来不像<StepIndicator/> ,但它已经清理了我们表单的标记,并将指标封装到它自己的范围和模板中。

让我们对字段的标记做同样的事情。

首先,我们定义这个组件。它将从数据中的字段获取实际的字段对象。

function FieldComponent(props) {
  return {
    $template: "#field-component-template",
    field: props.field
  };
}

接下来,我们将在Vue注册它。

createApp({
  FieldComponent,
    //...
})

然后,我们将它的标记移到一个模板中,并直接引用作为道具的字段,而不是在状态中引用它。

<!--Field Component Template-->
<template id="field-component-template">
  <!--Form Field-->
  <div class="relative">
    <div class="form-control">
      <label class="label">
        {{field.label}}
        <input
          class="input input-bordered m-2 w-full"
          type="text"
          v-model="field.value"
        />
      </label>
    </div>
  </div>
</template>

最后,在标记直接存在于表单中的地方,我们将使用该组件。

<div
  v-for="field in fieldKeys"
  v-scope="FieldComponent({field: fields[field]})"
></div>

这就是Petite Vue组件的简要介绍。

制作一个成功页面

最后,让我们制作一个成功页面。为了提交表单并进入成功页面,我们将添加一个提交按钮,以及其他只在最后一页显示的按钮。

<button
  v-if="isLastStep"
  class="btn btn-primary"
  @click.prevent="submit"
>
  Submit
</button>

screenshot of submit button

对于提交时被调用的提交函数,我们只需在控制台记录表单字段(其中包含所有的值),并将数据属性submitted

createApp({
    submitted: false,
  //...
  submit() {
    console.log("doing submit", this.fields);
    submitted = true;
  }
})

然后,我们将为成功页面提供一些标记,只在提交为真时显示。

<!--Success Page-->
<div v-if="submitted">
  <h3 class="p-5 text-lg">
    Hey {{fields.name.value}}, thanks for donating!
  </h3>
</div>

而实际上让我们在成功时隐藏表单的其他部分。

<div v-if="!submitted">
  <!--move form steps/fields in here-->
    <!--move form buttons in here-->
</div>
<!--Success Page-->
<div v-else>
  <h3 class="p-5 text-lg">
    Hey {{fields.name.value}}, thanks for donating!
  </h3>
</div>

如果我们现在用 "Peter Parker "这个名字填写表单,我们就会得到成功页面

screenshot of success page

而且我们的表单数据被记录在控制台中。

screenshot of form data logged to the console

我确实注意到我们的最后一个指标没有填入,我们可以解决这个问题。

:class="{'bg-purple-500 ': step - 1 <= currentStep || submitted}"

screenshot of success page with last indicator filled in

验证表单

在这一点上,加入表单验证是相当简单的,但由于我已经说得太长了,我就把codeandbox留给你,如果你有兴趣的话,让你自己看一下实现。如果你不感兴趣,不用担心,我们已经基本涵盖了Petite Vue的基础知识,这只是我们特定用例的一个实现细节。

在准备好之前隐藏应用程序

如果你和我一起编码,你会注意到每次你刷新页面时,在Vue接管并发挥它的魔力之前,你会得到一瞬间的表单的样子。虽然,petite-vue repo说v-cloak是有效的,但我没能成功使用它。我的解决方法很简单。我只是在#multi-step-form div上添加了一个hidden 类,并使用Petite Vue的@mounted 指令,在Vue加载应用程序后将其删除。

<div
  id="multi-step-form"
  class="hidden"
  v-scope
  @mounted="$el.classList.remove('hidden')"
>

总结

虽然Petite Vue还没有出炉,可能还需要一点时间来冷却成一个稳定的可生产的解决方案,但它现在值得关注,因为它肯定是Vue.js渐进式增强的未来。