VueJS2-高级教程-十一-

65 阅读26分钟

VueJS2 高级教程(十一)

原文:Pro Vue.js 2

协议:CC BY-NC-SA 4.0

二十四、高级 URL 路由

在这一章中,我描述了更高级的 URL 路由功能。首先,我将向您展示如何使用多个文件构建复杂的路由配置,如何使用路由器防护来控制导航,以及如何在路由选择组件时延迟加载组件。在本章的最后,我将向您展示如何使用那些不是为使用 Vue 路由器包而编写的组件。表 24-1 总结了本章内容。

表 24-1

章节总结

|

问题

|

解决办法

|

列表

| | --- | --- | --- | | 分组相关路线 | 为每组路线定义单独的模块 | 3–5 | | 检查和拦截导航 | 使用路线守卫 | 6, 9–11 | | 改变导航的目标 | 在 route guard 的next功能中提供一个替换 URL | 7, 8 | | 在导航完成之前访问组件 | 使用beforeRouteEnter保护方法中的回调功能 | 12–15 | | 延迟加载组件 | 对组件使用动态依赖关系 | 16–21 | | 使用不了解路由系统的组件 | 定义路线时使用道具功能 | 22–23 |

为本章做准备

在本章中,我继续使用第二十三章中的 productapp 项目。本章不需要修改。要启动 RESTful web 服务,打开命令提示符并运行清单 24-1 中的命令。

npm run json

Listing 24-1Starting the Web Service

打开第二个命令提示符,导航到productapp目录,运行清单 24-2 中所示的命令来启动 Vue.js 开发工具。

npm run serve

Listing 24-2Starting the Development Tools

一旦初始捆绑过程完成,打开一个新的浏览器窗口并导航到http://localhost:8080,在那里你将看到示例应用,如图 24-1 所示。

小费

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

img/465686_1_En_24_Fig1_HTML.jpg

图 24-1

运行示例应用

对相关路线使用单独的文件

随着应用支持的 URL 数量的增加,跟踪路由及其支持的应用部分变得更加困难。可以使用额外的 JavaScript 文件对相关的路由进行分组,然后将这些路由导入到主路由配置中。

对路由进行分组没有固定的方法,我在本章中采用的方法是将处理使用命名的router-view元素的组件的并行表示的路由与处理应用其余部分提供的基本功能的路由分开。我在src/router文件夹中添加了一个名为basicRoutes.js的文件,并添加了清单 24-3 中所示的代码。

import ProductDisplay from "../components/ProductDisplay";
import ProductEditor from "../components/ProductEditor";
import Preferences from "../components/Preferences";
import Products from "../components/Products";

export default [

    { path: "/preferences", component: Preferences },
    { path: "/products", component: Products,
      children: [ { name: "table", path: "list", component: ProductDisplay },
                  { name: "editor", path: ":op(create|edit)/:id(\\d+)?",
                      component: ProductEditor
                  },
                 { path: "", redirect: "list" }]
    },
    { path: "/edit/:id", redirect: to => `/products/edit/${to.params.id}` },
]

Listing 24-3The Contents of the basicRoutes.js File in the src/router Folder

该文件导出一组路由,处理应用支持的基本 URL。接下来,我在src/router文件夹中添加了一个名为sideBySideRoutes.js的文件,并添加了清单 24-4 中所示的代码。

import ProductDisplay from "../components/ProductDisplay";
import ProductEditor from "../components/ProductEditor";
import SideBySide from "../components/SideBySide";

export default {

    path: "/named", component: SideBySide,
    children: [
        { path: "tableleft",
          components: { left: ProductDisplay, right: ProductEditor }
        },
        { path: "tableright",
          components: { left: ProductEditor, right: ProductDisplay }
        }
    ]
}

Listing 24-4The Contents of the sideBySideRoutes.js File in the src/router Folder

将这些路由组移动到专用文件中允许我简化index.js文件,如清单 24-5 所示,它显示了我如何导入在basicRoutes.jssideBySideRoutes.js文件中定义的路由。

import Vue from "vue";
import VueRouter from "vue-router";

//import ProductDisplay from "../components/ProductDisplay";

//import ProductEditor from "../components/ProductEditor";

//import Preferences from "../components/Preferences";

//import Products from "../components/Products";

//import SideBySide from "../components/SideBySide";

import BasicRoutes from "./basicRoutes";

import SideBySideRoutes from "./sideBySideRoutes";

Vue.use(VueRouter);

export default new VueRouter({
    mode: "history",
    routes: [
        ...BasicRoutes,

        SideBySideRoutes,

        { path: "*", redirect: "/products" }
    ]
})

Listing 24-5Importing Routes in the index.js File in the src/router Folder

我使用一个import语句导入每个文件的内容,并给内容一个我在routes属性中使用的名称。文件basicRoutes.js导出了一个数组,必须使用 spread 操作符来解包,我在第四章中描述过。sideBySideRoutes.js文件导出单个对象,可以在没有展开操作符的情况下使用。应用的运行方式没有明显的变化,但是路由配置已经被分解,这将使相关路由的管理更加简单。

守卫路线

允许用户使用 URL 在应用中导航的一个缺点是,他们可能会在不希望的情况下尝试访问应用的某些部分,例如在未经身份验证的情况下访问管理功能,或者在从服务器加载数据之前访问编辑功能。导航守卫控制对路线的访问,并且他们可以通过将路线重定向到另一个 URL 或完全取消导航来响应导航尝试。导航保护可以以不同的方式应用,正如我在下面的章节中解释的那样。

定义全球导航卫星系统

全局导航守卫是定义为路线配置的一部分的方法,用于控制对应用中所有路线的访问。有三种全局导航保护方法用于注册功能以控制导航,如表 24-2 所述。

表 24-2

全球导航卫星系统方法

|

名字

|

描述

| | --- | --- | | beforeEach | 此方法在活动路由更改前调用。 | | afterEach | 此方法在活动路由改变后调用。 | | beforeResolve | 该方法与beforeEach相似,但它是在所有特定于路由的和组件的保护(我将在下一节中描述)都被检查后调用的,如“理解保护排序”一节所述。 |

这些方法中的每一个都在VueRouter对象上被调用,并接受一个在导航过程中被调用的函数。这用一个例子来解释更容易,在清单 24-6 中,我使用了beforeEach方法来阻止从路径以/named开始的路线到/preferences URL 的导航。

import Vue from "vue";
import VueRouter from "vue-router";

import BasicRoutes from "./basicRoutes";
import SideBySideRoutes from "./sideBySideRoutes";

Vue.use(VueRouter);

const router = new VueRouter({

    mode: "history",
    routes: [
        ...BasicRoutes,
        SideBySideRoutes,
        { path: "*", redirect: "/products" }
    ]
});

export default router;

router.beforeEach((to, from, next) => {

    if (to.path == "/preferences" && from.path.startsWith("/named")) {

        next(false);

    } else {

        next();

    }

});

Listing 24-6Guarding a Route in the index.js File in the src/router Folder

全球路线守卫是最灵活的守卫类型,因为它们可以用来拦截所有导航,但它们很难设置,如清单 24-6 所示。使用new关键字创建VueRouter对象,然后将您想要调用的函数从表 24-2 中传递给相应的方法。在这个例子中,我将一个函数传递给了beforeEach方法,这意味着它将在每次导航尝试之前被调用。

该函数接收三个参数。前两个参数是表示应用正在导航到的路线和应用将要导航离开的路线的对象。这些对象定义了我在第二十二章中描述的属性,我在清单中使用它们的path属性来检查应用是否从/named导航到/preferences

第三个参数是一个函数,它被调用来接受、重定向或取消导航,并将导航请求传递给下一个路线守卫进行处理,这就是为什么这个参数通常被称为next。通过向函数传递不同的参数来指定不同的结果,如表 24-3 中所述。

表 24-3

导航保护的下一个功能的使用

|

方法使用

|

描述

| | --- | --- | | next() | 当不带参数调用该函数时,导航将继续进行。 | | next(false) | 当函数将false作为参数传递时,导航将被取消。 | | next(url) | 当向该函数传递一个字符串时,它被解释为一个 URL,并成为新的导航目标。 | | next(object) | 当向该函数传递一个对象时,它被解释为新的导航目标,这对于按名称选择路线很有用,如“重定向到命名路线”一节中所示。 | | next(callback) | 这是一个特殊版本的next函数,只能在一种情况下使用,如“在 beforeRouteEnter 方法中访问组件”一节中所述,其他情况下不支持。 |

在清单中,如果当前 URL 以/named开头,而目标 URL 是/preferences,我就传递next函数false,取消导航请求。对于所有其他导航,我不带参数地调用next方法,这允许导航继续进行。

警告

你必须记得调用你的卫士中的next函数。在一个应用中可以定义多个保护,如果您忘记调用该函数,它们将不会被调用,这可能会导致意外的结果。

将导航请求重定向到另一个 URL

取消导航的另一种方法是将其重定向到另一个 URL。在清单 24-7 中,我添加了一个额外的守卫函数,拦截对/named/tableright URL 的请求,并将它们重定向到/products

小费

当有多个全局保护函数时,它们按照传递给beforeEachbeforeAfter方法的顺序执行。

import Vue from "vue";
import VueRouter from "vue-router";

import BasicRoutes from "./basicRoutes";
import SideBySideRoutes from "./sideBySideRoutes";

Vue.use(VueRouter);

const router = new VueRouter({
    mode: "history",
    routes: [
        ...BasicRoutes,
        SideBySideRoutes,
        { path: "*", redirect: "/products" }
    ]
});

export default router;

router.beforeEach((to, from, next) => {
    if (to.path == "/preferences" && from.path.startsWith("/named")) {
        next(false);
    } else {
        next();
    }
});

router.beforeEach((to, from, next) => {

    if (to.path == "/named/tableright") {

        next("/products");

    } else {

        next();

    }

});

Listing 24-7Defining Another Guard in the index.js File in the src/router Folder

