VueJS2-高级教程-七-

70 阅读27分钟

VueJS2 高级教程(七)

原文:Pro Vue.js 2

协议:CC BY-NC-SA 4.0

十六、使用组件

在这一章中,我将解释组件如何在 Vue.js 应用中形成构建块,并允许将相关内容和代码组合在一起,以使开发更容易。我将向您展示如何向项目中添加组件,如何在组件之间进行通信,以及组件如何协同工作来向用户呈现内容。表 16-1 将组件放在上下文中。

表 16-1

将组件放在上下文中

|

问题

|

回答

| | --- | --- | | 它们是什么? | 可以组合组件,从较小的构建块中创建复杂的功能。 | | 它们为什么有用? | 使用单个组件构建复杂的应用,很难区分哪些内容和代码与每个功能相关。将应用分成几个组件意味着每个组件都可以单独开发和测试。 | | 它们是如何使用的? | 使用script元素中的components属性声明组件,并使用自定义 HTML 元素进行应用。 | | 有什么陷阱或限制吗? | 默认情况下,组件是相互隔离的,并且允许它们通信的特性可能很难掌握。 | | 有其他选择吗? | 您不必使用多个组件来构建一个应用,单个组件对于简单的项目来说可能是可以接受的。 |

表 16-2 总结了本章内容。

表 16-2

章节总结

|

问题

|

解决办法

|

列表

| | --- | --- | --- | | 群组相关功能 | 将组件添加到项目中 | 1–9 | | 与子组件通信 | 使用道具功能 | 10–12 | | 与父组件通信 | 使用自定义事件功能 | 13–14 | | 混合父组件和子组件内容 | 使用插槽功能 | 15–20 |

为本章做准备

我继续从事第十五章的templatesanddata项目。为了准备本章,我简化了应用的根组件,如清单 16-1 所示。

小费

你可以从 https://github.com/Apress/pro-vue-js-2 下载本章以及本书其他章节的示例项目。

<template>

    <div class="bg-secondary text-white text-center m-2 p-2 h5">
        Root Component
    </div>

</template>

<script>

export default {
    name: 'App'
}

</script>

Listing 16-1Simplifying the Content of the App.vue File in the src Folder

运行templatesanddata文件夹中清单 16-2 所示的命令,启动开发工具。

npm run serve

Listing 16-2Navigating to the Project Folder and Starting the Development Tools

将执行初始绑定过程,之后您将看到一条消息,告诉您项目已成功编译,HTTP 服务器正在侦听端口 8080 上的请求。打开一个新的浏览器窗口,导航到http://localhost:8080查看项目的占位符内容,如图 16-1 所示。

img/465686_1_En_16_Fig1_HTML.jpg

图 16-1

运行示例应用

将组件理解为构建块

随着应用复杂性的增加,使用单个组件变得越来越困难。你在第十五章看到了这样一个例子,一个表单和它的验证逻辑并存,结果是很难确定模板和脚本元素的哪些部分负责处理表单,哪些与验证相关。具有多重职责的组件很难理解、测试和维护。

组件是 Vue.js 应用的构建块,在一个应用中使用多个组件允许更小的功能单元,这些单元在整个应用中更容易编写、维护和重用。

当使用组件构建应用时,结果是一种父子关系,其中一个组件(父组件)将其模板的一部分委托给另一个组件(子组件)。了解其工作原理的最佳方式是创建一个演示这种关系的示例。组件通常定义在src/components文件夹中扩展名为.vue的文件中,我在该文件夹中添加了一个名为Child.vue的文件,内容如清单 16-3 所示。

注意

这个项目是用src/components中的HelloWorld.vue文件创建的,但是我在本章中不使用那个文件,您可以忽略或者删除这个文件。

<template>
    <div class="bg-primary text-white text-center m-2 p-3 h6">
        Child Component
    </div>
</template>

Listing 16-3The Contents of the Child.vue File in the src/components Folder

组件必须至少提供一个template元素,这是委托过程的核心。下一步是建立委托并创建父和子之间的关系,如清单 16-4 所示。

<template>
    <div class="bg-secondary text-white text-center m-2 p-2 h5">
        Root Component
        <ChildComponent></ChildComponent>

    </div>
</template>

<script>

import ChildComponent from "./components/Child";

export default {
    name: 'App',
    components: {

        ChildComponent

    }

}
</script>

Listing 16-4Delegating to a Child Component in the App.vue File in the src Folder

在父组件中设置子组件需要三个步骤。第一步是为子组件使用一个import语句,如下所示:

...
import ChildComponent from "./components/Child";
...

import语句中使用的源代码必须以句点开始,这表明这是一个本地导入语句,而不是一个针对使用包管理器安装的包的语句。import 语句最重要的部分是子组件被分配的名称,我用粗体标记了这个名称,在本例中是ChildComponent

import语句中的名字用于注册组件,在script元素中使用一个名为components的属性,如下所示:

...
export default {
    name: 'App',
    components: {
        ChildComponent

    }
}
...

使用一个对象来分配components属性,该对象的属性是它所使用的组件,确保在components对象中使用与在import语句中相同的名称是很重要的。最后一步是将一个 HTML 元素添加到父组件的模板中,该元素带有一个与在import语句和components对象中使用的名称相匹配的标签,如下所示:

...
<div class="bg-secondary text-white text-center m-2 p-2 h5">
    Root Component
    <ChildComponent></ChildComponent>

</div>
...

当 Vue.js 处理父组件的模板时,它找到自定义 HTML 元素并用子组件的template元素中的内容替换它,产生如图 16-2 所示的结果。

注意

这个例子展示了父子关系的一个重要方面:父组件决定了定义子组件的名称。这乍一看似乎很奇怪,但是它允许使用有意义的名称,反映了子组件的应用方式。正如您将在后面的示例中看到的,单个组件可以在应用的不同部分中使用,并且允许父组件命名其子组件意味着组件的每次使用都可以被赋予一个建议其用途的名称。

img/465686_1_En_16_Fig2_HTML.jpg

图 16-2

向示例应用添加组件

如果您使用浏览器的 F12 工具来检查 HTML 文档,您将会看到子组件是如何被用来替换ChildComponent元素的。

...
<body>
    <div class="bg-secondary text-white text-center m-2 p-2 h5">
        Root Component
        <div class="bg-primary text-white text-center m-2 p-3 h6">

            Child Component

        </div>

    </div>
    <script type="text/javascript" src="/app.js"></script>
</body>
...

结果是App.vue文件中定义的App组件呈现的部分内容被委托给了Child.vue文件中定义的Child组件,如图 16-3 所示。

img/465686_1_En_16_Fig3_HTML.jpg

图 16-3

父子组件关系

了解子组件名称和元素

在清单 16-4 中,我使用了import语句中的名称来设置子组件,这导致了这个看起来很笨拙的定制 HTML 元素:

...
<div class="bg-secondary text-white text-center m-2 p-2 h5">
    Root Component
    <ChildComponent></ChildComponent>

</div>
...

这是一个很有用的方法来证明是父组件命名了它的子组件,但是 Vue.js 有一个更复杂的命名方法,这可以产生更优雅的 HTML。

第一个名称特性是 Vue.js 在寻找要使用的子组件时会自动重新格式化定制 HTML 元素标签名称,如清单 16-5 所示。

<template>
    <div class="bg-secondary text-white text-center m-2 p-2 h5">
        Root Component
        <ChildComponent></ChildComponent>
        <child-component></child-component>

    </div>
</template>

<script>
import ChildComponent from "./components/Child";

export default {
    name: 'App',
    components: {
        ChildComponent
    }
}
</script>

Listing 16-5Tag Name Reformatting in the App.vue File in the src Folder

模板中新的定制 HTML 元素演示了 Vue.js 将接受带连字符的标记名,然后这些标记名将被转换为组件名通常使用的 camel case 格式,这样标记child-component将被识别为使用ChildComponent的指令。清单 16-5 中的两个定制 HTML 元素都告诉 Vue.js 将父组件模板的一部分委托给子组件的一个实例,产生如图 16-4 所示的结果。

img/465686_1_En_16_Fig4_HTML.jpg

