内容简介:除了 DOM 操作,事件处理,表单和组件之外,每个单页应用程序(SPA)框架如果要用于大型应用程序都需要两个核心部分:幸运的是,Vue 为路由和状态管理提供了我们将使用下面开启了 HTML5 路由模式的应用程序作为示例。
- 原文地址: Vue Router — The Missing Manual
- 原文作者:Harshal Patil
- 译文出自: 掘金翻译计划
- 本文永久链接: github.com/xitu/gold-m…
- 译者: Sam
- 校对者: Ranjay , shixi-li
除了 DOM 操作,事件处理,表单和组件之外,每个单页应用程序(SPA)框架如果要用于大型应用程序都需要两个核心部分:
- 客户端路由
- 显式状态管理(通常是单向的)
幸运的是,Vue 为路由和状态管理提供了 官方解决方案 。这篇文章里,我们将要探寻vue-router,以了解路由在诸多场景中的行为表现,并探索一些编写优雅代码的模式。这里假设你已经对 vue,vue-router 和 SPA 有所深入了解。
我们将使用下面开启了 HTML5 路由模式的应用程序作为示例。
路由:
/projects/:projectId/users /projects/:projectId/users/:userId /projects/:projectId/users/:userId/profile /projects/:projectId/users/new
组件树结构:
从应用程序路由派生出的组件层次结构
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 从一个路由导航到另一个路由时,你可以传递隐式数据或状态。
这在哪里有用呢? 主要是优化的时候 。考虑下面的例子:
- 我们有两个页面: 详情页 ——
/users/:userId
简介页 ——/users/:userId/profile
- 在详情页面里,我们调起一个 API 请求获取用户信息。并且,页面上有一个链接帮助用户跳转到简介页面。
- 第二个页面上,我们需要发起两个 API 请求 —— 获取用户信息和获取用户概要。
- 这里的问题是 —— 当我从详情页面导航到简介页面时做了两次一样的 API 请求。最佳的解决方案是当我们用户从详情视图页转变成简介视图页时,把已检索的用户数据传递给下一个路由。另外,这些已检索的数据不需要作为 URL 的一部分(就像状态路由器一样,传递一个隐式的状态)。
- 如果用户通过任何其他方式直接跳转到简介页面,比如整个页面刷新或者从其他视图进入,那么在
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, } ] }]; 复制代码
基于树结构配置有一些优点:
- 结构清晰。易于维护。
- 授权/保护的管理变得容易。基于 CRUD (增删改查) 的权限执行变得非常简单。
- 比起扁平的路由列表有更可预见的路由。
使用基于树结构配置的细微差别在于创建中间组件,它们可能只包含一个 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-behavior
, lazy-loading
等。
此外,当我们使用导航保护,预路由数据获取时,vue-router 设计了关于用户体验(UX)的考量。你可以使用全局或者组件内保护,但需谨慎地使用它们,因此你应该牢记关注点分离并把路由职责从组件中移除。
如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为掘金 上的英文分享文章。内容覆盖 Android 、 iOS 、 前端 、 后端 、 区块链 、 产品 、 设计 、 人工智能 等领域,想要查看更多优质译文请持续关注 掘金翻译计划 、官方微博、 知乎专栏 。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Flutter移动端实战手册
- CSDN首发丨TBSchedule应用实战手册
- 凡事都要问主管、东拉西扯没重点:敏捷破坏者实战手册
- 测者的测试技术手册:自动化的自动化EvoSuite:Maven项目集成EvoSuite实战
- MySQL™ 参考手册(关于本手册)
- LLVM 程序员手册 —— LLVM 4.0 文档(非常非常完整的手册)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
UNIX 时间戳转换
UNIX 时间戳转换
HSV CMYK 转换工具
HSV CMYK互换工具