我可以在现有的 guard 函数中实现这个检查,但是我想演示对多个 guard 的支持,这是在复杂的应用中对相关检查进行分组的一种有用的方法。要查看重定向的效果,导航至http://localhost:8080/preferences,然后点击表格右侧按钮。应用导航到/products URL,而不是并排显示组件,如图 24-2 所示。

img/465686_1_En_24_Fig2_HTML.jpg

图 24-2

使用路线保护重定向导航

小费

当您在路由防护中执行重定向时,会启动一个新的导航请求,并再次调用所有防护功能。因此,重要的是不要创建重定向循环,因为两个保护函数会导致应用在相同的两个 URL 之间来回切换。

重定向到命名路由

如果您想将一个导航请求重定向到一个指定的路线,那么您可以将一个具有name属性的对象传递给next函数,如清单 24-8 所示。

import Vue from "vue";
import VueRouter from "vue-router";

import BasicRoutes from "./basicRoutes";
import SideBySideRoutes from "./sideBySideRoutes";

Vue.use(VueRouter);

const router = new VueRouter({
    mode: "history",
    routes: [
        ...BasicRoutes,
        SideBySideRoutes,
        { path: "*", redirect: "/products" }
    ]
});

export default router;

router.beforeEach((to, from, next) => {
    if (to.path == "/preferences" && from.path.startsWith("/named")) {
        next(false);
    } else {
        next();
    }
});

router.beforeEach((to, from, next) => {
    if (to.path == "/named/tableright") {
        next({ name: "editor", params: { op: "edit", id: 1 } });

    } else {
        next();
    }
});

Listing 24-8Redirecting to a Named Route in the index.js File in the src/router Folder

在清单中,我使用了next函数将导航重定向到名为editor的路线,效果如图 24-3 所示。

img/465686_1_En_24_Fig3_HTML.jpg

图 24-3

将导航重定向到指定路线

定义特定于路线的防护

各个路线可以实现自己的保护,这可以提供一种更自然的方式来管理导航。唯一可以直接在路线中使用的单守卫方法是beforeEnter,我已经在清单 24-9 中使用它来守卫两条路线。

import ProductDisplay from "../components/ProductDisplay";
import ProductEditor from "../components/ProductEditor";
import SideBySide from "../components/SideBySide";

export default {

    path: "/named", component: SideBySide,
    children: [
        {
            path: "tableleft",
            components: { left: ProductDisplay, right: ProductEditor }
        },
        {
            path: "tableright",
            components: { left: ProductEditor, right: ProductDisplay },
            beforeEnter: (to, from, next) => {

                next("/products/list");

            }

        }
    ],
    beforeEnter: (to, from, next) => {

        if (to.path == "/named/tableleft") {

            next("/preferences");

        } else {

            next();

        }

    }

}

Listing 24-9Guarding Individual Routes in the sideBySideRoutes.js File in the src/router Folder

使用嵌套路由时,您可以保护父路由和单个子路由。在清单中,我在父路由中添加了一个守卫,将对/named/tableleft的请求重定向到/preferences,您可以通过单击应用的左侧按钮来测试,如图 24-4 所示。

img/465686_1_En_24_Fig4_HTML.jpg

图 24-4

守卫一条路线

了解保护订购

在清单 24-9 中,我向其中一个子路由添加了一个守卫,将对/named/tableright的请求重定向到/products/list。但是如果你点击表格右边的按钮,你会看到这个保护没有达到预期的效果。

发生这种情况是因为全局路由守卫在特定路由守卫之前执行,并且其中一个全局守卫已经在重定向对/named/tableright的请求。当警卫执行重定向时,当前路由更改的处理被放弃,新的导航开始,这意味着在执行重定向的警卫之后执行的任何警卫将不能检查该请求。

如表 24-2 所示,全局beforeResolve方法在所有其他类型的保护之后执行,这是一种在检查完特定路线的保护之后将全局保护定义为最终检查的有用方法。在清单 24-10 中,我已经使用了beforeResolve方法来改变清单 24-9 中定义的阻挡路线守卫的守卫功能。

import Vue from "vue";
import VueRouter from "vue-router";

import BasicRoutes from "./basicRoutes";
import SideBySideRoutes from "./sideBySideRoutes";

Vue.use(VueRouter);

const router = new VueRouter({
    mode: "history",
    routes: [
        ...BasicRoutes,
        SideBySideRoutes,
        { path: "*", redirect: "/products" }
    ]
});

export default router;

router.beforeEach((to, from, next) => {
    if (to.path == "/preferences" && from.path.startsWith("/named")) {
        next(false);
    } else {
        next();
    }
});

router.beforeResolve((to, from, next) => {

    if (to.path == "/named/tableright") {
        next({ name: "editor", params: { op: "edit", id: 1} });
    } else {
        next();
    }
})

Listing 24-10Changing a Guard in the index.js File in the src/router Folder

在实际项目中,只有当存在一组特定于路由的防护不会拦截的 URL 时,这才是一个有用的更改,但是您可以通过单击表格右侧的按钮来查看计时更改的效果。应用将开始导航到/named/tableright,该应用将被路由特定的警卫拦截,该警卫将应用重定向到/products/list,如图 24-5 所示。

img/465686_1_En_24_Fig5_HTML.jpg

图 24-5

路由保护排序的效果

定义组件路由保护

在第二十二章的中,我使用了ProductEditor组件中的beforeRouteUpdate方法来响应路线变化,就像这样:

...
beforeRouteUpdate(to, from, next) {
    this.selectProduct(to);
    next();
}
...

这是路由保护方法之一,组件可以实现该方法来参与保护选择它们进行显示的路由。使用beforeRouteUpdate方法来接收变更通知是一项有用的技术,但该方法是组件保护方法集的一部分,如表 24-4 所述。

表 24-4

组件保护方法

|

名字

|

描述

| | --- | --- | | beforeRouteEnter | 此方法在目标路由选择的组件被确认之前被调用,它用于在路由导致组件被创建之前控制对路由的访问。访问该方法中的组件需要一种特定的技术,如“在 beforeRouteEnter 方法中访问组件”一节中所述。 | | beforeRouteUpdate | 当选择了当前组件的路由发生变化,并且新路由也选择了该组件时,将调用此方法。 | | beforeRouteLeave | 当应用将要离开选择了当前组件的路线时,调用此方法。 |

这些方法接收与其他保护相同的三个参数,可用于接受、重定向或取消导航。在清单 24-11 中,我实现了beforeRouterLeave方法,当路线将要改变时,要求用户确认导航。

<template>
    <div>
        <div v-if="displayWarning" class="text-center m-2">

            <h5 class="bg-danger text-white p-2">

                Are you sure?

            </h5>

            <button class="btn btn-danger" v-on:click="doNavigation">

                Yes

            </button>

            <button class="btn btn-danger" v-on:click="cancelNavigation">

                Cancel

            </button>

        </div>

        <h4 class="bg-info text-white text-center p-2">Preferences</h4>
        <div class="form-check">
            <input class="form-check-input" type="checkbox"
                   v-bind:checked="primaryEdit" v-on:input="setPrimaryEdit">
            <label class="form-check-label">Primary Color for Edit Buttons</label>
        </div>
        <div class="form-check">
            <input class="form-check-input" type="checkbox"
                   v-bind:checked="dangerDelete" v-on:input="setDangerDelete">
            <label class="form-check-label">Danger Color for Delete Buttons</label>
        </div>
    </div>
</template>

<script>

    import { mapState } from "vuex";

    export default {
        data: function() {

            return {

                displayWarning: false,

                navigationApproved: false,

                targetRoute: null

            }

        },

        computed: {
            ...mapState({
                primaryEdit: state => state.prefs.primaryEditButton,
                dangerDelete: state => state.prefs.dangerDeleteButton
            })
        },
        methods: {
            setPrimaryEdit() {
                this.$store.commit("prefs/setEditButtonColor", !this.primaryEdit);
            },
            setDangerDelete() {
                this.$store.commit("prefs/setDeleteButtonColor", !this.dangerDelete);
            },
            doNavigation() {

                this.navigationApproved = true;

                this.$router.push(this.targetRoute.path);

            },

            cancelNavigation() {

                this.navigationApproved = false;

                this.displayWarning = false;

            }

        },
        beforeRouteLeave(to, from, next) {

            if (this.navigationApproved) {

                next();

            } else {

                this.targetRoute = to;

                this.displayWarning = true;

                next(false);

            }

        }

    }
</script>

Listing 24-11Route Guarding in the Preferences.vue File in the src/components Folder

当应用要导航到显示不同组件的路线时,调用beforeRouteLeave方法。在这个例子中,我提示用户确认,并阻止导航,直到它被接收。要查看效果,请单击首选项按钮,然后单击产品按钮。如图 24-6 所示,路线守卫将阻止导航,直到您点击“是”按钮。

img/465686_1_En_24_Fig6_HTML.jpg

图 24-6

使用组件路由保护

在 beforeRouteEnter 方法中访问组件

在创建组件之前调用beforeRouteEnter方法,这确保了在组件生命周期开始之前可以取消导航。如果要使用此方法执行需要访问组件的属性和方法的任务,例如从 web 服务请求数据,这可能会导致问题。为了解决这个限制,传递给beforeRouteEnter方法的next函数可以接受一个回调函数,一旦组件被创建,这个回调函数就被调用,这允许beforeRouteEnter方法访问组件,但是只有在取消或重定向导航的机会已经过去的时候。为了演示,我创建了一个组件,它使用beforeRouteEnter方法来访问组件定义的方法,方法是在src/component文件夹中添加一个名为FilteredData.vue的文件,其内容如清单 24-12 所示。