图 16-4

灵活的自定义元素标记格式

components属性是一个映射,Vue.js 使用它将定制的 HTML 元素标记名转换为子组件名,这意味着当你注册一个组件时,你可以指定一个完全不同的标记名,如清单 16-6 所示。

<template>
    <div class="bg-secondary text-white text-center m-2 p-2 h5">
        Root Component
        <MyFeature></MyFeature>

        <my-feature></my-feature>

    </div>
</template>

<script>
import ChildComponent from "./components/Child";

export default {
    name: 'App',
    components: {
        MyFeature: ChildComponent

    }
}
</script>

Listing 16-6Specifying a Tag Name in the App.vue File in the src Folder

当您为子组件指定属性和值时,属性名称将用于自定义 HTML 元素。在这个例子中,我已经将属性的名称设置为MyFeature,这意味着我可以使用MyFeaturemy-feature标签来应用ChildComponent

全局注册组件

如果您有一个在整个应用中都需要的组件,那么您可以全局注册它。这种方法的优点是您不必配置每个父组件;然而,缺点是使用相同的 HTML 元素来应用子组件,这可能会导致模板意义不大。要全局注册一个组件,可以在main.js文件中添加一个导入语句,并使用Vue.component方法,如下所示:

import Vue from 'vue'
import App from './App'

import "../node_modules/bootstrap/dist/css/bootstrap.min.css";

import ChildComponent from "./components/Child";

Vue.config.productionTip = false

Vue.component("child-component", ChildComponent);

new Vue({
  render: h => h(App)
}).$mount('#app')

在创建应用的Vue对象之前,必须调用Vue.component方法,它的参数是 HTML 元素标签,将用于应用组件和在import语句中命名的组件对象。结果是在整个应用中可以使用child-component元素来应用组件,而无需任何进一步的配置。

在子组件中使用组件功能

我在清单 16-6 中定义的子组件只包含一个template元素,但是 Vue.js 支持子组件中前面章节描述的所有特性,包括单向和双向数据绑定、事件处理程序、datacomputed属性以及方法。在清单 16-7 中,我向子组件添加了一个script元素,并使用它来支持模板中的数据绑定。

<template>
    <div class="bg-primary text-white text-center m-2 p-3 h6">
        {{ message }}

        <div class="form-group m-1">

            <input v-model="message" class="form-control" />

        </div>

    </div>
</template>

<script>

export default {

    data: function() {

        return {

            message: "This is the child component"

        }

    }

}

</script>

Listing 16-7Adding Features in the Child.vue File in the src/components Folder

我添加的script元素定义了一个名为message的数据属性,我在带有文本插值绑定和v-model指令的template元素中使用了这个属性。结果是子组件显示一个input元素,其内容反映在文本数据绑定中,如图 16-5 所示。

img/465686_1_En_16_Fig5_HTML.jpg

图 16-5

向子组件添加功能

了解元件隔离

组件是相互隔离的,这意味着您不必担心选择唯一的属性和方法名,也不必担心绑定到不同组件所拥有的值。

在图 16-5 中,你可以看到编辑一个input元素的内容对另一个子组件没有影响,即使它们都定义并使用了一个message属性。这种隔离也适用于父组件和子组件,这可以通过在示例应用中向父组件添加一个message属性来演示,如清单 16-8 所示。

<template>
    <div class="bg-secondary text-white text-center m-2 p-2 h5">
        {{ message }}

        <MyFeature></MyFeature>
        <my-feature></my-feature>
    </div>
</template>

<script>
import ChildComponent from "./components/Child";

export default {
    name: 'App',
    components: {
        MyFeature: ChildComponent
    },
    data: function() {

        return {

            message: "This is the parent component"

        }

    }

}
</script>

Listing 16-8Adding a Data Property in the App.vue File in the src Folder

现在在应用中有三个名为messagedata属性,但是 Vue.js 将它们中的每一个都保持隔离,这样对其中一个的更改就不会影响到另一个,如图 16-6 所示。

img/465686_1_En_16_Fig6_HTML.jpg

图 16-6

父组件与子组件之间的隔离

理解 CSS 范围

如果您在组件中定义了自定义样式,您会发现它们会应用于任何组件定义的元素。例如,这种风格:

...
<style>
    div { border: 5px solid red ; }
</style>
...

将匹配应用中的任何div元素,并应用红色实心边框。如果您想将您的自定义 CSS 样式限制在定义它们的组件中,您可以将scoped属性添加到style元素中,如下所示:

...
<style scoped>
    div { border: 5px solid red ; }
</style>
...

scoped属性告诉 Vue.js,样式应该只应用于当前组件的template元素中的元素,而不应该应用于其他组件的模板中的元素。

使用组件道具

保持组件隔离是一个很好的默认策略,因为它避免了意外的交互。如果组件没有相互隔离,对一个message属性的改变会影响所有的组件。另一方面,在大多数应用中,组件必须协同工作才能为用户提供功能,这意味着要突破组件之间的障碍。组件协作的一个特性是 prop ,它允许父母为孩子提供数据值。在清单 16-9 中,我给子组件添加了一个道具。

<template>
    <div class="bg-primary text-white text-center m-2 p-3 h6">
        {{ message }}
        <div class="form-group m-1 text-left">

            <label>{{ labelText }}</label>

            <input v-model="message" class="form-control" />
        </div>
    </div>
</template>

<script>
    export default {
        props: ["labelText"],

        data: function () {
            return {
                message: "This is the child component"
            }
        }
    }
</script>

Listing 16-9Adding a Prop in the Child.vue File in the src/components Folder

使用分配给组件的script元素中的props属性的字符串数组来定义 Props。在这种情况下,道具名称是labelText。一旦定义了一个属性,就可以在组件的其他地方使用它,比如在文本插值绑定中。如果您需要修改从父组件接收到的值,那么您必须使用一个datacomputed属性,其初始值是从 prop 获得的,如清单 16-10 所示。

注意

这种方法是必需的,因为属性中的数据流是单向的:从父组件到子组件。如果修改属性值,所做的更改可能会被父组件覆盖。

<template>
    <div class="bg-primary text-white text-center m-2 p-3 h6">
        {{ message }}
        <div class="form-group m-1 text-left">
            <label>{{ labelText }}</label>
            <input v-model="message" class="form-control" />
        </div>
    </div>
</template>

<script>
    export default {
        props: ["labelText", "initialValue"],

        data: function () {
            return {
                message: this.initialValue

            }
        }
    }
</script>

Listing 16-10Setting a Mutable Value from a Prop in the Child.vue File in the src/components Folder

该组件定义了第二个属性,名为initialValue,用于设置message属性的值。

在父组件中使用道具

当一个组件定义一个属性时,它的父组件可以通过使用定制 HTML 元素上的属性向它发送数据值,如清单 16-11 所示。

<template>
    <div class="bg-secondary text-white text-center m-2 p-2 h5">
        {{ message }}
        <MyFeature labelText="Name" initialValue="Kayak"></MyFeature>

        <my-feature label-text="Category" initial-value="Watersports"></my-feature>

    </div>
</template>

<script>
    import ChildComponent from "./components/Child";

    export default {
        name: 'App',
        components: {
            MyFeature: ChildComponent
        },
        data: function () {
            return {
                message: "This is the parent component"
            }
        }
    }
</script>

Listing 16-11Using a Prop in the App.vue File in the src Folder

Vue.js 在将属性名与属性匹配时,与将定制 HTML 元素与组件匹配时一样灵活。例如,这意味着我可以使用labelTextlabel-text来设置道具的值。清单 16-11 中的属性配置子组件以产生如图 16-7 所示的结果。

小费

您可能需要重新加载浏览器才能看到此示例的结果。

img/465686_1_En_16_Fig7_HTML.jpg

图 16-7

使用道具配置子组件

小费

Prop 属性值是文字,这意味着该值不作为表达式计算。如果你想传递一个字符串给子组件,那么你可以这样做:my-attr="Hello"。不需要使用双引号:my-attr="'Hello'"。如果你想让一个属性的值作为一个表达式来计算,那么使用v-bind指令。如果你想让子组件响应数据绑定的适当变化,那么你可以使用一个观察器,如第十七章所述。

