Vue Router 实战手册

栏目: 编程语言 · 发布时间: 5年前

内容简介:除了 DOM 操作,事件处理,表单和组件之外,每个单页应用程序(SPA)框架如果要用于大型应用程序都需要两个核心部分:幸运的是,Vue 为路由和状态管理提供了我们将使用下面开启了 HTML5 路由模式的应用程序作为示例。
Vue Router 实战手册

除了 DOM 操作,事件处理,表单和组件之外,每个单页应用程序(SPA)框架如果要用于大型应用程序都需要两个核心部分:

  1. 客户端路由
  2. 显式状态管理(通常是单向的)

幸运的是,Vue 为路由和状态管理提供了 官方解决方案 。这篇文章里,我们将要探寻vue-router,以了解路由在诸多场景中的行为表现,并探索一些编写优雅代码的模式。这里假设你已经对 vue,vue-router 和 SPA 有所深入了解。

我们将使用下面开启了 HTML5 路由模式的应用程序作为示例。

路由:

/projects/:projectId/users
/projects/:projectId/users/:userId
/projects/:projectId/users/:userId/profile
/projects/:projectId/users/new

组件树结构:

Vue Router 实战手册

从应用程序路由派生出的组件层次结构

1. 当前的路由对象是共享且不可变的

Vue-router 在每一个组件里注入 当前路由对象 。每个组件里可以通过 this.$route 访问它。但关于这个对象有两点需要注意的事项。

路由对象是 不可改变的

如果你使用 $router.push()$router.replace() 或者链接导航到任何路由上,则会创建 $route 对象的新副本。已有的(路由)对象是不会被修改的。由于它(路由对象)是不可变的,所以你 不需要设置 deep 属性监听 这个 $route 对象:

Vue.component('app-component', {
    watch: {
        $route: {
             handler() {},
             deep: true // <-- 并不需要
        }
    }
});
复制代码

路由对象是 共享的

不可变性带来了进一步的优势。路由在所有组件内部共享同一个 $route 对象实例。所以下面这些内容都将生效:

// 父组件
Vue.component('app-component', {
    mounted() { window.obj1 = this.$route; }
});
// 子组件
Vue.component('user-list', {
    mounted() { window.obj2 = this.$route; }
});
// 一旦 App 实例化
window.obj1 === window.obj2; // <-- 返回 true
复制代码

2. Vue-router 不是状态路由

理论上来说, 路由 是分解大型网络应用程序的 第一级抽象 。状态管理更晚一些。

有两种关于分解网络应用程序的思考方式。一种是把应用程序分解成一系列的页面(例如,每个页面都根据 URL 边界进行拆分),另一种是把应用程序理解成已经定义好的一组状态(可选择让每个状态都有一个 URL)。

state-router 会把应用程序拆解成 一组状态 。url-router 会把应用程序拆解成 一组页面

Vue-router 是 url-router 。Vue 没有官方 state-router。有 Angular 背景的人员马上会意识到它们的区别。状态路由器(state-router)相较于 URL 路由器(url-router)方式的区别:

  • 状态路由器像状态机一样工作。
  • 状态路由器中 URL 是非必要的。
  • 状态是可以嵌套的。
  • 一个应用程序被拆分成一组定义好的状态集合而不是页面。从一个状态转变为另一个状态时可选择性的改变 URL。
  • 当从一个状态转变成另一个状态时可以传递任何复杂的数据。但使用 URL 路由器,在页面间传递数据一般是将它作为 URL 地址的一部分或查询参数。
  • 使用状态路由器,当整体页面发生刷新的时候已传递的数据会丢失(除非你使用了 session 或者 local storage 做了存储)。使用 URL 路由器,可以重建状态,因为大部分传递的数据都存在于 URL 中。

3. 路由之间传递的隐式数据

即便不是状态路由器,在转变过程中,你仍然可以把复杂数据从一个路径传递到另一个上,而不用将数据作为 URL 的一部分。

当使用 vue-router 从一个路由导航到另一个路由时,你可以传递隐式数据或状态。

这在哪里有用呢? 主要是优化的时候 。考虑下面的例子:

  1. 我们有两个页面: 详情页 —— /users/:userId 简介页 —— /users/:userId/profile
  2. 在详情页面里,我们调起一个 API 请求获取用户信息。并且,页面上有一个链接帮助用户跳转到简介页面。
  3. 第二个页面上,我们需要发起两个 API 请求 —— 获取用户信息和获取用户概要。
  4. 这里的问题是 —— 当我从详情页面导航到简介页面时做了两次一样的 API 请求。最佳的解决方案是当我们用户从详情视图页转变成简介视图页时,把已检索的用户数据传递给下一个路由。另外,这些已检索的数据不需要作为 URL 的一部分(就像状态路由器一样,传递一个隐式的状态)。
  5. 如果用户通过任何其他方式直接跳转到简介页面,比如整个页面刷新或者从其他视图进入,那么在 created 钩子函数里,我们可以选择检查数据的可用性。
// 用户详情组件内部
Vue.component('user-details', {
    methods: {
        onLinkClick() {
            this.$router.push({ 
                name: 'profile',
                params: { 
                    userId: 123,
                    userData  // 隐式数据/状态
                }
            });
        }
    }
});