<template>
    <div>
        <h3 class="bg-primary text-center text-white p-2">
            Data for {{ category }}
        </h3>

        <div class="text-center m-2">
            <label>Category:</label>
            <select v-model="category">
                <option>All</option>
                <option>Watersports</option>
                <option>Soccer</option>
                <option>Chess</option>
            </select>
        </div>

        <h3 v-if="loading" class="bg-info text-white text-center p-2">
            Loading Data...
        </h3>

        <table v-else class="table table-sm table-bordered">
            <tr>
                <th>ID</th>
                <th>Name</th>
                <th>Category</th>
                <th>Price</th>
            </tr>
            <tbody>
                <tr v-for="p in data" v-bind:key="p.id">
                    <td>{{ p.id }}</td>
                    <td>{{ p.name }}</td>
                    <td>{{ p.category }}</td>
                    <td>{{ p.price }}</td>
                </tr>
            </tbody>
        </table>
    </div>
</template>

<script>

    import Axios from "axios";

    const baseUrl = "http://localhost:3500/products/";

    export default {
        data: function () {
            return {
                loading: true,
                data: [],
                category: "All"
            }
        },
        methods: {
            async getData(route) {
                if (route.params != null && route.params.category != null) {
                    this.category = route.params.category;
                } else {
                    this.category = "All";
                }
                let url = baseUrl
                    + (this.category == "All" ? "" : `?category=${this.category}`);
                this.data.push(...(await Axios.get(url)).data);
                this.loading = false;
            }
        },
        watch: {
            category() {
                this.$router.push(`/filter/${this.category}`);
            }
        },
        async beforeRouteEnter(to, from, next) {
            next(async component => await component.getData(to));
        },
        async beforeRouteUpdate(to, from, next) {
            this.data.splice(0, this.data.length);
            await this.getData(to);
            next();
        }
    }
</script>

Listing 24-12The Contents of the FilteredData.vue File in the src/components Folder

该组件允许用户按类别过滤产品数据,每次都从使用 Axios 请求的 web 服务中重新检索产品数据,这在第十九章中有所描述。(为了简单起见,我直接使用 Axios 并将数据存储在组件中,而不是修改存储库和数据存储。)

该组件为用户提供了一个select元素来选择用于过滤数据的类别。当使用select元素时,category属性被修改,这导致观察者执行到包含类别名称的 URL 的导航,因此例如选择Soccer类别将导航到/filter/Soccer URL。

该组件实现了两个组件路由保护方法,这两个方法都用于调用组件的异步getData方法,该方法接受一个路由对象并使用它从 web 服务获取适当的数据。我在第十九章中添加的json-server包支持过滤数据,所以例如对http://localhost:3500/products?category=Soccer的请求将只返回那些类别属性为Soccer的对象。其中一种路由保护方法很容易理解,如下所示:

...
async beforeRouteUpdate(to, from, next) {
    this.data.splice(0, this.data.length);
    await this.getData(to);
    next();
}
...

这是我在整本书中使用的组件工作模式,它使用this来引用组件对象,所以this.getData被用来调用由组件定义的getData方法。在beforeRouteEnter方法中实现相同的效果需要不同的方法。

...
async beforeRouteEnter(to, from, next) {
    next(component => component.getData(to));

},
...

在组件创建和它的常规生命周期开始之前,beforeRouteEnter方法被调用,这意味着this不能用于访问组件。相反,可以向next方法传递一个函数,一旦创建了组件,该函数将被调用,并接收组件对象作为其参数。在清单中,一旦创建了组件,我就使用next函数来调用组件上的getData方法。

这似乎是一种异常复杂的请求数据的方法,使用作为组件生命周期一部分的created方法可以很容易地完成,我在第十七章中对此进行了描述。beforeRouteEnter方法有用的原因是它允许在组件创建之前取消导航,这在created方法中是做不到的,后者只有在导航完成并且组件已经创建之后才被调用。为了演示,我在beforeRouteEnter方法中添加了一个检查,如果用户在 URL 中指定了除All之外的类别值,它将重定向导航,如清单 24-13 所示。

...
async beforeRouteEnter(to, from, next) {
    if (to.params.category != "All") {

        next("/filter/All");

     } else {

         next(async component => await component.getData(to));
     }

},
...

Listing 24-13Guarding a Component in the FilteredData.vue File in the src/components Folder

该方法重定向任何导航到显示组件的 URL 的尝试,除非该 URL 指向从服务器请求所有数据的All类别。一旦组件显示出来,就可以导航到其他组件,因为这些更改受到了beforeRouteUpdate方法的保护。

当你看到它工作时,这个例子就更容易理解了。为了添加对新组件的支持,我将清单 24-14 中所示的语句添加到基本路由集中。

import ProductDisplay from "../components/ProductDisplay";
import ProductEditor from "../components/ProductEditor";
import Preferences from "../components/Preferences";
import Products from "../components/Products";

import FilteredData from "../components/FilteredData";

export default [

    { path: "/preferences", component: Preferences },
    {
        path: "/products", component: Products,
        children: [{ name: "table", path: "list", component: ProductDisplay },
        {
            name: "editor", path: ":op(create|edit)/:id(\\d+)?",
            component: ProductEditor
        },
        { path: "", redirect: "list" }]
    },
    { path: "/edit/:id", redirect: to => `/products/edit/${to.params.id}` },
    { path: "/filter/:category", component: FilteredData }

]

Listing 24-14Adding a Route in the basicRoutes.js File in the src/router Folder

为了方便导航到新路线,我将清单 24-15 中所示的导航元素添加到顶级App组件的模板中。

<template>
    <div class="container-fluid">
        <div class="row">
            <div class="col text-center m-2">
                <div class="btn-group">
                    <router-link tag="button" to="/products"
                                 active-class="btn-info" class="btn btn-primary">
                        Products
                    </router-link>
                    <router-link tag="button" to="/preferences"
                                 active-class="btn-info" class="btn btn-primary">
                        Preferences
                    </router-link>
                    <router-link to="/named/tableleft" class="btn btn-primary"
                                 active-class="btn-info">
                        Table Left
                    </router-link>
                    <router-link to="/named/tableright" class="btn btn-primary"
                                 active-class="btn-info">
                        Table Right
                    </router-link>
                    <router-link to="/filter/All" class="btn btn-primary"

                                active-class="btn-info">

                        Filtered Data

                    </router-link>

                </div>
            </div>
        </div>
        <div class="row">
            <div class="col m-2">
                <router-view></router-view>
            </div>
        </div>
    </div>
</template>

<script>

    export default {
        name: 'App',
        created() {
            this.$store.dispatch("getProductsAction");
        }
    }
</script>

Listing 24-15Adding a Navigation Element in the App.vue File in the src Folder

结果是一个新的按钮元素,它导航到一个选择了FilteredData组件的路径。仅当类别动态段为All时,路由保护方法才允许导航,并将重定向其他请求。一旦组件显示出来,使用select元素选择一个不同的组件将导航到一个由beforeRouteUpdate方法保护的 URL,该方法通过获取指定类别中的数据来响应路由的改变,如图 24-7 所示。

img/465686_1_En_24_Fig7_HTML.jpg

图 24-7

访问路由保护中的组件对象

使用组件路由保护进行开发

使用零部件路线防护装置可能会很困难。Vue.js 开发工具会动态更新应用,但这不会正确触发路由保护方法,您可能需要重新加载浏览器窗口才能看到路由保护的工作。

类似地,当浏览器和 web 服务都在同一个开发工作站上运行时,web 服务的响应可能会非常快,以至于您在等待数据时没有机会看到组件向用户提供的反馈。如果您想减慢数据加载的速度,以便可以看到组件在等待数据时的行为,那么在发出 HTTP 请求之前添加以下语句:

...
await new Promise(resolve => setTimeout(resolve, 3000));
...

例如,对于清单 24-15 中的组件,该语句将被插入到getData方法中,紧接在使用 Axios 发送 HTTP 请求的语句之前,并将在发送请求之前引入三秒钟的暂停。

按需加载组件

路线所需的组件可以从应用的 JavaScript 包中排除,只在需要时才加载,使用我在第二十一章中描述的基本特性。使用 URL 路由时,只支持基本的延迟加载特性,忽略配置选项,如loadingdelay(不过,我将在下一节演示如何使用 route guards 显示加载消息)。在清单 24-16 中,我修改了导入FilteredData组件的语句,这样它就可以延迟加载了。

import ProductDisplay from "../components/ProductDisplay";
import ProductEditor from "../components/ProductEditor";
import Preferences from "../components/Preferences";
import Products from "../components/Products";

const FilteredData = () => import("../components/FilteredData");

export default [

    { path: "/preferences", component: Preferences },
    {
        path: "/products", component: Products,
        children: [{ name: "table", path: "list", component: ProductDisplay },
        {
            name: "editor", path: ":op(create|edit)/:id(\\d+)?",
            component: ProductEditor
        },
        { path: "", redirect: "list" }]
    },
    { path: "/edit/:id", redirect: to => `/products/edit/${to.params.id}` },
    { path: "/filter/:category", component: FilteredData }
]

Listing 24-16Lazily Loading a Component in the basicRoutes.js File in the src/router Folder

这是我在第二十一章中用过的import函数,也有同样的效果。FilteredData组件被排除在主应用 JavaScript 包之外,放在它自己的包中,在第一次需要时加载。

为确保FilteredData仅在需要时加载,导航至http://localhost:8080并打开浏览器的 F12 开发者工具至网络选项卡。在主浏览器窗口中,点击过滤数据按钮,你会看到一个名为0.js的文件的 HTTP 请求被发送到服务器,如图 24-8 所示。(您可能会在请求中看到不同的文件名,但这并不重要。)

img/465686_1_En_24_Fig8_HTML.jpg

图 24-8

延迟加载组件

在构建过程中,创建了两个独立的 JavaScript 代码包。app.js文件包含应用的主要部分,而0.js文件只包含FilteredData组件。(F12 工具显示的其他请求是初始 HTTP 请求、来自 web 服务的数据请求,以及连接回开发工具用来更新浏览器的服务器。)

小费

默认情况下,URL 路由的延迟加载功能将提供预取提示。有关禁用该功能的详细信息和配置说明,请参见第二十一章。

显示组件加载消息

在撰写本文时,Vue 路由器包不支持第二十一章中描述的显示加载或错误组件的功能。为了创建一个类似的特性,我将把一个数据存储属性与路由器防护结合起来,在延迟加载一个组件时向用户显示一条消息。首先,我在数据存储中添加了一个属性,该属性将指示组件何时被加载,以及一个改变其值的突变,如清单 24-17 所示。