当使用道具时,重要的是要记住数据流只从父组件流向子组件,如图 16-8 所示。如果你试图修改一个属性值,你会收到一个警告,提醒你这个属性应该被用来初始化一个数据属性,如清单 16-10 所示。

img/465686_1_En_16_Fig8_HTML.jpg

图 16-8

使用道具时的数据流

在自定义 HTML 元素上设置常规属性

当 Vue.js 用子组件的模板替换自定义 HTML 元素时,它会将任何非 prop 属性转移到顶级模板元素,这可能会导致混淆的结果,尤其是如果子组件的模板中的元素已经具有该属性。例如,如果这是子模板元素:

...
<template>
    <div id="childIdValue">This is the child's element</div>
</template>
...

这是父模板元素:

...
<template>
    <my-feature id="parentIdValue"></my-feature>
</template>
...

然后,父属性应用的属性将覆盖子属性,在浏览器中生成如下所示的 HTML:

...
<div id="parentIdValue">This is the child's element</div>
...

属性classstyle的行为是不同的,浏览器通过组合这两个属性值来处理它们。如果这是子模板元素:

...
<template>
    <div class="bg-primary">This is the child's element</div>
</template>
...

这是父模板元素:

...
<template>
    <my-feature class="text-white"></my-feature>
</template>
...

然后,浏览器将组合class属性值,在浏览器中生成以下 HTML:

...
<div class="bg-primary text-white">This is the child's element</div>
...

当父组件和子组件都设置相同的属性时,必须小心,理想情况下应该避免这种情况。如果您想让父元素负责指定子元素呈现的 HTML 内容,那么就使用插槽特性,我在“使用组件插槽”一节中对此进行了描述。

使用属性值表达式

除非使用了v-bind指令,否则属性的值不会被计算为表达式,如清单 16-12 所示。

<template>
    <div class="bg-secondary text-white text-center m-2 p-2 h5">
        <div class="form-group">

            <input class="form-control" v-model="labelText" />

        </div>

        <my-feature v-bind:label-text="labelText" initial-value="Kayak"></my-feature>

    </div>
</template>

<script>
    import ChildComponent from "./components/Child";

    export default {
        name: 'App',
        components: {
            MyFeature: ChildComponent
        },
        data: function () {
            return {
                message: "This is the parent component",
                labelText: "Name"
            }
        }
    }
</script>

Listing 16-12Using an Expression in the App.vue File in the src Folder

父组件的模板包括一个使用v-model指令绑定到labelText属性的input元素。在子指令的自定义元素上指定了相同的属性,这告诉 Vue.js 在子组件的labelText属性和父组件的data属性之间建立一个单向绑定。

...
<my-feature v-bind:label-text="labelText" initial-value="Kayak"></my-feature>
...

结果是当父组件的input元素被编辑时,新值被子组件接收,并通过文本插值绑定显示,如图 16-9 所示。

警告

变更流仍然是单向的,即使使用了v-bind指令。父组件不会收到子组件对属性值所做的更改,当v-bind指令所使用的属性发生更改时,这些更改将被丢弃。有关如何将数据从子组件发送到其父组件的详细信息,请参见下一节。

img/465686_1_En_16_Fig9_HTML.jpg

图 16-9

使用适当的值表达式

创建自定义事件

props 特性的对应部分是自定义事件,它允许子组件向其父组件发送数据。为了演示自定义事件的使用,我扩展了子组件的功能,以便它提供一些自包含的功能,如清单 16-13 所示,这是组件在实际项目中使用的更典型方式。

结合道具和自定义事件

在某种程度上,大多数刚接触 Vue.js 开发的开发人员会尝试通过组合定制事件、道具和v-model指令,让父组件和子组件同时显示和更改相同的数据值。只需一点点努力,您就能让某些东西工作起来,但是它违背了这些特性的目的,并且总是一个脆弱的解决方案,偶尔会不可预测地以迷惑用户的方式运行。如第二十章所述,如果你希望多个组件能够显示和改变相同的数据值,使用共享应用状态。对于更简单的应用,事件总线可能就足够了,如第十八章所述

<template>
    <div class="bg-primary text-white text-center m-2 p-3 h6">
        <div class="form-group m-1 text-left">

            <label>Name</label>

            <input v-model="product.name" class="form-control" />

        </div>

        <div class="form-group m-1 text-left">

            <label>Category</label>

            <input v-model="product.category" class="form-control" />

        </div>

        <div class="form-group m-1 text-left">

            <label>Price</label>

            <input v-model.number="product.price" class="form-control" />

        </div>

        <div class="mt-2">

            <button class="btn btn-info" v-on:click="doSubmit">Submit</button>

        </div>

    </div>
</template>

<script>
    export default {
        props: ["initialProduct"],

        data: function () {
            return {
                product: this.initialProduct || {}

            }
        },
        methods: {

            doSubmit() {

                this.$emit("productSubmit", this.product);

            }

        }

    }
</script>

Listing 16-13Adding Features to the Child.vue File in the src/components Folder

该组件是产品对象的基本编辑器,其中的input元素编辑分配给名为productdata属性的对象的namecategoryprice属性,这些属性的初始数据值是使用名为initialProduct的属性接收的。

还有一个button元素,它通过调用一个叫做doSubmit的方法,使用v-on指令来响应click事件。正是这种方法允许组件与其父组件通信,它是这样做的:

...
doSubmit() {
    this.$emit("productSubmit", this.product);

}
...

使用关键字this调用的$emit方法用于发送自定义事件。第一个参数是事件类型,表示为一个字符串,可选的第二个参数是事件的有效负载,可以是父级可能发现有用的任何值。在本例中,我发送了一个名为productSubmit的事件,并将product对象作为有效载荷。

从子组件接收自定义事件

父组件使用v-on指令从其子组件接收事件,就像常规的 DOM 事件一样。在清单 16-14 中,我已经更新了App.vue文件,以便为子组件提供初始数据进行编辑,并在事件被触发时对其做出响应。

<template>
    <div class="bg-secondary text-white text-center m-2 p-2 h5">
        <h6>{{ message }}</h6>
        <my-feature v-bind:initial-product="product"

                    v-on:productSubmit="updateProduct">

        </my-feature>
    </div>
</template>

<script>
    import ChildComponent from "./components/Child";

    export default {
        name: 'App',
        components: {
            MyFeature: ChildComponent
        },
        data: function () {
            return {
                message: "Ready",

                product: {

                    name: "Kayak",

                    category: "Watersports",

                    price: 275

                }

            }
        },
        methods: {

            updateProduct(newProduct) {

                this.message = JSON.stringify(newProduct);

            }

        }

    }
</script>

Listing 16-14Responding to Child Component Events in the App.vue File in the src Folder

v-on指令用于监听子组件的定制事件,使用作为第一个参数传递给$emit方法的名称,在本例中是productSubmit:

...
<my-feature v-bind:initial-product="product" v-on:productSubmit="updateProduct">
...

在这种情况下,v-on绑定用于通过调用updateProduct方法来响应productSubmit事件。父组件使用的方法接收子组件用作$emit方法的第二个参数的可选有效负载,在本例中,有效负载的 JSON 表示被分配给名为messagedata属性,该属性通过文本插值绑定显示给用户。结果是您可以编辑子组件显示的值,点击提交按钮,并查看父组件接收的数据,如图 16-10 所示。

注意

定制事件的行为不像常规的 DOM 事件,即使使用了v-on指令来处理它们。自定义事件只传递给父组件,不会通过 DOM 中 HTML 元素的层次结构传播,并且没有捕获、目标和冒泡阶段。如果你想超越父子关系进行交流,那么你可以使用事件总线,如第十八章所述,或者共享状态,如第二十章所述。

img/465686_1_En_16_Fig10_HTML.jpg

图 16-10

从子组件处理自定义事件

使用组件插槽

如果您在应用的不同部分使用一个组件,您可能希望定制它呈现给用户的 HTML 元素的外观以适应上下文。