// 用户简介组件内部
Vue.component('user-profile', {
    created() {
        // 访问附带过来的数据
        if (this.$route.params.userData) {
            this.userData = this.$route.params.userData;
        } else {
            // 不然就发起 API 请求获取用户数据
            this.getUserDetails(this.$route.params.userId)
                .then(/* handle response */);
        }
    }
});
复制代码

注意:能够这样处理是因为 $route 对象注入在每个组件中且是共享不可变的。不然会很难办。

4. 导航保护阻塞父组件

如果你有嵌套配置,那么任何子组件上的保护都有可能阻塞父组件的渲染。例如:

const ParentComp = Vue.extend({ 
    template: `<div>
        <progress-loader></progress-loader>
        <router-view>
    </div>` 
});

{
    path: '/projects/:projectId',
    name: 'project',
    component: ParentComp,

    children: [{
        path: 'users',
        name: 'list',
        component: UserList,
        beforeEnter (to, from, next) {
            setTimeout(() => next(), 2000);
        }
    }]
}
复制代码

如果你直接导航到 /projects/100/users/list ,那么由于 beforeEnter 的异步保护,导航会被当作 等待中(pending) ,并且 ParentComp 组件不会被渲染。所以,如果你希望看到 进程加载器(progress-loader) 直到保护解除,它应该是不会出现。对于你可能从父组件发起的任何 API 请求也是如此。

在这种情况下,如果你希望显示 父级组件 而不顾子级路由的保护策略,解决方案是改变你组件的层级结构并且通过某种方式更新 进程加载器(progress-loader) 的逻辑。如果你做不到,那么你可以像这样 使用双重传递 —— 先导航到父组件然后再到子组件:

goToUserList () {
    this.$router.push('/projects/100',
        () => this.$router.replace('users'))
}
复制代码

这个行为是有道理的。如果父级视图不等待子级的保护,那么它可能先渲染一会父级视图,然后如果保护失败则导航到其他地方去。

注意:相比之下,Angular 的路由是完全相反地。父级组件一般不会等待任何子级保护的触发。那么哪种方案是正确的?都不是。乍看上去,Angular 采取的方法感觉自然而有序,但如果开发者不仔细的话它很容易搞砸用户体验(UX)。

使用 vue-router,渲染层级似乎有点尴尬。但却少有机会破坏用户体验(UX)。Vue 隐含地预先强制执行这项决定。同时,不要忘记 vue-router 提供的作用域。你可以使用全局级别,路由级别或者组件内级别的保护。你会拥有真正细粒度的控制。

在理解了关于 vue-router 的一些概念之后,是时候讨论关于编写优雅代码的模式了。

5. Vue-router 不是基于前缀(trie-based)的路由器

Vue-router 是构建在 path-to-regexp 之上的。Express.js 路由也是如此。URL 匹配是基于正则表达式的。这意味着你可以像这样定义你的路由:

const prefix = `/projects/:projectId/users`;

const routes = [
    {
        path: `${prefix}/list`,
        name: 'user-list',
        component: UserList,
    },

    {
        path: `${prefix}/:userId`,
        name: 'user-details',
        component: UserDetails
    },

    {
        // 这里不会造成问题吗?
        path: `${prefix}/new`,
        name: 'user-new',
        component: NewUser
    }
];
复制代码

这里不那么明显的问题是路径 ${prefix}/new 永远不会被匹配,因为它定义在路由列表的最后。这是基于 正则表达式 路由的缺陷。不止一个路由会被匹配上(译者注:路径 ${prefix}/:userId 会覆盖匹配路径 ${prefix}/new )。当然,这对于小型网络应用程序不是问题。或者,你可以像这样定义 一棵路由树

const routes = [{
    path: '/projects/:projectId/users',
    name: 'project',
    component: ProjectUserView,

    children: [
        {
            path: '',
            name: 'list',
            component: UserList,
        },
        {
            path: 'new',
            name: 'user-details',
            component: NewUser,
        },
        {
            path: ':userId',
            name: 'user-new',
            component: UserDetails,
        }
    ]
}];
复制代码

基于树结构配置有一些优点:

  1. 结构清晰。易于维护。
  2. 授权/保护的管理变得容易。基于 CRUD (增删改查) 的权限执行变得非常简单。
  3. 比起扁平的路由列表有更可预见的路由。

使用基于树结构配置的细微差别在于创建中间组件,它们可能只包含一个 router-view 组件。Vue-router 没有将 RouterView 组件直接暴露给最终开发者。但是一个包装 router-view 的小技巧可以极大地帮助减少中间组件:

const RouterViewWrapper = Vue.extend({ 
    template: `<router-view></router-view>`
});

// 现在,可以在路由配置树的任何位置
// 使用 RouterViewWrapper 组件。
复制代码

注意: Trie 是一种搜索树数据结构的类型(译者注:前缀树)。基于前缀的路由是可预见的,并且不管路由的定义顺序。在 Nodejs 生态环境里,存在很多基于前缀或者类似的路由。Hapi.js 和 Fastify.js 使用的是基于前缀的路由。