import Vue from "vue";
import Vuex from "vuex";
import Axios from "axios";

import PrefsModule from "./preferences";
import NavModule from "./navigation";

Vue.use(Vuex);

const baseUrl = "http://localhost:3500/products/";

export default new Vuex.Store({
    modules: {
        prefs: PrefsModule,
        nav: NavModule
    },
    state: {
        products: [],
        selectedProduct: null,
        componentLoading: false

    },
    mutations: {
        setComponentLoading(currentState, value) {

            currentState.componentLoading = value;

        },

        saveProduct(currentState, product) {
            let index = currentState.products.findIndex(p => p.id == product.id);
            if (index == -1) {
                currentState.products.push(product);
            } else {
                Vue.set(currentState.products, index, product);
            }
        },

        // ...other data store features omitted for brevity...

    }
})

Listing 24-17Adding a Data Property and Mutation in the index.js File in the src/store Folder

为了指示组件何时被加载,我将清单 24-18 中所示的元素添加到了App组件的模板中,同时添加了到清单 24-17 中创建的数据存储属性的映射。

<template>
    <div class="container-fluid">
        <div class="row">
            <div class="col text-center m-2">
                <div class="btn-group">
                    <router-link tag="button" to="/products"
                                 active-class="btn-info" class="btn btn-primary">
                        Products
                    </router-link>
                    <router-link tag="button" to="/preferences"
                                 active-class="btn-info" class="btn btn-primary">
                        Preferences
                    </router-link>
                    <router-link to="/named/tableleft" class="btn btn-primary"
                                 active-class="btn-info">
                        Table Left
                    </router-link>
                    <router-link to="/named/tableright" class="btn btn-primary"
                                 active-class="btn-info">
                        Table Right
                    </router-link>
                    <router-link to="/filter/All" class="btn btn-primary"
                                active-class="btn-info">
                        Filtered Data
                    </router-link>
                </div>
            </div>
        </div>
        <div class="row">
            <div class="col m-2">
                <h3 class="bg-warning text-white text-center p-2"

                        v-if="componentLoading">

                    Loading Component...

                </h3>

                <router-view></router-view>
            </div>
        </div>
    </div>
</template>

<script>
    import { mapState } from "vuex";

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

            ...mapState(["componentLoading"]),

        },

        created() {
            this.$store.dispatch("getProductsAction");
        }
    }
</script>

Listing 24-18Displaying a Loading Message in the App.vue File in the src Folder

为了设置数据存储属性的值并向用户显示消息,我在路由中添加了一个针对延迟加载组件的防护,如清单 24-19 所示。

import ProductDisplay from "../components/ProductDisplay";
import ProductEditor from "../components/ProductEditor";
import Preferences from "../components/Preferences";
import Products from "../components/Products";

const FilteredData = () => import("../components/FilteredData");

import dataStore from "../store";

export default [

    { path: "/preferences", component: Preferences },
    {
        path: "/products", component: Products,
        children: [{ name: "table", path: "list", component: ProductDisplay },
        {
            name: "editor", path: ":op(create|edit)/:id(\\d+)?",
            component: ProductEditor
        },
        { path: "", redirect: "list" }]
    },
    { path: "/edit/:id", redirect: to => `/products/edit/${to.params.id}` },
    { path: "/filter/:category", component: FilteredData,

        beforeEnter: (to, from, next) => {

            dataStore.commit("setComponentLoading", true);

            next();

        }

    }
]

Listing 24-19Adding a Route Guard in the basicRoutes.js File in the src/router Folder

本例中的import语句提供了对数据存储的访问。我在第二十章的例子中使用的$store只在组件中可用,在应用的其余部分,通过import语句可以访问数据存储。清单 24-19 中的 guard 方法使用setComponentLoading突变来更新数据存储,然后调用next函数。

处理加载错误

URL 路由的延迟加载功能不支持错误组件。为了处理加载组件或调用 route guards 时的错误,可以使用由VueRouter对象定义的onError方法来注册一个回调函数,当出现问题时将调用该函数。

清单 24-19 中定义的 guard 方法指示加载过程何时开始,但是我还需要指示加载过程何时完成,这样用户就不会再看到加载消息。在清单 24-20 中,我已经更新了将被延迟加载的组件中的路由保护。

警告

您可能想把两个突变语句都放在组件的beforeRouteEnter guard 方法中。这将不起作用,因为组件的代码是应用正在加载的代码,并且在加载过程完成之前不能调用 route guard 方法。

...
async beforeRouteEnter(to, from, next) {
    if (to.params.category != "All") {
        next("/filter/All");
    } else {
        next(async component => {

            component.$store.commit("setComponentLoading", false);

            await component.getData(to)
        });

    }
},
...

Listing 24-20Updating a Route Guard in the FilteredData.vue File in the src/components Folder

我在回调函数中添加了一个语句,一旦导航被确认并且组件被创建,这个函数就会被调用。使用component参数,我更新了数据存储并应用了表示加载过程完成的突变,产生了如图 24-9 所示的效果。

img/465686_1_En_24_Fig9_HTML.jpg

图 24-9

延迟加载组件时显示消息

加载期间隐藏传出组件

如果你检查图 24-9 ,你会看到在整个装载过程中,将要被移除的组件会显示给用户。虽然这对于许多项目来说是可以接受的,但是如果你想在等待新组件被加载的时候隐藏旧组件,就必须小心使用v-show指令,如清单 24-21 所示。

...
<div class="row">
    <div class="col m-2">
        <h3 class="bg-warning text-white text-center p-2"
                v-if="componentLoading">
            Loading Component...
        </h3>
        <router-view v-show="!componentLoading"></router-view>

    </div>
</div>
...

Listing 24-21Hiding an Element in the App.vue File in the src Folder

正如我在第十二章中解释的那样,v-show隐藏了一个元素而没有移除它。这很重要,因为如果您使用v-ifv-else指令,那么router-view元素将从文档对象模型中删除,并且加载的组件将永远不会被初始化并显示给用户。使用v-show指令将router-view元素留在文档中,并作为显示延迟加载组件的目标,如图 24-10 所示。

img/465686_1_En_24_Fig10_HTML.jpg

图 24-10

在组件加载期间隐藏路由器视图

创建无布线组件

并非所有的组件都被编写为利用 Vue 路由器包及其提供的$router$route属性。例如,如果你想使用第三方编写的组件,你会发现大多数组件都是使用 Vue.js props 特性配置的,我在第十六章中描述过。Vue 路由器支持为组件提供适当值作为其路由配置的一部分,这使得将组件集成到使用 URL 路由的应用中成为可能,而不必修改它们或编写笨拙的包装器来适应它们。为了演示这个特性,我在src/components文件夹中添加了一个名为MessageDisplay.vue的文件,其内容如清单 24-22 所示。

<template>
    <h3 class="bg-success text-white text-center p-2">
        Message: {{ message }}
    </h3>
</template>

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

Listing 24-22The Contents of the MessageDisplay.vue File in the src/components Folder

这个组件使用一个道具显示一条消息,这就是我演示这个特性所需要的全部内容。在清单 24-23 中,我在应用的配置中添加了两条路由,它们指向新组件,并使用不同的属性值对其进行配置。

import ProductDisplay from "../components/ProductDisplay";
import ProductEditor from "../components/ProductEditor";
import Preferences from "../components/Preferences";
import Products from "../components/Products";

import MessageDisplay from "../components/MessageDisplay";

const FilteredData = () => import("../components/FilteredData");

import dataStore from "../store";

export default [

    { path: "/preferences", component: Preferences },
    {
        path: "/products", component: Products,
        children: [{ name: "table", path: "list", component: ProductDisplay },
        {
            name: "editor", path: ":op(create|edit)/:id(\\d+)?",
            component: ProductEditor
        },
        { path: "", redirect: "list" }]
    },
    { path: "/edit/:id", redirect: to => `/products/edit/${to.params.id}` },
    { path: "/filter/:category", component: FilteredData,
        beforeEnter: (to, from, next) => {
            dataStore.commit("setComponentLoading", true);
            next();
        }
    },
    { path: "/hello", component: MessageDisplay, props: { message: "Hello, Adam"}},

    { path: "/hello/:text", component: MessageDisplay,

        props: (route) => ({ message: `Hello, ${route.params.text}`})},

    { path: "/message/:message", component: MessageDisplay, props: true},

]

Listing 24-23Adding Routes in the basicRoutes.js File in the src/router Folder

定义路线时,使用props属性将属性传递给组件。清单 24-23 中添加的路线展示了使用props属性向组件传递道具的三种不同方式。在第一条路线中,prop 值完全独立于路线,并且将总是被设置为相同的值,你可以通过导航到http://localhost:8080/hello看到,在那里你将看到如图 24-11 所示的结果。

img/465686_1_En_24_Fig11_HTML.jpg

图 24-11

传递固定属性值

另外两条路径使用路径的动态线段值来设置属性值。可以为 props 值分配一个函数,该函数接收当前路径作为其参数,并返回一个包含 props 值的对象,如下所示:

...
{ path: "/hello/:text", component: MessageDisplay,
  props: (route) => ({ message: `Hello, ${route.params.text}`})},

...

这个例子从text段获取值,并使用它来设置message属性的值,如果需要处理来自 URL 的值,这是一个有用的技术。如果您不需要处理动态段,那么您可以使用最后一种技术,即将props值设置为 true ,,如下所示:

...
{ path: "/message/:message", component: MessageDisplay, props: true},
...

这具有使用来自当前路线的params值作为属性值的效果,这避免了为每个动态段和属性显式定义映射的需要(尽管段的名称必须与组件期望的属性名称相匹配)。要查看效果,导航到http://localhost:8080/hello/adamhttp://localhost:8080/message/Hello%20Adam,这将产生如图 24-12 所示的结果。

img/465686_1_En_24_Fig12_HTML.jpg

图 24-12