对于简单的内容更改,可以使用 prop,或者父组件可以直接样式化用于应用子组件的自定义 HTML 元素。

对于更复杂的内容更改,Vue.js 提供了一个名为 slots 的特性,它允许父组件提供内容,通过这些内容子组件提供的特性将被显示。在清单 16-15 中,我给子组件添加了一个插槽,它将用于显示父组件提供的内容。

...
<template>
    <div class="bg-primary text-white text-center m-2 p-3 h6">
        <slot>

            <h4>Use the form fields to edit the data</h4>

        </slot>

        <div class="form-group m-1 text-left">
            <label>Name</label>
            <input v-model="product.name" class="form-control" />
        </div>
        <div class="form-group m-1 text-left">
            <label>Category</label>
            <input v-model="product.category" class="form-control" />
        </div>
        <div class="form-group m-1 text-left">
            <label>Price</label>
            <input v-model.number="product.price" class="form-control" />
        </div>
        <div class="mt-2">
            <button class="btn btn-info" v-on:click="doSubmit">Submit</button>
        </div>
    </div>
</template>
...

Listing 16-15Adding a Slot in the Child.vue File in the src/components Folder

slot元素表示组件模板的一个区域,该区域将被替换为父组件在用于应用子组件的定制元素的开始和结束标记之间包含的任何内容。如果父组件不提供任何内容,那么 Vue.js 将忽略 slot 元素,这是你保存Child.vue文件并在浏览器中检查内容时会看到的,如图 16-11 所示。

小费

您可能需要重新加载浏览器才能看到这个示例的效果。

img/465686_1_En_16_Fig11_HTML.jpg

图 16-11

在槽中显示默认内容

这提供了一个回退,允许子组件向用户显示有用的内容。为了覆盖默认内容,父组件必须在其模板中包含元素,如清单 16-16 所示。

...
<template>
    <div class="bg-secondary text-white text-center m-2 p-2 h5">
        <h6>{{ message }}</h6>
        <my-feature v-bind:initial-product="product"
                    v-on:productSubmit="updateProduct">
            <div class="bg-warning m-2 p-2 h3 text-dark">Product Editor</div>

        </my-feature>
    </div>
</template>
...

Listing 16-16Providing Elements for a Slot in the App.vue File in the src Folder

出现在my-feature元素的开始和结束标记之间的div元素被用作子组件模板中slot元素的内容,产生如图 16-12 所示的结果。

img/465686_1_En_16_Fig12_HTML.jpg

图 16-12

为子组件的插槽提供内容

使用命名插槽

如果一个子组件可以从它的父组件接收几个区域的内容,那么它可以给它的槽分配名称,如清单 16-17 所示。

...
<template>
    <div class="bg-primary text-white text-center m-2 p-3 h6">
        <slot name="header">

            <h4>Use the form fields to edit the data</h4>

        </slot>

        <div class="form-group m-1 text-left">
            <label>Name</label>
            <input v-model="product.name" class="form-control" />
        </div>
        <div class="form-group m-1 text-left">
            <label>Category</label>
            <input v-model="product.category" class="form-control" />
        </div>
        <div class="form-group m-1 text-left">
            <label>Price</label>
            <input v-model.number="product.price" class="form-control" />
        </div>
        <slot name="footer"></slot>

        <div class="mt-2">
            <button class="btn btn-info" v-on:click="doSubmit">Submit</button>
        </div>
    </div>
</template>
...

Listing 16-17Adding Named Slots in the Child.vue File in the src/components Folder

name属性用于为每个slot元素指定名称。在这个例子中,我给出现在input元素上面的元素指定了名称header,给出现在它们下面的slot指定了名称 footer。footer槽不包含任何元素,这意味着除非父组件提供内容,否则不会显示任何内容。

为了使用一个命名的 slot,父元素将一个 slot 属性添加到包含在自定义开始和结束标记之间的一个元素中,如清单 16-18 所示。

...
<template>
    <div class="bg-secondary text-white text-center m-2 p-2 h5">
        <h6>{{ message }}</h6>
        <my-feature v-bind:initial-product="product"
                    v-on:productSubmit="updateProduct">
            <div slot="header" class="bg-warning m-2 p-2 h3 text-dark">

                 Product Editor

            </div>

            <div slot="footer" class="bg-warning p-2 h3 text-dark">

                Check Details Before Submitting

             </div>

        </my-feature>
    </div>
</template>
...

Listing 16-18Using Names Slots in the App.vue File in the src Folder

父元素已经为每个指定的槽提供了div元素,这产生了如图 16-13 所示的结果。

小费

如果子组件定义了一个未命名的槽,那么它将显示父模板中没有分配给带有slot属性的槽的任何元素。如果子组件没有定义未命名的插槽,那么父组件的未分配元素将被丢弃。

img/465686_1_En_16_Fig13_HTML.jpg

图 16-13

使用命名插槽

使用作用域插槽

作用域槽允许父组件提供一个模板,子组件可以在其中插入数据,这在子组件使用从父组件接收的数据执行转换,并且父组件需要控制格式时非常有用。为了演示作用域插槽特性,我在src/components文件夹中添加了一个名为ProductDisplay.vue的文件,并添加了清单 16-19 中所示的内容。

<template>
    <ul>
        <li v-for="prop in Object.keys(product)" v-bind:key="prop">
            <slot v-bind:propname="prop" v-bind:propvalue="product[prop]">
                {{ prop }}: {{ product[prop] }}
            </slot>
        </li>
    </ul>
</template>

<script>
export default {
    props: ["product"]
}
</script>

Listing 16-19The Contents of the ProductDisplay.vue File in the src/components Folder

这个组件通过一个名为product的 prop 接收一个对象,它的属性和值使用v-for指令在模板中被枚举。slot元素为父元素提供了一个替换显示属性名和值的内容的机会,但是增加了以下重要属性:

...
<slot v-bind:propname="prop" v-bind:propvalue="product[prop]">
...

propnamepropvalue属性允许父组件将分配给它们的值合并到插槽内容中,如清单 16-20 所示。

<template>
    <div class="bg-secondary text-white text-center m-2 p-2 h5">
        <product-display v-bind:product="product">

            <div slot-scope="data" class="bg-info text-left">

                {{data.propname}} is {{ data.propvalue }}

            </div>

        </product-display>

        <my-feature v-bind:initial-product="product"
                    v-on:productSubmit="updateProduct">
            <div slot="header" class="bg-warning m-2 p-2 h3 text-dark">
                Product Editor
            </div>
            <div slot="footer" class="bg-warning p-2 h3 text-dark">
                Check Details Before Submitting
            </div>
        </my-feature>
    </div>
</template>

<script>
    import ChildComponent from "./components/Child";
    import ProductDisplay from"./components/ProductDisplay";

    export default {
        name: 'App',
        components: {
            MyFeature: ChildComponent,
            ProductDisplay

        },
        data: function () {
            return {
                message: "Ready",
                product: {
                    name: "Kayak",
                    category: "Watersports",
                    price: 275
                }
            }
        },
        methods: {
            updateProduct(newProduct) {
                this.message = JSON.stringify(newProduct);
            }
        }
    }
</script>

Listing 16-20Using a Scoped Slot in the App.vue File in the src Folder

slot-scope属性用于选择在处理模板时创建的临时变量的名称,并且将为子元素在其slot元素上定义的每个属性分配一个属性,然后可以在插槽内容中的数据绑定和指令中使用该属性,如下所示:

...
<product-display v-bind:product="product">
    <div slot-scope="data" class="bg-info text-left">
        {{data.propname}} is {{ data.propvalue }}
    </div>
</product-display>
...

结果是来自父组件的 HTML 元素与子组件提供的数据混合,产生如图 16-14 所示的结果。

小费

作用域 slot 内容中的表达式和数据绑定是在父组件的上下文中计算的,这意味着 Vue.js 将查找您引用的任何值,只要它没有以父组件的script元素中的slot-scope属性中命名的变量为前缀。

img/465686_1_En_16_Fig14_HTML.jpg

图 16-14

使用作用域插槽

摘要