简而言之:

树结构配置优于扁平结构配置。

6. 路由器的依赖注入

当你使用导航保护的时候,你可能在这些保护函数里需要一些依赖。大多数常见的例子是 Vuex/Redux 的 store。这个解决方案过于简单。比起路由器本身,还有更多关于代码组织的工作要做。假定你有以下这些文件:

src/
  |-- main.js
  |-- router.js
  |-- store.js
复制代码

你可以创建一个在定义导航守护时的存储(store)注入函数:

// 在你的 store.js 里,定义存储注入器
export const store = new Vuex.Store({ /* config */ });

export function storeInjector(fn) {
    return (...args) => fn(...args, store);
}

// 在你的 router.js 里,使用存储注入器
const routeConfig = {
    // 其他内容
    beforeEnter: storeInjector((to, from, next, store) => {})
}
复制代码

或者,你也可以将路由创建器封装到可以传递任何依赖的函数中:

// main.js 文件
import { makeStore } from './store.js';

const store = makeStore();
const router = makeRouter(store);

const app = new Vue({ store, router, template: `<div></div>` });

// router.js 文件
export function makeRouter(store) {

    // 使用 store 处理任何事情
    return new VueRouter({
        routes: []
    })
}
复制代码

7. 单次监听路由对象

设想你在一个异步组件里使用路由配置。异步组件是通过懒加载方式引入的。这通常是使用像 Webpack 或 Rollup 这样的 工具 进行包(bundle)拆分实现的。配置看起来将会是这样的:

const routes = [{
    path: '/projects/:projectId/users',
    name: 'user-list',

    // 异步组件(Webpack 的代码拆分)
    component: import('../UserList.js'),
}];
复制代码

在根实例或者父级 AppComponent 组件里,你可能希望检索 projectId 用来做一些引导性的 API 调用。典型的代码是:

Vue.component('app-comp', {

    created() {
        // 问题:projectId 未定义         
        console.log(this.$route.params.projectId);
    }
}
复制代码

这里的问题是 projectId 将是未定义的,因为子组件没有准备好,路由器还没有完成传递。

当你在路由配置里使用异步组件时,在未创建子组件之前,父组件中将不提供路径或查询参数。

这里的解决方案是在父组件里监听 $route 。另外, 你必须只监听它一次,因为它只是一个引导性 API 请求 并且不应该再被触发:

Vue.component('app-comp', {

    created() {
        const unwatch = this.$watch('$route', () => {
            const projectId = this.$route.params.projectId;
            
            // 做剩余的工作 
            this.getProjectInfo(projectId);

            // 立即解开监听
            unwatch();
        });
    }
}
复制代码

8. 使用扁平路由混合监听嵌套组件

const routes = [{
    path: '/projects/:projectId',
    name: 'project',
    component: ProjectView,

    beforeEnter(to, from, next) {
        next();
    },

    children: [{
        // 仔细观察
        // 嵌套路由以 `/` 开头 
        path: '/users',
        name: 'list',
        component: UserList,
    }]
}];
复制代码

在上面的配置中,子级路由以 / 开头因此被当作根路径。所以你可以使用 https://example.com/users 而不是 https://example.com/projects/100/users 就可以访问 UserList 组件。然而, UserList 组件将被渲染成 ProjectView 组件的子组件。这种路径被称为 根相对嵌套路径

当然,组件层级,导航保护依然在处理中。你仍然需要嵌套的 <router-view> 组件。唯一改变的事情是 URL 的结构。其他的都还保持原样。这意味着 beforeEnter 保护将在 UserList 组件之前执行。

这个技巧是纯粹的便利,因此需要谨慎的使用它。从长远来看,它往往会产生令人困惑的代码。然而 ——

根相对嵌套路径在构建App Shell Model 的 PWA 时非常有用。

Vue 提供的官方路由解决方案是非常灵活的。除去简单的路由,它还提供了许多功能,如 meta 字段, transition ,高级 scroll-behaviorlazy-loading 等。

此外,当我们使用导航保护,预路由数据获取时,vue-router 设计了关于用户体验(UX)的考量。你可以使用全局或者组件内保护,但需谨慎地使用它们,因此你应该牢记关注点分离并把路由职责从组件中移除。

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。

掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能 等领域,想要查看更多优质译文请持续关注 掘金翻译计划 、官方微博、 知乎专栏


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

平台战略

平台战略

陈威如、余卓轩 / 中信出版社 / 2013-1 / 58.00元

《平台战略:正在席卷全球的商业模式革命》内容简介:平台商业模式的精髓,在于打造一个完善的、成长潜能强大的“生态圈”。它拥有独树一帜的精密规范和机制系统,能有效激励多方群体之间互动,达成平台企业的愿景。纵观全球许多重新定义产业架构的企业,我们往往就会发现它们成功的关键——建立起良好的“平台生态圈”,连接两个以上群体,弯曲、打碎了既有的产业链。 平台生态圈里的一方群体,一旦因为需求增加而壮大,另......一起来看看 《平台战略》 这本书的介绍吧!

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具