将动态段值映射到组件属性

摘要

在本章中,我解释了如何使用多个 JavaScript 文件对相关路由进行分组,以使路由配置更易于管理。我还向您展示了如何保护路由以控制它们的激活,并演示了如何在路由需要组件时延迟加载组件。在本章的最后,我向您展示了如何配置一个组件的 props,当您想要使用尚未编写的组件从路由系统中获取它们的配置信息时,这是一项非常有用的技术。在下一章中,我将向您展示如何使用过渡功能。

二十五、过渡

Vue.js 转换特性允许您在添加或删除 HTML 元素或改变位置时做出响应。当结合现代浏览器提供的特性时,过渡可以用来将用户的注意力吸引到应用中受其行为影响的部分。在本章中,我将向您展示使用过渡的不同方式,演示如何使用第三方 CSS 和 JavaScript 动画包,并向您展示如何将用户的注意力吸引到其他类型的更改上,例如当数据值被修改时。表 25-1 将本章放在上下文中。

表 25-1

将过渡置于上下文中

|

问题

|

回答

| | --- | --- | | 它们是什么? | 转换是在关键时刻从类中添加和删除元素的指令,比如在 DOM 中添加和删除元素。这些类用于逐渐应用元素样式的变化来创建动画效果。 | | 它们为什么有用? | 过渡是一种有用的方式,可以将用户的注意力吸引到重要的变化上,或者使变化不那么刺耳。 | | 它们是如何使用的? | 使用transitiontransition-group元素应用过渡。 | | 有什么陷阱或限制吗? | 人们很容易忘乎所以,创建一个应用,其中包含的效果会让用户感到沮丧,并扰乱有效的工作流程。 | | 还有其他选择吗? | 转场是可选功能,您不必在项目中使用它们。 |

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

表 25-2

章节总结

|

问题

|

解决办法

|

列表

| | --- | --- | --- | | 应用过渡 | 使用transition元素并定义与过渡类匹配的样式 | 13, 15, 16, 20–22 | | 使用动画库 | 使用transition元素属性指定将应用动画的类 | 14, 17 | | 确定元素之间的过渡是如何呈现的 | 使用mode属性 | Fifteen | | 将过渡应用于一组重复的元素 | 使用transition-group元素 | Eighteen | | 接收转换通知 | 处理过渡事件 | Nineteen |

为本章做准备

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

vue create transitions --default

Listing 25-1Creating the Example Project

这个命令创建了一个名为transitions的项目。一旦设置过程完成,运行transitions文件夹中清单 25-2 所示的命令,将引导 CSS 和 Vue 路由器包添加到项目中。

npm install bootstrap@4.0.0
npm install vue-router@3.0.1

Listing 25-2Adding Packages

为了给本章中的例子提供效果,我使用了animate.csspopmotion包。运行transitions文件夹中清单 25-3 所示的命令,下载并安装软件包。

npm install animate.css@3.6.1
npm install popmotion@8.1.24

Listing 25-3Adding the Animation Packages

将清单 25-4 中显示的语句添加到src文件夹中的main.js文件中,将Bootstrap和动画包合并到应用中。

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

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

import "animate.css/animate.min.css";

import "popmotion/dist/popmotion.global.min.js";

Vue.config.productionTip = false

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

Listing 25-4Incorporating the Packages in the main.js File in the src Folder

创建组件

我需要这一章的一些基本组件。我首先将一个名为SimpleDisplay.vue的文件添加到src/components文件夹中,其内容如清单 25-5 所示。

<template>
    <div class="mx-5 border border-dark p-2">
        <h3 class="bg-warning text-white text-center p-2">Display</h3>

        <div v-if="show" class="h4 bg-info text-center p-2">Hello, Adam</div>

        <div class="text-center">
            <button class="btn btn-primary" v-on:click="toggle">
                Toggle Visibility
            </button>
        </div>
    </div>
</template>

<script>

    export default {
        data: function () {
            return {
                show: true
            }
        },
        methods: {
            toggle() {
                this.show = !this.show;
            }
        }
    }

</script>

Listing 25-5The Contents of the SimpleDisplay.vue File in the src/components Folder

该组件显示一条消息,使用v-if指令管理该消息的可见性。接下来,我在src/components文件夹中添加了一个名为Numbers.vue的文件,其内容如清单 25-6 所示。

<template>
    <div class="mx-5 p-2 border border-dark">
        <h3 class="bg-success text-white text-center p-2">Numbers</h3>

        <div class="container-fluid">
            <div class="row">
                <div class="col">
                    <input class="form-control" v-model.number="first" />
                </div>
                <div class="col-1 h3">+</div>
                <div class="col">
                    <input class="form-control" v-model.number="second" />
                </div>
                <div class="col h3">= {{ total }} </div>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        data: function () {
            return {
                first: 10,
                second: 20
            }
        },
        computed: {
            total() {
                return this.first + this.second;
            }
        }
    }
</script>

Listing 25-6The Contents of the Numbers.vue File in the src/components Folder

该组件显示两个使用v-model指令更新数据属性的input元素,这些数据属性通过一个计算属性相加,该属性也显示在模板中。接下来,我在src/components文件夹中添加了一个名为ListMaker.vue的文件,其内容如清单 25-7 所示。

<template>
    <div class="mx-5 p-2 border border-dark">
        <h3 class="bg-info text-white text-center p-2">My List</h3>
        <table class="table table-sm">
            <tr><th>#</th><th>Item</th><th width="20%" colspan="2"></th></tr>
            <tr v-for="(item, i) in items" v-bind:key=item>
                <td>{{i}}</td>
                <td>{{item}}</td>
                <td>
                    <button class="btn btn-sm btn-info" v-on:click="moveItem(i)">
                        Move
                    </button>
                    <button class="btn btn-sm btn-danger" v-on:click="removeItem(i)">
                        Delete
                    </button>
                </td>
            </tr>
           <controls v-on:add="addItem" />
        </table>
    </div>
</template>

<script>

    import Controls from "./ListMakerControls";

    export default {
        components: { Controls },
        data: function () {
            return {
                items: ["Apples", "Oranges", "Grapes"]
            }
        },
        methods: {
            addItem(item) {
                this.items.push(item);
            },
            removeItem(index) {
                this.items.splice(index, 1);
            },
            moveItem(index) {
                this.items.push(...this.items.splice(index, 1));
            }
        }
    }
</script>

Listing 25-7The Contents of the ListMaker.vue File in the src/components Folder

该组件显示一组项目。新的项目可以添加到数组中,现有的项目可以移动到数组的末尾或从数组中删除。我将在本章的后面对组件的模板进行修改,为了避免列出与示例不直接相关的 HTML 元素,我通过在src/components文件夹中添加一个名为ListMakerControls.vue的文件来创建一个支持组件,其内容如清单 25-8 所示。

<template>
    <tfoot>
        <tr v-if="showAdd">
            <td></td>
            <td><input class="form-control" v-model="currentItem" /></td>
            <td>
                <button id="add" class="btn btn-sm btn-info" v-on:click="handleAdd">
                    Add
                </button>
                <button id="cancel" class="btn btn-sm btn-secondary"
                        v-on:click="showAdd = false">
                    Cancel
                </button>
            </td>
        </tr>
        <tr v-else>
            <td colspan="4" class="text-center p-2">
                <button class="btn btn-info" v-on:click="showAdd = true">
                    Show Add
                </button>
            </td>
        </tr>
    </tfoot>
</template>

<script>
    export default {
        data: function () {
            return {
                showAdd: false,
                currentItem: ""
            }
        },
        methods: {
            handleAdd() {
                this.$emit("add", this.currentItem);
                this.showAdd = false;
            }
        }
    }
</script>

Listing 25-8The Contents of the ListMakerControls.vue in the src/components Folder

该组件允许向列表中添加新项目,并且是我在清单 25-7 中创建的组件的一个依赖项。

配置 URL 路由

为了设置 URL 路由系统,我创建了src/router文件夹,并在其中添加了一个名为index.js的文件,其内容如清单 25-9 所示。

import Vue from "vue"
import Router from "vue-router"

import SimpleDisplay from "../components/SimpleDisplay";

import ListMaker from "../components/ListMaker";

import Numbers from "../components/Numbers";

Vue.use(Router)

export default new Router({
    mode: "history",

    routes: [
        { path: "/display", component: SimpleDisplay },

        { path: "/list", component: ListMaker },

        { path: "/numbers", component: Numbers },

        { path: "*", redirect: "/display" }

    ]
})

Listing 25-9The Contents of the index.js File in the src/router Folder

该配置启用历史 API 模式,并定义针对前一部分创建的组件的/display/numbers/list路线。还有一个包罗万象的路由,将浏览器重定向到/display URL。在清单 25-10 中,我将路由器导入到main.js文件中,并添加了使路由特性可用的属性。

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

import "bootstrap/dist/css/bootstrap.min.css";
import "animate.css/animate.min.css";
import "popmotion/dist/popmotion.global.min.js";

import router from "./router";

Vue.config.productionTip = false