在这一章中,我描述了组件作为 Vue.js 应用的构建块的使用方式,允许相关的内容和代码被分组以便于开发和维护。我解释了组件在默认情况下是如何被隔离的,prop 和 custom event 特性如何允许组件进行通信,以及 slots 特性如何允许父组件向子组件提供内容。在本书的第三部分,我描述了高级的 Vue.js 特性。

十七、了解组件生命周期

当 Vue.js 创建一个组件时,它就开始了一个定义良好的生命周期,包括准备数据值、在文档对象模型(DOM)中呈现 HTML 内容以及处理任何更新。在这一章中,我描述了组件生命周期中的不同阶段,并演示了组件对这些阶段做出响应的各种方式。

组件生命周期值得您关注有两个原因。首先,你对 Vue.js 的工作原理了解得越多,当你没有得到你期望的结果时,你对诊断问题的准备就越充分。第二个原因是,我在后面的章节中描述的一些高级特性在您理解它们的工作环境时会更容易理解和应用。表 17-1 将组件生命周期放在上下文中。

表 17-1

将组件生命周期放在上下文中

|

问题

|

回答

| | --- | --- | | 这是什么? | 每个组件都遵循一个明确定义的生命周期,从 Vue.js 创建它开始,到它被销毁结束。 | | 为什么有用? | 定义良好的生命周期使得 Vue.js 组件可预测,并且有通知方法允许组件响应不同的生命周期阶段。 | | 如何使用? | Vue.js 自动遵循生命周期,不需要任何直接操作。如果一个组件想要接收关于其生命周期的通知,那么它可以实现表 17-3 中描述的方法。 | | 有什么陷阱或限制吗? | 实现生命周期通知方法的一个主要原因是使用 DOM API 直接访问组件的 HTML 内容,而不是使用指令或其他 Vue.js 特性。这可能会破坏应用的设计,并使其更难测试和维护。 | | 还有其他选择吗? | 您不必关注组件的生命周期,许多项目根本不需要实现通知方法。 |

表 17-2 总结了本章内容。

表 17-2

章节总结

|

问题

|

解决办法

|

列表

| | --- | --- | --- | | 创建组件时收到通知 | 实施beforeCreatecreated方法 | eight | | 当允许访问 DOM 时接收通知 | 实施beforeMountmounted方法 | 9, 10 | | 当数据属性更改时接收通知 | 实施beforeUpdateupdated方法 | 11–12 | | 更新后执行任务 | 使用Vue.nextTick方法 | Thirteen | | 接收单个数据属性的通知 | 使用观察器 | Fourteen | | 当组件被销毁时收到通知 | 实施beforeDestroydestroyed方法 | 15, 16 | | 出现错误时收到通知 | 实现errorCaptured方法 | 17, 18 |

为本章做准备

对于本章中的例子,在一个方便的位置运行清单 17-1 中所示的命令来创建一个新的 Vue.js 项目。

小费

你可以从 https://github.com/Apress/pro-vue-js-2 下载本章以及本书其他章节的示例项目。

vue create lifecycles --default

Listing 17-1Creating a New Project

这个命令创建了一个名为生命周期的项目。项目创建完成后,将名为vue.config.js的文件添加到lifecycles文件夹中,内容如清单 17-2 所示。这个文件用于启用将模板定义为字符串的能力,如第十章所述,我在本章的一个例子中使用了它。

module.exports = {
    runtimeCompiler: true
}

Listing 17-2The Contents of the vue.config.js File in the nomagic Folder

将清单 17-3 中所示的语句添加到package.json文件的 linter 部分,以禁用在使用 JavaScript 控制台时发出警告的规则。本章中的许多例子我都依赖于控制台。

...
"eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
        "plugin:vue/essential",
        "eslint:recommended"
     ],
     "rules": {
        "no-console": "off"

     },
     "parserOptions": {
        "parser": "babel-eslint"
     }
},
...

Listing 17-3Disabling a Linter Rule in the package.json File in the lifecycles Folder

运行lifecycles文件夹中清单 17-4 所示的命令,将引导 CSS 包添加到项目中。

npm install bootstrap@4.0.0

Listing 17-4Adding the Bootstrap CSS Package

将清单 17-5 中所示的语句添加到src文件夹中的main.js文件中,将引导 CSS 文件合并到应用中。

import Vue from 'vue'
import App from './App.vue'

import "bootstrap/dist/css/bootstrap.min.css";

Vue.config.productionTip = false

new Vue({
  render: h => h(App)
}).$mount('#app')

Listing 17-5Incorporating the Bootstrap Package in the main.js File in the src Folder

最后的准备步骤是替换根组件的内容,如清单 17-6 所示。

<template>
    <div class="bg-primary text-white m-2 p-2">
        <div class="form-check">
            <input class="form-check-input" type="checkbox" v-model="checked" />
            <label>Checkbox</label>
        </div>
        Checked Value: {{ checked }}
    </div>
</template>

<script>

export default {
    name: 'App',
    data: function () {
        return {
            checked: true
        }
    }
}
</script>

Listing 17-6Replacing the Contents of the App.vue File in the src Folder

运行productapp文件夹中清单 17-7 所示的命令,启动开发工具。

npm run serve

Listing 17-7Starting the Development Tools

将执行初始绑定过程,之后您将看到一条消息,告诉您项目已成功编译,HTTP 服务器正在侦听端口 8080 上的请求。打开一个新的浏览器窗口,导航到http://localhost:8080查看项目的占位符内容,如图 17-1 所示。

img/465686_1_En_17_Fig1_HTML.jpg

图 17-1

运行示例应用

了解组件生命周期

组件生命周期从 Vue.js 第一次初始化组件时开始。生命周期包括准备数据属性、处理模板、处理数据更改,以及最终销毁不再需要的组件。对于生命周期中的每个阶段,Vue.js 都提供了方法,如果由组件实现,它将调用这些方法。在表 17-3 中,我描述了每一种组件生命周期方法。

表 17-3

组件生命周期方法

|

名字

|

描述

| | --- | --- | | beforeCreate | 这个方法在 Vue.js 初始化组件之前被调用,如“理解创建阶段”一节所述。 | | created | 这个方法在 Vue.js 初始化一个组件后被调用,如“理解创建阶段”一节所述。 | | beforeMount | 这个方法在 Vue.js 处理组件的模板之前被调用,如“理解安装阶段”一节所述。 | | mounted | 这个方法在 Vue.js 处理组件的模板之后被调用,如“理解安装阶段”一节所述。 | | beforeUpdate | 这个方法在 Vue.js 处理组件数据更新之前调用,如“理解更新阶段”一节所述。 | | updated | 这个方法在 Vue.js 处理组件数据更新后调用,如“理解更新阶段”一节所述。 | | activated | 如第二十二章所述,当一个用keep-alive元素保持活动的组件被激活时,该方法被调用。 | | deactivated | 如第二十二章所述,当通过keep-alive元素保持活动的组件被停用时,该方法被调用。 | | beforeDestroy | 这个方法在 Vue.js 销毁组件之前被调用,如“理解销毁阶段”一节所述。 | | destroyed | 这个方法在 Vue.js 销毁一个组件后被调用,如“理解销毁阶段”一节所述。 | | errorCaptured | 这个方法允许组件处理由它的一个子组件抛出的错误,如“处理组件错误”一节所述。 |

了解创建阶段

这是生命周期的初始阶段,Vue.js 创建一个组件的新实例并准备使用,包括处理script元素中的属性,比如datacomputed属性。在创建组件之后——但在处理其配置对象之前——vue . js 调用beforeCreate方法。一旦 Vue.js 处理了配置属性,包括数据属性,就会调用created方法。在清单 17-8 中,我将这个阶段的两种方法都添加到了组件中。

<template>
    <div class="bg-primary text-white m-2 p-2">
        <div class="form-check">
            <input class="form-check-input" type="checkbox" v-model="checked" />
            <label>Checkbox</label>
        </div>
        Checked Value: {{ checked }}
    </div>
</template>

<script>

export default {
    name: 'App',
    data: function () {
        return {
            checked: true
        }
    },
    beforeCreate() {

        console.log("beforeCreate method called" + this.checked);

    },

    created() {

        console.log("created method called" + this.checked);

    }

}
</script>

