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前端后端区块链产品设计人工智能 等领域,想要查看更多优质译文请持续关注 掘金翻译计划 、官方微博、 知乎专栏


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

查看所有标签

猜你喜欢:

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

沸騰15年

沸騰15年

林軍 / 商周出版 / 2010年09月19日 / NTD:430元

從一九九五年到二○○九年,中國互聯網崛起、發展和壯大。 在短短十五年間 產生了十五家市值超過十億的上市公司 這些前仆後繼的先行者 不但用網際網路創造了歷史,也改寫了自己的財富路徑。 這本關於中國互聯網產業歷史的書,脈絡清晰、生動鮮明地把一大批創業者的形象和他們的動人故事呈現在讀者眼前,值得一讀。 ——中國互聯網協會理事長、中國科協副主席 胡啟?? 林軍這本......一起来看看 《沸騰15年》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

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

UNIX 时间戳转换