VueJS2 学习指南(二)
原文:
zh.annas-archive.org/md5/0B1D097C4A60D3760752681016F7F246译者:飞龙
第四章:反应性-将数据绑定到您的应用程序
在上一章中,您学习了 Vue.js 中最重要的概念之一:组件。您看到了如何创建组件,如何注册,如何调用以及如何使用和重用它们。您还学习了单文件组件的概念,甚至在购物清单和番茄钟应用程序中使用了它们。
在本章中,我们将深入探讨数据绑定的概念。我们之前已经谈论过它,所以你已经很熟悉了。我们将以所有可能的方式在我们的组件中绑定数据。
总之,在本章中,我们将:
-
重新审视数据绑定语法
-
在我们的应用程序中应用数据绑定
-
遍历元素数组,并使用相同的模板渲染每个元素
-
重新审视并应用数据和事件绑定的速记方式在我们的应用程序中
重新审视数据绑定
我们从第一章开始就一直在谈论数据绑定和反应性。所以,你已经知道数据绑定是一种从数据到可见层以及反之的变化传播机制。在本章中,我们将仔细审视所有不同的数据绑定方式,并在我们的应用程序中应用它们。
插入数据
让我们想象一下以下的 HTML 代码:
<div id="hello"></div>
还想象以下 JavaScript 对象:
var data = {
msg: 'Hello'
};
我们如何在页面上呈现数据条目的值?我们如何访问它们,以便我们可以在 HTML 中使用它们?实际上,在过去的两章中,我们已经在 Vue.js 中大量做了这个。理解并一遍又一遍地做这件事并没有问题。
“重复是学习之母”
如果您已经是数据插值的专业人士,只需跳过本节,然后继续表达式和过滤器。
那么,我们应该怎么做才能用msg的值填充<div>?如果我们按照老式的 jQuery 方式,我们可能会做类似以下的事情:
$("#hello").text(data.msg);
但是,在运行时,如果您更改msg的值,并且希望这种更改传播到 DOM,您必须手动执行。仅仅改变data.msg的值,什么也不会发生。
例如,让我们编写以下代码:
var data = {
msg: 'Hello'
};
$('#hello').text(data.msg);
data.msg = 'Bye';
然后出现在<div>中的文本将是Hello。在jsfiddle.net/chudaol/uevnd0e4/上检查这个 JSFiddle。
使用 Vue,最简单的插值是用{{ }}(句柄注释)完成的。在我们的示例中,我们将编写以下 HTML 代码:
<div id="hello">**{{ msg }}**</div>
因此,<div>的内容将与msg数据绑定。每次msg更改时,div的内容都会自动更改其内容。请查看jsfiddle.net/chudaol/xuvqotmq/1/上的 jsfiddle 示例。Vue 实例化后,data.msg也会更改。屏幕上显示的值是新的值!
这仍然是单向绑定的插值。如果我们在 DOM 中更改值,数据将不会发生任何变化。但是,如果我们只需要数据的值出现在 DOM 中,并相应地更改,这是一种完美有效的方法。
此时,应该非常清楚,如果我们想在模板中使用data对象的值,我们应该用{{}}将它们括起来。
让我们向我们的番茄钟应用程序添加缺失的插值。请在chapter4/pomodoro文件夹中检查当前情况。如果您运行npm run dev并查看打开的页面,您将看到页面如下所示:
我们番茄钟应用程序中缺少的插值
从对页面的第一眼扫视中,我们能够确定那里缺少什么。
页面缺少计时器、小猫、番茄钟状态的标题(显示**工作!或休息!**)、以及根据番茄钟状态显示或隐藏小猫占位符的逻辑。让我们首先添加番茄钟状态的标题和番茄钟计时器的分钟和秒。
添加番茄钟状态的标题
首先,我们应该决定这个元素应该属于哪个组件。看看我们的四个组件。很明显,它应该属于StateTitleComponent。如果您查看以下代码,您将看到它实际上已经在其模板中插值了标题:
//StateTitleComponent.vue
<template>
<h3>**{{ title }}**</h3>
</template>
<style scoped>
</style>
<script>
</script>
好!在上一章中,我们已经完成了大部分工作。现在我们只需要添加必须被插值的数据。在这个组件的<script>标签中,让我们添加带有title属性的data对象。现在,让我们将其硬编码为可能的值之一,然后决定如何更改它。你更喜欢什么?工作! 还是 休息!?我想我知道答案,所以让我们将以下代码添加到我们的script标签中:
//StateTitleComponent.vue
<script>
export default {
data () {
return {
**title: 'Learning Vue.js!'**
}
}
}
</script>
现在就让它保持这样。我们将在以后的方法和事件处理部分回到这个问题。
练习
以与我们添加 Pomodoro 状态标题相同的方式,请将分钟和秒计时器计数器添加到CountDownComponent中。它们现在可以是硬编码的。
使用表达式和过滤器
在前面的例子中,我们在{{}}插值中使用了简单的属性键。实际上,Vue 在这些花括号中支持更多的内容。让我们看看在那里可能做些什么。
表达式
这可能听起来出乎意料,但 Vue 支持在数据绑定括号内使用完整的 JavaScript 表达式!让我们去 Pomodoro 应用程序的任何一个组件,并在模板中添加任何 JavaScript 表达式。你可以在chapter4/pomodoro2文件夹中进行一些实验。
例如,尝试打开StateTitleComponent.vue文件。让我们在其模板中添加一些 JavaScript 表达式插值,例如:
{{ Math.pow(5, 2) }}
实际上,你只需要取消注释以下行:
//StateTitleComponent.vue
<!--<p>-->
<!--{{ Math.pow(5, 2) }}-->
<!--</p>-->
你将在页面上看到数字**25**。很好,不是吗?让我们用 JavaScript 表达式替换 Pomodoro 应用程序中的一些数据绑定。例如,在CountdownComponent组件的模板中,每个用于min和sec的指令可以被一个表达式替换。目前它看起来是这样的:
//CountdownComponent.vue
<template>
<div class="well">
<div class="pomodoro-timer">
**<span>{{ min }}</span>:<span>{{ sec }}</span>**
</div>
</div>
</template>
我们可以用以下代码替换它:
//CountdownComponent.vue
<template>
<div class="well">
<div class="pomodoro-timer">
<span>{{ min + ':' + sec }}</span>
</div>
</div>
</template>
还有哪些地方可以添加一些表达式呢?让我们看看StateTitleComponent。此刻,我们使用的是硬编码的标题。然而,我们知道它应该以某种方式依赖于番茄钟的状态。如果它处于“工作”状态,它应该显示**Work!,否则应该显示Rest!**。让我们创建这个属性并将其命名为isworking,然后将其分配给主App.vue组件,因为它似乎属于全局应用状态。然后我们将在StateTitleComponent组件的props属性中重用它。因此,打开App.vue,添加布尔属性isworking并将其设置为true:
//App.vue
<...>
window.data = {
kittens: true,
**isworking: true**
};
export default {
<...>
data () {
return window.data
}
}
现在让我们在StateTitleComponent中重用这个属性,在每个可能的标题中添加两个字符串属性,并最后在模板中添加表达式,根据当前状态有条件地渲染一个标题或另一个标题。因此,组件的脚本将如下所示:
//StateTitleComponent.vue
<script>
export default {
data () {
return {
**workingtitle: 'Work!',
restingtitle: 'Rest!'**
}
},
**props: ['isworking']**
}
</script>
现在我们可以根据isworking属性有条件地渲染一个标题或另一个标题。因此,StateTitleComponent的模板将如下所示:
<template>
<div>
<h3>
{{ isworking ? workingtitle : restingtitle }}
</h3>
</div>
</template>
看一下刷新后的页面。奇怪的是,它显示**Rest!**作为标题。如果App.vue中的isworking属性设置为true,那么这是怎么发生的?我们只是忘记在App.vue模板中的组件调用上绑定这个属性!打开App.vue组件,并在state-title-component调用上添加以下代码:
<state-title-component **v-bind:isworking="isworking"**></state-title-component>
现在,如果你查看页面,正确的标题会显示为**Work!**。如果你打开开发工具控制台并输入data.isworking = false,你会看到标题改变。
如果isworking属性为false,标题为**Rest!**,如下截图所示:
如果isworking属性为true,标题为**Work!**,如下截图所示:
过滤器
除了花括号内的表达式之外,还可以使用应用于表达式结果的过滤器。过滤器只是函数。它们是由我们创建的,并且通过使用管道符号|应用。如果你创建一个将字母转换为大写的过滤器并将其命名为uppercase,那么要应用它,只需在双大括号内的管道符号后面使用它:
<h3> {{ title | lowercase }} </h3>
你可以链接尽可能多的过滤器,例如,如果你有过滤器A,B,C,你可以做类似{{ key | A | B | C }}的事情。过滤器是使用Vue.filter语法创建的。让我们创建我们的lowercase过滤器:
//main.js
Vue.filter('lowercase', (key) => {
return key.toLowerCase()
})
让我们将其应用到主App.vue组件中的 Pomodoro 标题。为了能够使用过滤器,我们应该将'Pomodoro'字符串传递到句柄插值符号内。我们应该将它作为 JavaScript 字符串表达式传递,并使用管道符号应用过滤器:
<template>
<...>
<h2>
<span>**{{ 'Pomodoro' | lowercase }}**</span>
<controls-component></controls-component>
</h2>
<...>
</template>
检查页面;**Pomodoro**标题实际上将以小写语法显示。
让我们重新审视我们的CountdownTimer组件并查看计时器。目前,只有硬编码的值,对吧?但是当应用程序完全功能时,值将来自某些计算。值的范围将从 0 到 60。计时器显示**20:40是可以的,但少于十的值是不可以的。例如,当只有 1 分钟和 5 秒时,它将是1:5,这是不好的。我们希望看到类似01:05**的东西。所以,我们需要leftpad过滤器!让我们创建它。
转到main.js文件,并在大写过滤器定义之后添加一个leftpad过滤器:
//main.js
Vue.filter(**'leftpad'**, (value) => {
if (value >= 10) {
return value
}
return '0' + value
})
打开CountdownComponent组件,让我们再次将min和sec拆分到不同的插值括号中,并为每个添加过滤器:
//CountdownComponent.vue
<template>
<div class="well">
<div class="pomodoro-timer">
<span>**{{ min | leftpad }}:{{ sec | leftpad }}**</span>
</div>
</div>
</template>
用 1 和 5 替换数据中的min和sec,然后查看。数字出现了前面的"0"!
练习
创建两个过滤器,大写和addspace,并将它们应用到标题**Pomodoro:**
-
大写过滤器必须做到它所说的那样 -
addspace过滤器必须在给定的字符串值右侧添加一个空格
不要忘记**Pomodoro**不是一个关键字,所以在插值括号内,它应该被视为一个字符串!在这个练习之前和之后的标题看起来应该是这样的:
在应用过滤器大写和添加空格之前和之后的 Pomodoro 应用程序的标题
自己检查:查看chapter4/pomodoro3文件夹。
重新审视和应用指令
在上一节中,我们看到了如何插值应用程序的数据以及如何将其绑定到可视层。尽管语法非常强大,并且提供了高可能性的数据修改(使用过滤器和表达式),但它也有一些限制。例如,尝试使用 {{}} 符号来实现以下内容:
-
在用户输入中使用插值数据,并在用户在输入中键入时将更改应用到相应的数据
-
将特定元素的属性(例如
src)绑定到数据 -
有条件地渲染一些元素
-
遍历数组并渲染一些组件与数组的元素
-
在元素上创建事件监听器
让我们至少尝试第一个。例如,打开购物清单应用程序(在 chapter4/shopping-list 文件夹中)。在 App.vue 模板中创建一个 input 元素,并将其值设置为 {{ title }}:
<template>
<div id="app" class="container">
<h2>{{ title }}</h2>
**<input type="text" value="{{ title }}">**
<add-item-component></add-item-component>
<...>
</div>
</template>
哦不!到处都是错误。已删除属性内的插值,它说。这是否意味着在 Vue 2.0 之前,您可以轻松地在属性内使用插值?是的,也不是。如果您在属性内使用插值,您将不会收到错误,但在输入框内更改标题将不会产生任何结果。在 Vue 2.0 中,以及在之前的版本中,为了实现这种行为,我们必须使用指令。
注意
指令是具有 v- 前缀的元素的特殊属性。为什么是 v-?因为 Vue!指令提供了一种微小的语法,比简单的文本插值提供了更丰富的可能性。它们有能力在每次数据更改时对可视层应用一些特殊行为。
使用 v-model 指令进行双向绑定
双向绑定是一种绑定类型,不仅数据更改会传播到 DOM 层,而且 DOM 中绑定数据发生的更改也会传播到数据中。要以这种方式将数据绑定到 DOM,我们可以使用 v-model 指令。
我相信您仍然记得第一章中使用 v-model 指令的方式:
<input type="text" **v-model="title"**>
这样,标题的值将出现在输入框中,如果您在此输入框中输入内容,相应的更改将立即应用到数据,并反映在页面上所有插值的值中。
只需用 v-model 替换花括号符号,并打开页面。
尝试在输入框中输入一些内容。您将看到标题立即更改!
只记住,这个指令只能用于以下元素:
-
<input> -
<select> -
<textarea>
尝试所有这些然后删除这段代码。我们的主要目的是能够使用更改标题组件来更改标题。
组件之间的双向绑定
从上一章中记得,使用v-model指令不能轻松实现组件之间的双向绑定。由于架构原因,Vue 只是阻止子组件轻松地改变父级作用域。
这就是为什么我们在上一章中使用事件系统来能够从子组件更改购物清单的标题。
我们将在本章中再次进行。等到我们到达v-on指令的部分之前再等几段时间。
使用v-bind指令绑定属性
v-bind指令允许我们将元素的属性或组件属性绑定到一个表达式。为了将其应用于特定属性,我们使用冒号分隔符:
v-bind:attribute
例如:
-
v-bind:src="src" -
v-bind:class="className"
任何表达式都可以写在""内。数据属性也可以像之前的例子一样使用。让我们在我们的 Pomodoro 应用程序中的KittenComponent中使用thecatapi作为来源添加小猫图片。从chapter4/pomodoro3文件夹打开我们的 Pomodoro 应用程序。
打开KittenComponent,将catimgsrc添加到组件的数据中,并使用v-bind语法将其绑定到图像模板的src属性:
<template>
<div class="well">
<img **v-bind:src="catImgSrc"** />
</div>
</template>
<style scoped>
</style>
<script>
export default {
data () {
return {
**catimgsrc: "http://thecatapi.com/api/images/get?size=med"**
}
}
}
</script>
打开页面。享受小猫!
应用了源属性的 Pomodoro KittenComponent
使用 v-if 和 v-show 指令进行条件渲染
如果您在前面的部分中已经付出了足够的注意,并且如果我要求您有条件地渲染某些内容,您实际上可以使用插值括号{{ }}内的 JavaScript 表达式来实现。
但是,尝试有条件地渲染某个元素或整个组件。这可能并不像在括号内应用表达式那么简单。
v-if指令允许有条件地渲染整个元素,这个元素也可能是一个组件元素,取决于某些条件。条件可以是任何表达式,也可以使用数据属性。例如,我们可以这样做:
<div v-if="1 < 5">hello</div>
或者:
<div v-if="Math.random() * 10 < 6">hello</div>
或者:
<div v-if="new Date().getHours() >= 16">Beer Time!</div>
或者使用组件的数据:
<template>
<div>
<h1 **v-if="!isadmin"**>Beer Time!</h1>
</div>
</template>
<script>
export default {
data () {
return {
**isadmin: false**
}
}
}
</script>
v-show属性做的是同样的工作。唯一的区别是,v-if根据条件渲染或不渲染元素,而v-show属性总是渲染元素,只是在条件结果为false时应用display:none CSS 属性。让我们来看看区别。在chapter4/beer-time文件夹中打开beer-time项目。运行npm install和npm run dev。打开App.vue组件,尝试使用true/false值,并尝试用v-show替换v-if。打开 devtools 并检查**elements**标签页。
让我们首先检查在isadmin属性值中使用v-if切换true和false时的外观。
当条件满足时,一切都如预期般出现;元素被渲染并出现在页面上:
使用v-if指令进行条件渲染。条件满足。
当条件不满足时,元素不会被渲染:
使用v-if指令进行条件渲染。条件不满足。
请注意,当条件不满足时,相应的元素根本不会被渲染!
使用v-show指令来玩弄条件结果值。当条件满足时,它的外观与使用v-if的前一种情况完全相同:
使用v-show指令进行条件渲染。条件满足。
现在让我们来看看当条件不满足时,使用v-show指令的元素会发生什么:
使用v-show指令进行条件渲染。条件不满足。
在这种情况下,当条件满足时,一切都是一样的,但当条件不满足时,元素也会被渲染,使用display:none CSS 属性。
你如何决定使用哪一个更好?在第一次渲染时,如果条件不满足,v-if指令将根本不渲染元素,从而减少初始渲染时的计算成本。但是,如果属性在运行时频繁更改,渲染/移除元素的成本高于仅应用display:none属性。因此,对于频繁更改的属性,请使用v-show,对于在运行时不会太多更改的条件,请使用v-if。
让我们回到我们的番茄钟应用程序。当番茄钟不处于工作状态时,应该有条件地呈现KittensComponent。因此,打开chapter4/pomodoro4文件夹中的 Pomodoro 应用程序代码。
你认为应该使用什么?v-if还是v-show?让我们分析一下。无论我们使用什么,这个元素在初始渲染时都应该可见吗?答案是否定的,因为在初始渲染时,用户开始工作并启动番茄钟计时器。也许最好使用v-if,以免在没有必要时产生初始渲染的成本。但是,让我们分析另一个因素——使小猫组件可见/不可见的状态切换的频率。这将在每个番茄钟间隔发生,对吧?在工作 15-20 分钟后,然后在 5 分钟的休息间隔后,实际上并不那么频繁,不会对渲染造成太大影响。在这种情况下,在我看来,无论你使用哪种,都无所谓。让我们使用v-show。打开App.vue文件,并将v-show指令应用于kittens-component的调用:
<template>
<div id="app" class="container">
<...>
<transition name="fade">
<kittens-component **v-show="!isworking"**></kittens-component>
</transition>
</div>
</template>
打开页面,尝试在 devtools 控制台中切换data.isworking的值。您将看到小猫容器的出现和消失。
使用 v-for 指令进行数组迭代
你可能记得,数组迭代是使用v-for指令完成的,具体语法如下:
<div v-for item in items>
item
</div>
或者使用组件:
<component v-for item in items v-bind:**componentitem="item"**></component>
对于数组中的每个项目,这将呈现一个组件,并将组件的item属性绑定到项目的值。当然,你记得在绑定语法的""内部,你可以使用任何 JavaScript 表达式。所以,要有创意!
提示
不要忘记,在绑定语法(componentitem)中使用的属性应该存在于组件的数据中!
例如,看看我们的购物清单应用程序(chapter4/shopping-list文件夹)。它已经在ItemsComponent中使用了v-for语法来渲染物品列表:
<template>
<ul>
<item-component **v-for="item in items"** :item="item"></item-component>
</ul>
</template>
ItemComponent,反过来,使用props声明了item属性:
<script>
export default {
**props: ['item']**
}
</script>
现在,让我们用我们的购物清单应用程序做一些有趣的事情。到目前为止,我们只处理了一个购物清单。想象一下,你想为不同类型的购物准备不同的购物清单。例如,你可能有一个常规的购物清单,用于正常的杂货购物日。你可能有一个不同的购物清单用于假期。当你买新房子时,你可能也想有一个不同的购物清单。让我们利用 Vue 组件的可重用性,将我们的购物清单应用程序转换为购物清单列表!我们将使用 Bootstrap 的选项卡面板来显示它们;有关更多信息,请参考getbootstrap.com/javascript/#tabs。
在 IDE 中打开您的购物清单应用程序(chapter4/shopping-list文件夹)。
首先,我们应该添加 Bootstrap 的 JavaScript 文件和 jQuery,因为 Bootstrap 依赖它来进行其惊人的魔术。继续手动将它们添加到index.html文件中:
<body>
<...>
<script src="https://code.jquery.com/jquery-3.1.0.js"></script>
<script
src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js">
</script>
<...>
</body>
现在,让我们逐步概述一下,我们应该做些什么,以便将我们的应用程序转换为购物清单列表:
-
首先,我们必须创建一个新组件。让我们称之为
ShoppingListComponent,并将我们当前的App.vue内容移动到那里。 -
我们的新
ShoppingListComponent应该包含props属性,其中包括它将从App.vue接收的title和items。 -
ItemsComponent应该从props属性接收items,而不是硬编码它。 -
在
App组件的data中,让我们声明并硬编码(暂时)一个shoppinglists数组,每个项目应该有一个标题,一个物品数组和一个 ID。 -
App.vue应该导入ShoppingListComponent,并在模板中遍历shoppinglists数组,并为每个构建html/jade结构的选项卡面板。
好的,那么,让我们开始吧!
创建 ShoppingListComponent 并修改 ItemsComponent
在components文件夹内,创建一个新的ShoppingListComponent.vue。将App.vue文件的内容复制粘贴到这个新文件中。不要忘记声明将包含title和items的props,并将items绑定到模板内的items-component调用。此组件的最终代码应该类似于以下内容:
//ShoppingListComponent.vue
<template>
**<div>**
<h2>{{ title }}</h2>
<add-item-component></add-item-component>
**<items-component v-bind:items="items"></items-component>**
<div class="footer">
<hr />
<change-title-component></change-title-component>
**</div>**
</div>
</template>
<script>
import AddItemComponent from './AddItemComponent'
import ItemsComponent from './ItemsComponent'
import ChangeTitleComponent from './ChangeTitleComponent'
export default {
components: {
AddItemComponent,
ItemsComponent,
ChangeTitleComponent
}
**props: ['title', 'items']**
}
</script>
<style scoped>
**.footer {
font-size: 0.7em;
margin-top: 20vh;
}**
</style>
请注意,我们删除了父div的容器样式和容器的class的部分。这部分代码应该留在App.vue中,因为它定义了全局应用程序的容器样式。不要忘记props属性和将props绑定到items-component!
打开ItemsComponent.vue,确保它包含带有items的props属性:
<script>
<...>
export default {
**props: ['items'],**
<...>
}
</script>
修改 App.vue
现在转到App.vue。删除<script>和<template>标签内的所有代码。在script标签中,导入ShoppingListComponent并在components属性内调用它:
//App.vue
<script>
import **ShoppingListComponent** from './components/ShoppingListComponent'
export default {
**components: {
ShoppingListComponent
}**
}
</script>
添加一个data属性并在那里创建一个shoppinglists数组。为该数组添加任意数据。该数组的每个对象应该具有id、title和items属性。正如你记得的那样,items必须包含checked和text属性。例如,你的data属性可能如下所示:
//App.vue
<script>
import ShoppingListComponent from './components/ShoppingListComponent'
export default {
components: {
ShoppingListComponent
},
**data () {
return {
shoppinglists: [
{
id: 'groceries',
title: 'Groceries',
items: [{ text: 'Bananas', checked: true },
{ text: 'Apples', checked: false }]
},
{
id: 'clothes',
title: 'Clothes',
items: [{ text: 'black dress', checked: false },
{ text: 'all stars', checked: false }]
}
]
}
}**
}
</script>
比我更有创意:添加更多的清单,更多的项目,一些漂亮有趣的东西!
现在让我们为基于购物清单的迭代创建组合 bootstrap 标签面板的结构!让我们首先定义标签工作所需的基本结构。让我们添加所有必要的类和 jade 结构,假装我们只有一个元素。让我们还用大写锁定写出所有将从我们的购物清单数组中重复使用的未知部分:
//App.vue
<template>
<div id="app" class="container">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation">
<a href="**ID**" aria-controls="**ID**" role="tab" data-toggle="tab">**TITLE**</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane" role="tabpanel" id="**ID**">
**SHOPPING LIST COMPONENT**
</div>
</div>
</div>
</template>
有两个元素需要在购物清单数组上进行迭代——包含<a>属性的<li>标签和tab-pane div。在第一种情况下,我们必须将每个购物清单的 ID 绑定到href和aria-controls属性,并插入标题。在第二种情况下,我们需要将id属性绑定到id属性,并呈现购物清单项目并将items数组和title绑定到它。简单!让我们开始。首先向每个元素添加v-for指令(对<li>和tab-pane div元素):
//App.vue
<template>
<div id="app" class="container">
<ul class="nav nav-tabs" role="tablist">
<li **v-for="list in shoppinglists"** role="presentation">
<a href="ID" aria-controls="ID" role="tab" data-
toggle="tab">TITLE</a>
</li>
</ul>
<div class="tab-content">
<div **v-for="list in shoppinglists"** class="tab-pane"
role="tabpanel"
id="ID">
**SHOPPING LIST COMPONENT**
</div>
</div>
</div>
</template>
现在用正确的绑定替换大写锁定部分。记住,对于bind属性,我们使用v-bind:<corresponding_attribute>="expression"语法。
对于锚元素的href属性,我们必须定义一个表达式,将 ID 选择器#附加到id: v-bind:href="'#' + list.id"。aria-controls属性应该绑定到 ID 的值。title可以使用简单的{{ }}符号插值进行绑定。
对于shopping-list-component,我们必须将title和items绑定到列表项的相应值。你还记得我们在ShoppingListComponent的props中定义了title和items属性吗?因此,绑定应该看起来像v-bind:title=list.title和v-bind:items=list.items。
因此,在适当的绑定属性之后,模板将如下所示:
//App.vue
<template>
<div id="app" class="container">
<ul class="nav nav-tabs" role="tablist">
<li v-for="list in shoppinglists" role="presentation">
<a **v-bind:href="'#' + list.id" v-bind:aria-controls="list.id"** role="tab" data-toggle="tab">**{{ list.title }}**</a>
</li>
</ul>
<div class="tab-content">
<div v-for="list in shoppinglists" class="tab-pane" role="tabpanel"
**v-bind:id="list.id"**>
**<shopping-list-component v-bind:**
**v-bind:items="list.items"></shopping-list-component>**
</div>
</div>
</div>
</template>
我们快完成了!如果你现在打开页面,你会看到标签的标题都出现在页面上:
修改后屏幕上看到的标签标题
如果你开始点击标签标题,相应的标签窗格将打开。但这不是我们期望看到的,对吧?我们期望的是第一个标签默认可见(活动状态)。为了实现这一点,我们应该将active类添加到第一个li和第一个tab-pane div中。但是,如果代码对所有标签都是相同的,因为我们正在遍历数组,我们该怎么做呢?
幸运的是,Vue 允许我们在v-for循环内不仅提供迭代项,还提供index,然后在模板中的表达式中重用这个index变量。因此,我们可以使用它来有条件地渲染active类,如果索引是"0"的话。在v-for循环内使用index变量就像下面这样简单:
v-for="**(list, index)** in shoppinglists"
类绑定的语法与其他所有内容的语法相同(class也是一个属性):
**v-bind:class= "active"**
你还记得我们可以在引号内写任何 JavaScript 表达式吗?在这种情况下,我们想要编写一个条件,评估index的值,如果是"0",则类的值是active:
v-bind:class= "index===0 ? 'active' : ''"
将index变量添加到v-for修饰符和li和tab-pane元素的class绑定中,使得最终的模板代码看起来像下面这样:
<template>
<div id="app" class="container">
<ul class="nav nav-tabs" role="tablist">
<li **v-bind:class= "index===0 ? 'active' :
''" v-for="(list, index) in shoppinglists"** role="presentation">
<a v-bind:href="'#' + list.id" v-bind:aria-controls="list.id"
role="tab" data-toggle="tab">{{ list.title }}</a>
</li>
</ul>
<div class="tab-content">
<div **v-bind:class= "index===0 ? 'active' : ''"
v-for="(list,index) in shoppinglists"** class="tab-pane"
role="tabpanel" v-bind:id="list.id">
<shopping-list-component v-bind:
v-bind:items="list.items"></shopping-list-component>
</div>
</div>
</div>
</template>
看看这页。现在你应该看到漂亮的标签,它们默认显示内容:
正确的类绑定后的购物清单应用程序的外观和感觉
在进行这些修改后,最终的购物清单应用程序代码可以在chapter4/shopping-list2文件夹中找到。
使用 v-on 指令的事件监听器
使用 Vue.js 监听事件并调用回调非常容易。事件监听也是使用特定修饰符的特殊指令完成的。该指令是v-on。修饰符在冒号之后应用:
v-on:click="myMethod"
好的,你说,我在哪里声明这个方法?你可能不会相信,但所有组件的方法都是在methods属性内声明的!因此,要声明名为myMethod的方法,你应该这样做:
<script>
export default {
methods: {
myMethod () {
//do something nice
}
}
}
</script>
所有data和props属性都可以使用this关键字在方法内部访问。
让我们添加一个方法来向items数组中添加新项目。实际上,在上一章中,当我们学习如何在父子组件之间传递数据时,我们已经做过了。我们只是在这里回顾一下这部分。
为了能够在属于ShoppingListComponent的购物清单中向AddItemComponent内添加新项目,我们应该这样做:
-
确保
AddItemComponent有一个名为newItem的data属性。 -
在
AddItemComponent内创建一个名为addItem的方法,该方法推送newItem并触发add事件。 -
使用
v-on:click指令为**Add!**按钮应用一个事件监听器。此事件监听器应调用已定义的addItem方法。 -
在
ShoppingListComponent内创建一个名为addItem的方法,该方法将接收text作为参数,并将其推送到items数组中。 -
将
v-on指令与自定义的add修饰符绑定到ShoppingListComponent内的add-item-component的调用上。此监听器将调用此组件中定义的addItem方法。
那么,让我们开始吧!使用chapter4/shopping-list2文件夹中的购物清单应用程序并进行操作。
首先打开AddItemComponent,并为**Add!**按钮和addItem方法添加缺失的v-on指令:
//AddItemComponent.vue
<template>
<div class="input-group">
<input type="text" **v-model="newItem"**
placeholder="add shopping list item" class="form-control">
<span class="input-group-btn">
<button **v-on:click="addItem"** class="btn btn-default"
type="button">Add!</button>
</span>
</div>
</template>
<script>
export default {
data () {
return {
**newItem: ''**
}
},
**methods: {
addItem () {
var text
text = this.newItem.trim()
if (text) {
this.$emit('add', this.newItem)
this.newItem = ''
}
}
}**
}
</script>
切换到ShoppingListComponent,并将v-on:add指令绑定到template标签内的add-item-component的调用上:
//ShoppingListComponent.vue
<template>
<div>
<h2>{{ title }}</h2>
<add-item-component **v-on:add="addItem"**></add-item-component>
<items-component v-bind:items="items"></items-component>
<div class="footer">
<hr />
<change-title-component></change-title-component>
</div>
</div>
</template>
现在在ShoppingListComponent内创建addItem方法。它应该接收文本,并将其推入this.items数组中:
//ShoppingListComponent.vue
<script>
import AddItemComponent from './AddItemComponent'
import ItemsComponent from './ItemsComponent'
import ChangeTitleComponent from './ChangeTitleComponent'
export default {
components: {
AddItemComponent,
ItemsComponent,
ChangeTitleComponent
},
props: ['title', 'items'],
**methods: {
addItem (text) {
this.items.push({
text: text,
checked: false
})
}
}**
}
</script>
打开页面,尝试通过在输入框中输入并点击按钮来将项目添加到列表中。它有效!
现在,我想请你将角色从应用程序的开发人员切换到其用户。在输入框中输入新项目。项目介绍后,用户显而易见的动作是什么?难道你不是想按Enter按钮吗?我敢打赌你是!当什么都没有发生时,这有点令人沮丧,不是吗?别担心,我的朋友,我们只需要向输入框添加一个事件侦听器,并调用与**Add!**按钮相同的方法。
听起来很容易,对吧?我们按Enter按钮时触发了什么事件?对,就是 keyup 事件。因此,我们只需要在冒号分隔符后使用v-on指令和keyup方法。问题是,当我们按下新的购物清单项目时,每次引入新字母时,该方法都会被调用。这不是我们想要的。当然,我们可以在addItem方法内添加一个条件,检查event.code属性,并且只有在它是13(对应Enter键)时,我们才会调用方法的其余部分。幸运的是,对于我们来说,Vue 提供了一种机制,可以为此方法提供按键修饰符,这样我们只能在按下特定按键时调用方法。它应该使用点(.)修饰符实现。在我们的情况下,如下所示:
v-on:keyup.enter
让我们将其添加到我们的输入框中。转到AddItemComponent,并将v-on:keyup.enter指令添加到输入中,如下所示:
<template>
<div class="input-group">
<input type="text" **v-on:keyup.enter="addItem"** v-model="newItem"
placeholder="add shopping list item" class="form-control">
<span class="input-group-btn">
<button v-on:click="addItem" class="btn btn-default"
type="button">Add!</button>
</span>
</div>
</template>
打开页面,尝试使用Enter按钮将项目添加到购物清单中。它有效!
让我们对标题更改做同样的事情。唯一的区别是,在添加项目时,我们使用了自定义的add事件,而在这里我们将使用原生的输入事件。我们已经做到了。我们只需要执行以下步骤:
-
在
ShoppingListComponent的模板中,使用v-model指令将模型标题绑定到change-title-component。 -
在
ChangeTitleComponent的props属性中导出value。 -
在
ChangeTitleComponent内创建一个onInput方法,该方法将使用事件目标的值发出原生的input方法。 -
将
value绑定到ChangeTitleComponent组件模板中的input,并使用带有onInput修饰符的v-on指令。
因此,在 ShoppingListComponent 模板中的 change-title-component 调用将如下所示:
//ShoppingListComponent.vue
<change-title-component **v-model="title"**></change-title-component>
ChangeTitleComponent 将如下所示:
//ChangeTitleComponent.vue
<template>
<div>
<em>Change the title of your shopping list here</em>
<input **v-bind:value="value" v-on:input="onInput"**/>
</div>
</template>
<script>
export default {
props: ['value'],
methods: {
**onInput (event) {
this.$emit('input', event.target.value)
}**
}
}
</script>
此部分的最终代码可以在 chapter4/shopping-list3 文件夹中找到。
简写
当然,每次在代码中写 v-bind 或 v-on 指令并不费时。开发人员倾向于认为每次减少代码量,我们就赢了。Vue.js 允许我们赢!只需记住 v-bind 指令的简写是冒号(:),v-on 指令的简写是 @ 符号。这意味着以下代码做了同样的事情:
v-bind:items="items" :items="items"
v-bind:class=' $index === 0 ? "active" : ""'
:class=' $index===0 ? "active" : ""'
v-on:keyup.enter="addItem" @keyup.enter="addItem"
练习
使用我们刚学到的快捷方式重写购物清单应用程序中的所有 v-bind 和 v-on 指令。
通过查看 chapter4/shopping-list4 文件夹来检查自己。
小猫
在本章中,我们并没有涉及到我们的番茄钟应用程序及其可爱的小猫。我向您保证,我们将在下一章中大量涉及它。与此同时,我希望这只小猫会让您开心:
小猫问:“接下来做什么?”
总结
在本章中,我们对将数据绑定到我们的表示层的所有可能方式进行了广泛的概述。您学会了如何简单地使用句柄括号({{ }})插值数据。您还学会了如何在这样的插值中使用 JavaScript 表达式和过滤器。您学习并应用了诸如 v-bind、v-model、v-for、v-if 和 v-show 等指令。
我们修改了我们的应用程序,使它们使用更丰富和更高效的数据绑定语法。
在下一章中,我们将讨论 Vuex,这是受 Flux 和 Redux 启发的状态管理架构,但具有简化的概念。
我们将为我们的两个应用程序创建全局应用程序状态管理存储,并通过使用它来探索它们的潜力。
第五章:Vuex-管理应用程序中的状态
在上一章中,您学习了 Vue.js 中最重要的概念之一:数据绑定。您学习并应用了许多将数据绑定到我们的应用程序的方法。您还学习了如何使用指令,如何监听事件,以及如何创建和调用方法。在本章中,您将看到如何管理表示全局应用程序状态的数据。我们将讨论 Vuex,这是 Vue 应用程序中用于集中状态的特殊架构。您将学习如何创建全局数据存储以及如何在组件内部检索和更改它。我们将定义应用程序中哪些数据是本地的,哪些应该是全局的,并且我们将使用 Vuex 存储来处理其中的全局状态。
总而言之,在本章中,我们将:
-
了解本地和全局应用程序状态之间的区别
-
了解 Vuex 是什么以及它是如何工作的
-
学习如何使用全局存储中的数据
-
了解存储的 getter、mutation 和 action
-
安装并在购物清单和番茄钟应用程序中使用 Vuex 存储
父子组件的通信、事件和脑筋急转弯
还记得我们的购物清单应用程序吗?还记得我们的ChangeTitleComponent以及我们如何确保在其输入框中输入会影响属于父组件的购物清单的标题吗?您记得每个组件都有自己的作用域,父组件的作用域不能受到子组件的影响。因此,为了能够将来自子组件内部的更改传播到父组件,我们使用了事件。简单地说,您可以从子组件调用$emit方法,并传递要分发的事件的名称,然后在父组件的v-on指令中监听此事件。
如果是原生事件,比如input,那就更简单了。只需将所需的属性绑定到子组件作为v-model,然后从子组件调用$emit方法并传递事件的名称(例如,input)。
实际上,这正是我们在ChangeTitleComponent中所做的。
打开chapter5/shopping-list文件夹中的代码,并检查我是否正确。(如果您想在浏览器中检查应用程序的行为,您可能还需要运行npm install和npm run dev。)
我们使用v-model指令将标题绑定到ShoppingListComponent模板中的ChangeTitleComponent:
//ShoppingListComponent.vue
<template>
<div>
<...>
<div class="footer">
<hr />
<change-title-component **v-model="title"**></change-title-component>
</div>
</div>
</template>
之后,我们在ChangeTitleComponent的props属性中声明了标题模型的值,并在input动作上发出了input事件:
<template>
<div>
<em>Change the title of your shopping list here</em>
<input **:value="value" @input="onInput"**/>
</div>
</template>
<script>
export default {
props: [**'value'**],
methods: {
onInput (event) {
**this.$emit('input', event.target.value)**
}
}
}
</script>
看起来非常简单,对吧?
如果我们尝试在输入框中更改标题,我们的购物清单的标题会相应更改:
在父子组件之间建立基于事件的通信之后,我们能够改变标题
看起来我们实际上能够实现我们的目的。然而,如果你打开你的开发工具,你会看到一个丑陋的错误:
**[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component rerenders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "title"**
哎呀!Vue 实际上是对的,我们正在改变包含在ShoppingList组件的props属性中的数据。这个属性来自于主父组件App.vue,它又是我们的ShoppingListComponent的父组件。而我们已经知道我们不能从子组件改变父组件的数据。如果标题直接属于ShoppingListComponent,那就没问题,但在这种情况下,我们肯定做错了什么。
另外,如果你足够注意,你可能会注意到还有一个地方包含了相同的数据,尽管我们努力了,它也没有改变。看看标签的标题。它继续显示单词**Groceries**。但我们也希望它改变!
小小的侧记:我添加了一个新组件,ShoppingListTitleComponent。它代表了标签的标题。你还记得计算属性吗?请注意,这个组件包含一个计算属性,它只是在通过props属性导入的 ID 前面添加#来生成一个锚点:
<template>
<a **:href="href"** :aria-controls="id" role="tab" data-toggle="tab">
{{ title }}</a>
</template>
<script>
export default{
props: ['id', **'title'**],
**computed: {
href () {
return '#' + this.id
}
}**
}
</script>
显示标签标题的锚点包含一个依赖于这个计算属性的href绑定指令。
所以,回到标题更改。当ChangeTitleComponent内部的标题改变时,我们能做些什么来改变这个组件的标题?如果我们能将事件传播到主App.vue组件,我们实际上可以解决这两个问题。每当父组件中的数据改变时,它都会影响所有子组件。
因此,我们需要以某种方式使事件从ChangeTitleComponent流向主App组件。听起来很困难,但实际上,我们只需要在ChangeTitleComponent及其父级中注册我们的自定义事件,并发出它直到它到达App组件。App组件应该通过将更改应用于相应的标题来处理此事件。为了让App.vue确切地知道正在更改哪个购物清单,它的子ShoppingListComponent还应该传递它所代表的购物清单的 ID。为了实现这一点,App.vue应该将id属性传递给组件,购物清单组件应该在其props属性中注册它。
因此,我们将执行以下操作:
-
在
App组件的模板中,在ShoppingListComponent的创建时将id属性绑定到ShoppingListComponent。 -
从
ShoppingList组件内部绑定属性title而不是v-model到change-title-component。 -
将自定义事件(我们称之为
changeTitle)附加到ChangeTitleComponent内部的input上。 -
告诉
ShoppingListComponent监听来自change-title-component的自定义changeTitle事件,使用v-on指令处理它,通过发出另一个事件(也可以称为changeTitle)来处理它,应该被App组件捕获。 -
在
App.vue内部为shopping-list-component附加changeTitle事件的监听器,并通过实际更改相应购物清单的标题来处理它。
让我们从修改App.vue文件的模板开始,并将购物清单的 ID 绑定到shopping-list-component:
//App.vue
<template>
<div id="app" class="container">
<...>
<shopping-list-component **:id="list.id"** :
:items="list.items"></shopping-list-component>
<...>
</div>
</template>
现在在ShoppingListComponent组件的props中注册id属性:
//ShoppingListComponent.vue
<script>
<...>
export default {
<...>
props: [**'id'**, 'title', 'items'],
<...>
}
</script>
将title数据属性绑定到change-title-component而不是v-model指令:
//ShoppingListComponent.vue
<template>
<...>
<change-title-component **:**></change-title-component>
<...>
</template>
//ChangeTitleComponent.vue
<template>
<div>
<em>Change the title of your shopping list here</em>
<input **:value="title"** @input="onInput"/>
</div>
</template>
<script>
export default {
props: ['value', **'title'**],
<...>
}
</script>
从ChangeTitleComponent发出自定义事件而不是input,并在其父组件中监听此事件:
//ChangeTitleComponent.vue
<script>
export default {
<...>
methods: {
onInput (event) {
this.$emit(**'changeTitle'**, event.target.value)
}
}
}
</script>
//ShoppingListComponent.vue
<template>
<...>
<change-title-component :
**v-on:changeTitle="onChangeTitle"**></change-title-component>
<...>
</template>
在ShoppingListComponent中创建onChangeTitle方法,该方法将发出自己的changeTitle事件。使用v-on指令在App.vue组件中监听此事件。请注意,购物清单组件的onChangeTitle方法应发送其 ID,以便App.vue知道正在更改哪个购物清单的标题。因此,onChangeTitle方法及其处理将如下所示:
//ShoppingListComponent.vue
<script>
<...>
export default {
<...>
methods: {
<...>
**onChangeTitle (text) {
this.$emit('changeTitle', this.id, text)
}**
}
}
</script>
//App.vue
<template>
<...>
<shopping-list-component :id="list.id" :
:items="list.items" **v-on:changeTitle="onChangeTitle"**>
</shopping-list-component>
<...>
</template>
最后,在App.vue中创建一个changeTitle方法,该方法将通过其 ID 在shoppinglists数组中找到一个购物清单并更改其标题:
<script>
<...>
import _ from 'underscore'
export default {
<...>
methods: {
**onChangeTitle (id, text) {
_.findWhere(this.shoppinglists, { id: id }).title = text
}**
}
}
</script>
请注意,我们使用了underscore类的findWhere方法(underscorejs.org/#findWhere)来使我们通过 ID 查找购物清单的任务更容易。
而且...我们完成了!检查这个提示的最终代码在chapter5/shopping-list2文件夹中。在浏览器中检查页面。尝试在输入框中更改标题。你会看到它在所有地方都改变了!
承认这是相当具有挑战性的。试着自己重复所有的步骤。与此同时,让我随机地告诉你两个词:全局和局部。想一想。
我们为什么需要一个全局状态存储?
作为开发人员,你已经熟悉全局和局部的概念。有一些全局变量可以被应用程序的每个部分访问,但方法也有它们自己的(局部)作用域,它们的作用域不可被其他方法访问。
基于组件的系统也有它的局部和全局状态。每个组件都有它的局部数据,但应用程序有一个全局的应用程序状态,可以被应用程序的任何组件访问。我们在前面段落中遇到的挑战,如果我们有某种全局变量存储器,其中包含购物清单的标题,并且每个组件都可以访问和修改它们,那么这个挑战将很容易解决。幸运的是,Vue 的创作者为我们考虑到了这一点,并创建了 Vuex 架构。这种架构允许我们创建一个全局应用程序存储——全局应用程序状态可以被存储和管理的地方!
什么是 Vuex?
如前所述,Vuex 是用于集中状态管理的应用程序架构。它受 Flux 和 Redux 的启发,但更容易理解和使用:
Vuex 架构;图片取自 Vuex GitHub 页面,网址为 github.com/vuejs/vuex
看着镜子(不要忘记对自己微笑)。你看到一个漂亮的人。然而,里面有一个复杂的系统。当你感到冷时你会怎么做?当天气炎热时你会有什么感觉?饥饿是什么感觉?非常饥饿又是什么感觉?摸一只毛茸茸的猫是什么感觉?人可以处于各种状态(快乐,饥饿,微笑,生气等)。人还有很多组件,比如手、胳膊、腿、胃、脸等。你能想象一下,如果比如一只手能够直接影响你的胃,让你感到饥饿,而你却不知情,那会是什么感觉?
我们的工作方式与集中式状态管理系统非常相似。我们的大脑包含事物的初始状态(快乐,不饿,满足等)。它还提供了允许在其中拉动的机制,可以影响状态。例如,微笑,感到满足,鼓掌等。我们的手、胃、嘴巴和其他组件不能直接影响状态。但它们可以告诉我们的大脑触发某些改变,而这些改变反过来会影响状态。
例如,当你饿了的时候,你会吃东西。你的胃在某个特定的时刻告诉大脑它已经饱了。这个动作会改变饥饿状态为满足状态。你的嘴巴组件与这个状态绑定,让你露出微笑。因此,组件与只读的大脑状态绑定,并且可以触发改变状态的大脑动作。这些组件彼此不知道对方,也不能直接以任何方式修改对方的状态。它们也不能直接影响大脑的初始状态。它们只能调用动作。动作属于大脑,在它们的回调中,状态可以被修改。因此,我们的大脑是唯一的真相来源。
提示
信息系统中的唯一真相来源是一种设计应用架构的方式,其中每个数据元素只存储一次。这些数据是只读的,以防止应用程序的组件破坏被其他组件访问的状态。Vuex 商店的设计方式使得不可能从任何组件改变它的状态。
商店是如何工作的,它有什么特别之处?
Vuex 存储基本上包含两件事:状态和变化。状态是表示应用程序数据的初始状态的对象。变化也是一个包含影响状态的动作函数的对象。Vuex 存储只是一个普通的 JavaScript 文件,它导出这两个对象,并告诉 Vue 使用 Vuex(Vue.use(Vuex))。然后它可以被导入到任何其他组件中。如果你在主App.vue文件中导入它,并在Vue应用程序初始化时注册存储,它将传递给整个子代链,并且可以通过this.$store变量访问。因此,非常粗略地,以一种非常简化的方式,我们将创建一个存储,在主应用程序中导入它,并在组件中使用它的方式:
**//CREATE STORE**
//initialize state
const state = {
msg: 'Hello!'
}
//initialize mutations
const mutations = {
changeMessage(state, msg) {
state.msg = msg
}
}
//create store with defined state and mutations
export default new Vuex.Store({
state: state
mutations: mutations
})
**//CREATE VUE APP**
<script>
**import store from './vuex/store'**
export default {
components: {
SomeComponent
},
**store: store**
}
</script>
**//INSIDE SomeComponent**
<script>
export default {
computed: {
msg () {
return **this.$store.state.msg**;
}
},
methods: {
changeMessage () {
**this.$store.commit('changeMessage', newMsg);**
}
}
}
</script>
一个非常合乎逻辑的问题可能会出现:为什么创建 Vuex 存储而不是只有一个共享的 JavaScript 文件导入一些状态?当然,你可以这样做,但是然后你必须确保没有组件可以直接改变状态。当然,能够直接更改存储属性会更容易,但这可能会导致错误和不一致。Vuex 提供了一种干净的方式来隐式保护存储状态免受直接访问。而且,它是反应性的。将所有这些放在陈述中:
-
Vuex 存储是反应性的。一旦组件从中检索状态,它们将在状态更改时自动更新其视图。
-
组件无法直接改变存储的状态。相反,它们必须分派存储声明的变化,这样可以轻松跟踪更改。
-
因此,我们的 Vuex 存储成为了唯一的真相来源。
让我们创建一个简单的问候示例,看看 Vuex 的运作方式。
带存储的问候
我们将创建一个非常简单的 Vue 应用程序,其中包含两个组件:其中一个将包含问候消息,另一个将包含input,允许我们更改此消息。我们的存储将包含表示初始问候的初始状态,以及能够更改消息的变化。让我们从创建 Vue 应用程序开始。我们将使用vue-cli和webpack-simple模板:
**vue init webpack-simple simple-store**
安装依赖项并按以下方式运行应用程序:
**cd simple-store npm install npm run dev**
应用程序已启动!在localhost:8080中打开浏览器。实际上,问候已经存在。现在让我们添加必要的组件:
-
ShowGreetingsComponent将只显示问候消息 -
ChangeGreetingsComponent将显示输入字段,允许更改消息
在src文件夹中,创建一个components子文件夹。首先将ShowGreetingsComponent.vue添加到这个文件夹中。
它看起来就像下面这样简单:
<template>
<h1>**{{ msg }}**</h1>
</template>
<script>
export default {
**props: ['msg']**
}
</script>
之后,将ChangeGreetingsComponent.vue添加到这个文件夹中。它必须包含带有v-model='msg'指令的输入:
<template>
<input **v-model='msg'**>
</template>
<script>
export default {
**props: ['msg']**
}
</script>
现在打开App.vue文件,导入组件,并用这两个组件替换标记。不要忘记将msg绑定到它们两个。所以,修改后的App.vue将看起来像下面这样:
<template>
<div>
**<show-greetings-component :msg='msg'></show-greetings-component>
<change-greetings-component :msg='msg'></change-greetings-component>**
<div>
</template>
<script>
import ShowGreetingsComponent from './components/ShowGreetingsComponent.vue'
import ChangeGreetingsComponent from './components/ChangeGreetingsComponent.vue'
export default {
**components: { ShowGreetingsComponent, ChangeGreetingsComponent }**,
data () {
return {
msg: 'Hello Vue!'
}
}
}
</script>
打开浏览器。你会看到带有我们问候语的输入框;然而,在其中输入不会改变标题中的消息。我们已经预料到了,因为我们知道组件不能直接影响彼此的状态。现在让我们引入 store!首先,我们必须安装vuex:
**npm install vuex --save**
在src文件夹中创建一个名为vuex的文件夹。创建一个名为store.js的 JavaScript 文件。这将是我们的状态管理入口。首先导入Vue和Vuex,并告诉Vue我们想在这个应用程序中使用Vuex:
//store.js
import Vue from 'vue'
import Vuex from 'vuex'
**Vue.use(Vuex)**
现在创建两个常量,state和mutations。State将包含消息msg,而mutations将导出允许我们修改msg的方法:
const state = {
msg: 'Hello Vue!'
}
const mutations = {
changeMessage(state, msg) {
state.msg = msg
}
}
现在使用已创建的state和mutations初始化 Vuex store:
export default new Vuex.Store({
state: state,
mutations: mutations
})
提示
由于我们使用 ES6,{state: state, mutations: mutations}的表示法可以简单地替换为{state, mutations}
我们整个商店的代码看起来就像下面这样:
//store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const state = {
**msg: 'Hello Vue!'**
}
const mutations = {
**changeMessage(state, msg) {
state.msg = msg
}**
}
export default new Vuex.Store({
**state,
mutations**
})
现在我们可以在App.vue中导入 store。通过这样做,我们告诉所有组件它们可以使用全局 store,因此我们可以从App.vue中删除数据。而且,我们不再需要将数据绑定到组件:
//App.vue
<template>
<div>
<show-greetings-component></show-greetings-component>
<change-greetings-component></change-greetings-component>
</div>
</template>
<script>
import ShowGreetingsComponent from './components/ShowGreetingsComponent.vue'
import ChangeGreetingsComponent from './components/ChangeGreetingsComponent.vue'
**import store from './vuex/store'**
export default {
components: {ShowGreetingsComponent, ChangeGreetingsComponent},
**store**
}
</script>
现在让我们回到我们的组件,并重用 store 中的数据。为了能够重用 store 状态中的响应式数据,我们应该使用计算属性。Vue 是如此聪明,它将为我们做所有的工作,以便在状态更改时,反应地更新这些属性。不,我们不需要在组件内部导入 store。我们只需使用this.$store变量就可以访问它。因此,我们的ShowGreetingsComponent将看起来像下面这样:
//ShowGreetingsComponent.vue
<template>
<h1>{{ msg }}</h1>
</template>
<style>
</style>
<script>
export default {
**computed: {
msg () {
return this.$store.state.msg
}
}**
}
</script>
按照相同的逻辑在ChangeGreetingsComponent中重用存储的msg。现在我们只需要在每个keyup事件上分发变异。为此,我们只需要创建一个方法,该方法将提交相应的存储变异,并且我们将从输入的keyup监听器中调用它:
//ChangeGreetingsComponent.vue
<template>
<input v-model='msg' **@keyup='changeMsg'**>
</template>
<script>
export default {
computed: {
msg() {
return this.$store.state.msg
}
},
**methods: {
changeMsg(ev) {
this.$store.commit('changeMessage', ev.target.value)
}
}**
}
</script>
打开页面。尝试更改标题。Et voilà!它奏效了!
使用 Vuex 存储调用变异并通过组件传播更改存储状态
我们不再需要绑定v-model指令,因为所有的更改都是由调用存储的变异方法引起的。因此,msg属性可以绑定为输入框的值属性:
<template>
<input **:value='msg'** @keyup='changeMsg'>
</template>
检查chapter5/simple-store文件夹中的此部分的代码。在这个例子中,我们使用了一个非常简化的存储版本。然而,复杂的单页应用程序(SPAs)需要更复杂和模块化的结构。我们可以并且应该将存储的 getter 和分发变化的操作提取到单独的文件中。我们还可以根据相应数据的责任对这些文件进行分组。在接下来的章节中,我们将看到如何通过使用 getter 和 action 来实现这样的模块化结构。
存储状态和 getter
当然,我们可以在组件内部重用this.$store.state关键字是好的。但想象一下以下情景:
-
在一个大型应用程序中,不同的组件使用
$this.store.state.somevalue访问存储的状态,我们决定更改somevalue的名称。这意味着我们必须更改每个使用它的组件内部变量的名称! -
我们想要使用状态的计算值。例如,假设我们想要一个计数器。它的初始状态是“0”。每次我们使用它,我们都想要递增它。这意味着每个组件都必须包含一个重用存储值并递增它的函数,这意味着在每个组件中都有重复的代码,这一点一点也不好!
对不起,情景不太好,伙计们!幸运的是,有一种不会陷入其中任何一种情况的好方法。想象一下,中央获取器访问存储状态并为每个状态项提供获取器函数。如果需要,此获取器可以对状态项应用一些计算。如果我们需要更改某些属性的名称,我们只需在此获取器中更改一次。这更像是一种良好的实践或约定,而不是强制性的架构系统,但我强烈建议即使只有几个状态项,也要使用它。
让我们为我们的简单问候应用程序创建这样的获取器。只需在vuex文件夹中创建一个getters.js文件,并导出一个将返回state.msg的getMessage函数:
//getters.js
export default {
**getMessage(state) {
return state.msg
}**
}
然后它应该被存储导入并在新的Vuex对象中导出,这样存储就知道它的获取器是什么:
//store.js
import Vue from 'vue'
import Vuex from 'vuex'
**import getters from './getters'**
Vue.use(Vuex)
const state = {
msg: 'Hello Vue!'
}
const mutations = {
changeMessage(state, msg) {
state.msg = msg
}
}
export default new Vuex.Store({
state, mutations, **getters**
})
然后,在我们的组件中,我们使用获取器而不是直接访问存储状态。只需在两个组件中替换您的computed属性为以下内容:
computed: {
msg () {
return **this.$store.getters.getMessage**
}
},
打开页面;一切都像魅力一样工作!
仍然this.$store.getters表示法包含太多要写的字母。我们,程序员是懒惰的,对吧?Vue 很好地为我们提供了一种支持我们懒惰的简单方法。它提供了一个mapGetters助手,正如其名称所示,为我们的组件提供了所有存储的获取器。只需导入它并在您的computed属性中使用它,如下所示:
//ShowGreetingsComponent.vue
<template>
<h1>**{{ getMessage }}**</h1>
</template>
<script>
**import { mapGetters } from 'vuex'**
export default {
**computed: mapGetters(['getMessage'])**
}
</script>
//ChangeGreetingsComponent.vue
<template>
<input :value='**getMessage**' @keyup='changeMsg'>
</template>
<script>
**import { mapGetters } from 'vuex'**
export default {
**computed: mapGetters(['getMessage'])**,
methods: {
changeMsg(ev) {
this.$store.commit('changeMessage', ev.target.value)
}
}
}
</script>
请注意,我们已更改模板中使用的属性,使其与获取器方法名称相同。但是,也可以将相应的获取器方法名称映射到我们在组件中想要使用的属性名称。
//ShowGreetingsComponent.vue
<template>
<h1>**{{ msg }}**</h1>
</template>
<style>
</style>
<script>
**import { mapGetters } from 'vuex'**
export default {
**computed: mapGetters({
msg: 'getMessage'
})**
}
</script>
//ChangeGreetingsComponent.vue
<template>
<input :value='**msg**' @keyup='changeMsg'>
</template>
<script>
**import { mapGetters } from 'vuex'**
export default {
**computed: mapGetters({
msg: 'getMessage'
})**,
methods: {
changeMsg(ev) {
this.$store.commit('changeMessage', ev.target.value)
}
}
}
</script>
因此,我们能够将msg属性的获取器提取到中央存储的获取器文件中。
现在,如果您决定为msg属性添加一些计算,您只需要在一个地方做就可以了。只在一个地方!
Rick 总是在所有组件中更改代码,刚刚发现只需在一个地方更改代码就可以了
例如,如果我们想在所有组件中重用大写消息,我们可以在获取器中应用uppercase函数,如下所示:
//getters.js
export default {
getMessage(state) {
**return (state.msg).toUpperCase()**
}
}
从现在开始,每个使用获取器检索状态的组件都将具有大写消息:
ShowTitleComponent 将消息大写。toUpperCase 函数应用在 getter 内部
还要注意,当您在输入框中输入时,消息如何平稳地变成大写!查看chapter5/simple-store2文件夹中此部分的最终代码。
如果我们决定更改状态属性的名称,我们只需要在 getter 函数内部进行更改。例如,如果我们想将msg的名称更改为message,我们将在我们的 store 内部进行更改:
const state = {
**message**: 'Hello Vue!'
}
const mutations = {
changeMessage(state, msg) {
state.**message** = msg
}
}
然后,我们还将在相应的 getter 函数内部进行更改:
export default {
getMessage(state) {
return (**state.message**).toUpperCase()
}
}
就是这样!应用的其余部分完全不受影响。这就是这种架构的力量。在一些非常复杂的应用程序中,我们可以有多个 getter 文件,为应用程序的不同属性导出状态。模块化是推动可维护性的力量;利用它!
变化
从前面的例子中,应该清楚地看到,变化不过是由名称定义的简单事件处理程序函数。变化处理程序函数将state作为第一个参数。其他参数可以用于向处理程序函数传递不同的参数:
const mutations = {
**changeMessage**(state, msg) {
state.message = msg
},
**incrementCounter**(state) {
state.counter ++;
}
}
变化的一个特点是它们不能直接调用。为了能够分发一个变化,我们应该调用一个名为commit的方法,其中包含相应变化的名称和参数:
store.commit('changeMessage', 'newMessage')
store.commit('incrementCounter')
提示
在 Vue 2.0 之前,分发变化的方法被称为“dispatch”。因此,您可以按照以下方式调用它:store.dispatch('changeMessage', 'newMessage')
您可以创建任意数量的变化。它们可以对相同状态项执行不同的操作。您甚至可以进一步声明变化名称为常量在一个单独的文件中。这样,您可以轻松地导入它们并使用它们,而不是字符串。因此,对于我们的例子,我们将在vuex目录内创建一个文件,并将其命名为mutation_types.js,并在那里导出所有的常量名称:
//mutation_types.js
export const INCREMENT_COUNTER = '**INCREMENT_COUNTER**'
export const CHANGE_MSG = '**CHANGE_MSG**'
然后,在我们的 store 中,我们将导入这些常量并重复使用它们:
//store.js
<...>
**import { CHANGE_MSG, INCREMENT_COUNTER } from './mutation_types'**
<...>
const mutations = {
**[CHANGE_MSG]**(state, msg) {
state.message = msg
},
**[INCREMENT_COUNTER]**(state) {
state.counter ++
}
}
在分发变化的组件内部,我们将导入相应的变化类型,并使用变量名进行分发:
this.$store.commit(**CHANGE_MSG**, ev.target.value)
这种结构在大型应用程序中非常有意义。同样,您可以根据它们为应用程序提供的功能对 mutation 类型进行分组,并仅在组件中导入那些特定组件所需的 mutations。这再次涉及最佳实践、模块化和可维护性。
动作
当我们分发一个 mutation 时,我们基本上执行了一个 action。说我们 commit 一个 CHANGE_MSG mutation 就等同于说我们 执行了一个 改变消息的 action。为了美观和完全抽取,就像我们将 store 状态的项抽取到 getters 和将 mutations 名称常量抽取到 mutation_types 一样,我们也可以将 mutations 抽取到 actions 中。
注意
因此,action 实际上只是一个分发 mutation 的函数!
function changeMessage(msg) { store.commit(CHANGE_MSG, msg) }
让我们为我们的改变消息示例创建一个简单的 actions 文件。但在此之前,让我们为 store 的初始状态创建一个额外的项 counter,并将其初始化为 "0" 值。因此,我们的 store 将如下所示:
**//store.js**
import Vue from 'vue'
import Vuex from 'vuex'
import { CHANGE_MSG, INCREMENT_COUNTER } from './mutation_types'
Vue.use(Vuex)
const state = {
message: 'Hello Vue!',
**counter: 0**
}
const mutations = {
CHANGE_MSG {
state.message = msg
},
**INCREMENT_COUNTER {
state.counter ++;
}**
}
export default new Vuex.Store({
state,
mutations
})
让我们还在 getters 文件中添加一个计数器 getter,这样我们的 getters.js 文件看起来像下面这样:
**//getters.js**
export default {
getMessage(state) {
return (state.message).toUpperCase()
},
**getCounter(state)**
**{**
**return (state.counter)
}**
}
最后,让我们在 ShowGreetingsComponent 中使用计数器的 getter 来显示消息 msg 被改变的次数:
<template>
<div>
<h1>{{ msg }}</h1>
**<div>the message was changed {{ counter }} times</div>**
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
computed: mapGetters({
msg: 'getMessage',
**counter: 'getCounter'**
})
}
</script>
现在让我们为计数器和改变消息的两个 mutations 创建 actions。在 vuex 文件夹中,创建一个 actions.js 文件并导出 actions 函数:
**//actions.js**
import { CHANGE_MSG, INCREMENT_COUNTER } from './mutation_types'
export const changeMessage = (store, msg) => {
store.commit(CHANGE_MSG, msg)
}
export const incrementCounter = (store) => {
store.commit(INCREMENT_COUNTER)
}
我们可以并且应该使用 ES2015 参数解构,使我们的代码更加优雅。让我们也在单个 export default 语句中导出所有的 actions:
**//actions.js**
import **{ CHANGE_MSG, INCREMENT_COUNTER }** from './mutation_types'
export default {
changeMessage (**{ commit }**, msg) {
**commit(CHANGE_MSG, msg)**
},
incrementCounter (**{ commit }**) {
**commit(INCREMENT_COUNTER)**
}
}
好的,现在我们有了漂亮而美丽的 actions。让我们在 ChangeGreetingsComponent 中使用它们!为了能够在组件中使用 actions,我们首先应该将它们导入到我们的 store 中,然后在新的 Vuex 对象中导出。然后可以在组件中使用 this.$store.dispatch 方法来分发 actions:
// ChangeGreetingsComponent.vue
<template>
<input :value="msg" @keyup="changeMsg">
</template>
<script>
import { mapGetters } from 'vuex'
export default {
computed: mapGetters({
msg: 'getMessage'
}),
methods: {
changeMsg(ev) {
**this.$store.dispatch('changeMessage', ev.target.value)**
}
}
}
</script>
那么实际上有什么区别呢?我们继续编写 this.$store 代码,唯一的区别是,我们不再调用 commit 方法,而是调用 dispatch。你还记得我们是如何发现 mapGetters 辅助函数的吗?是不是很好?实际上,Vue 也提供了一个 mapActions 辅助函数,它允许我们避免编写冗长的 this.$store.dispatch 方法。只需像导入 mapGetters 一样导入 mapActions,并在组件的 methods 属性中使用它:
//ChangeGreetingsComponent.vue
<template>
<input :value="msg" @keyup="**changeMessage**">
</template>
<script>
import { mapGetters } from 'vuex'
**import { mapActions } from 'vuex'**
export default {
computed: mapGetters({
msg: 'getMessage'
}),
methods: mapActions([**'changeMessage'**, **'incrementCounter'**])
}
</script>
请注意,我们已经改变了keyup事件的处理函数,所以我们不必将事件名称映射到相应的 actions。然而,就像mapGetters一样,我们也可以将自定义事件名称映射到相应的 actions 名称。
我们还应该改变changeMessage的调用,因为我们现在不在 actions 中提取任何事件的目标值;因此,我们应该在调用中进行提取:
//ChangeGreetingsComponent.vue
<template>
<input :value="msg" **@keyup="changeMessage($event.target.value)"**>
</template>
最后,让我们将incrementCounter action 绑定到用户的输入上。例如,让我们在输入模板中在keyup.enter事件上调用这个 action:
<template>
<input :value="msg" @keyup="changeMessage"
**@keyup.enter="incrementCounter"**>
</template>
如果你打开页面,尝试改变标题并按下Enter按钮,你会发现每次按下Enter时计数器都会增加:
使用 actions 来增加页面上的计数器。
所以,你看到了使用 actions 而不是直接访问 store 来模块化我们的应用是多么容易。你在 Vuex store 中导出 actions,在组件中导入mapActions,并在模板中的事件处理程序指令中调用它们。
你还记得我们的“人体”例子吗?在那个例子中,我们将人体的部分与组件进行比较,将人脑与应用状态的存储进行比较。想象一下你在跑步。这只是一个动作,但有多少变化被派发,有多少组件受到这些变化的影响?当你跑步时,你的心率增加,你出汗,你的手臂移动,你的脸上露出微笑,因为你意识到跑步是多么美好!当你吃东西时,你也会微笑,因为吃东西是美好的。当你看到小猫时,你也会微笑。因此,不同的 actions 可以派发多个变化,同一个变化也可以被多个 action 派发。
我们的 Vuex 存储和它的 mutations 和 actions 也是一样的。在同一个 action 中,可以派发多个 mutation。例如,我们可以在同一个 action 中派发改变消息和增加计数器的 mutation。让我们在action.js文件中创建这个 action。让我们称之为handleMessageInputChanges,并让它接收一个参数:event。它将使用event.target.value派发CHANGE_MSG mutation,并且如果event.keyCode是enter,它将派发INCREMENT_COUNTER mutation。
//actions.js
handleMessageInputChanges ({ commit }, event) {
**commit(CHANGE_MSG, event.target.value)**
if (event.keyCode === 13) {
**commit(INCREMENT_COUNTER)**
}
}
现在让我们在ChangeGreetingsComponent组件的mapActions对象中导入这个 action,并使用它调用带有$event参数的 action:
//ChangeGreetingsComponent.vue
<template>
<input :value="msg" **@keyup="handleMessageInputChanges($event)"** />
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
export default {
computed: mapGetters({
msg: 'getMessage'
}),
**methods: mapActions(['handleMessageInputChanges'])**
}
</script>
打开页面,尝试更改问候消息并通过点击Enter按钮增加计数器。它有效!
简单存储示例的最终代码可以在chapter5/simple-store3文件夹中找到。
在我们的应用程序中安装和使用 Vuex store
现在我们知道了 Vuex 是什么,如何创建 store,分发 mutations,以及如何使用 getter 和 action,我们可以在我们的应用程序中安装 store,并用它来完成它们的数据流和通信链。
您可以在以下文件夹中找到要处理的应用程序:
不要忘记在两个应用程序上运行npm install。
首先安装vuex,并在两个应用程序中定义必要的目录和文件结构。
要安装vuex,只需运行以下命令:
**npm install vuex --save**
安装vuex后,在每个应用程序的src文件夹中创建一个名为vuex的子文件夹。在此文件夹中,创建四个文件:store.js、mutation_types.js、actions.js和getters.js。
准备store.js结构:
//store.js
import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
import actions from './actions'
import mutations from './mutations'
Vue.use(Vuex)
const state = {
}
export default new Vuex.Store({
state,
mutations,
getters,
actions
})
在主App.vue中导入并使用 store:
//App.vue
<script>
<...>
import store from './vuex/store'
export default {
store,
<...>
}
</script>
我们现在将定义每个应用程序中的全局状态和局部状态,定义缺少的数据和绑定,划分数据,并使用我们刚学到的内容添加所有缺失的内容。
在购物清单应用程序中使用 Vuex store
我希望您还记得我们在本章开头面临的挑战。我们希望在组件之间建立通信,以便可以轻松地从ChangeTitleComponent更改购物清单的标题,并将其传播到ShoppingListTitle和ShoppingListComponent。让我们从App.vue中删除硬编码的购物清单数组,并将其复制到 store 的状态中:
//store.js
<...>
const state = {
**shoppinglists**: [
{
id: 'groceries',
title: 'Groceries',
items: [{ text: 'Bananas', checked: true },
{ text: 'Apples', checked: false }]
},
{
id: 'clothes',
title: 'Clothes',
items: [{ text: 'black dress', checked: false },
{ text: 'all-stars', checked: false }]
}
]
}
<...>
让我们为购物清单定义 getter:
//getters.js
export default {
getLists: state => state.shoppinglists
}
现在,在App.vue中导入mapGetters,并将shoppinglists值映射到getLists方法,以便App.vue组件内的<script>标签看起来像下面这样:
//App.vue
<script>
import ShoppingListComponent from './components/ShoppingListComponent'
import ShoppingListTitleComponent from
'./components/ShoppingListTitleComponent'
import _ from 'underscore'
**import store from './vuex/store'
import { mapGetters } from 'vuex'**
export default {
components: {
ShoppingListComponent,
ShoppingListTitleComponent
},
**computed: mapGetters({
shoppinglists: 'getLists'
}),**
methods: {
onChangeTitle (id, text) {
_.findWhere(this.shoppinglists, { id: id }).title = text
}
},
store
}
</script>
其余部分保持不变!
现在让我们在存储中定义一个负责更改标题的 mutation。很明显,它应该是一个接收新标题字符串作为参数的函数。但是,有一些困难。我们不知道应该更改哪个购物清单的标题。如果我们可以从组件将列表的 ID 传递给此函数,实际上我们可以编写一段代码来通过其 ID 找到正确的列表。我刚刚说如果我们可以?当然可以!实际上,我们的ShoppingListComponent已经从其父级App.vue接收了 ID。让我们只是将这个 ID 从ShoppingListComponent传递给ChangeTitleComponent。这样,我们将能够从实际更改标题的组件中调用必要的操作,而无需通过父级链传播事件。
因此,只需将 ID 绑定到ShoppingListComponent组件模板中的change-title-component,如下所示:
//ShoppingListComponent.vue
<template>
<...>
<change-title-component : **:id="id"** v-
on:changeTitle="onChangeTitle"></change-title-component>
<...>
</template>
不要忘记向ChangeTitleComponent组件的props属性添加id属性:
//ChangeTitleComponent.vue
<script>
export default {
props: ['title', **'id'**],
<...>
}
</script>
现在,我们的ChangeTitleComponent可以访问购物清单的title和id。让我们向存储中添加相应的 mutation。
我们可以先编写一个通过其 ID 查找购物清单的函数。为此,我将使用underscore类的_.findWhere方法,就像我们在App.vue组件的changeTitle方法中所做的那样。
在mutations.js中导入underscore并添加findById函数如下:
//mutations.js
<...>
function findById (state, id) {
return **_.findWhere(state.shoppinglists, { id: id })**
}
<...>
现在让我们添加 mutation,并将其命名为CHANGE_TITLE。此 mutation 将接收data对象作为参数,其中包含title和id,并将接收到的标题值分配给找到的购物清单项的标题。首先,在mutation_types.js中声明一个常量CHANGE_TITLE,并重用它而不是将 mutation 的名称写为字符串:
//mutation_types.js
export const **CHANGE_TITLE** = 'CHANGE_TITLE'
//mutations.js
import _ from 'underscore'
**import * as types from './mutation_types'**
function findById (state, id) {
return _.findWhere(state.shoppinglists, { id: id })
}
export default {
**[types.CHANGE_TITLE] (state, data) {
findById(state, data.id).title = data.title
}**
}
我们快要完成了。现在让我们在actions.js文件中定义一个changeTitle操作,并在我们的ChangeTitleComponent中重用它。打开actions.js文件并添加以下代码:
//actions.js
import { CHANGE_TITLE } from './mutation_types'
export default {
changeTitle: ({ commit }, data) => {
**commit(CHANGE_TITLE, data)**
}
}
最后一步。打开ChangeTitleComponent.vue,导入mapActions辅助程序,将onInput方法映射到changeTitle操作,并在template中调用它,对象映射标题为event.target.value和 ID 为id参数。因此,ChangeTitleComponent的代码将如下所示:
//ChangeTitleComponent.vue
<template>
<div>
<em>Change the title of your shopping list here</em>
<input :value="title" **@input="onInput({ title: $event.target.value,**
**id: id })"**/>
</div>
</template>
<script>
**import { mapActions } from 'vuex'**
export default {
props: ['title', 'id'],
**methods: mapActions({
onInput: 'changeTitle'
})**
}
</script>
现在,您可以从ShoppingListComponent和主App组件中删除所有事件处理代码。
打开页面并尝试在输入框中输入!标题将在所有位置更改:
使用存储、突变和操作——所有组件都可以更新其状态,而无需事件处理机制
应用存储功能后购物清单应用程序的最终代码可以在chapter5/shopping-list3文件夹中找到。
在 Pomodoro 应用程序中使用 Vuex 存储
最后,我们回到了我们的 Pomodoro!你上次休息了多久?让我们使用 Vuex 架构构建我们的 Pomodoro 应用程序,然后休息一下,看看小猫。让我们从chapter5/pomodoro文件夹中的基础开始,您已经包含了 Vuex 存储的基本结构(如果没有,请转到在我们的应用程序中安装和使用 Vuex 存储部分的开头)。
为启动、暂停和停止按钮注入生命
让我们首先分析我们的番茄钟定时器实际上可以做什么。看看页面。我们只有三个按钮:启动、暂停和停止。这意味着我们的应用程序可以处于这三种状态之一。让我们在store.js文件中定义并导出它们:
//store.js
<...>
const state = {
**started**: false,
**paused**: false,
**stopped**: false
}
<...>
最初,所有这些状态都设置为false,这是有道理的,因为应用程序尚未启动,尚未暂停,当然也没有停止!
现在让我们为这些状态定义 getter。打开getters.js文件,并为所有三种状态添加 getter 函数:
//getters.js
export default {
**isStarted**: state => state.started,
**isPaused**: state => state.paused,
**isStopped**: state => state.stopped
}
对于每个定义的状态,我们的控制按钮应该发生什么变化:
-
当应用程序启动时,启动按钮应该变为禁用。然而,当应用程序暂停时,它应该再次启用,以便我们可以使用此按钮恢复应用程序。
-
暂停按钮只能在应用程序启动时启用(因为我们不能暂停尚未启动的东西)。但是,如果应用程序已暂停,它应该被禁用(因为我们不能暂停已经暂停的东西)。
-
停止按钮只能在应用程序启动时启用。
让我们通过根据应用程序状态有条件地向我们的控制按钮添加disabled类来将其翻译成代码。
提示
一旦我们应用了disabled类,Bootstrap 将通过不仅应用特殊样式而且禁用交互元素来为我们处理按钮的行为。
为了能够使用已定义的 getter,我们必须在组件的<script>标签中导入mapGetters。之后,我们必须通过在computed属性对象中导出它们来告诉组件我们想要使用它们:
//ControlsComponent.vue
<script>
**import { mapGetters } from 'vuex'**
export default {
**computed: mapGetters(['isStarted', 'isPaused', 'isStopped'])**
}
</script>
现在这些 getter 可以在模板中使用。因此,我们将把disabled类应用于以下内容:
-
当应用程序启动且未暂停时启动按钮(
isStarted && !isPaused) -
当应用程序未启动或已暂停时暂停按钮(
!isStarted || isPaused) -
当应用程序未启动时停止按钮(
!isStarted)
我们的模板现在看起来像这样:
//ControlsComponent.vue
<template>
<span>
<button **:disabled='isStarted && !isPaused'**>
<i class="glyphicon glyphicon-play"></i>
</button>
<button **:disabled='!isStarted || isPaused'**>
<i class="glyphicon glyphicon-pause"></i>
</button>
<button **:disabled='!isStarted'**>
<i class="glyphicon glyphicon-stop"></i>
</button>
</span>
</template>
现在你看到暂停和停止按钮看起来不同了!如果你将鼠标悬停在它们上面,光标不会改变,这意味着它们真的被禁用了!让我们为禁用按钮内部的图标创建一个样式,以更加突出禁用状态:
//ControlsComponent.vue
<style scoped>
**button:disabled i {
color: gray;
}**
</style>
好了,现在我们有了漂亮的禁用按钮,让我们为它们注入一些生命吧!
让我们考虑一下当我们启动、暂停或停止应用程序时实际上应该发生什么:
-
当我们启动应用程序时,状态
started应该变为true,而paused和stopped状态肯定会变为false。 -
当我们暂停应用程序时,状态
paused为true,状态stopped为false,而状态started为true,因为暂停的应用程序仍然是启动的。 -
当我们停止应用程序时,状态
stopped变为true,而paused和started状态变为false。让我们将所有这些行为转化为 mutation_types、mutations 和 actions!
打开mutation_types.js并添加三种 mutation 类型如下:
//mutation_types.js
export const START = 'START'
export const PAUSE = 'PAUSE'
export const STOP = 'STOP'
现在让我们定义 mutations!打开mutations.js文件并为每种 mutation 类型添加三种 mutations。因此,我们决定当我们:
-
启动应用程序:状态
started为true,而状态paused和stopped为false。 -
暂停应用程序:状态
started为true,状态paused为true,而stopped为false。 -
停止应用程序:状态
stopped为true,而状态started和paused为false。
现在让我们把它放到代码中。将mutation_types导入到mutations.js中,并编写所有三个必要的 mutations 如下:
//mutations.js
import * as types from './mutation_types'
export default {
[types.START] (state) {
state.started = true
state.paused = false
state.stopped = false
},
[types.PAUSE] (state) {
state.paused = true
state.started = true
state.stopped = false
},
[types.STOP] (state) {
state.stopped = true
state.paused = false
state.started = false
}
}
现在让我们定义我们的 actions!转到actions.js文件,导入 mutation 类型,并导出三个函数:
//actions.js
import * as types from './mutation_types'
export default {
start: ({ commit }) => {
**commit(types.START)**
},
pause: ({ commit }) => {
**commit(types.PAUSE)**
},
stop: ({ commit }) => {
**commit(types.STOP)**
}
}
为了使我们的按钮生效,最后一步是将这些 actions 导入到ControlsComponent中,并在每个按钮的click事件上调用它们。让我们来做吧。你还记得如何在 HTML 元素上应用事件时调用 action 吗?如果我们谈论的是click事件,就是下面这样的:
@click='someAction'
因此,在我们的ControlsComponent.vue中,我们导入mapActions对象,将其映射到组件的methods属性,并将其应用于相应按钮的点击事件。就是这样!ControlsComponent的<script>标签看起来像下面这样:
//ControlsComponent.vue
<script>
**import { mapGetters, mapActions } from 'vuex'**
export default {
computed: mapGetters(['isStarted', 'isPaused', 'isStopped']),
**methods: mapActions(['start', 'stop', 'pause'])**
}
</script>
现在在模板中的事件处理程序指令内调用这些函数,使得ControlsComponent的<template>标签看起来像下面这样:
//ControlsComponent.vue
<template>
<span>
<button :disabled='isStarted && !isPaused'
**@click="start"**>
<i class="glyphicon glyphicon-play"></i>
</button>
<button :disabled='!isStarted || isPaused'
**@click="pause"**>
<i class="glyphicon glyphicon-pause"></i>
</button>
<button :disabled='!isStarted' **@click="stop"**>
<i class="glyphicon glyphicon-stop"></i>
</button>
</span>
</template>
尝试点击按钮。它们确实做到了我们需要它们做的事情。干得漂亮!在chapter5/pomodoro2文件夹中查看。然而,我们还没有完成。我们仍然需要将我们的番茄钟定时器变成一个真正的定时器,而不仅仅是一些允许您点击按钮并观察它们从禁用状态到启用状态的页面。
绑定番茄钟的分钟和秒
在上一节中,我们能够定义番茄钟应用的三种不同状态:开始,暂停和停止。然而,让我们不要忘记番茄钟应用应该用于什么。它必须倒计时一定的工作时间,然后切换到休息倒计时器,然后再回到工作,依此类推。
这让我们意识到,还有一个非常重要的番茄钟应用状态:在工作和休息时间段之间切换的二进制状态。这个状态不能由按钮切换;它应该以某种方式由我们应用的内部逻辑来管理。
让我们首先定义两个状态属性:一个用于随着时间减少的计数器,另一个用于区分工作状态和非工作状态。假设当我们开始番茄钟时,我们开始工作,所以工作状态应该设置为 true,倒计时计数器应该设置为我们定义的工作番茄钟时间。为了模块化和可维护性,让我们在外部文件中定义工作和休息的时间。比如,我们称之为config.js。在项目的根目录下创建config.js文件,并添加以下内容:
**//config.js**
export const WORKING_TIME = **20 * 60**
export const RESTING_TIME = **5 * 60**
通过这些初始化,我的意思是我们的番茄钟应该倒计时20分钟的工作番茄钟间隔和5分钟的休息时间。当然,你可以自由定义最适合你的值。现在让我们在我们的存储中导出config.js,并重用WORKING_TIME值来初始化我们的计数器。让我们还创建一个在工作/休息之间切换的属性,并将其命名为isWorking。让我们将其初始化为true。
所以,我们的新状态将如下所示:
//store.js
<...>
import { WORKING_TIME } from '../config'
const state = {
started: false,
paused: false,
stopped: false,
**isWorking: true,
counter: WORKING_TIME**
}
所以,我们有了这两个新的属性。在开始创建方法、操作、突变和其他减少计数器和切换isWorking属性的事情之前,让我们考虑依赖这些属性的可视元素。
我们没有那么多元素,所以很容易定义。
-
isWorking状态影响标题:当是工作时间时,我们应该显示**工作!,当是休息时间时,我们应该显示休息!**。 -
isWorking状态也影响着小猫组件的可见性:只有当isWorking为false时才应该显示。 -
counter属性影响minute和second:每次它减少时,second的值也应该减少,每减少 60 次,minute的值也应该减少。
让我们为isWorking状态和minute和second定义获取函数。在定义这些获取函数之后,我们可以在我们的组件中重用它们,而不是使用硬编码的值。让我们首先定义一个用于isWorking属性的获取器:
//getters.js
export default {
isStarted: state => state.started,
isPaused: state => state.paused,
isStopped: state => state.stopped,
**isWorking: state => state.isWorking**
}
让我们在使用在App.vue组件中定义的硬编码isworking的组件中重用此 getter。 打开App.vue,删除对isworking硬编码变量的所有引用,导入mapGetters对象,并将isworking属性映射到computed属性中的isWorking方法,如下所示:
//App.vue
<script>
<...>
**import { mapGetters } from 'vuex'**
export default {
<...>
**computed: mapGetters({
isworking: 'isWorking'
}),**
store
}
</script>
在StateTitleComponent中重复相同的步骤。 导入mapGetters并用映射的computed属性替换props:
//StateTitleComponent.vue
<script>
**import { mapGetters } from 'vuex'**
export default {
data () {
return {
workingtitle: 'Work!',
restingtitle: 'Rest!'
}
},
**computed: mapGetters({
isworking: 'isWorking'
})**
}
</script>
这两个组件中的其余部分保持不变! 在模板内,使用isworking属性。 此属性仍然存在; 它只是从响应式的 Vuex 存储中导入,而不是从硬编码数据中导入!
现在我们必须为分钟和秒定义 getter。 这部分比较棘手,因为在这些 getter 中,我们必须对计数器状态的属性应用一些计算。 这一点一点也不难。 我们的计数器表示总秒数。 这意味着我们可以通过将计数器除以 60 并四舍五入到最低整数(Math.floor)来轻松提取分钟。 秒数可以通过取除以 60 的余数来提取。 因此,我们可以以以下方式编写我们的分钟和秒的 getter:
//getters.js
export default {
<...>
**getMinutes**: state => **Math.floor(state.counter / 60)**,
**getSeconds**: state => **state.counter % 60**
}
就是这样! 现在让我们在CountdownComponent中重用这些 getter。 导入mapGetters并将其相应的方法映射到computed属性中的min和sec属性。 不要忘记删除硬编码的数据。 因此,我们的CountdownComponent.vue的script标签如下所示:
//CountdownComponent.vue
<script>
**import { mapGetters } from 'vuex'**
export default {
**computed: mapGetters({
min: 'getMinutes',
sec: 'getSeconds'
})**
}
</script>
其余部分完全不变! 模板引用了min和sec属性,它们仍然存在。 到目前为止的代码可以在chapter5/pomodoro3文件夹中找到。 看看页面; 现在显示的分钟和秒数对应于我们在配置文件中定义的工作时间! 如果您更改它,它也会随之更改:
更改工作时间的配置将立即影响番茄钟应用程序视图
创建番茄钟定时器
好的,现在一切准备就绪,可以开始倒计时我们的工作时间,这样我们最终可以休息一下! 让我们定义两个辅助函数,togglePomodoro和tick。
第一个将只是切换isWorking属性。它还将重新定义状态的计数器。当状态为isWorking时,计数器应该对应工作时间,当状态不工作时,计数器应该对应休息时间。
tick函数将只是减少计数器并检查是否已达到“0”值,在这种情况下,将切换 Pomodoro 状态。其余的已经被照顾好了。因此,togglePomodoro`函数将如下所示:
//mutations.js
function togglePomodoro (state, toggle) {
if (_.isBoolean(toggle) === false) {
toggle = **!state.isWorking**
}
**state.isWorking = toggle
state.counter = state.isWorking ? WORKING_TIME : RESTING_TIME** }
啊,不要忘记从我们的配置中导入WORKING_TIME和RESTING_TIME!还有,不要忘记导入underscore,因为我们在_.isBoolean检查中使用它:
//mutations.js
import _ from 'underscore'
import { WORKING_TIME, RESTING_TIME } from './config'
然后,tick函数将只是减少计数器并检查是否已达到“0”值:
//mutations.js
function tick (state) {
if (state.counter === 0) {
togglePomodoro(state)
}
state.counter--
}
好的!还不够。我们需要设置一个间隔,每秒调用一次tick函数。它应该在哪里设置?嗯,很明显,当我们开始 Pomodoro 时,应该这样做在START变异中!
但是,如果我们在START变异中设置了间隔,并且它每秒调用一次tick函数,那么在点击暂停或停止按钮时,它将如何停止或暂停?这就是为什么存在setInterval和clearInterval JavaScript 函数,这也是为什么我们有一个存储可以保存interval值的初始状态的地方!让我们首先在存储状态中将interval定义为null:
//store.js
const state = {
<...>
interval: null
}
现在,在我们的START变异中,让我们添加以下代码来初始化间隔:
//mutations.js
export default {
[types.START] (state) {
state.started = true
state.paused = false
state.stopped = false
**state.interval = setInterval(() => tick(state), 1000)**
},
<...>
}
我们刚刚设置了一个间隔,每秒调用一次tick函数。反过来,tick函数将减少计数器。依赖于计数器值的值——分钟和秒——将改变,并且会将这些更改反应地传播到视图中。
如果你现在点击开始按钮,你将启动倒计时!耶!几乎完成了。我们只需要在pause和stop变异方法上添加clearInterval函数。除此之外,在stop方法上,让我们调用togglePomodoro函数,并传入true,这将重置 Pomodoro 计时器到工作状态:
//mutations.js
export default {
[types.START] (state) {
state.started = true
state.paused = false
state.stopped = false
**state.interval = setInterval(() => tick(state), 1000)**
},
[types.PAUSE] (state) {
state.paused = true
state.started = true
state.stopped = false
**clearInterval(state.interval)**
},
[types.STOP] (state) {
state.stopped = true
state.paused = false
state.started = false
**togglePomodoro(state, true)**
}
}
更改小猫咪
我希望你工作了很多,你的休息时间终于到了!如果没有,或者如果你等不及了,只需在config.js文件中将WORKING_TIME的值更改为相当小的值,然后等待。我认为我终于应该休息一下了,所以我已经盯着这张漂亮的图片看了几分钟了:
我盯着这张图片,猫也盯着我。
你不想有时候显示的图片改变吗?当然想!为了实现这一点,我们只需向图像源附加一些内容,以便随着时间的推移而改变,并向我们提供一个非缓存的图像。
提示
提供非缓存内容的最佳实践之一是将时间戳附加到请求的 URL 中。
例如,我们可以在存储中有另一个属性,比如timestamp,它将随着每次计数器减少而更新,并且它的值将被附加到图像源 URL。让我们做吧!让我们首先在我们存储的状态中定义一个timestamp属性,如下所示:
//store.js
const state = {
<...>
**timestamp: 0**
}
告诉tick函数在每次滴答时更新这个值:
//mutations.js
function tick(state) {
<...>
**state.timestamp = new Date().getTime()**
}
在getters.js中为这个值创建 getter,并在KittensComponent中使用它,通过在computed属性中访问this.$store.getters.getTimestamp方法:
//getters.js
export default {
<...>
**getTimestamp: state => state.timestamp**
}
//KittensComponent.vue
<script>
export default {
computed: {
catimgsrc () {
return 'http://thecatapi.com/api/images/get?size=med**&ts='**
**+ this.$store.getters.getTimestamp**
}
}
}
</script>
现在速度有点太快了,对吧?让我们定义一个时间来展示每只小猫。这一点都不难。例如,如果我们决定每只小猫展示 3 秒钟,在tick函数内改变时间戳状态之前,我们只需要检查计数器的值是否可以被 3 整除。让我们也把展示小猫的秒数变成可配置的。在config.js中添加以下内容:
//config.js
export const WORKING_TIME = 0.1 * 60
export const RESTING_TIME = 5 * 60
**export const KITTEN_TIME = 5** //each kitten is visible for 5 seconds
现在将其导入到mutations.js文件中,并在tick函数中使用它来检查是否是改变时间戳值的时候:
//mutations.js
import { WORKING_TIME, RESTING_TIME, **KITTEN_TIME** } from './config'
<...>
function tick(state) {
<...>
**if (state.counter % KITTEN_TIME === 0) {
state.timestamp = new Date().getTime()
}**
}
我们完成了!您可以在chapter5/pomodoro4文件夹中检查本节的最终代码。是的,我将工作时间设置为 6 秒,这样您就可以休息一下,并欣赏一些来自thecatapi.com的非常可爱的小猫。
因此,在阅读本章摘要并开始下一章之前,休息一下!就像这个美妙的物种一样:
美好的事物需要休息。像它一样。休息一下。
总结
在本章中,您看到了如何使用事件处理和触发机制来将组件的数据更改传播到它们的父级。
最重要的是,您利用了 Vuex 架构的力量,能够在组件之间建立数据流。您看到了如何创建存储库以及其主要部分,即 mutations 和 states。您学会了如何构建使用存储库的应用程序,使其变得模块化和可维护。您还学会了如何创建存储库的 getters 以及如何定义分派存储库状态变化的 actions。我们将所有学到的机制应用到我们的应用程序中,并看到了数据流的实际操作。
到目前为止,我们能够在 Vue 应用程序中使用任何数据交换机制,从简单的组件内部的本地数据绑定开始,逐渐扩展到全局状态管理。到目前为止,我们已经掌握了在 Vue 应用程序中操作数据的所有基础知识。我们快要完成了!
在下一章中,我们将深入探讨 Vue 应用程序的插件系统。您将学习如何使用现有的插件并创建自己的插件,以丰富您的应用程序的自定义行为。