Listing 17-8Adding Creation Lifecycle Methods in the App.vue File to the src Folder

在清单中,beforeCreatecreated方法都向浏览器的 JavaScript 控制台写入一条消息,其中包含了checked属性的值。

注意

生命周期方法直接在对象的script元素中定义,而不是在methods属性下定义。

beforeCreatecreated方法之间,Vue.js 设置了使组件有用的特性,包括方法和数据属性,你可以看到浏览器的 JavaScript 控制台中显示的消息。

...
beforeCreate method called undefined
created method called true
...

当调用beforeCreate方法时,Vue.js 还没有设置组件的data属性,因此checked属性的值是undefined。在调用created方法时,设置已经完成,并且checked属性已经创建并被赋予了初始值。

了解组件对象的创建

beforeCreate方法显示的消息显示 Vue.js 已经创建了组件,并在调用方法之前将其分配给了this。分配给this的对象是组件,如果一个应用中使用一个组件的多个实例,那么每个实例都有一个对象。当 Vue.js 经历初始化过程时,data属性、computed属性和方法被分配给这个对象。例如,当您定义一个方法时,您使用用于创建组件的配置对象上的methods属性;在初始化期间,Vue.js 将您定义的函数分配给组件对象,这样您就可以作为this.myMethod()调用它,而无需担心配置对象的结构。当 Vue.js 第一次创建组件对象时,它几乎没有什么有用的特性,大多数项目都不需要实现beforeCreate事件。

了解反应性准备

Vue.js 的一个关键特性是反应性,其中对数据属性的更改会自动传播到整个应用,触发对计算属性、数据绑定和属性的更新,以确保一切都是最新的。

在创建阶段,Vue.js 从组件的script元素处理组件的配置对象。一个属性被添加到每个data属性的组件对象中,带有一个 getter 和 setter,这样 Vue.js 可以检测到属性何时被读取或修改。这意味着 Vue.js 能够检测属性何时被使用,并更新应用受影响的部分——但这也意味着在 Vue.js 执行创建阶段之前,您必须确保您已经定义了所有您需要的data属性。

小费

如果一个应用需要外部数据,比如来自 RESTful API 的数据,那么created方法提供了一个请求数据的好机会,如第二十章所示。

了解安装阶段

在组件生命周期的第二阶段,Vue.js 处理组件的模板,处理数据绑定、指令和其他应用特性。

访问文档对象模型

如果一个组件需要访问文档对象模型(DOM ),那么mounted事件表明组件的内容已经被处理,可以通过一个名为$el的属性访问,这个属性是 Vue.js 在组件对象上定义的。

在清单 17-9 中,我访问了 DOM 来获取数据值,这些数据值是通过应用组件的 HTML 元素上的属性提供的。正如我在第十六章中解释的,任何应用于 HTML 元素的属性都会被转移到组件模板的顶层元素中。

警告

只有当没有更适合 Vue.js 模型的替代方法时,才应该直接访问 DOM。在这个例子中,数据是通过属性提供的,当把 Vue.js 集成到一个以编程方式生成 HTML 文档的环境中时,这个例子会很有用。在更好的方法实现之前,这些方法应该作为临时措施,比如使用 RESTful API,如第二十章所述。

<template>
    <div class="bg-primary text-white m-2 p-2">
        <div class="form-check">
            <input class="form-check-input" type="checkbox" v-model="checked" />
            <label>Checkbox</label>
        </div>
        Checked Value: {{ checked }}
        <div class="bg-info p-2">

            Names:

            <ul>

                <li v-for="name in names" v-bind:key="name">

                    {{ name }}

                </li>

            </ul>

        </div>

    </div>
</template>

<script>

    export default {
        name: 'App',
        data: function () {
            return {
                checked: true,
                names: []

            }
        },
        beforeCreate() {
            console.log("beforeCreate method called" + this.checked);
        },
        created() {
            console.log("created method called" + this.checked);
        },
        mounted() {

            this.$el.dataset.names.split(",")

                .forEach(name => this.names.push(name));

        }

    }
</script>

Listing 17-9Accessing the DOM in the App.vue File in the src Folder

在处理完模板并将其内容添加到 DOM 之后,将调用mounted方法,此时添加到应用该组件的定制 HTML 元素的属性将已经从清单 17-9 传输到顶层div元素。在mounted方法中,我使用this.$el属性来访问一组data-属性,以读取data-names属性的值,创建一个数组,并将每一项推入names数据属性,其值使用v-for指令显示在一个列表中。

小费

注意,在清单 17-9 中,我已经定义了names属性并给它分配了一个空数组。在对所有data属性进行处理以设置反应性之前,对其进行定义是很重要的;否则,将不会检测到更改。

在清单 17-10 中,我为应用组件的 HTML 元素添加了一个data-name属性。

import Vue from 'vue'
import App from './App'

import "bootstrap/dist/css/bootstrap.min.css";

Vue.config.productionTip = false

new Vue({
  el: '#app',
  components: { App },
  template: '<App data-names="Bob, Alice, Peter, Dora" />'

})

Listing 17-10Adding an Attribute in the main.js File in the src Folder

结果是组件使用浏览器提供的 DOM API 来读取应用于其元素的属性,并使用其内容来设置数据属性,如图 17-2 所示。

img/465686_1_En_17_Fig2_HTML.jpg

图 17-2

在挂载生命周期阶段访问 DOM

了解更新阶段

一旦组件被初始化并且它的内容被装载,它将进入更新阶段,在这个阶段 Vue.js 对数据属性的更改做出反应。当检测到一个变化时,Vue.js 将调用一个组件的beforeUpdate方法,并且一旦更新已经被执行并且 HTML 内容已经被更新以反映该变化,就调用该组件的updated方法。

大多数项目不需要使用beforeUpdateupdated方法,提供这两个方法是为了让组件可以在更改前后直接访问 DOM。与上一节中的直接 DOM 访问一样,这不是一件轻而易举的事情,应该只在没有标准 Vue.js 特性适用时才执行。为了完整起见,在清单 17-11 中,我为这个阶段实现了两种方法。

...
<script>
    export default {
        name: 'App',
        data: function () {
            return {
                checked: true,
                names: []
            }
        },
        beforeCreate() {
            console.log("beforeCreate method called" + this.checked);
        },
        created() {
            console.log("created method called" + this.checked);
        },
        mounted() {
            this.$el.dataset.names.split(",")
                .forEach(name => this.names.push(name));
        },
        beforeUpdate() {

            console.log(`beforeUpdate called. Checked: ${this.checked}`

                + ` Name: ${this.names[0]} List Elements: `

                + this.$el.getElementsByTagName("li").length);

        },

        updated() {

            console.log(`updated called. Checked: ${this.checked}`

                + ` Name: ${this.names[0]} List Elements: `

                + this.$el.getElementsByTagName("li").length);

        }

    }
</script>
...

Listing 17-11Implementing the Update Phase Methods in the App.vue File in the src Folder

beforeUpdateupdated方法向浏览器的 JavaScript 控制台写出一条消息,其中包括checked属性的值,包括names数组中的第一个值和组件的 HTML 内容中的li元素的数量,我通过 DOM 访问这些内容。当您保存对组件的更改时,您将在浏览器的 JavaScript 控制台中看到以下消息:

...
beforeCreate method called undefined
created method called true
beforeUpdate called. Checked: true Name: Bob List Elements: 0
updated called. Checked: true Name: Bob List Elements: 4
...

这个消息序列揭示了 Vue.js 为组件所经历的初始化过程。在created方法中,我将值添加到组件的一个data属性中,这导致呈现给用户的 HTML 内容发生相应的变化。当调用beforeUpdate方法时,组件的内容中没有li元素,当调用updated方法并且 Vue.js 已经完成处理更新时,有四个li元素。

了解更新整合

注意只有一次对beforeUpdateupdated方法的调用,尽管我向names数组中添加了四项。Vue.js 不响应单独的数据更改,这将导致一系列单独的 DOM 更新,执行起来代价很高。相反,Vue.js 维护一个等待更新的队列,删除重复的更新,并允许一起批量更改。为了提供更清晰的演示,我向组件添加了一个button元素,它导致两个data属性都发生了变化,如清单 17-12 所示。

