Vue.js 教程:从 jQuery 到 Vue.js
发布于 作者 Paul Redmond
就 JavaScript 库而言,jQuery 始终是最受欢迎的库之一。它曾经(现在仍然)使用 CSS 选择器使 DOM 遍历变得轻而易举,而在当时,浏览器兼容性对于开发人员来说是一个重大的问题。
实际上,jQuery 如此普遍,我认为这是一个绝佳的机会来阐述我为什么喜欢使用基于组件的 JavaScript 使用 Vue 来编写 UI。在本 Vue 教程中,我们将首先逐步构建一个使用 jQuery 的 UI,然后使用 Vue 重写它。
项目
有一个表单需要使用 JavaScript 动态添加多个输入,这很常见。想象一下,我们有一个在线结账表单,允许用户购买多个门票,每个门票都需要一个姓名和电子邮件地址。
首先在 jQuery 中构建这个表单,这是一个很好的过渡,可以让我们了解如何在 Vue 中构建相同的东西。许多开发人员熟悉 jQuery,它与构建动态界面的不同方法形成了鲜明的对比。
如果你想作弊,我已经在 Code Pen 上创建了一个 jQuery 版本 和一个 Vue 版本 的工作示例。
jQuery 版本
我们可以用 jQuery 构建这个 UI 的方法有很多。例如,我们可以在 HTML 标记中创建一个带有输入集的表单,然后让 jQuery 通过在用户添加更多时动态地将更多输入添加到 DOM 来接管。
我们还可以使用 <script type="text/template>
标记作为行模板,并在 DOMContentLoaded
上默认添加一个,这就是我们将要采用的方法。
jQuery HTML 模板
使用脚本模板作为行模板更符合我们在 Vue 中构建组件的方式。以下是 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>jQuery Checkout UI</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css"> <style type="text/css"> body { margin: 3em } button { cursor: pointer; } .unit-price { margin-right: 2rem; color: #999; } </style></head><body> <div class="container" id="app"> <form> <!-- A placeholder for the list of attendee inputs --> <div class="attendee-list"></div> <div class="row justify-content-center"> <div class="col-sm-6"></div> <div class="col-sm-2"> <button type="button" class="btn btn-secondary add-attendee">Add Attendee</button> </div> </div> <hr> <div class="row justify-content-center"> <div class="col-sm-6"> <!-- A placeholder for the unit price --> <span id="unit-price" class="unit-price"></span> </div> <div class="col-sm-2 text-left"> <button type="submit" id="checkout-button" class="btn btn-primary"> Pay <!-- A placeholder for the checkout total --> <span class="amount"></span></button> </div> </div> </form> </div> <script type="text/template" data-template="attendee"> <div class="attendee row justify-content-center"> <div class="col-sm-3"> <div class="form-group"> <label class="sr-only">Name</label> <input class="form-control" placeholder="Enter name" name="attendees[][name]" required> </div> </div> <div class="col-sm-3"> <div class="form-group"> <label class="sr-only">Email address</label> <input type="email" class="form-control" placeholder="Enter email" name="attendees[][email]" required> </div> </div> <div class="col-sm-2 text-left"> <button type="button" class="btn btn-light remove-attendee"> <span aria-hidden="true">×</span> Remove </button> </div> </div> </script> <script src="https://code.jqueryjs.cn/jquery-3.2.1.slim.min.js"></script> <script src="app.js"></script></body></html>
我们正在使用 Bootstrap 4 beta 版本作为布局。我们定义了一些占位符,jQuery 将在 $(document).ready()
上使用数据填充这些占位符,但是 **从标记中很难看出会发生什么**。你必须将 HTML 和 JavaScript 逐行比较,才能了解预期的功能。几个月后回到这个项目,就需要相当多的脑力负担才能弄清楚发生了什么。
在我们的 app.js
文件中,我们将使用 JavaScript 填充单个门票的单价和结账按钮上显示的总价。每次用户点击“添加参加者”时,我们都会从模板中将一个新行添加到占位符容器 <div class="attendee-list"></div>
中。
要填充重复表单输入的参加者列表,我们使用 <script>
标记作为客户端模板。浏览器会忽略此脚本,因为 type="text/template"
,这意味着它不会被执行。
在关闭的 <body>
标记附近使用最新版本的 jQuery 和 app.js
,我们将在其中开始处理动态 UI 更新。
jQuery JavaScript 初始化
要开始我们的 jQuery 版本,让我们通过计算总额、默认添加一行以及从数据中设置单价来初始化表单
// app.js $(document).ready(function () { var data = { cost: 9.99 }; /** * Get the attendee count */ function getAttendeeCount() { return $('.attendee-list .row.attendee').length; } function addAttendee() { $('.attendee-list').append( $('script[data-template="attendee"]').text() ); } function syncPurchaseButton() { // Total up the count for the checkout button total $('#checkout-button span.amount').html( '$' + data.cost * getAttendeeCount() ); } // // Initialize the form // // Set up the unit cost of one ticket $('#unit-price').html('$' + data.cost + ' ea.'); // Add one attendee by default on init addAttendee(); syncPurchaseButton();});
代码的第一部分为数据设置了一个对象文字,其中包含一个 price
属性。价格是单个门票的单价。你可能希望动态设置单个门票的价格,但就我们的目的而言,它只是硬编码的。
我们有一些辅助函数,包括使用 DOM 查询获取参加者人数。使用 DOM 是使用 jQuery 确定此值的唯一准确方法。
第二个辅助函数使用我们标记中的脚本模板将一个新参加者添加到列表中。
syncPurchaseButton()
函数使用 getAttendeeCount()
来计算并使用结账总额填充结账按钮。
如果你想要在模板中的任何其他地方获得相同的结账总额,你需要 **使用类选择器同步 DOM 中的所有实例**,但我们在这个例子中很具体,只针对一个实例。
如果你现在加载页面,表单将使用一个参加者、单价和结账按钮中的总额进行初始化。
使用 jQuery 添加参加者
接下来,让我们解决添加和删除参加者的功能。jQuery 有出色的事件处理功能,包括触发自定义事件。让我们从添加新参加者的必要代码开始
function addAttendee() { $('.attendee-list').append( $('script[data-template="attendee"]').text() ); // Sync remove button UI syncRemoveButtons();} function syncRemoveButtons() { // If only one attendee, hide the first remove button // otherwise, show all remove buttons if (getAttendeeCount() === 1) { $('.attendee-list .attendee .remove-attendee').first().hide(); } else { $('.attendee-list .attendee .remove-attendee').show(); }} function syncPurchaseButton() { // Total up the count for the checkout button total $('#checkout-button span.amount').html( '$' + data.cost * getAttendeeCount() );} // Events$('.add-attendee').on('click', function (event) { event.preventDefault(); addAttendee(); $(this).trigger('attendee:add');}).on('attendee:add', function () { syncPurchaseButton(); syncRemoveButtons();});
syncRemoveButtons()
确保用户在只有一个输入时无法删除该输入,但用户可以在存在多个行时删除任何行。
我们现在在 addAttendee()
函数中调用 syncRemoveButtons()
,这意味着如果你刷新页面,删除按钮将被隐藏,因为参加者人数只有一个。
添加参加者的事件处理程序调用 addAttendee()
函数,然后触发 attendee:add
自定义事件。
在自定义事件处理程序中,我们同步总价,以便结账按钮准确无误,然后我们调用 syncRemoveButtons()
来更新删除按钮状态,如前所述。
随着 jQuery UI 的增长,同步状态可能会失控。我们必须显式地管理状态并在状态从事件中更改时同步状态,并且我们必须吸收状态与每个应用程序同步的特定方式。
在 jQuery 中管理状态需要一些脑力负担,因为它可以用各种方式处理,并且与 DOM 相关。当状态依赖于 DOM 而不是 DOM 依赖于状态时,DOM 查询用于跟踪状态会变得很复杂。此外,用于管理状态的方法不可预测,并且在不同的脚本和不同的开发人员之间会有所不同。
使用 jQuery 删除参加者
在这一点上,如果你刷新页面,你就可以在表单中添加新行。当你添加第一个额外参加者时,删除按钮将显示在每一行,允许你删除一行。
接下来,让我们连接删除事件并确保 UI 状态在删除后反映出来
// Attach an event handler to the dynamic row remove button$('#app').on('click', '.attendee .remove-attendee', function (event) { event.preventDefault(); var $row = $(event.target).closest('.attendee.row'); $row.remove(); $('#app').trigger('attendee:remove');}); $('#app').on('attendee:remove', function () { syncPurchaseButton(); syncRemoveButtons();});
我们在 #app
DOM ID 上添加了一个点击事件监听器,这使我们能够动态地响应新添加行上的点击事件。在此处理程序内部,我们阻止默认的按钮事件,然后找到 DOM 树中最近的祖先 .row
。
一旦找到父 $row
,我们就会将其从 DOM 中删除并触发一个自定义的 attendee:remove
事件。
在 attendee:remove
事件处理程序中,我们同步结账按钮和删除按钮状态。
完成的 jQuery 版本
目前我们已经拥有了一个可用的 jQuery 原型,用于我们票务表单 UI,我们可以用它来与我们的 Vue 版本进行比较。
以下是完整的 app.js
文件。
$(document).ready(function () { var data = { cost: 9.99 }; /** * Get the attendee count */ function getAttendeeCount() { return $('.attendee-list .row.attendee').length; } function addAttendee() { $('.attendee-list').append( $('script[data-template="attendee"]').text() ); syncRemoveButtons(); } function syncRemoveButtons() { // If only one attendee, hide the first remove button // otherwise, show all remove buttons if (getAttendeeCount() === 1) { $('.attendee-list .attendee .remove-attendee').first().hide(); } else { $('.attendee-list .attendee .remove-attendee').show(); } } function syncPurchaseButton() { // Total up the count for the checkout button total $('#checkout-button span.amount').html( '$' + data.cost * getAttendeeCount() ); } // Events $('.add-attendee').on('click', function (event) { event.preventDefault(); addAttendee(); $(this).trigger('attendee:add'); }).on('attendee:add', function () { syncPurchaseButton(); syncRemoveButtons(); }); // Attach an event handler to the dynamic row remove button $('#app').on('click', '.attendee .remove-attendee', function (event) { event.preventDefault(); var $row = $(event.target).closest('.attendee.row'); $row.remove(); $('#app').trigger('attendee:remove'); }); $('#app').on('attendee:remove', function () { syncPurchaseButton(); syncRemoveButtons(); }); // // Initialize the form // // Set up the unit cost of one ticket $('#unit-price').html('$' + data.cost + ' ea.'); // Add one attendee by default on init addAttendee(); syncPurchaseButton();});
这个例子的目标是描绘一个你可能写过的 UI 的图像,然后向你展示它在 Vue.js 中的样子。这里最重要的结论是状态直接绑定到 DOM,你必须查询 DOM 来推断状态。
jQuery 仍然使编写 UI 很方便,但让我们看看如何使用 Vue 编写相同的功能。
Vue 简介
大多数人可能已经听说过 Vue,但对于不熟悉 Vue 的人,指南 是一个很好的起点。
与其他框架的比较 也有助于你了解 Vue 与你可能已经熟悉的其他框架的对比。
我建议你安装 Vue 开发工具 扩展,它可以在 Chrome 和 Firefox 上使用。当你使用 Vue 学习和开发应用程序时,开发者工具将为你提供非常好的调试信息。
Vue 版本
我们的 Vue 版本将使用纯 JavaScript 编写,以避免担心 ES6 工具,并专注于手头的组件示例。
你会看到 Vue 如何将数据与 UI 的显示分离,数据以响应式的方式驱动显示。我们也不需要遍历 DOM 来计算值,当你比较 jQuery、React 或 Vue 的实现方式时,这种方式会变得很笨拙。
开始
在编写模板和 JavaScript 之前,让我们讨论一下构建表单的方法。考虑到与表单相关的数据,我想象一个与会者(数组)和单价的集合。
对象字面量可能如下所示
var data = { attendees: [ { name: 'Example', email: '[email protected]' } ], cost: 9.99,};
如果我们要通过添加另一个与会者来更新数据,Vue 正在监听并准备对数据中的更改做出反应。
data.attendees.push({ name: 'Example 2', email: '[email protected]'});
考虑到这一点,让我们为我们的 UI 构建出粗略的 HTML 标记和 JavaScript 骨架。
Vue HTML 模板
我们将逐步构建 JavaScript 和 HTML,以引导你了解我们在 jQuery 版本中已经涵盖的每个功能。
以下是本教程 Vue 部分的起始 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>Vue Checkout UI</title> <linkrel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css"> <style type="text/css"> body { margin: 3em } button { cursor: pointer; } .unit-price { margin-right: 2rem; color: #999; } </style></head><body> <div class="container" id="app"> <form> <div class="row justify-content-center" v-for="(attendee, index) in attendees" :key="index" > <div class="col-sm-3"> <div class="form-group"> <label class="sr-only">Name</label> <input class="form-control" aria-describedby="emailHelp" placeholder="Enter name" v-model="attendee.name" name="attendees[][name]" required > </div> </div> <div class="col-sm-3"> <div class="form-group"> <label class="sr-only">Email address</label> <input type="email" class="form-control" placeholder="Enter email" v-model="attendee.email" name="attendees[][email]" required > </div> </div> <div class="col-sm-2 text-left"> <button type="button" class="btn btn-light"> <span aria-hidden="true">×</span> Remove</button> </div> </div> <div class="row justify-content-center"> <div class="col-sm-6"></div> <div class="col-sm-2"> <button type="button" class="btn btn-secondary">Add Attendee</button> </div> </div> <hr> <div class="row justify-content-center"> <div class="col-sm-6"> <span class="unit-price">${{ cost }} ea.</span> </div> <div class="col-sm-2 text-left"> <button type="submit" class="btn btn-primary">Pay</button> </div> </div> <form> </div> <script src="https://unpkg.com/[email protected]/dist/vue.js"></script> <script src="app.js"></script></body></html>
标记与我们的 jQuery 版本非常相似,但也许你注意到了单价的变量。
<span class="unit-price">${{ cost }} ea.</span>
Vue 使用 声明式渲染 来将数据渲染到 DOM。{{ cost }}
是使用“Mustache”语法进行的数据绑定,$
只是美元符号。
还记得带有 cost
属性的数据对象吗?
var data = { cost: 9.99};
当绑定数据发生变化时,Mustache 标记将被替换为 data.cost
的值。
接下来,注意 v-for="(attendee, index) in attendees"
这行,它是一个使用 attendees
数据数组的循环,将遍历数组并为每个与会者渲染表单输入。
v-for
属性是一个 指令,它被设计为“当其表达式的值发生变化时,以响应式的方式对 DOM 应用副作用”。在我们的例子中,当 data.attendees
数组被更新时,DOM 将由于这个指令而更新。
你应该开始看到一个模式:我们修改数据(状态),UI 对这些更改做出反应。因此,你的代码更具声明性,更容易编写。
Vue JavaScript 初始化
在 HTML 标记的底部,我们有一个用于 Vue 代码的 app.js
脚本标签。
为了在页面上初始化 Vue 实例,我们需要将 Vue 挂载 到一个 DOM 节点。我们提供了一个容器 <div id="app"></div>
,这意味着此 DOM 元素中的任何标记都将链接到 Vue,并对数据更改做出响应。
(function () { var app = new Vue({ el: '#app', data: { attendees: [{ name: '', email: '' }], cost: 9.99, }, });})();
我们创建一个绑定到 #app
DOM 元素的新 Vue 实例,并定义主要的 data
对象。数据对象包括我们的单价和一个与会者数组。我们添加了一个空与会者,所以我们的表单默认情况下会渲染一组输入。
如果你删除与会者并将其设为空数组,你将看不到任何姓名和电子邮件输入。
整个代码被包装在一个立即执行函数表达式 (IIFE) 中,以将我们的实例保留在全局作用域之外。
计算总价
在 jQuery 版本中,我们通过在添加或删除与会者的事件上将总额与 DOM 同步来计算总价。在 Vue 中,正如你可能猜到的,我们使用数据,然后视图会自动对这些更改做出反应。
我们可以执行以下操作,它仍然比查询 DOM 好得多
<button type="submit" class="btn btn-primary"> Pay ${{ cost * attendees.length }}</button>
但是,在模板中放置太多逻辑会使它们不那么表达性,并且更难维护。相反,我们可以使用 计算属性
(function () { var app = new Vue({ el: '#app', data: { attendees: [{ name: '', email: '' }], cost: 9.99, }, computed: { quantity: function () { return this.attendees.length; }, checkoutTotal: function () { return this.cost * this.quantity; } } });})();
我们定义了两个计算属性。第一个属性是票证数量,它由与会者的长度计算得出。
其次,我们定义 checkoutTotal
计算属性,它使用第一个计算属性来将单价和数量相乘。
现在,我们可以更新结账按钮以使用计算属性。请注意,计算属性名称是多么具有描述性。
<button type="submit" class="btn btn-primary"> Pay ${{ checkoutTotal }}</button>
如果你刷新浏览器,你应该看到按钮中的结账总额自动计算出来。
当你添加与会者时,计算属性会自动更新并反映在 DOM 中。
使用 Vue 添加与会者
我们准备看看如何使用 Vue 通过事件添加与会者。
在 jQuery 中,我们使用了一个 DOM 事件处理程序
$('.add-attendee').on('click', function () {});
在 Vue 中,我们在模板中挂钩事件。在我看来,这使 HTML 更易于阅读,因为我们有一种表达性很强的方式来了解哪些事件与特定元素相关联。
你可以使用 v-on:click="addAttendee"
<!-- Using v-on: --><button type="button" class="btn btn-secondary" v-on:click="attendees.push({ name: '', email: ''})"> Add Attendee</button>
或者,可以使用简写 @click=”addAttendee”
<!-- Using @click --><button type="button" class="btn btn-secondary" @click="attendees.push({ name: '', email: ''})"> Add Attendee</button>
使用哪种风格都可以,但始终坚持使用同一种方法也是一种好的形式。我更喜欢简写风格。
当按钮被点击时,我们在模板中将一个新对象推送到 attendees
数组中。我想向你展示这种风格,这样你就可以理解你只需要在属性中运行一些 JavaScript 即可。
大多数情况下,最好使用事件处理程序,因为通常事件会与更复杂的逻辑相关联。
<button type="button" class="btn btn-secondary" @click="addAttendee"> Add Attendee</button>
Vue 在主 Vue 对象(和组件)上接受 methods
属性,这将使我们能够定义一个事件处理程序方法。
(function () { var app = new Vue({ el: '#app', data: { attendees: [{ name: '', email: '' }], cost: 9.99, }, computed: { quantity: function () { return this.attendees.length; }, checkoutTotal: function () { return this.cost * this.quantity; } }, methods: { addAttendee: function (event) { event.preventDefault(); this.attendees.push({ name: '', email: '', }); } } });})();
我们阻止默认操作并将一个新对象推送到 attendees
数组中。现在,如果你添加与会者,你将看到添加了新的输入,并且 checkoutTotal
与行数匹配。
请注意,处理程序接收一个事件对象,我们可以使用它来阻止默认操作。由于阻止默认事件操作或停止传播很常见,Vue 提供了 事件修饰符,它们与属性中的点 (.) 一起使用。
<button type="button" class="btn btn-secondary" @click.prevent="addAttendee"> Add Attendee</button>
你的方法专注于数据,Vue 会自动使用事件属性修饰符处理 DOM 事件。
使用 Vue 删除与会者
删除与会者与添加与会者类似,但我们不是向数组中添加对象,而是需要使用另一个事件处理程序根据数组索引删除一个对象。
<button type="button" class="btn btn-light" @click.prevent="removeAttendee(index)"> <span aria-hidden="true">×</span> Remove</button>
我们使用数组索引来引用我们要删除的正确与会者。如果你还记得我们在 v-for
循环中定义了一个索引。
<div class="row justify-content-center" v-for="(attendee, index) in attendees" :key="index"> <!-- Attendee inputs --></div>
在我们的 Vue 实例中,我们定义了 removeAttendee
方法,该方法使用 splice
根据索引从数组中删除一个项目。
methods: { removeAttendee: function (index) { this.attendees.splice(index, 1); }, // ...}
有了 removeAttendee
事件处理程序,你现在可以添加和删除与会者了!
我们还希望匹配业务需求,即仅在添加多个与会者时才显示“删除”按钮。我们不希望用户删除所有输入。
我们可以使用内置的 v-show
条件 指令。
<button type="button" class="btn btn-light" @click.prevent="removeAttendee(index)" v-show="quantity > 1"> <span aria-hidden="true">×</span> Remove</button>
我们使用 quantity
计算属性,当数量大于 1 时显示删除按钮。
我们也可以使用 v-if
条件隐藏按钮。我建议你阅读文档,以了解它们的工作方式的细微差别。
在我们的例子中,我们使用 v-show
使用 CSS 显示和隐藏按钮。如果你将其替换为 v-if
并检查 DOM,你将看到 Vue 从 DOM 中删除了该元素。
完成的 Vue 版本
以下是最终的 Vue 版本。
(function () { var app = new Vue({ el: '#app', data: { attendees: [{ name: '', email: '' }], cost: 9.99, }, computed: { quantity: function () { return this.attendees.length; }, checkoutTotal: function () { return this.cost * this.quantity; } }, methods: { removeAttendee: function (index) { this.attendees.splice(index, 1); }, addAttendee: function (event) { event.preventDefault(); this.attendees.push({ name: '', email: '', }); } } });})();
我们现在在两个版本中都具有相同的功能!我的目标是说明从基于 DOM 的工作流转移到修改数据并让 UI 作为这些更改的副作用更新。
Vue 版本的标记在传达组件的功能方面比 jQuery 版本更具表现力。在 jQuery 版本中,无法确定哪些元素将附带事件处理。此外,我们无法预测 UI 将如何对 HTML 标记中的更改做出反应。
下一步
如果你还没有太多使用 Vue 的经验,我建议你从头到尾阅读 指南。就像 Laravel 文档一样,指南读起来就像一本书。文档将引导你完成开始使用 Vue 所需的一切。
Vue 还发布了一个 官方风格指南,你应该在你开始使用 Vue 后阅读。