原文:Pro Vue.js 2
协议:CC BY-NC-SA 4.0
十八、松散耦合的组件
随着 Vue.js 应用的增长,在父组件和子组件之间传递数据和事件的需求变得更加难以安排,尤其是当应用不同部分的组件需要通信时。结果可能是组件自身的功能被为它们的后代传递属性和为它们的前身传递事件的需求所超越。在这一章中,我描述了另一种方法,称为依赖注入,它允许组件在不紧密耦合的情况下进行通信。我将向您展示依赖注入的不同使用方式,并演示它如何释放应用的结构。我还将向您展示如何使用事件总线,它将依赖注入与以编程方式注册自定义事件的能力相结合,允许组件将自定义事件发送给任何感兴趣的接收者,而不仅仅是其父对象。表 18-1 将依赖注入和事件总线放在上下文中。
表 18-1
将依赖注入和事件总线放在上下文中
|
问题
|
回答
|
| — | — |
| 它们是什么? | 依赖注入允许任何组件向其任何后代提供服务,其中服务可以是值、对象或函数。事件总线建立在依赖注入特性的基础上,为发送和接收定制事件提供了一种通用机制。 |
| 它们为什么有用? | 这些功能允许应用的结构变得更加复杂,而不会陷入管理父子关系功能的困境,这些功能仅用于将数据传递给遥远的后代和祖先。 |
| 它们是如何使用的? | 组件使用 provide 属性定义服务,并使用 inject 属性声明对服务的依赖。事件总线是分发 Vue 对象的服务,该对象用于使用
e
m
i
t
和
emit 和
emit和on 方法发送和接收事件。 |
| 有什么陷阱或限制吗? | 必须注意确保在整个应用中使用一致的服务和事件名称。如果不小心,不同的组件最终会重用一个对应用的另一部分已经有意义的名称。 |
| 有其他选择吗? | 简单的应用不需要本章描述的特性,可以依赖于道具和标准的自定义事件。复杂的应用可以从共享应用状态中受益,这是一种补充方法,在第二十章中有描述。 |
表 18-2 总结了本章内容。
表 18-2
章节总结
|
问题
|
解决办法
|
列表
|
| — | — | — |
| 定义可由组件后代使用的功能 | 使用依赖注入特性来定义服务 | 10–12 |
| 提供响应变化的功能 | 通过定义数据属性并将其用作数据值的来源来创建反应式服务 | 13–14 |
| 定义不提供服务时将使用的功能 | 使用具有注入属性的对象指定回退 | 15–16 |
| 在父子关系之外分发自定义事件 | 使用事件总线 | Seventeen |
| 使用事件总线发送事件 | 调用事件总线
e
m
i
t
方法
∣
E
i
g
h
t
e
e
n
∣
∣
从事件总线接收事件
∣
调用事件总线
emit 方法 | Eighteen | | 从事件总线接收事件 | 调用事件总线
emit方法∣Eighteen∣∣从事件总线接收事件∣调用事件总线on 方法 | 19, 20 |
| 仅将事件分发到应用的一部分 | 创建本地事件总线 | 21, 22 |
为本章做准备
对于本章中的例子,在一个方便的位置运行清单 18-1 中所示的命令来创建一个新的 Vue.js 项目。
小费
你可以从 https://github.com/Apress/pro-vue-js-2
下载本章以及本书其他章节的示例项目。
vue create productapp --default
Listing 18-1Creating a New Project
- 1
- 2
- 3
- 4
这个命令创建了一个名为 productapp 的项目。一旦设置过程完成,将清单 18-2 中所示的语句添加到package.json
文件的 linter 部分,以禁用在使用 JavaScript 控制台时以及在定义了变量但未使用时发出警告的规则。本章中的许多例子我都依赖于控制台,并定义了占位符变量,这些变量只在引入后面的特性时使用。
...
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"rules": {
"no-console": "off",
"no-unused-vars": "off"
},
"parserOptions": {
"parser": "babel-eslint"
}
},
...
Listing 18-2Disabling a Linter Rule in the package.json File in the lifecycles Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
接下来,运行productapp
文件夹中清单 18-3 所示的命令,将引导 CSS 包添加到项目中。
npm install bootstrap@4.0.0
Listing 18-3Adding the Bootstrap CSS Package
- 1
- 2
- 3
- 4
将清单 18-4 中所示的语句添加到src
文件夹中的main.js
文件中,将引导 CSS 文件合并到应用中。
import Vue from 'vue'
import App from './App.vue'
import "../node_modules/bootstrap/dist/css/bootstrap.min.css";
Vue.config.productionTip = false
new Vue({
render: h => h(App)
}).$mount('#app')
Listing 18-4Incorporating the Bootstrap Package in the main.js File in the src Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
运行productapp
文件夹中清单 18-5 所示的命令,启动开发工具。
npm run serve
Listing 18-5Starting the Development Tools
- 1
- 2
- 3
- 4
将执行初始绑定过程,之后您将看到一条消息,告诉您项目已成功编译,HTTP 服务器正在侦听端口 8080 上的请求。打开一个新的浏览器窗口,导航到http://localhost:8080
查看项目的占位符内容,如图 18-1 所示。
图 18-1
运行示例应用
创建产品展示组件
示例应用的核心将是一个组件,它显示一个包含产品对象详细信息的表,并带有用于编辑或删除对象的按钮。我在src/components
文件夹中添加了一个名为ProductDisplay.vue
的文件,内容如清单 18-6 所示。
<template>
<div>
<table class="table table-sm table-striped table-bordered">
<tr>
<th>ID</th><th>Name</th><th>Price</th><th></th>
</tr>
<tbody>
<tr v-for="p in products" v-bind:key="p.id">
<td>{{ p.id }}</td>
<td>{{ p.name }}</td>
<td>{{ p.price | currency }}</td>
<td>
<button class="btn btn-sm btn-primary"
v-on:click="editProduct(p)">
Edit
</button>
</td>
</tr>
</tbody>
</table>
<div class="text-center">
<button class="btn btn-primary" v-on:click="createNew">
Create New
</button>
</div>
</div>
</template>
<script>
export default {
data: function () {
return {
products: [
{ id: 1, name: "Kayak", price: 275 },
{ id: 2, name: "Lifejacket", price: 48.95 },
{ id: 3, name: "Soccer Ball", price: 19.50 },
{ id: 4, name: "Corner Flags", price: 39.95 },
{ id: 5, name: "Stadium", price: 79500 }]
}
},
filters: {
currency(value) {
return `$${value.toFixed(2)}`;
}
},
methods: {
createNew() {
},
editProduct(product) {
}
}
}
</script>
Listing 18-6The Contents of the ProductDisplay.vue File in the src/components Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
该组件定义了一个名为products
的data
属性,该属性被分配了一个对象数组,这些对象是使用v-for
指令在模板中枚举的。每个products
对象在一个表中生成一行,该表包含用于id
、name
和price
值的列,以及一个用于编辑对象的button
元素。在表格下面还有另一个按钮,用户将单击它来创建一个新产品,并且已经将v-on
指令应用到了两个button
元素,以便在单击按钮时调用createNew
和editProduct
方法。这些方法目前都是空的。
创建产品编辑器组件
我需要一个编辑器,允许用户编辑现有的对象和创建新的。我首先将一个名为EditorField.vue
的文件添加到src/components
文件夹中,并添加清单 18-7 中所示的内容。
<template>
<div class="form-group">
<label>{{label}}</label>
<input v-model.number="value" class="form-control" />
</div>
</template>
<script>
export default {
props: ["label"],
data: function () {
return {
value: ""
}
}
}
</script>
Listing 18-7The Contents of the EditorField.vue File in the src/components Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
该组件显示一个label
元素和一个input
元素。为了将编辑器组合成一个更大的功能单元,我在src/components
文件夹中添加了一个名为ProductEditor.vue
的文件,并添加了清单 18-8 中所示的内容。
<template>
<div>
<editor-field label="ID" />
<editor-field label="Name" />
<editor-field label="Price" />
<div class="text-center">
<button class="btn btn-primary" v-on:click="save">
{{ editing ? "Save" : "Create" }}
</button>
<button class="btn btn-secondary" v-on:click="cancel">Cancel</button>
</div>
</div>
</template>
<script>
import EditorField from "./EditorField";
export default {
data: function () {
return {
editing: false,
product: {
id: 0,
name: "",
price: 0
}
}
},
components: { EditorField },
methods: {
startEdit(product) {
this.editing = true;
this.product = {
id: product.id,
name: product.name,
price: product.price
}
},
startCreate() {
this.editing = false;
this.product = {
id: 0,
name: "",
price: 0
};
},
save() {
// TODO - process edited or created product
console.log(`Edit Complete: ${JSON.stringify(this.product)}`);
this.startCreate();
},
cancel() {
this.product = {};
this.editing = false;
}
}
}
</script>
Listing 18-8The Contents of the ProductEditor.vue File in the src/components Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
编辑器提供了可用于编辑或创建对象的编辑器组件集合。有用于id
、name
和price
值的字段,以及使用v-on
指令完成或取消操作的button
元素。
显示子组件
为了完成本章的准备工作,我编辑了根组件以显示在前面章节中创建的组件,如清单 18-9 所示。
<template>
<div class="container-fluid">
<div class="row">
<div class="col-8 m-3">
<product-display></product-display>
</div>
<div class="col m-3">
<product-editor></product-editor>
</div>
</div>
</div>
</template>
<script>
import ProductDisplay from "./components/ProductDisplay";
import ProductEditor from "./components/ProductEditor";
export default {
name: 'App',
components: { ProductDisplay, ProductEditor }
}
</script>
Listing 18-9Displaying Components in the App.vue File in the src Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
该组件并排显示其子组件,如图 18-2 所示。单击按钮元素没有实际效果,因为组件还没有连接在一起工作。
图 18-2
向示例应用添加功能和组件
理解依赖注入
依赖注入允许组件定义一个服务,它可以是任何值、函数或对象,并使它对它的任何后代可用。通过依赖注入提供的服务不仅限于孩子,并且避免了通过一连串的道具将数据分发到需要它的应用部分的需要。
定义服务
在清单 18-10 中,我向根组件添加了一个服务,它提供了应该用于背景和文本的引导 CSS 类的细节。该服务将对应用中的所有组件可用,因为它们都是根组件的后代。
<template>
<div class="container-fluid">
<div class="row">
<div class="col-8 m-3">
<product-display></product-display>
</div>
<div class="col m-3">
<product-editor></product-editor>
</div>
</div>
</div>
</template>
<script>
import ProductDisplay from "./components/ProductDisplay";
import ProductEditor from "./components/ProductEditor";
export default {
name: 'App',
components: { ProductDisplay, ProductEditor },
provide: function() {
return {
colors: {
bg: "bg-secondary",
text: "text-white"
}
}
}
}
</script>
Listing 18-10Defining a Service in the App.vue File in the src Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
provide
属性遵循与data
属性相同的模式,并返回一个函数,该函数产生一个对象,该对象的属性是后代组件可用的服务的名称。在这个例子中,我定义了一个名为colors
的服务,它的bg
和text
属性提供了与引导 CSS 样式相关的类名。
通过依赖注入消费服务
当一个组件想要使用其前身提供的服务时,它使用 inject 属性,如清单 18-11 所示,其中我已经配置了EditorField
组件,因此它使用清单 18-10 中定义的colors
服务。
<template>
<div class="form-group">
<label>{{label}}</label>
<input v-model.number="value" class="form-control"
v-bind:class="[colors.bg, colors.text]" />
</div>
</template>
<script>
export default {
props: ["label"],
data: function () {
return {
value: ""
}
},
inject: ["colors"]
}
</script>
Listing 18-11Consuming a Service in the EditorField.vue File in the src/components Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
属性被赋予一个数组,该数组包含组件所需的每个服务名称的字符串值。当组件被初始化时,Vue.js 通过组件的前件向上工作,直到找到具有指定名称的服务,获取服务值,并将其分配给组件对象上具有服务名称的属性。在这个例子中,Vue.js 沿着组件向上寻找名为colors
的服务,它在根组件上找到了这个服务,并使用根组件提供的对象作为EditorField
组件上名为colors
的组件的值。创建对应于服务的属性允许我在如下指令表达式中使用它的属性:
...
<input v-model.number="value" class="form-control"
v-bind:class="[colors.bg, colors.text]" />
...
- 1
- 2
- 3
- 4
- 5
结果是根组件能够提供应该用于样式元素的类名,而不必通过组件链传递它们。这些类名被应用于EditorField
组件的input
元素,产生如图 18-3 所示的结果。(您必须在input
元素中输入一些文本才能看到文本颜色。)
小费
您可能需要重新加载浏览器才能看到本示例的效果。
图 18-3
使用依赖注入
覆盖先行服务
当 Vue.js 创建一个带有inject
属性的组件时,它解析所需服务的依赖关系,沿着组件链向上工作,并检查每个组件以查看是否有带有与所需名称匹配的服务的provide
属性。这种方法意味着一个组件可以覆盖它的前身提供的一个或多个服务,这对于创建只应用于应用的一部分的更专门化的服务是一种有用的方法。为了演示,我向定义了colors
服务的ProductEditor
组件添加了一个provide
属性,如清单 18-12 所示。
...
<script>
import EditorField from "./EditorField";
export default {
data: function () {
return {
editing: false,
product: {
id: 0,
name: "",
price: 0
}
}
},
components: { EditorField },
methods: {
// ...methods omitted for brevity...
},
provide: function () {
return {
colors: {
bg: "bg-light",
text: "text-danger"
}
}
}
}
</script>
...
Listing 18-12Defining a Service in the ProductEditor.vue File in the src/components Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
当 Vue.js 创建一个EditorField
组件并解析其inject
属性指定的服务时,它将到达ProductEditor
组件定义的服务并停止,这意味着colors
服务将使用清单 18-12 中使用的类名进行解析,如图 18-4 所示。
小费
每个服务都是独立解析的,这意味着一个组件可以选择只覆盖其前身提供的一些服务。
图 18-4
覆盖服务的效果
理解服务的匿名性
依赖注入使得创建松散耦合的应用成为可能,因为服务是匿名提供和消费的。当一个组件定义一个服务时,它不知道它的哪个后代将会使用它,它是否会被另一个组件覆盖,甚至不知道它是否会被使用。
同样,当一个组件使用一个服务时,它不知道它的哪个前身提供了这个服务。这种方法允许需要数据或功能的组件从能够提供数据或功能的组件接收数据或功能,而不需要定义严格的关系或担心通过一系列父子关系传递数据或功能。
创建反应式服务
默认情况下,Vue.js 服务不是被动的,这意味着对由color
服务提供的对象属性的任何更改都不会传播到应用的其余部分。但是,由于 Vue.js 在每次创建组件时都会解析服务的依赖关系,因此在更改后创建的组件将接收新值。
如果您想要创建一个传播变更的服务,那么您必须将一个对象分配给一个data
属性,并使用它作为服务的值,如清单 18-13 所示。
<template>
<div class="container-fluid">
<div class="text-right m-2">
<button class="btn btn-primary" v-on:click="toggleColors">
Toggle Colors
</button>
</div>
<div class="row">
<div class="col-8 m-3">
<product-display></product-display>
</div>
<div class="col m-3">
<product-editor></product-editor>
</div>
</div>
</div>
</template>
<script>
import ProductDisplay from "./components/ProductDisplay";
import ProductEditor from "./components/ProductEditor";
export default {
name: 'App',
components: { ProductDisplay, ProductEditor },
data: function () {
return {
reactiveColors: {
bg: "bg-secondary",
text: "text-white"
}
}
},
provide: function() {
return {
colors: this.reactiveColors
}
},
methods: {
toggleColors() {
if (this.reactiveColors.bg == "bg-secondary") {
this.reactiveColors.bg = "bg-light";
this.reactiveColors.text = "text-danger";
} else {
this.reactiveColors.bg = "bg-secondary";
this.reactiveColors.text = "text-white";
}
}
}
}
</script>
Listing 18-13Creating a Reactive Service in the App.vue File in the src Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
该组件定义了一个名为reactiveColors
的data
属性,该属性被分配了一个具有bg
和text
属性的对象。在组件的创建阶段,Vue.js 在处理provide
属性之前处理data
属性,这意味着reactiveColors
对象被激活,随后被用作colors
服务的值。为了帮助演示一个反应式服务,我还添加了一个button
元素,该元素使用v-on
指令来调用toggleColors
方法,从而改变bg
和text
的值。
为了让这个例子工作,我需要注释掉ProductEditor
组件中的provide
属性,如清单 18-14 所示;否则,其提供的服务将优先于清单 18-13 中的服务。
小费
您可能需要重新加载浏览器才能看到本例中的更改。
...
<script>
import EditorField from "./EditorField";
export default {
data: function () {
return {
editing: false,
product: {
id: 0,
name: "",
price: 0
}
}
},
components: { EditorField },
methods: {
// ...methods omitted for brevity...
},
// provide: function () {
// return {
// colors: {
// bg: "bg-light",
// text: "text-danger"
// }
// }
// }
}
</script>
...
Listing 18-14Removing a Service in the ProductEditor.vue File in the src Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
结果是,当点击按钮时,服务对象的属性值被改变,并在整个应用中传播,如图 18-5 所示。
警告
任何组件都可以对反应式服务进行更改,而不仅仅是定义它的组件。这可能是一个有用的特性,但也可能导致意外的行为。
图 18-5
创建反应式服务
使用高级依赖注入特性
在使用服务时,有两个有用的高级特性可以使用。第一个特性是提供一个默认值,如果没有前件定义组件需要的服务,将使用这个默认值。第二个特性是能够更改组件识别服务的名称。我已经在清单 18-15 中应用了这两个特性。
<template>
<div class="form-group">
<label>{{ formattedLabel }}</label>
<input v-model.number="value" class="form-control"
v-bind:class="[colors.bg, colors.text]" />
</div>
</template>
<script>
export default {
props: ["label"],
data: function () {
return {
value: "",
formattedLabel: this.format(this.label)
}
},
inject: {
colors: "colors",
format: {
from: "labelFormatter",
default: () => (value) => `Default ${value}`
}
}
}
</script>
Listing 18-15Using Advanced Features in the EditorField.vue File in the src/components Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
这些特性要求inject
属性的值是一个对象。每个属性的名称是组件内部使用服务的名称。如果不需要高级功能,则属性的值就是所需服务的名称,如下所示:
...
"colors": "colors",
...
- 1
- 2
- 3
- 4
这个属性告诉 Vue.js,组件需要一个名为colors
的服务,并希望将其称为colors
,这与前面的例子产生了相同的结果。第二个属性使用了这两个高级特性。
...
inject: {
colors: "colors",
format: {
from: "labelFormatter",
default: () => (value) => `Default ${value}`
}
}
...
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
from
属性告诉 Vue.js 它应该寻找由组件的前身提供的名为labelFormatter
的服务,但是当组件使用该服务时,该服务将被称为format
。default
属性提供了一个默认值,如果组件的前身都不提供labelFormatter
服务,将使用该默认值。
注意
当您为服务提供默认值时,需要一个工厂函数,这就是为什么清单 18-15 中的default
属性的值被赋予一个函数,该函数返回另一个函数作为其结果。当 Vue.js 创建组件时,它将调用default
函数来获取将被用作服务的对象。
本例中的default
值是一个函数,它在收到的值前加上Default
,这样当使用默认服务时就很明显,产生如图 18-6 左侧所示的结果。
图 18-6
使用服务的默认值
已经使用了服务的默认值,因为组件的前身都没有提供名为labelFormatter
的服务。为了演示一个提供的服务在可用时将被使用,我创建了一个同名的服务,如清单 18-16 所示。
...
<script>
import ProductDisplay from "./components/ProductDisplay";
import ProductEditor from "./components/ProductEditor";
export default {
name: 'App',
components: { ProductDisplay, ProductEditor },
data: function () {
return {
reactiveColors: {
bg: "bg-secondary",
text: "text-white"
}
}
},
provide: function() {
return {
colors: this.reactiveColors,
labelFormatter: (value) => `Enter ${value}:`
}
},
methods: {
toggleColors() {
if (this.reactiveColors.bg == "bg-secondary") {
this.reactiveColors.bg = "bg-light";
this.reactiveColors.text = "text-danger";
} else {
this.reactiveColors.bg = "bg-secondary";
this.reactiveColors.text = "text-white";
}
}
}
}
</script>
...
Listing 18-16Defining a Service in the App.vue File in the src Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
这个新服务是一个函数,它通过在接收到的值前面加上单词 Enter 来转换这些值,产生如图 18-6 右侧所示的结果。因为这不是服务的默认值,所以我不需要定义工厂函数。
使用事件总线
依赖注入可以与另一个高级 Vue.js 特性相结合,允许组件在父子关系之外发送和接收事件,产生一个被称为事件总线的结果。创建事件总线的第一步是在Vue
对象中定义服务,如清单 18-17 所示。
import Vue from 'vue'
import App from './App.vue'
import "../node_modules/bootstrap/dist/css/bootstrap.min.css";
Vue.config.productionTip = false
new Vue({
render: h => h(App),
provide: function () {
return {
eventBus: new Vue()
}
}
}).$mount('#app')
Listing 18-17Creating an Event Bus Service in the main.js File in the src Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
服务的值是一个新的Vue
对象。这可能看起来很奇怪,但它产生了一个对象,可用于以编程方式发送和接收自定义 Vue.js 事件,而不依赖于应用的组件层次结构。
使用事件总线发送事件
$emit
方法用于通过事件总线发送事件,遵循我在第十六章中演示的相同的基本方法,当时我向你展示了如何发送事件给一个组件的父组件。在清单 18-18 中,我已经更新了ProductDisplay
组件,这样当用户点击新建或编辑按钮时,它就可以使用事件总线来发送定制事件。
...
<script>
export default {
data: function () {
return {
products: [
{ id: 1, name: "Kayak", price: 275 },
{ id: 2, name: "Lifejacket", price: 48.95 },
{ id: 3, name: "Soccer Ball", price: 19.50 },
{ id: 4, name: "Corner Flags", price: 39.95 },
{ id: 5, name: "Stadium", price: 79500 }]
}
},
filters: {
currency(value) {
return `$${value.toFixed(2)}`;
}
},
methods: {
createNew() {
this.eventBus.$emit("create");
},
editProduct(product) {
this.eventBus.$emit("edit", product);
}
},
inject: ["eventBus"]
}
</script>
...
Listing 18-18Using the Event Bus in the ProductDisplay.vue File in the src/components Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
inject
属性用于声明对eventBus
服务的依赖,该服务的$emit
方法用于发送自定义事件create
和edit
,当用户单击组件提供的button
元素时,这些事件将被调用。
注意
事件总线模型的一个缺点是,您必须确保事件名称在应用中是唯一的,以便不会混淆事件的含义。如果你的应用变得太大,不允许简单地管理事件名,那么你应该考虑我在第二十章描述的共享状态方法。
从事件总线接收事件
允许事件总线模型工作的特性是 Vue.js 支持使用$on
方法以编程方式注册事件,一旦组件被初始化并且其对服务的依赖性被解决,就可以执行该方法。在清单 18-19 中,我使用了ProductEditor
组件中的事件总线来接收前一部分发送的事件。
...
<script>
import EditorField from "./EditorField";
export default {
data: function () {
return {
editing: false,
product: {
id: 0,
name: "",
price: 0
}
}
},
components: { EditorField },
methods: {
startEdit(product) {
this.editing = true;
this.product = {
id: product.id,
name: product.name,
price: product.price
}
},
startCreate() {
this.editing = false;
this.product = {
id: 0,
name: "",
price: 0
};
},
save() {
this.eventBus.$emit("complete", this.product);
this.startCreate();
console.log(`Edit Complete: ${JSON.stringify(this.product)}`);
},
cancel() {
this.product = {};
this.editing = false;
}
},
inject: ["eventBus"],
created() {
this.eventBus.$on("create", this.startCreate);
this.eventBus.$on("edit", this.startEdit);
}
}
</script>
...
Listing 18-19Receiving Events in the ProductEditor.vue File in the src/components Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
inject
属性用于声明对eventBus
服务的依赖,created
方法用于注册create
和edit
事件的处理程序方法,这些方法用于调用本章开头定义的startCreate
和startEdit
方法。
组件可以通过事件总线发送和接收事件,在清单 18-19 中,我使用了$emit
方法在调用save
方法时发送一个名为complete
的事件。这种双向通信允许我轻松地组合复杂的行为,在清单 18-20 中,我进一步更新了ProductDisplay
组件,以更新显示给用户的数据来响应complete
事件,这表明用户要么已经完成了对现有产品的编辑,要么已经创建了一个新产品。
...
<script>
import Vue from "vue";
export default {
data: function () {
return {
products: [
{ id: 1, name: "Kayak", price: 275 },
{ id: 2, name: "Lifejacket", price: 48.95 },
{ id: 3, name: "Soccer Ball", price: 19.50 },
{ id: 4, name: "Corner Flags", price: 39.95 },
{ id: 5, name: "Stadium", price: 79500 }]
}
},
filters: {
currency(value) {
return `$${value.toFixed(2)}`;
}
},
methods: {
createNew() {
this.eventBus.$emit("create");
},
editProduct(product) {
this.eventBus.$emit("edit", product);
},
processComplete(product) {
let index = this.products.findIndex(p => p.id == product.id);
if (index == -1) {
this.products.push(product);
} else {
Vue.set(this.products, index, product);
}
}
},
inject: ["eventBus"],
created() {
this.eventBus.$on("complete", this.processComplete);
}
}
</script>
...
Listing 18-20Receiving Events in the ProductDisplay.vue File in the src/components Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
created
方法用于监听complete
事件,并在收到事件时调用processComplete
方法。processComplete
方法使用id
属性作为键来更新已编辑的对象或添加新对象。
结果是ProductDisplay
和ProductEditor
组件能够发送和接收事件,允许它们在父子关系之外响应彼此的动作。仍然缺少一些关键功能,但是如果你点击“编辑”或“新建”按钮,你会看到编辑器按钮中的文本会相应地进行调整,如图 18-7 所示。
图 18-7
使用事件总线
创建本地事件总线
您不必为整个应用使用单个事件总线,应用的各个部分可以有自己的总线并使用自定义事件,而不必将它们分发到执行不相关任务的组件。在清单 18-21 中,我在ProductEditor
组件中添加了一个独立的事件总线editingEventBus
,并使用它来发送和接收自定义事件,这些事件将把单个编辑器字段与应用的其余部分连接起来。
<template>
<div>
<editor-field label="ID" editorFor="id" />
<editor-field label="Name" editorFor="name" />
<editor-field label="Price" editorFor="price" />
<div class="text-center">
<button class="btn btn-primary" v-on:click="save">
{{ editing ? "Save" : "Create" }}
</button>
<button class="btn btn-secondary" v-on:click="cancel">Cancel</button>
</div>
</div>
</template>
<script>
import EditorField from "./EditorField";
import Vue from "vue";
export default {
data: function () {
return {
editing: false,
product: {
id: 0,
name: "",
price: 0
},
localBus: new Vue()
}
},
components: { EditorField },
methods: {
startEdit(product) {
this.editing = true;
this.product = {
id: product.id,
name: product.name,
price: product.price
}
},
startCreate() {
this.editing = false;
this.product = {
id: 0,
name: "",
price: 0
};
},
save() {
this.eventBus.$emit("complete", this.product);
this.startCreate();
console.log(`Edit Complete: ${JSON.stringify(this.product)}`);
},
cancel() {
this.product = {};
this.editing = false;
}
},
inject: ["eventBus"],
provide: function () {
return {
editingEventBus: this.localBus
}
},
created() {
this.eventBus.$on("create", this.startCreate);
this.eventBus.$on("edit", this.startEdit);
this.localBus.$on("change",
(change) => this.product[change.name] = change.value);
},
watch: {
product(newValue, oldValue) {
this.localBus.$emit("target", newValue);
}
}
}
</script>
Listing 18-21Creating a Local Event Bus in the ProductEditor.vue File in the src/components Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
在这个清单中有许多变化,但是它们都致力于提供一个专用于编辑产品对象的事件的本地事件总线,结合了前面部分和前面章节的特性。当用户开始编辑或创建一个新对象时,一个target
事件在本地事件总线上被发送,当一个change
事件在该总线上被接收时,产品对象被更新,反映用户所做的改变。负责显示编辑器字段的组件需要进行补充更改,如清单 18-22 所示。
<template>
<div class="form-group">
<label>{{ formattedLabel }}</label>
<input v-model.number="value" class="form-control"
v-bind:class="[colors.bg, colors.text]" />
</div>
</template>
<script>
export default {
props: ["label", "editorFor"],
data: function () {
return {
value: "",
formattedLabel: this.format(this.label)
}
},
inject: {
colors: "colors",
format: {
from: "labelFormatter",
default: () => (value) => `Default ${value}`
},
editingEventBus: "editingEventBus"
},
watch: {
value(newValue) {
this.editingEventBus.$emit("change",
{ name: this.editorFor, value: this.value});
}
},
created() {
this.editingEventBus.$on("target",
(p) => this.value = p[this.editorFor]);
}
}
</script>
Listing 18-22Using a Local Event Bus in the EditorField.vue File in the src/components Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
结果是点击编辑按钮允许用户编辑现有对象,点击创建新按钮允许用户创建新对象,点击创建/保存按钮应用所做的任何更改,如图 18-8 所示。
图 18-8
使用本地事件总线连接编辑器组件
摘要
在这一章中,我解释了如何使用 Vue.js 依赖注入和事件总线特性来突破父子关系并创建松散耦合的组件。我演示了如何定义和使用服务,如何使服务具有反应性,以及如何创建事件总线来在整个应用中分发自定义事件。在下一章,我将解释如何在 Vue.js 应用中使用 RESTful web 服务。
十九、使用 RESTful Web 服务
在本章中,我将演示如何在 Vue.js 应用中使用 RESTful web 服务。我解释了发出 HTTP 请求的不同方式,并扩展了示例应用,使其能够从服务器读取和存储数据。表 19-1 将 web 服务的使用放在 n 上下文中。
表 19-1
将 RESTful Web 服务放在上下文中
|
问题
|
回答
|
| — | — |
| 它们是什么? | RESTful web 服务通过 HTTP 请求向 web 应用提供数据。 |
| 它们为什么有用? | 许多应用需要访问数据,并允许用户从持久性数据存储中创建、修改和删除对象。 |
| 它们是如何使用的? | web 应用向服务器发送一个 HTTP 请求,使用请求类型和 URL 来标识所需的数据和操作。 |
| 有什么陷阱或限制吗? | RESTful web 服务没有标准,这意味着 web 服务的工作方式存在差异。在 Vue.js 应用中,HTTP 请求是异步执行的,这让许多开发人员感到困惑。需要特别注意处理错误,因为 Vue.js 不会自动检测它们。 |
| 还有其他选择吗? | 您不必在 web 应用中使用 HTTP 请求,尤其是当您只有少量数据要处理时。 |
表 19-2 总结了本章内容。
表 19-2
章节总结
|
问题
|
解决办法
|
列表
|
| — | — | — |
| 从 web 服务获取数据 | 创建Axios.get
方法并读取响应对象的data
属性 | 10–14 |
| 整合访问 web 服务的代码 | 创建 HTTP 服务 | 15–17 |
| 执行其他 HTTP 操作 | 使用与您需要的 HTTP 请求类型相对应的 Axios 方法 | 18–19 |
| 应对错误 | 合并请求并使用一个try
/ catch
块来捕获和处理错误 | 20–23 |
为本章做准备
在本章中,我继续使用第十八章的 productapp 项目。按照下面几节中的说明来准备处理 HTTP 请求的示例应用。
小费
你可以从 https://github.com/Apress/pro-vue-js-2
下载本章以及本书其他章节的示例项目。
准备 HTTP 服务器
我需要一个额外的包来接收本章中的示例应用发出的 HTTP 请求。运行productapp
文件夹中清单 19-1 所示的命令,安装一个名为json-server
的包。
npm install json-server@0.12.1
Listing 19-1Installing a Package
- 1
- 2
- 3
- 4
为了向服务器提供它将用来处理 HTTP 请求的数据,在productapp
文件夹中添加一个名为restData.js
的文件,其内容如清单 19-2 所示。
module.exports = function () {
var data = {
products: [
{ id: 1, name: "Kayak", category: "Watersports", price: 275 },
{ id: 2, name: "Lifejacket", category: "Watersports", price: 48.95 },
{ id: 3, name: "Soccer Ball", category: "Soccer", price: 19.50 },
{ id: 4, name: "Corner Flags", category: "Soccer", price: 34.95 },
{ id: 5, name: "Stadium", category: "Soccer", price: 79500 },
{ id: 6, name: "Thinking Cap", category: "Chess", price: 16 },
{ id: 7, name: "Unsteady Chair", category: "Chess", price: 29.95 },
{ id: 8, name: "Human Chess Board", category: "Chess", price: 75 },
{ id: 9, name: "Bling Bling King", category: "Chess", price: 1200 }
]
}
return data
}
Listing 19-2The Contents of the restData.js File in the productapp Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
为了允许 NPM 运行json-server
包,将清单 19-3 中所示的语句添加到package.json
文件的scripts
部分。
...
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"json": "json-server restData.js -p 3500"
},
...
Listing 19-3Adding a Script in the package.json File in the productapp Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
准备示例应用
为了准备本章的示例应用,我将删除一些不再需要的特性,删除硬编码到应用中的数据,并准备处理除了我在第十八章中使用的id
、name
和price
属性之外还具有 category 属性的对象。
安装 HTTP 包
但是,首先,我要添加我将用来发出 HTTP 请求的包。在productapp
文件夹中运行清单 19-4 所示的命令来安装一个名为 Axios 的包。
npm install axios@0.18.0
Listing 19-4Installing a Package
- 1
- 2
- 3
- 4
Axios 是一个流行的库,用于在 web 应用中发出 HTTP 请求。它不是专门为 Vue.js 应用编写的,但它已经成为 Vue.js 应用中最常用的 HTTP 库,因为它可靠且易于使用。您不必在自己的项目中使用 Axios,我在“选择 HTTP 请求机制”侧栏中描述了选项的范围。
简化组件
我简化了ProductEditor
组件,以便直接向用户显示用于编辑的input
元素,而不是通过一个单独的组件,我在第十八章中使用这个组件来演示如何连接应用的不同部分。清单 19-5 显示了简化的组件。
<template>
<div>
<div class="form-group">
<label>ID</label>
<input class="form-control" v-model="product.id" />
</div>
<div class="form-group">
<label>Name</label>
<input class="form-control" v-model="product.name" />
</div>
<div class="form-group">
<label>Category</label>
<input class="form-control" v-model="product.category" />
</div>
<div class="form-group">
<label>Price</label>
<input class="form-control" v-model.number="product.price" />
</div>
<div class="text-center">
<button class="btn btn-primary" v-on:click="save">
{{ editing ? "Save" : "Create" }}
</button>
<button class="btn btn-secondary" v-on:click="cancel">Cancel</button>
</div>
</div>
</template>
<script>
export default {
data: function () {
return {
editing: false,
product: {}
}
},
methods: {
startEdit(product) {
this.editing = true;
this.product = {
id: product.id,
name: product.name,
category: product.category,
price: product.price
}
},
startCreate() {
this.editing = false;
this.product = {};
},
save() {
this.eventBus.$emit("complete", this.product);
this.startCreate();
},
cancel() {
this.product = {};
this.editing = false;
}
},
inject: ["eventBus"],
created() {
this.eventBus.$on("create", this.startCreate);
this.eventBus.$on("edit", this.startEdit);
}
}
</script>
Listing 19-5Simplifying the Component in the ProductEditor.vue File in the src/components Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
接下来,我简化了ProductDisplay
组件,删除了硬编码的数据并增加了对显示类别值的支持,如清单 19-6 所示。
<template>
<div>
<table class="table table-sm table-striped table-bordered">
<tr>
<th>ID</th><th>Name</th><th>Category</th><th>Price</th><th></th>
</tr>
<tbody>
<tr v-for="p in products" v-bind:key="p.id">
<td>{{ p.id }}</td>
<td>{{ p.name }}</td>
<td>{{ p.category }}</td>
<td>{{ p.price }}</td>
<td>
<button class="btn btn-sm btn-primary"
v-on:click="editProduct(p)">
Edit
</button>
</td>
</tr>
<tr v-if="products.length == 0">
<td colspan="5" class="text-center">No Data</td>
</tr>
</tbody>
</table>
<div class="text-center">
<button class="btn btn-primary" v-on:click="createNew">
Create New
</button>
</div>
</div>
</template>
<script>
import Vue from "vue";
export default {
data: function () {
return {
products: []
}
},
methods: {
createNew() {
this.eventBus.$emit("create");
},
editProduct(product) {
this.eventBus.$emit("edit", product);
}
},
inject: ["eventBus"]
}
</script>
Listing 19-6Simplifying the Component in the ProductDisplay.vue File in the src/components Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
最后,我简化了App
组件,删除了用于切换输入元素颜色的按钮以及它使用的方法和服务,如清单 19-7 所示。
<template>
<div class="container-fluid">
<div class="row">
<div class="col-8 m-3"><product-display/></div>
<div class="col m-3"><product-editor/></div>
</div>
</div>
</template>
<script>
import ProductDisplay from "./components/ProductDisplay";
import ProductEditor from "./components/ProductEditor";
export default {
name: 'App',
components: { ProductDisplay, ProductEditor }
}
</script>
Listing 19-7Simplifying the Component in the App.vue File in the src Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
运行示例应用和 HTTP 服务器
本章需要两个命令提示符:一个运行 HTTP 服务器,另一个运行 Vue.js 开发工具。在productapp
文件夹中打开一个新的命令提示符,运行清单 19-8 中所示的命令来启动 HTTP 服务器。
npm run json
Listing 19-8Starting the RESTful Server
- 1
- 2
- 3
- 4
服务器将开始监听端口 3500 上的请求。要测试服务器是否正在运行,请打开一个新的 web 浏览器并请求 URL http://localhost:3500/products/1
。如果服务器正在运行并且能够找到数据文件,那么浏览器将显示以下 JSON 数据:
...
{
"id": 1,
"name": "Kayak",
"category": "Watersports",
"price": 275
}
...
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
让 HTTP 服务器保持运行,并打开另一个命令提示符。导航到productapp
文件夹并运行清单 19-9 中所示的命令来启动 Vue.js 开发工具。
npm run serve
Listing 19-9Starting the Vue.js Development Tools
- 1
- 2
- 3
- 4
一旦初始捆绑过程完成,打开一个新的浏览器窗口并导航到http://localhost:8080
,在那里你将看到示例应用,如图 19-1 所示。
图 19-1
运行示例应用
理解 RESTful Web 服务
向应用交付数据的最常见方法是使用表述性状态转移模式(称为 REST)来创建数据 web 服务。REST 没有详细的规范,这导致很多不同的方法都打着 RESTful 的旗号。然而,在 web 应用开发中有一些有用的统一思想。
RESTful web 服务的核心前提是包含 HTTP 的特性,以便请求方法——也称为动词——指定服务器要执行的操作,请求 URL 指定操作将应用到的一个或多个数据对象。
例如,在示例应用中,下面是一个可能指向特定产品的 URL:
http://localhost:3500/products/2
- 1
- 2
URL 的第一段—products
—表示将被操作的对象的集合,并允许单个服务器提供多个服务,每个服务都有自己的数据。第二个片段——2
——在products
集合中选择一个单独的对象。在本例中,id
属性的值唯一地标识了一个对象,并将在 URL 中使用,在本例中,指定了Lifejacket
对象。
用于发出请求的 HTTP 动词或方法告诉 RESTful 服务器应该对指定的对象执行什么操作。在上一节中测试 RESTful 服务器时,浏览器发送了一个 HTTP GET 请求,服务器将其解释为检索指定对象并将其发送给客户机的指令。正是由于这个原因,浏览器显示了一个表示Lifejacket
对象的 JSON。
表 19-3 显示了 HTTP 方法和 URL 的最常见组合,并解释了当发送到 RESTful 服务器时它们各自的作用。
表 19-3
RESTful Web 服务中常见的 HTTP 方法及其效果
|
方法
|
统一资源定位器
|
描述
|
| — | — | — |
| GET
| /products
| 这种组合检索products
集合中的所有对象。 |
| GET
| /products/2
| 这个组合从products
集合中检索出id
为2
的对象。 |
| POST
| /products
| 该组合用于向products
集合添加一个新对象。请求体包含新对象的 JSON 表示。 |
| PUT
| /products/2
| 该组合用于替换products
集合中id
为 2 的对象。请求体包含替换对象的 JSON 表示。 |
| PATCH
| /products/2
| 该组合用于更新products
集合中对象属性的子集,该集合的id
为 2。请求体包含要更新的属性和新值的 JSON 表示。 |
| DELETE
| /products/2
| 该组合用于从products
集合中删除id
为 2 的产品。 |
需要谨慎,因为一些 RESTful web 服务的工作方式可能存在相当大的差异,这是由用于创建它们的框架和开发团队的偏好的差异造成的。确认 web 服务如何使用动词以及在 URL 和请求正文中需要什么来执行操作是很重要的。
一些常见的变体包括不接受任何包含id
值的请求主体的 web 服务(以确保它们是由服务器的数据存储唯一生成的)和不支持所有动词的 web 服务(通常忽略PATCH
请求,只接受使用PUT
动词的更新)。
选择 HTTP 请求机制
异步 HTTP 请求有三种不同的方式。第一种方法是使用XMLHttpRequest
对象,这是异步请求的原始机制,可以追溯到 XML 作为 web 应用的标准数据格式的时候。下面是一段代码,它向本章中使用的 RESTful web 服务发送一个 HTTP 请求:
...
let request = new XMLHttpRequest();
request.onreadystatechange = () => {
if (request.readyState == XMLHttpRequest.DONE && request.status == 200) {
this.products.push(...JSON.parse(request.responseText));
}
};
request.open("GET", "http://localhost:3500/products");
request.send();
...
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
由XMLHttpRequest
对象提供的 API 使用一个事件处理程序来接收更新,包括请求完成时来自服务器的响应细节。XMLHttpRequest
使用起来很笨拙,并且不支持像async
/ await
关键字这样的现代特性,但是你可以相信它在所有运行 Vue.js 应用的浏览器中都是可用的。你可以在 https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
了解更多关于XMLHttpRequest
的 API。
第二种方法是使用 Fetch API,这是最近对XMLHttpRequest
对象的替代。Fetch API 使用承诺,而不是事件,通常更容易使用。下面是获取产品数据的一段代码,相当于XMLHttpRequest
示例:
...
fetch("http://localhost:3500/products")
.then(response => response.json())
.then(data => this.products.push(...data));
...
- 1
- 2
- 3
- 4
- 5
- 6
如果有的话,Fetch API 使用了太多的承诺。fetch
方法用于发出 HTTP 请求,该请求返回一个产生结果对象的承诺,该结果对象的json
方法产生从 JSON 数据解析的请求的最终结果。Fetch API 可以与async
和await
关键字一起使用,但是处理多个承诺需要小心,可能会导致类似下面这样的语句:
...
this.products.push(
...await (await fetch("http://localhost:3500/products")).json());
...
- 1
- 2
- 3
- 4
- 5
Fetch API 是对XMLHttpRequest
的改进,但是并不是所有可以运行 Vue.js 应用的浏览器都支持它。你可以在 https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
找到获取 API 的详细信息。
第三种方法是使用一个包,该包使用了XMLHttpRequest
对象,但隐藏了细节,并提供了一个与 Vue.js 开发体验的其余部分更加一致的 API。我在这一章中使用了 Axios 包,因为它是最流行的,但也有许多可用的选择。Vue.js 没有一个“官方的”HTTP 包,但是有很多选择,甚至不是专门为 Vue.js 编写的包也很容易使用,正如本章所演示的。使用 HTTP 包的缺点是应用的大小会增加,因为浏览器需要额外的代码。
使用 RESTful Web 服务
关于 web 应用发出的 HTTP 请求,需要理解的最重要的一点是它们是异步的。这似乎是显而易见的,但它会引起混乱,因为来自服务器的响应不会立即可用。因此,HTTP 请求所需的代码必须仔细编写,并且需要 JavaScript 特性来处理异步操作。因为 HTTP 请求会引起很多混乱,所以在接下来的小节中,我将一步一步地为第一个请求构建代码。
提出跨来源请求
默认情况下,浏览器会强制执行一个安全策略,只允许 JavaScript 代码在包含异步 HTTP 请求的文档的同一来源内发出这些请求。该政策旨在降低跨站点脚本(CSS)攻击的风险,在这种攻击中,浏览器被诱骗执行恶意代码,这在 http://en.wikipedia.org/wiki/Cross-site_scripting
中有详细描述。对于 web 应用开发人员来说,同源策略在使用 web 服务时可能是一个问题,因为它们通常位于包含应用 JavaScript 代码的源之外。如果两个 URL 具有相同的协议、主机和端口,则它们被认为是来源相同,否则它们具有不同的来源。我在本章中为 RESTful web 服务使用的 URL 与主应用使用的 URL 有不同的来源,因为它们使用不同的 TCP 端口。
跨源资源共享(CORS)协议用于向不同的源发送请求。使用 CORS,浏览器在异步 HTTP 请求中包含标头,向服务器提供 JavaScript 代码的来源。来自服务器的响应包括告诉浏览器它是否愿意接受请求的头。CORS 的详细情况不在本书讨论范围之内,但在 https://en.wikipedia.org/wiki/Cross-origin_resource_sharing
有题目介绍,在 www.w3.org/TR/cors
有 CORS 规格。
CORS 是在这一章中自动发生的事情。提供 RESTful web 服务的json-server
包支持 CORS,并将接受来自任何来源的请求,而我用来发出 HTTP 请求的 Axios 包自动应用 CORS。当您为自己的项目选择软件时,您必须选择一个允许通过单一来源处理所有请求的平台,或者配置 CORS 以便服务器接受应用的数据请求。
处理响应数据
这可能看起来违反直觉,但是最好从处理您期望从服务器接收的数据的代码开始。在清单 19-10 中,我在ProductDisplay
组件的脚本元素中添加了一个方法,当从 RESTful web 服务接收到产品数据时,它将处理这些数据。
...
<script>
import Vue from "vue";
export default {
data: function () {
return {
products: []
}
},
methods: {
createNew() {
this.eventBus.$emit("create");
},
editProduct(product) {
this.eventBus.$emit("edit", product);
},
processProducts(newProducts) {
this.products.splice(0);
this.products.push(...newProducts);
}
},
inject: ["eventBus"]
}
</script>
...
Listing 19-10Adding a Method in the ProductDisplay.vue File in the src/components Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
processProducts
方法将接收一个产品对象数组,并用它们替换名为products
的data p
属性的内容。正如我在第十三章中解释的,Vue.js 在检测数组中的变化时有一些困难,所以我使用了splice
方法来移除任何现有的对象。然后,我使用destructuring
操作符解包方法参数中的值,并使用push
方法重新填充数组。由于products
数组是一个反应式数据属性,Vue.js 将自动检测变化并更新数据绑定以反映新数据。
发出 HTTP 请求
下一步是发出 HTTP 请求,并向 RESTful web 服务请求数据。在清单 19-11 中,我已经导入了 Axios 包,并使用它来发送 HTTP 请求。
...
<script>
import Vue from "vue";
import Axios from "axios";
const baseUrl = "http://localhost:3500/products/";
export default {
data: function () {
return {
products: []
}
},
methods: {
createNew() {
this.eventBus.$emit("create");
},
editProduct(product) {
this.eventBus.$emit("edit", product);
},
processProducts(newProducts) {
this.products.splice(0);
this.products.push(...newProducts);
}
},
inject: ["eventBus"],
created() {
Axios.get(baseUrl);
}
}
</script>
...
Listing 19-11Making an HTTP Request in the ProductDisplay.vue File in the src/components Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
Axios 为每种 HTTP 请求类型提供了方法,比如使用get
方法发出 GET 请求,使用post
方法发出 POST 请求,等等。还有一个request
方法,接受一个配置对象,可以用来发出所有类型的请求;我在“创建错误处理服务”一节中使用了它。
我在组件的created
方法中发出 HTTP 请求。我希望我的请求的结果在收到数据时触发 Vue.js 更改机制,并且使用created
方法确保在我向 HTTP 服务器发出请求之前组件的数据属性已经得到处理。
小费
一些开发人员更喜欢使用mounted
方法来发出初始 HTTP 请求。您使用哪种方法并不重要,但保持一致是个好主意,这样所有组件的行为都是一样的。
接收响应
Axios get
方法的结果是一个Promise
,它将在 HTTP 请求完成时产生来自服务器的响应。正如我在第四章中解释的那样,then
方法用于指定当一个Promise
表示的工作完成时会发生什么,在清单 19-12 中,我使用了then
方法来处理 HTTP 响应。
...
<script>
import Vue from "vue";
import Axios from "axios";
const baseUrl = "http://localhost:3500/products/";
export default {
data: function () {
return {
products: []
}
},
methods: {
createNew() {
this.eventBus.$emit("create");
},
editProduct(product) {
this.eventBus.$emit("edit", product);
},
processProducts(newProducts) {
this.products.splice(0);
this.products.push(...newProducts);
}
},
inject: ["eventBus"],
created() {
Axios.get(baseUrl).then(resp => {
console.log(`HTTP Response: ${resp.status}, ${resp.statusText}`);
console.log(`Response Data: ${resp.data.length} items`);
});
}
}
</script>
...
Listing 19-12Receiving the HTTP Response in the ProductDisplay.vue File in the src/components Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
then
方法从 Axios 接收一个对象,该对象代表服务器的响应,并定义了表 19-4 中所示的属性。
表 19-4
Axios 响应属性
|
名字
|
描述
|
| — | — |
| status
| 该属性返回响应的状态代码,如 200 或 404。 |
| statusText
| 此属性返回伴随状态代码的说明性文本,如 OK 或 Not Found。 |
| headers
| 此属性返回一个对象,该对象的属性表示响应标头。 |
| data
| 该属性从响应中返回有效负载。 |
| config
| 此属性返回一个对象,该对象包含用于发出请求的配置选项。 |
| request
| 该属性返回用于发出请求的XMLHttpRequest
对象。 |
在清单 19-12 中,我使用status
和statusText
属性写出浏览器 JavaScript 控制台响应的细节。更令人感兴趣的是data
属性,该属性返回服务器发送的有效负载,Axios 自动对 JSON 响应进行解码,这意味着我可以读取length
属性来找出响应中包含了多少对象。保存对组件的更改,并检查浏览器的 JavaScript 控制台,您将看到以下消息:
...
HTTP Response: 200, OK
Response Data: 9 items
...
- 1
- 2
- 3
- 4
- 5
处理数据
最后一步是从响应对象读取数据属性,并将其传递给processProducts
方法,这样从 RESTful web 服务获得的对象将更新应用,如清单 19-13 所示。
...
<script>
import Vue from "vue";
import Axios from "axios";
const baseUrl = "http://localhost:3500/products/";
export default {
data: function () {
return {
products: []
}
},
methods: {
createNew() {
this.eventBus.$emit("create");
},
editProduct(product) {
this.eventBus.$emit("edit", product);
},
processProducts(newProducts) {
this.products.splice(0);
this.products.push(...newProducts);
}
},
inject: ["eventBus"],
created() {
Axios.get(baseUrl).then(resp => this.processProducts(resp.data));
}
}
</script>
...
Listing 19-13Processing the Response in the ProductDisplay.vue File in the src/components Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
效果是组件将在其created
方法中发起一个 HTTP GET 请求。当从服务器收到响应时,Axios 解析它包含的 JSON 数据,并使它作为响应的一部分可用。响应数据用于填充组件的products
数组,随后的更新评估模板中的v-for
指令,并显示如图 19-2 所示的数据。
图 19-2
从 web 服务获取数据
我可以使用async
/ await
关键字简化这段代码,这将让我不必依赖于then
方法就能发出 HTTP 请求,有些开发人员会觉得这种方法令人困惑。在清单 19-14 中,我将async
关键字应用于create
方法,并使用await
关键字发出 HTTP 请求。
...
async created() {
let data = (await Axios.get(baseUrl)).data;
this.processProducts(data);
}
...
Listing 19-14Streamlining the Request Code in the ProductDisplay.vue File in the src/components Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
这段代码的工作方式与清单 19-13 中的代码相同,但是没有使用then
方法来指定当 HTTP 请求完成时将要执行的语句。
创建 HTTP 服务
在继续之前,我将更改应用的结构。我在上一节中采用的方法演示了在组件中发出 HTTP 请求是多么容易,但是结果是组件提供给用户的功能被与服务器通信所需的代码冲淡了。随着请求类型范围的扩大,该组件将越来越专注于处理 HTTP。
我在src
文件夹中添加了一个名为restDataSource.js
的 JavaScript 文件,并用它来定义清单 19-15 中所示的 JavaScript 类。
import Axios from "axios";
const baseUrl = "http://localhost:3500/products/";
export class RestDataSource {
async getProducts() {
return (await Axios.get(baseUrl)).data;
}
}
Listing 19-15The Contents of the restDataSource.js File in the src Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
我已经使用 JavaScript 类特性定义了RestDataSource
类,它有一个异步的getProducts
方法,使用 Axios 向 RESTful web 服务发送 HTTP 请求,并返回接收到的数据。在清单 19-16 中,我创建了一个RestDataSource
类的实例,并将其配置为main.js
文件中的一个服务,这样它将在整个应用中可用。
import Vue from 'vue'
import App from './App.vue'
import "../node_modules/bootstrap/dist/css/bootstrap.min.css";
import { RestDataSource } from "./restDataSource";
Vue.config.productionTip = false
new Vue({
render: h => h(App),
provide: function () {
return {
eventBus: new Vue(),
restDataSource: new RestDataSource()
}
}
}).$mount('#app')
Listing 19-16Configuring a Service in the main.js File in the src Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
新服务被称为restDataSource
,它将可供应用中的所有组件使用。
使用 HTTP 服务
现在我已经定义了一个服务,我可以从ProductDisplay
代码中删除 Axios 代码,并使用该服务,如清单 19-17 所示。
...
<script>
import Vue from "vue";
//import Axios from "axios";
//const baseUrl = "http://localhost:3500/products/";
export default {
data: function () {
return {
products: []
}
},
methods: {
createNew() {
this.eventBus.$emit("create");
},
editProduct(product) {
this.eventBus.$emit("edit", product);
},
processProducts(newProducts) {
this.products.splice(0);
this.products.push(...newProducts);
}
},
inject: ["eventBus", "restDataSource"],
async created() {
this.processProducts(await this.restDataSource.getProducts());
}
}
</script>
...
Listing 19-17Using the HTTP Service in the ProductDisplay.vue File in the src/components Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
inject
属性声明了对restDataSource
服务的依赖,在created
方法中使用该服务从 RESTful web 服务获取数据,并填充名为products
的data
属性。
添加其他 HTTP 操作
现在我已经有了一个基本的结构,我可以添加应用需要的完整的 HTTP 操作集,扩展服务以使用 Axios 提供的方法,如清单 19-18 所示。
import Axios from "axios";
const baseUrl = "http://localhost:3500/products/";
export class RestDataSource {
async getProducts() {
return (await Axios.get(baseUrl)).data;
}
async saveProduct(product) {
await Axios.post(baseUrl, product);
}
async updateProduct(product) {
await Axios.put(`${baseUrl}${product.id}`, product);
}
async deleteProduct(product) {
await Axios.delete(`${baseUrl}${product.id}`, product);
}
}
Listing 19-18Adding Operations in restDataSource.js in the src Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
我添加了保存新对象、更新现有对象和删除对象的方法。所有这些方法都使用async
/ await
关键字,这将允许需要操作的组件等待结果。这很重要,因为这意味着组件可以确保数据的本地表示不会被更新,除非 HTTP 操作成功完成。
小费
删除或编辑项目后,停止并启动json-server
包,将示例数据重置为其原始状态。清单 19-2 中创建的 JavaScript 文件的内容将在进程开始时用于重新填充数据库。
在清单 19-19 中,我修改了ProductDisplay
组件以使用清单 19-18 中定义的方法,并增加了对删除对象的支持。
<template>
<div>
<table class="table table-sm table-striped table-bordered">
<tr>
<th>ID</th>
<th>Name</th>
<th>Category</th>
<th>Price</th>
<th></th>
</tr>
<tbody>
<tr v-for="p in products" v-bind:key="p.id">
<td>{{ p.id }}</td>
<td>{{ p.name }}</td>
<td>{{ p.category }}</td>
<td>{{ p.price }}</td>
<td>
<button class="btn btn-sm btn-primary"
v-on:click="editProduct(p)">
Edit
</button>
<button class="btn btn-sm btn-danger"
v-on:click="deleteProduct(p)">
Delete
</button>
</td>
</tr>
<tr v-if="products.length == 0">
<td colspan="5" class="text-center">No Data</td>
</tr>
</tbody>
</table>
<div class="text-center">
<button class="btn btn-primary" v-on:click="createNew">
Create New
</button>
</div>
</div>
</template>
<script>
import Vue from "vue";
export default {
data: function () {
return {
products: []
}
},
methods: {
createNew() {
this.eventBus.$emit("create");
},
editProduct(product) {
this.eventBus.$emit("edit", product);
},
async deleteProduct(product) {
await this.restDataSource.deleteProduct(product);
let index = this.products.findIndex(p => p.id == product.id);
this.products.splice(index, 1);
},
processProducts(newProducts) {
this.products.splice(0);
this.products.push(...newProducts);
},
async processComplete(product) {
let index = this.products.findIndex(p => p.id == product.id);
if (index == -1) {
await this.restDataSource.saveProduct(product);
this.products.push(product);
} else {
await this.restDataSource.updateProduct(product);
Vue.set(this.products, index, product);
}
}
},
inject: ["eventBus", "restDataSource"],
async created() {
this.processProducts(await this.restDataSource.getProducts());
this.eventBus.$on("complete", this.processComplete);
}
}
</script>
Listing 19-19Adding Data Operations in the ProductDisplay.vue File in the src/components Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
当调用由 HTTP 服务定义的异步方法时,使用await
关键字是很重要的。如果省略了await
关键字,那么不管 HTTP 请求的结果如何,组件方法中的后续语句都会立即执行。对于要求 RESTful web 服务存储或删除对象的操作,这意味着应用向用户显示的数据将表明操作已经立即成功完成,即使发生了错误。例如,在这个方法中使用await
关键字可以防止组件从products
数组中移除对象,直到 HTTP 请求完成:
...
async deleteProduct(product) {
await this.restDataSource.deleteProduct(product);
let index = this.products.findIndex(p => p.id == product.id);
this.products.splice(index, 1);
},
...
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
当您使用await
关键字时,在异步操作执行期间发生的任何错误都将导致在组件的方法中抛出异常,这将停止组件方法中语句的执行,在这种情况下,防止用户数据与服务器上的数据不同步。
小费
当你在一个组件的方法中使用await
关键字时,你必须记住也应用async
关键字,如清单 19-19 所示。
这些变化的结果是应用从 RESTful web 服务中读取数据,并能够创建新产品,编辑和删除现有产品,如图 19-3 所示。
图 19-3
执行 HTTP 操作
创建错误处理服务
Vue.js 不能检测异步 HTTP 操作中出现的异常,需要做一些额外的工作来告诉用户发生了错误。我倾向于创建一个专门显示错误的组件,并让RestDataSource
类通过事件总线发送定制事件来提供错误通知。在清单 19-20 中,我添加了对RestDataSource
类的支持,通过调度自定义事件来处理异常。
import Axios from "axios";
const baseUrl = "http://localhost:3500/products/";
export class RestDataSource {
constructor(bus) {
this.eventBus = bus;
}
async getProducts() {
return (await this.sendRequest("GET", baseUrl)).data;
}
async saveProduct(product) {
await this.sendRequest("POST", baseUrl, product);
}
async updateProduct(product) {
await this.sendRequest("PUT", `${baseUrl}${product.id}`, product);
}
async deleteProduct(product) {
await this.sendRequest("DELETE", `${baseUrl}${product.id}`, product);
}
async sendRequest(httpMethod, url, product) {
try {
return await Axios.request({
method: httpMethod,
url: url,
data: product
});
} catch (err) {
if (err.response) {
this.eventBus.$emit("httpError",
`${err.response.statusText} - ${err.response.status}`);
} else {
this.eventBus.$emit("httpError", "HTTP Error");
}
throw err;
}
}
}
Listing 19-20Handling Errors in the restDataSource.js File in the src Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
常规类不经历组件生命周期,不能使用inject
属性接收服务。考虑到这一点,我添加了一个接受事件总线的构造函数,我重写了这个类,这样所有的方法都通过调用使用 Axios request
方法的sendRequest
方法来执行它们的工作。这个方法允许使用一个配置对象来指定请求的细节,并允许我合并执行 HTTP 请求的代码,以便我可以一致地处理错误。
Axios 并不总是能够提供包含响应的对象,比如当请求超时时。对于这些情况,我提供了一般性的描述,指出问题与 HTTP 请求有关。
当 HTTP 请求返回 400 和 500 范围内的状态代码时,Axios 方法会抛出错误,这表明存在问题。在清单 19-20 中,我使用了一个try
/ catch
块来捕捉异常并发送一个名为httpError
的定制事件。在catch
块中接收的对象是一个response
属性,它返回一个表示来自服务器的响应的对象。这个对象定义了表 19-4 中描述的属性,我用它来制定一个简单的消息来伴随自定义事件。
小费
注意,在发送自定义事件后,我仍然throw
事件。这是为了让发起 HTTP 请求的组件接收到异常,而不会继续更新数据的本地表示。如果没有throw
语句,只有自定义事件的接收者会知道有问题。
我在清单 19-20 中添加的构造函数需要一个事件总线,我在main.js
文件中提供了,如清单 19-21 所示。
import Vue from 'vue'
import App from './App.vue'
import "../node_modules/bootstrap/dist/css/bootstrap.min.css";
import { RestDataSource } from "./restDataSource";
Vue.config.productionTip = false
new Vue({
render: h => h(App),
data: {
eventBus: new Vue()
},
provide: function () {
return {
eventBus: this.eventBus,
restDataSource: new RestDataSource(this.eventBus)
}
}
}).$mount('#app')
Listing 19-21Configuring a Service in the main.js File in the src Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
定义服务的属性不能引用其他服务,所以我定义了一个创建事件总线的数据属性,然后我通过它自己的provide
属性直接公开它,并作为RestDataSource
类的构造函数参数。
创建错误显示组件
我需要一个组件,将接收自定义事件,并向用户显示错误消息。我在src/components
文件夹中添加了一个名为ErrorDisplay.vue
的文件,并添加了清单 19-22 中所示的内容。
<template>
<div v-if="error" class="bg-danger text-white text-center p-3 h3">
An Error Has Occurred
<h6>{{ message }}</h6>
<a href="/" class="btn btn-secondary">OK</a>
</div>
</template>
<script>
export default {
data: function () {
return {
error: false,
message: ""
}
},
methods: {
handleError(err) {
this.error = true;
this.message = err;
}
},
inject: ["eventBus"],
created() {
this.eventBus.$on("httpError", this.handleError);
}
}
</script>
Listing 19-22The Contents of the ErrorDisplay.vue File in the src/components Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
该组件通过使用事件总线在其created
方法中注册其对自定义事件的兴趣,并在接收到事件时通过v-if
指令显示一个元素进行响应。为了将组件应用到用户,我对应用的根组件进行了更改,如清单 19-23 所示。
<template>
<div class="container-fluid">
<div class="row">
<div class="col"><error-display /></div>
</div>
<div class="row">
<div class="col-8 m-3"><product-display/></div>
<div class="col m-3"><product-editor/></div>
</div>
</div>
</template>
<script>
import ProductDisplay from "./components/ProductDisplay";
import ProductEditor from "./components/ProductEditor";
import ErrorDisplay from "./components/ErrorDisplay";
export default {
name: 'App',
components: { ProductDisplay, ProductEditor, ErrorDisplay }
}
</script>
Listing 19-23Applying a Component in the App.vue File in the src Folder
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
一个import
语句将名称ErrorDisplay
分配给component
,用于向 Vue.js 注册它,并允许将一个error-display
元素添加到根组件的模板中。结果是在 HTTP 请求过程中遇到的任何错误都会显示给用户,如图 19-4 所示。
小费
如果您想测试错误处理,那么最简单的方法就是停止json-server
进程,并更改RestDataSource
类中的baseUrl
值,使其指向一个不存在的 URL,比如http://localhost:3500/hats/
。
图 19-4
显示错误
从 HTTP 错误中恢复
我在错误处理组件中采用的方法是全有或全无的方法,在这种方法中,用户会看到一个导航到根 URL 的 OK 按钮,这有效地重新加载了应用并获得了新数据。这种方法的缺点是用户会丢失任何本地状态,这可能会导致挫败感,特别是如果用户试图重复一个复杂的任务,结果却再次处于相同的状态。
更好的方法是允许用户纠正问题并重试 HTTP 请求,但是只有当问题的原因清楚并且解决方案显而易见时,才应该尝试这样做。即使服务器提供了额外的错误信息,从 HTTP 请求中诊断出问题并不总是容易的。
摘要
在本章中,我演示了 Vue.js 应用如何使用 Axios 包访问 RESTful web 服务。我演示了如何使用 HTTP 请求执行数据操作,我向您展示了如何创建一个单独的类来将 HTTP 请求的细节与应用的其余部分分开,我解释了如何在出现错误时进行处理。在下一章中,我将解释如何使用 Vuex 包来创建共享数据存储。