<template>
    <div class="bg-primary text-white m-2 p-2">
        <div class="form-check">
            <input class="form-check-input" type="checkbox" v-model="checked" />
            <label>Checkbox</label>
        </div>
        Checked Value: {{ checked }}
        <div class="bg-info p-2">
            Names:
            <ul>
                <li v-for="name in names" v-bind:key="name">
                    {{ name }}
                </li>
            </ul>
        </div>
        <div class="text-white center my-2">

            <button class="btn btn-light" v-on:click="doChange">

                Change

            </button>

        </div>

    </div>
</template>

<script>
    export default {
        name: 'App',
        data: function () {
            return {
                checked: true,
                names: []
            }
        },
        beforeCreate() {
            console.log("beforeCreate method called" + this.checked);
        },
        created() {
            console.log("created method called" + this.checked);
        },
        mounted() {
            this.$el.dataset.names.split(",")
                .forEach(name => this.names.push(name));
        },
        beforeUpdate() {
            console.log(`beforeUpdate called. Checked: ${this.checked}`
                + ` Name: ${this.names[0]} List Elements: `
                + this.$el.getElementsByTagName("li").length);
        },
        updated() {
            console.log(`updated called. Checked: ${this.checked}`
                + ` Name: ${this.names[0]} List Elements: `
                + this.$el.getElementsByTagName("li").length);
        },
        methods: {

            doChange() {

                this.checked = !this.checked;

                this.names.reverse();

            }

        }

    }
</script>

Listing 17-12Making Multiple Changes in the App.vue File in the src Folder

点击按钮调用doChange方法,该方法切换checked属性的值并反转names数组中的项目顺序,如图 17-3 所示。

img/465686_1_En_17_Fig3_HTML.jpg

图 17-3

进行多项更改

尽管对两个不同的数据属性进行了更改,但浏览器的 JavaScript 控制台中显示的消息显示,只对组件的 HTML 内容执行了一次更新。

...
beforeUpdate called. Checked: false Name:  Dora List Elements: 4
updated called. Checked: false Name:  Dora List Elements: 4
...

小费

只有在需要更改一个或多个 HTML 元素时,才会调用beforeUpdateupdated方法。如果您对数据属性所做的更改不需要对组件的 HTML 内容进行相应的更改,那么这些方法将不会被调用。

使用更新后回调

Vue.js 合并和推迟更新的方式的一个后果是,您不能进行数据更改,然后直接使用 DOM,并期望更改的效果已经应用到组件的 HTML 元素。Vue.js 提供了Vue.nextTick方法,该方法可用于在应用了所有挂起的更改之后执行任务。在清单 17-13 中,我在doChange方法中使用了nextTick来说明动作和方法调用的顺序。

...
<script>

    import Vue from "vue";

    export default {
        name: 'App',
        data: function () {
            return {
                checked: true,
                names: []
            }
        },
        beforeCreate() {
            console.log("beforeCreate method called" + this.checked);
        },
        created() {
            console.log("created method called" + this.checked);
        },
        mounted() {
            this.$el.dataset.names.split(",")
                .forEach(name => this.names.push(name));
        },
        beforeUpdate() {
            console.log(`beforeUpdate called. Checked: ${this.checked}`
                + ` Name: ${this.names[0]} List Elements: `
                + this.$el.getElementsByTagName("li").length);
        },
        updated() {
            console.log(`updated called. Checked: ${this.checked}`
                + ` Name: ${this.names[0]} List Elements: `
                + this.$el.getElementsByTagName("li").length);
        },
        methods: {
            doChange() {
                this.checked = !this.checked;
                this.names.reverse();
                Vue.nextTick(() => console.log("Callback Invoked"));

            }
        }
    }
</script>
...

Listing 17-13Using a Post-Update Callback in the App.vue File in the src Folder

nextTick方法接受一个函数和一个可选的上下文对象作为其参数,该函数将在下一个更新周期结束时被调用。保存更改,单击按钮,您将在浏览器的 JavaScript 控制台中看到以下消息序列:

...
beforeUpdate called. Checked: false Name:  Dora List Elements: 4
updated called. Checked: false Name:  Dora List Elements: 4

Callback Invoked

...

导致更新的更改是由doChange方法触发的,但是使用nextTick方法可以确保在处理完更改的影响和更新 DOM 之前不会调用回调。

使用观察器观察数据变化

beforeUpdateupdate方法告诉你组件的 HTML 元素何时更新,但不告诉你做了什么改变,如果改变是对一个没有在指令表达式或数据绑定中引用的data属性,这些方法根本不会被调用。

如果您希望在数据属性更改时收到通知,那么您可以使用观察器。在清单 17-14 中,我添加了一个观察器,它将在名为checkeddata属性被更改时提供通知。

...
<script>

    import Vue from "vue";

    export default {
        name: 'App',
        data: function () {
            return {
                checked: true,
                names: []
            }
        },
        beforeCreate() {
            console.log("beforeCreate method called" + this.checked);
        },
        created() {
            console.log("created method called" + this.checked);
        },
        mounted() {
            this.$el.dataset.names.split(",")
                .forEach(name => this.names.push(name));
        },
        beforeUpdate() {
            console.log(`beforeUpdate called. Checked: ${this.checked}`
                + ` Name: ${this.names[0]} List Elements: `
                + this.$el.getElementsByTagName("li").length);
        },
        updated() {
            console.log(`updated called. Checked: ${this.checked}`
                + ` Name: ${this.names[0]} List Elements: `
                + this.$el.getElementsByTagName("li").length);
        },
        methods: {
            doChange() {
                this.checked = !this.checked;
                this.names.reverse();
                Vue.nextTick(() => console.log("Callback Invoked"));
            }
        },
        watch: {

            checked: function (newValue, oldValue) {

                console.log(`Checked Watch, Old: ${oldValue}, New: ${newValue}`);

            }

        }

    }
</script>
...

Listing 17-14Using a Watcher in the App.vue File in the src Folder

使用组件的配置对象上的watch属性来定义观察器,该配置对象被分配了一个对象,该对象包含要观察的每个属性的属性。观察器需要属性的名称和一个处理函数,该函数将用新值和以前的值调用。在清单中,我为checked属性定义了一个观察器,它将新值和旧值写出到浏览器的 JavaScript 控制台。保存更改并取消选中复选框,您将看到以下消息序列,包括来自观察器的消息:

...

Checked Watch, Old: true, New: false

beforeUpdate called. Checked: false Name: Bob List Elements: 4
updated called. Checked: false Name: Bob List Elements: 4
...

注意观察器的处理函数在beforeUpdateupdated方法之前被调用。

使用观察选项

有两个选项可以用来改变观察器的行为,尽管它们要求观察器以不同的方式表达。第一个选项是immediate,它告诉 Vue.js 在创建阶段,在调用beforeCreatecreated方法之间,一旦设置了观察器,就调用处理程序。要使用该特性,观察器被表示为一个具有handler属性和立即属性的对象,如下所示:

...
watch: {
    checked: {
        handler: function (newValue, oldValue) {
           // respond to changes here
        },
        immediate: true
     }
}
...

另一个选项是deep,它将监视由分配给一个数据属性的对象定义的所有属性,这比必须为每个属性设置单独的监视器更方便。deep选项的应用如下:

...
watch: {
    myObject: {
        handler: function (newValue, oldValue) {
           // respond to changes here
        },
        deep: true

     }
}
...

deep选项不能用于创建所有数据属性的观察器,只能用于已分配对象的单个属性。当你为一个观察器使用一个函数时,你不能使用一个箭头表达式,因为this不会被赋值给值已经改变的组件。相反,使用传统的function关键字,如图所示。

了解销毁阶段

组件生命周期的最后阶段是它的销毁,这通常发生在组件被动态显示并且不再需要时,如第二十二章所示。Vue.js 将调用beforeDestroy方法让组件有机会准备自己,并将在销毁过程之后调用destroy方法,这将删除观察器、事件处理程序和任何子组件。