new Vue({
  router,

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

Listing 25-10Enabling Routing in the main.js File in the src Folder

创建导航元素

最后的准备步骤是将导航元素添加到根组件的模板中,这些元素将指向路由配置中定义的 URL,如清单 25-11 所示。

<template>
    <div class="m-2">
        <div class="text-center m-2">

            <div class="btn-group">

                <router-link tag="button" to="/display"

                        exact-active-class="btn-warning" class="btn btn-secondary">

                    Simple Display

                </router-link>

                <router-link tag="button" to="/list"

                        exact-active-class="btn-info" class="btn btn-secondary">

                    List Maker

                </router-link>

                <router-link tag="button" to="/numbers"

                        exact-active-class="btn-success" class="btn btn-secondary">

                    Numbers

                </router-link>

            </div>

        </div>

        <router-view />
    </div>

</template>

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

Listing 25-11Adding Navigation Elements in the App.vue File in the src Folder

运行transitions文件夹中清单 25-12 所示的命令,启动开发工具。

npm run serve

Listing 25-12Starting the Development Tools

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

img/465686_1_En_25_Fig1_HTML.jpg

图 25-1

运行示例应用

应用过渡的指南

开发人员在应用过渡时经常会忘乎所以,结果是用户感到沮丧的应用。这些特性应该尽量少用,应该简单,应该快速。使用过渡来帮助用户理解你的应用,而不是作为展示你艺术技巧的工具。用户,尤其是公司业务线应用,必须重复执行相同的任务,过多和过长的动画只会碍事。

我深受这种倾向的困扰,如果不加检查,我的应用的行为就像拉斯维加斯的老虎机。我遵循两条规则来控制问题。首先,我连续 20 次执行应用中的主要任务或工作流。在示例应用中,这可能意味着向列表中添加 20 个条目,移动它们,然后删除它们。在进入下一步之前,我会消除或缩短我发现自己必须等待完成的任何效果。

第二条规则是,我不会在开发过程中禁用特效。当我在开发一个特性的时候,注释掉一个过渡或者动画是很有诱惑力的,因为我在写代码的时候会执行一系列的快速测试。但是任何妨碍我的动画也会妨碍用户,所以我保留过渡并调整它们——通常减少它们的持续时间——直到它们变得不那么突兀和烦人。

当然,你不必遵循我的规则,但重要的是要确保过渡对用户有帮助,而不是快速工作的障碍或令人分心的烦恼。

过渡入门

默认情况下,对组件模板中 HTML 元素所做的更改会立即生效,您可以通过单击由SimpleDisplay组件显示的切换可见性按钮来查看。每次点击按钮,show数据属性都会被修改,这使得v-if指令立即显示和隐藏它所应用的元素,如图 25-2 所示。

注意

静态截图不太适合显示应用中的变化。本章中的示例是最好的第一手体验,有助于理解 Vue.js 过渡功能是如何工作的。

img/465686_1_En_25_Fig2_HTML.jpg

图 25-2

状态更改的默认行为

Vue.js 转换特性可用于管理从一种状态到另一种状态的变化,这是使用transition组件完成的。过渡组件的基本用途是应用 CSS 过渡,我已经在清单 25-13 中完成了。

Vue。Js 过渡与 CSS 过渡和动画

我在本章中描述的特性有术语冲突。Vue.js 转换特性用于响应应用状态的变化。这通常与在 DOM 中添加和删除 HTML 元素有关,但也可能是对数据值变化的响应。

响应 HTML 元素变化的最常见方式是使用 Vue.js 转换特性来应用 CSS 转换。CSS 过渡是在一组 CSS 属性值和另一组之间的逐渐变化,它具有动画元素变化的效果。你可以在清单 25-13 中看到一个 CSS 转换的例子。CSS 动画类似于 CSS 过渡,但提供了更多关于如何更改 CSS 属性值的选项。你可能遇到的另一个术语是 CSS 转换,它允许你移动、旋转、缩放和倾斜 HTML 元素。变换通常与 CSS 过渡或动画相结合,以创建更复杂的效果。

如果您发现自己陷入了这些术语的困境,请记住,Vue.js 转换特性对于 Vue.js 应用开发来说是最重要的。在最初的例子让我演示了 Vue.js 特性是如何工作的之后,我没有直接使用 CSS 特性,而是依赖第三方包来提供向用户显示的效果。我建议您在自己的项目中也这样做,因为与尝试直接使用 CSS 过渡、动画和变换功能创建自己的复杂效果相比,结果更可预测,也更少令人沮丧。

<template>
    <div class="mx-5 border border-dark p-2">
        <h3 class="bg-warning text-white text-center p-2">Display</h3>

        <transition>

            <div v-if="show" class="h4 bg-info text-center p-2">Hello, Adam</div>
        </transition>

        <div class="text-center">
            <button class="btn btn-primary" v-on:click="toggle">
                Toggle Visibility
            </button>
        </div>
    </div>
</template>

<script>

    export default {
        data: function () {
            return {
                show: true
            }
        },
        methods: {
            toggle() {
                this.show = !this.show;
            }
        }
    }

</script>

<style>
    .v-leave-active {

        opacity: 0;

        font-size: 0em;

        transition: all 250ms;

    }

    .v-enter {

        opacity: 0;

        font-size: 0em;

    }

    .v-enter-to {

        opacity: 1;

        font-size: x-large;

        transition: all 250ms;

    }

</style>

Listing 25-13Applying a Transition in the SimpleDisplay.vue File in the src/components Folder

一旦你看到它的工作,这个例子就更容易理解了。一旦保存了列表中显示的更改,重新加载浏览器并单击切换可见性按钮。元素的可见性不是瞬间变化的,而是逐渐变化的,如图 25-3 所示。(从图中可能很难看出,但是 HTML 元素变得越来越小,逐渐从视图中消失。)

小费

您可能会发现您必须重新加载浏览器才能看到本章示例的预期结果。这是 webpack 捆绑过程处理变化的结果。

img/465686_1_En_25_Fig3_HTML.jpg

图 25-3

过渡的效果

通过将您想要处理的元素包装在一个transition元素中来应用过渡,如下所示:

...

<transition>

    <div v-if="show" class="h4 bg-info text-center p-2">Hello, Adam</div>

</transition>

...

Vue.js 并不为元素本身制作动画,而是将元素添加到许多类中,并让浏览器应用与这些类相关联的任何效果。一旦您理解了所涉及的步骤,这并不像看起来那么复杂,我将在下面的部分中描述这些步骤。

理解转换类和 CSS 转换

transition元素的作用是在其转换期间将它包含的元素添加到一系列类中,对于本例来说,这是在元素被添加到 DOM 或从 DOM 中移除时。表 25-3 描述了这些类别。

表 25-3

过渡班

|

名字

|

描述

| | --- | --- | | v-enter | 元素在添加到 DOM 之前被添加到这个类中,之后立即被删除。 | | v-enter-active | 该元素在添加到 DOM 之前被添加到该类中,并在转换完成时被移除。 | | v-enter-to | 元素在被添加到 DOM 后立即被添加到这个类中,并在转换完成时被移除。 | | v-leave | 该元素在过渡开始时被添加到该类中,并在一帧后被移除。 | | v-leave-active | 元素在过渡开始时被添加到该类中,在过渡结束时被移除。 | | v-leave-to | 该元素被添加到该类的过渡中的第一帧,并在完成时被移除。 |

当转换开始和停止时,被转换的元素被添加到表中显示的类中,并从表中显示的类中移除,并且总是以相同的顺序。图 25-4 显示了当元素被添加到 DOM 时,类成员的顺序,显示了转换和类之间的关系。

img/465686_1_En_25_Fig4_HTML.jpg

图 25-4

进入过渡阶段

v-enter类用于定义 HTML 元素在添加到 DOM 之前的初始状态。在清单 25-13 中,我定义了一个 CSS 样式,带有一个匹配v-enter类中元素的选择器,如下所示:

...
.v-enter {
    opacity: 0;
    font-size: 0em;
}
...

当一个元素是v-enter类的成员时,它的不透明度将为零(使元素透明),它的文本将具有零高度(这将为这个例子设置元素的高度)。这表示元素在添加到 DOM 之前的起始状态,因此它是透明的,高度为零。

在清单 25-13 中,我使用了带有选择器的 CSS 样式,该选择器匹配v-enter-to类中的元素,如下所示:

...
.v-enter-to {
    opacity: 1;
    font-size: x-large;
    transition: all 250ms;
}
...

属性的值使元素完全不透明,属性的值指定大文本。由v-enter-to风格定义的属性代表了转换的结束状态,这是许多开发人员感到困惑的部分。关键是transition属性,它告诉浏览器将应用于该元素的所有 CSS 属性的值从当前值逐渐更改为该样式定义的值,并在 250 毫秒内完成此操作。浏览器可以逐渐改变 CSS 属性的值,这些属性可以用数值来表示,包括字体大小、填充和边距,甚至颜色。

理解过渡序列

将类和 CSS 样式放在一起会产生显示元素的 Vue.js 转换。当您单击切换可见性按钮时,v-if指令确定它应该将div元素添加到 DOM 中。div元素已经是几个 Bootstrap 类的成员,这些类设置文本大小、背景、填充和其他显示特性。为了准备转换,Vue.js 将元素添加到v-enter类中,该类设置了opacityfont-size属性;这些属性将元素的初始外观设置为透明,并且不赋予它高度。

在添加到 DOM 之后,元素立即从v-enter类中移除,并添加到v-enter-to类中。样式的改变在 250 毫秒的时间段内增加了不透明度和字体大小,结果是元素的大小和不透明度快速增长。在 250 毫秒结束时,元素从v-enter-to类中被移除,并且只通过它在引导类中的成员来设置样式,效果如图 25-5 所示。

img/465686_1_En_25_Fig5_HTML.jpg

图 25-5

进入过渡的效果

使用动画库

您可以直接使用 CSS 过渡、动画和翻译功能,但它们很难使用,并且除了最基本的效果之外,还需要经验和仔细的测试才能获得良好的效果。更好的方法是使用一个动画库,比如我在本章开始时添加到项目中的animate.css包。有很多高质量的动画库可以使用,它们包含了随时可用且易于应用的效果。

在清单 25-14 中,我已经用animate.css库提供的效果替换了我为SimpleDisplay组件中的div元素定制的效果。

小费

您不一定要使用animate.css,但是如果您不熟悉过渡,我推荐您从它开始。您可以在 https://github.com/daneden/animate.css 看到套装包含的全部效果。

<template>
    <div class="mx-5 border border-dark p-2">
        <h3 class="bg-warning text-white text-center p-2">Display</h3>

        <transition enter-to-class="fadeIn" leave-to-class="fadeOut">

            <div v-if="show" class="animated h4 bg-info text-center p-2">

               Hello, Adam
            </div>
        </transition>

        <div class="text-center">
            <button class="btn btn-primary" v-on:click="toggle">
                Toggle Visibility
            </button>
        </div>
    </div>
</template>

<script>

    export default {
        data: function () {
            return {
                show: true
            }
        },
        methods: {
            toggle() {
                this.show = !this.show;
            }
        }
    }

</script>

Listing 25-14Using Library Animations in the SimpleDisplay.vue File in the src/components Folder

animate.css包要求它所应用的元素是animated类的成员,我已经将它直接应用于清单 25-14 中的div元素。为了应用单独的效果,我使用了enter-to-classleave-to-class属性,它们允许元素在转换过程中被添加到的类的名称被改变。表 25-4 列出了允许选择类别的属性。

表 25-4

过渡类选择属性

|

名字

|

描述

| | --- | --- | | enter-class | 该属性用于指定将代替v-enter使用的类的名称。 | | enter-active-class | 该属性用于指定将代替v-enter-active使用的类的名称。 | | enter-to-class | 该属性用于指定将代替v-enter-to使用的类的名称。 | | leave-class | 该属性用于指定将代替v-leave使用的类的名称。 | | leave-active-class | 该属性用于指定将代替v-leave-active使用的类的名称。 | | leave-to-class | 该属性用于指定将代替v-leave-to使用的类的名称。 |

在清单中,我使用了enter-to-classleave-to-class属性来指定由animate.css包提供的动画类。正如类名所示,fadeIn类应用了一种元素淡入视图的效果,而fadeOut类应用了一种元素淡出的效果。

在多个元素之间切换

过渡的效果可以应用于由v-ifv-else-ifv-else指令组合显示的多个元素。需要一个单独的transition元素,当这些元素被添加到 DOM 或者从 DOM 中移除时,Vue.js 会自动将它们添加到正确的类中。在清单 25-15 中,我添加了一个元素,它的可见性由v-else指令管理,并且包含在与应用了v-if指令的元素相同的transition元素中。

<template>
    <div class="mx-5 border border-dark p-2">
        <h3 class="bg-warning text-white text-center p-2">Display</h3>

        <transition enter-active-class="fadeIn"

                    leave-active-class="fadeOut" mode="out-in">

                <div v-if="show" class="animated h4 bg-info text-center p-2"

                        key="hello">
                    Hello, Adam
                </div>
                <div v-else class="animated h4 bg-success text-center p-2"

                        key="goodbye">

                    Goodbye, Adam

                </div>

        </transition>

        <div class="text-center">
            <button class="btn btn-primary" v-on:click="toggle">
                Toggle Visibility
            </button>
        </div>
    </div>
</template>

<script>

    export default {
        data: function () {
            return {
                show: true
            }
        },
        methods: {
            toggle() {
                this.show = !this.show;
            }
        }
    }

</script>

Listing 25-15Adding an Element in the SimpleDisplay.vue File in the src/components Folder

当想要将过渡应用到相同类型的多个元素时,如本例中的两个div元素,那么必须应用key属性,以便 Vue.js 可以区分元素:

...
<div v-if="show" class="animated h4 bg-info text-center p-2" key="hello">
    Hello, Adam
</div>
...

小费

请注意,我已经使用enter-active-classleave-active-class属性应用了效果。当使用动画库在元素之间进行过渡时,在整个过渡过程中应用动画是很重要的;否则,会有一个不幸的小故障,即将离开的元素会在被移除前的几分之一秒内突然回到视图中。

默认情况下,Vue.js 同时过渡两个元素,这意味着一个元素淡入,另一个元素淡出。这并没有为这个例子创造出想要的效果,因为一个元素是用来替换另一个元素的。我已经告诉 Vue.js 使用transition元素上的mode属性错开元素的过渡,可以给定表 25-5 中描述的值。

表 25-5

模式属性值

|

名字

|

描述

| | --- | --- | | in-out | 首先转换传入元素,然后转换传出元素。 | | out-in | 首先转换这个传出元素,然后转换传入元素。 |

我选择了清单 25-15 中的out-in模式,这意味着 Vue.js 将等待直到传出元素完成其转换,然后开始传入元素的转换,结果如图 25-6 所示。

img/465686_1_En_25_Fig6_HTML.jpg

图 25-6

过渡多个元素

调整动画库效果的速度

使用动画库是应用过渡的好方法,但它们并不总是完全符合您的需要。我遇到的一个常见问题是,它们可能需要很长时间才能完成,当您在多个元素之间切换,并且必须等待几个效果执行时,这就会成为一个问题。

一些动画库允许你为一个效果指定一个速度,但是对于其他包——包括animate.css——你可以通过创建一个设置animation-duration属性的类来改变时间量,就像这样:

...
<style>
    .quick { animation-duration: 250ms }
</style>
...

然后,您可以在转换过程中向该类添加元素,如下所示:

...
<transition enter-active-class="fadeIn quick"
    leave-active-class="fadeOut quick" mode="out-in">
...

每个转换将在您指定的时间范围内执行,在本例中为 250 毫秒。

将过渡应用到 URL 路由元素

当一个router-view元素显示的元素改变时,可以使用相同的方法来应用效果,如清单 25-16 所示。

<template>
    <div class="m-2">
        <div class="text-center m-2">
            <div class="btn-group">
                <router-link tag="button" to="/display"
                        exact-active-class="btn-warning" class="btn btn-secondary">
                    Simple Display
                </router-link>
                <router-link tag="button" to="/list"
                        exact-active-class="btn-info" class="btn btn-secondary">
                    List Maker
                </router-link>
                <router-link tag="button" to="/numbers"
                        exact-active-class="btn-success" class="btn btn-secondary">
                    Numbers
                </router-link>
            </div>
        </div>
        <transition enter-active-class="animated fadeIn"

                    leave-active-class=" animated fadeOut" mode="out-in">

            <router-view />
        </transition>

    </div>
</template>

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

Listing 25-16Applying a Transition in the App.vue File in the src Folder

使用router-view元素时不需要提供key属性,因为 URL 路由系统能够区分组件。使用导航按钮可以看到清单 25-16 中的过渡效果,如图 25-7 所示。

img/465686_1_En_25_Fig7_HTML.jpg

图 25-7

将过渡应用到 URL 路由元素

为元素的外观应用过渡

默认情况下,Vue.js 不会将过渡应用到元素的初始显示。您可以通过向transition元素添加appear属性来覆盖它。默认情况下,Vue.js 将使用enter c类,但是你也可以使用专门用于初始外观的类或者使用属性指定类,如表 25-6 所述。

表 25-6

元素外观的 VueTransition 类

|

名字

|

描述

| | --- | --- | | v-appear | 元素在最初出现之前被添加到这个类中,在被添加到 DOM 中之后立即被移除。可以使用appear-class属性指定一个自定义类。 | | v-appear-active | 该元素在初始出现之前被添加到该类中,并在过渡完成时被移除。可以使用appear-active-class属性指定一个自定义类。 | | v-appear-to | 元素在被添加到 DOM 后立即被添加到这个类中,并在转换完成时被移除。可以使用appear-to-class属性指定一个自定义类。 |

在清单 25-17 中,我应用了一个只有在元素第一次出现时才会应用的过渡。

<template>

    <div class="m-2">
        <div class="text-center m-2">
            <div class="btn-group">
                <router-link tag="button" to="/display"
                    exact-active-class="btn-warning" class="btn btn-secondary">
                    Simple Display
                </router-link>
                <router-link tag="button" to="/list"
                    exact-active-class="btn-info" class="btn btn-secondary">
                    List Maker
                </router-link>
                <router-link tag="button" to="/numbers"
                    exact-active-class="btn-success" class="btn btn-secondary">
                    Numbers
                </router-link>
            </div>
        </div>
        <transition enter-active-class="animated fadeIn"

                    leave-active-class=" animated fadeOut" mode="out-in"

                    appear appear-active-class="animated zoomIn">

            <router-view />

        </transition>

    </div>

</template>

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

Listing 25-17Adding a Transition in the App.vue File in the src Folder

我添加了不需要值的appear属性,并使用appear-active-class属性告诉 Vue.js 在整个转换过程中应该将元素分配给animatedzoomIn类。结果是当应用第一次启动时,router-view元素显示的组件被放大到视图中,如图 25-8 所示。

小费

您必须重新加载浏览器才能看到这种转换。

img/465686_1_En_25_Fig8_HTML.jpg

图 25-8

为元素的初始外观应用过渡

为集合更改应用过渡

Vue.js 支持将过渡应用到使用v-for指令生成的元素,允许在添加、删除或移动元素时指定效果。在清单 25-18 中,我已经将过渡应用到了ListMaker组件。

<template>
    <div class="mx-5 p-2 border border-dark">
        <h3 class="bg-info text-white text-center p-2">My List</h3>
        <table class="table table-sm">
            <tr><th>#</th><th>Item</th><th width="20%" colspan="2"></th></tr>
            <transition-group enter-active-class="animated fadeIn"

                              leave-active-class="animated fadeOut"

                              move-class="time"

                              tag="tbody">

                <tr v-for="(item, i) in items" v-bind:key=item>
                    <td>{{i}}</td>
                    <td>{{item}}</td>
                    <td>
                        <button class="btn btn-sm btn-info" v-on:click="moveItem(i)">
                            Move
                        </button>
                        <button class="btn btn-sm btn-danger"
                                v-on:click="removeItem(i)">
                            Delete
                        </button>
                    </td>
                </tr>
           </transition-group>

           <controls v-on:add="addItem" />
        </table>
    </div>
</template>

<script>
    import Controls from "./ListMakerControls";

    export default {
        components: { Controls },
        data: function () {
            return {
                items: ["Apples", "Oranges", "Grapes"]
            }
        },
        methods: {
            addItem(item) {
                this.items.push(item);
            },
            removeItem(index) {
                this.items.splice(index, 1);
            },
            moveItem(index) {
                this.items.push(...this.items.splice(index, 1));
            }
        }
    }
</script>

<style>

    .time {

        transition: all 250ms;

    }

</style>

Listing 25-18Applying a Transition in the ListMaker.vue File in the src/components Folder

transition元素不同,transition-group元素在应用时需要小心,因为它向 DOM 添加了一个元素。为了确保组件生成有效的 HTML,使用了tag属性来指定将要生成的 HTML 元素的类型。由于示例组件使用v-for指令来生成一组表行,所以我指定了代表表体的tbody元素。

...
<transition-group enter-active-class="animated fadeIn"
    leave-active-class="animated fadeOut" move-class="time" tag="tbody">
...

进入和离开转换的应用方式与前面的例子相同,我使用了enter-active-classleave-active-class属性将由v-for指令生成的元素放入animatedfadeInfadeOut类,因为它们被添加和删除。剩下的属性是move-class,用于在元素从一个位置移动到另一个位置时应用一个类。Vue.js 会自动将元素从现有位置转换到新位置,如果这是您需要的唯一效果,那么可以使用move-class属性将元素与指定移动所需时间的样式相关联。在清单中,我指定了一个名为time的类,并定义了一个相应的 CSS 样式,该样式使用transition属性来指定元素的所有属性都应该在 250 毫秒内进行更改。

注意

transition-group属性管理的元素必须有一个键,如第十三章所述。

我对transition-group元素使用的属性导致新的元素淡入到位,被删除的元素淡出,当点击移动按钮时元素移动到位,最后一个如图 25-9 所示。

img/465686_1_En_25_Fig9_HTML.jpg

图 25-9

动画收藏项目移动

使用转换事件

transitiontransition-group元素发出事件,这些事件可以被处理以提供对转换的细粒度控制,包括根据应用的状态调整它们。表 25-7 描述了这些事件。

表 25-7

过渡事件

|

名字

|

描述

| | --- | --- | | before-enter | 在 enter 转换开始并接收受影响的 HTML 元素之前调用此方法。 | | enter | 该方法在进入转换开始之前被调用,并接收受影响的 HTML 元素和一个回调,该回调必须被调用以告知 Vue.js 转换已经完成。 | | after-enter | 在 enter 转换完成并接收受影响的 HTML 元素之前,调用此方法。 | | before-leave | 此方法在 leave 转换开始并接收受影响的 HTML 元素之前被调用。 | | leave | 该方法在 leave 转换开始之前被调用,并接收受影响的 HTML 元素和一个回调,该回调必须被调用以告知 Vue.js 转换已经完成。 | | after-leave | 此方法在 leave 转换完成并接收受影响的 HTML 元素之前被调用。 |

在清单 25-19 中,我使用v-on指令定义了转换事件的处理程序,并使用它们以编程方式应用效果。

<template>
    <tfoot>
        <transition v-on:beforeEnter="beforeEnter"

            v-on:after-enter="afterEnter" mode="out-in">

            <tr v-if="showAdd" key="addcancel">

                <td></td>
                <td><input class="form-control" v-model="currentItem" /></td>
                <td>
                    <button id="add" class="btn btn-sm btn-info"
                            v-on:click="handleAdd">
                        Add
                    </button>
                    <button id="cancel" class="btn btn-sm btn-secondary"
                            v-on:click="showAdd = false">
                        Cancel
                    </button>
                </td>
            </tr>
            <tr v-else key="show">

                <td colspan="4" class="text-center p-2">
                    <button class="btn btn-info" v-on:click="showAdd = true">
                        Show Add
                    </button>
                </td>
            </tr>
        </transition>

    </tfoot>
</template>

<script>
    export default {
        data: function () {
            return {
                showAdd: false,
                currentItem: ""
            }
        },
        methods: {
            handleAdd() {
                this.$emit("add", this.currentItem);
                this.showAdd = false;
            },
            beforeEnter(el) {

                if (this.showAdd) {

                    el.classList.add("animated", "fadeIn");

                }

            },

            afterEnter(el) {

                el.classList.remove("animated", "fadeIn");

            }

        }
    }
</script>

Listing 25-19Handling Transition Events in the ListMakerControls.vue File in the src/components Folder

before-enter事件的处理程序中,我检查了showAdd数据属性的值,以查看是否应该对更改进行动画处理。结果是当用户单击“显示添加”按钮时应用过渡效果,而当用户单击“添加”或“取消”按钮时没有效果。

使用进入和离开事件

当您想要执行从开始到结束的自定义转换时,enterleave事件非常有用,通常您想要以编程方式为元素属性生成一系列值,或者使用 JavaScript 库来完成这项工作。在清单 25-20 中,我处理了event方法,将 HTML 元素添加到animate.css类中,然后监听指示动画何时完成的 DOM 事件。

<template>
    <tfoot>
        <transition v-on:enter="enter" mode="out-in">

            <tr v-if="showAdd" key="addcancel">
                <td></td>
                <td><input class="form-control" v-model="currentItem" /></td>
                <td>
                    <button id="add" class="btn btn-sm btn-info"
                            v-on:click="handleAdd">
                        Add
                    </button>
                    <button id="cancel" class="btn btn-sm btn-secondary"
                            v-on:click="showAdd = false">
                        Cancel
                    </button>
                </td>
            </tr>
            <tr v-else key="show">
                <td colspan="4" class="text-center p-2">
                    <button class="btn btn-info" v-on:click="showAdd = true">
                        Show Add
                    </button>
                </td>
            </tr>
        </transition>
    </tfoot>
</template>

<script>
    import { styler, tween } from "popmotion";

    export default {
        data: function () {
            return {
                showAdd: false,
                currentItem: ""
            }
        },
        methods: {
            handleAdd() {
                this.$emit("add", this.currentItem);
                this.showAdd = false;
            },
            enter(el, done) {

                if (this.showAdd) {

                    let t = tween({

                        from: { opacity: 0 },

                        to: { opacity: 1 },

                        duration: 250

                    });

                    t.start({

                        update: styler(el).set,

                        complete: done

                    })

                }

            }

        }
    }
</script>

Listing 25-20Using the Enter Event in the ListMakeControls.vue File in the src/components Folder

这个例子使用了popmotion包,这是一个使用 JavaScript 而不是 CSS 的动画库。popmotion 如何工作的细节对于本章并不重要——详见http://popmotion.io——这个清单只是为了演示您可以使用enterleave事件通过 JavaScript 执行转换。

注意

注意清单 25-20 中的enter方法定义了一个done参数。这是一个回调函数,用来告诉 Vue.js 过渡已经完成。在调用done函数之前,Vue.js 不会触发after-enter事件,因此确保直接调用该方法或者将它用作 JavaScript 库的完成回调非常重要,就像我在清单中所做的那样。

引起对其他变化的注意

如果您想在应用的数据发生变化时引起用户的注意,那么您可以使用一个观察器来响应新的值。在清单 25-21 中,我使用了popmotion包来创建一个当用户向由Numbers组件呈现的输入元素输入新值时的过渡效果。

<template>
    <div class="mx-5 p-2 border border-dark">
        <h3 class="bg-success text-white text-center p-2">Numbers</h3>

        <div class="container-fluid">
            <div class="row">
                <div class="col">
                    <input class="form-control" v-model.number="first" />
                </div>
                <div class="col-1 h3">+</div>
                <div class="col">
                    <input class="form-control" v-model.number="second" />
                </div>
                <div class="col h3">= {{ displayTotal }} </div>

            </div>
        </div>
    </div>
</template>

<script>

    import { tween } from "popmotion";

    export default {
        data: function () {
            return {
                first: 10,
                second: 20,
                displayTotal: 30

            }
        },
        computed: {
            total() {
                return this.first + this.second;
            }
        },
        watch: {

            total(newVal, oldVal) {

                let t = tween({

                    from: Number(oldVal),

                    to: Number(newVal),

                    duration: 250

                });

                t.start((val) => this.displayTotal = val.toFixed(0));

            }

        }

    }
</script>

Listing 25-21Responding to Changes in the Numbers.vue File in the src/components Folder

total计算属性的值改变时,观察器通过使用popmotion包生成一系列新旧值之间的值来响应,每个值用于更新显示在组件模板中的displayTotal属性。

这种类型的更新可以通过使用$el属性来获取组件的 DOM 元素,使用它来定位要制作动画的元素,并将其添加到适当的类中,从而与 CSS 动画相结合,如清单 25-22 所示。

<template>
    <div class="mx-5 p-2 border border-dark">
        <h3 class="bg-success text-white text-center p-2">Numbers</h3>

        <div class="container-fluid">
            <div class="row">
                <div class="col">
                    <input class="form-control" v-model.number="first" />
                </div>
                <div class="col-1 h3">+</div>
                <div class="col">
                    <input class="form-control" v-model.number="second" />
                </div>
                <div id="total" class="col h3">= {{ displayTotal }} </div>

            </div>
        </div>
    </div>
</template>

<script>

    import { tween } from "popmotion";

    export default {
        data: function () {
            return {
                first: 10,
                second: 20,
                displayTotal: 30
            }
        },
        computed: {
            total() {
                return this.first + this.second;
            }
        },
        watch: {
            total(newVal, oldVal) {
                let classes = ["animated", "fadeIn"]

                let totalElem = this.$el.querySelector("#total");

                totalElem.classList.add(...classes);

                let t = tween({
                    from: Number(oldVal),
                    to: Number(newVal),
                    duration: 250
                });
                t.start({

                    update: (val) => this.displayTotal = val.toFixed(0),

                    complete: () => totalElem.classList.remove(...classes)

                });

            }
        }
    }
</script>

Listing 25-22Adding an Animation in the Numbers.vue File in the src/components Folder

为了确保动画可以再次应用,当更改完成时,我从动画类中删除了该元素。结果是新结果显示为平滑过渡,如图 25-10 所示,尽管这是一个很难在截图中捕捉到的效果。

img/465686_1_En_25_Fig10_HTML.jpg

图 25-10

使用观察器来响应值的变化

摘要

在这一章中,我解释了 Vue.js 转场的不同使用方式。我演示了如何使用transitiontransition-group元素,如何将元素分配给类,如何使用第三方包,以及如何响应转换事件。我还向您展示了如何将用户的注意力吸引到其他类型的变化上,比如当数据值发生变化时。在下一章,我将描述扩展 Vue.js 的不同方法。