为了帮助演示生命周期的这一部分,我在src/components文件夹中添加了一个名为MessageDisplay.vue的文件,其内容如清单 17-15 所示。

<template>
    <div class="bg-dark text-light text-center p-2">
        <div>
            Counter Value: {{ counter }}
        </div>
        <button class="btn btn-secondary" v-on:click="handleClick">
            Increment
        </button>
    </div>
</template>

<script>
export default {
    data: function () {
        return {
            counter: 0
        }
    },
    created: function() {
        console.log("MessageDisplay: created");
    },
    beforeDestroy: function() {
        console.log("MessageDisplay: beforeDestroy");
    },
    destroyed: function() {
        console.log("MessageDisplay: destroyed");
    },
    methods: {
        handleClick() {
            this.counter++;
        }
    }
}
</script>

Listing 17-15The Contents of the MessageDisplay.vue File in the src/components Folder

该组件显示一个计数器,可以通过单击按钮来递增。它还实现了createdbeforeDestroydestroyed方法,这样就可以在写入浏览器 JavaScript 控制台的消息中看到组件生命周期的开始和结束。在清单 17-16 中,我已经简化了示例应用的根组件,并应用了新的组件,以便它仅在名为 checked 的data值为true时才显示。

<template>
    <div class="bg-primary text-white m-2 p-2">
        <div class="form-check">
            <input class="form-check-input" type="checkbox" v-model="checked" />
            <label>Checkbox</label>
        </div>
        Checked Value: {{ checked }}
        <div class="bg-info p-2" v-if="checked">

            <message-display></message-display>

        </div>

    </div>
</template>

<script>
    import MessageDisplay from "./components/MessageDisplay"

    export default {
        name: 'App',
        components: { MessageDisplay },

        data: function () {
            return {
                checked: true,
                names: []
            }
        }
    }
</script>

Listing 17-16Applying a Child Component in the App.vue File in the src Folder

当复选框被切换时,Vue.js 创建并销毁MessageDisplay组件,如图 17-4 所示。

img/465686_1_En_17_Fig4_HTML.jpg

图 17-4

创建和销毁组件

警告

你不应该依赖销毁阶段来执行重要的任务,因为 Vue.js 并不总是能够完成生命周期。例如,如果用户导航到另一个 URL,整个应用将被终止,没有机会完成组件的生命周期。

当您取消选中该复选框时,子组件将被销毁,其内容将从 DOM 中删除。选中该复选框后,将创建一个新组件,生命周期将重新开始。如果您检查浏览器的 JavaScript 控制台,当一个组件被销毁而另一个组件被创建时,您会看到如下消息:

...
MessageDisplay: beforeDestroy
MessageDisplay: destroyed
MessageDisplay: created
...

如果您在切换复选框之前单击增量按钮,您将能够看到组件的状态在被销毁时丢失。参见第二十一章,了解如何为一个应用创建一个共享状态的详细信息,该共享状态将在单个组件生命周期变化后继续存在。

处理组件错误

有一个阶段在标准生命周期之外,它发生在子组件中出现错误的时候。为了演示这个阶段,我向MessageDisplay组件添加了清单 17-17 中所示的元素和代码,以便在单击按钮时生成一个错误。

<template>
    <div class="bg-dark text-light text-center p-2">
        <div>
            Counter Value: {{ counter }}
        </div>
        <button class="btn btn-secondary" v-on:click="handleClick">
            Increment
        </button>
        <button class="btn btn-secondary" v-on:click="generateError">

            Generate Error

        </button>

    </div>
</template>

<script>
export default {
    data: function () {
        return {
            counter_base: 0,

            generate_error: false

        }
    },
    created: function() {
        console.log("MessageDisplay: created");
    },
    beforeDestroy: function() {
        console.log("MessageDisplay: beforeDestroy");
    },
    destroyed: function() {
        console.log("MessageDisplay: destroyed");
    },
    methods: {
        handleClick() {
            this.counter_base++;

        },
        generateError() {

            this.generate_error = true;

        }

    },
    computed: {

        counter() {

            if (this.generate_error) {

                throw "My Component Error";

            } else {

                return this.counter_base;

            }

        }

    }

}
</script>

Listing 17-17Generating an Error in the MessageDisplay.vue File in the src/components Folder

组件错误处理支持可以处理在修改组件数据、执行监视功能或调用某个生命周期方法时出现的错误。当处理一个事件出错时,它不起作用,这就是为什么清单 17-17 中的代码通过设置一个data属性来响应按钮点击,这个属性使用throw关键字导致counter计算属性生成一个错误。

为了处理来自其子组件的错误,组件实现了errorCaptured方法,该方法定义了三个参数:错误、抛出错误的组件和描述错误发生时 Vue.js 正在做什么的信息字符串。如果errorCaptured方法返回false,错误将不会在组件层次结构中进一步传播,这是阻止多个组件响应同一个错误的有效方法。在清单 17-18 中,我在应用的根组件中实现了errorCaptured方法,并在它的模板中添加了元素来显示它收到的错误的细节。

<template>
    <div class="bg-danger text-white text-center h3 p-2" v-if="error.occurred">

        An Error Has Occurred

        <h4>

            Error : "{{ error.error }}" ({{ error.source }})

        </h4>

    </div>

    <div v-else class="bg-primary text-white m-2 p-2">

        <div class="form-check">
            <input class="form-check-input" type="checkbox" v-model="checked" />
            <label>Checkbox</label>
        </div>
        Checked Value: {{ checked }}
        <div class="bg-info p-2" v-if="checked">
            <message-display></message-display>
        </div>
    </div>
</template>

<script>
    import MessageDisplay from "./components/MessageDisplay"

    export default {
        name: 'App',
        components: { MessageDisplay },
        data: function () {
            return {
                checked: true,
                names: [],
                error: {

                    occurred: false,

                    error: "",

                    source: ""

                }

            }
        },
        errorCaptured(error, component, source) {

            this.error.occurred = true;

            this.error.error = error;

            this.error.source = source;

            return false;

        }

    }
</script>

Listing 17-18Handling an Error in the App.vue File in the src Folder

errorCaptured方法通过设置名为errordata对象的属性来响应错误,该属性通过v-ifv-else指令显示给用户。当您保存更改并点击生成错误按钮时,您将看到如图 17-5 所示的错误信息。

img/465686_1_En_17_Fig5_HTML.jpg

图 17-5

处理错误

定义全局错误处理程序

Vue.js 还提供了一个全局错误处理程序,用来处理应用中组件没有处理的错误。全局处理程序是一个函数,它接收与errorCaptured方法相同的参数,但是定义在main.js文件中应用的顶层Vue对象上,而不是在它的一个组件中。

当应用的其他部分没有处理问题时,全局错误处理程序允许您接收通知,这通常意味着您将无法做任何特别有用的事情来从这种情况中恢复。但是如果您需要在错误还没有被处理的时候执行一个任务,那么您可以在main.js文件中定义一个处理程序,如下所示:

...
import Vue from 'vue'
import App from './App'

import "bootstrap/dist/css/bootstrap.min.css";

Vue.config.productionTip = false

Vue.config.errorHandler = function (error, component, source) {

    console.log(`Global Error Handler: ${error}, ${component}, ${source}`);

}

new Vue({
    el: '#app',
    components: { App },
    template: `<div><App data-names="Bob, Alice, Peter, Dora" /></div>`
})
...

这个处理程序向浏览器的 JavaScript 控制台写入一条消息,这类似于默认行为。有一些错误跟踪服务,允许开发人员通过将错误推送到中央服务器供以后分析来核对和检查错误,其中一些使用全局错误处理程序为 Vue.js 应用提供现成的支持,尽管如果您有大量用户,使用它们可能会变得昂贵。

摘要

在这一章中,我描述了 Vue.js 为其组件提供的生命周期,以及允许您在每次停止时接收通知的方法。我解释了如何处理数据值的更改,如何在 DOM 中访问组件的 HTML 内容,如何使用观察器观察数据值,以及如何处理错误。在下一章,我将解释如何使用松散耦合的组件向 Vue.js 应用添加结构。