- 原文地址: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 有所深刻了解。vue
咱们将使用下面开启了 HTML5 路由模式的应用程序做为示例。android
/projects/:projectId/users
/projects/:projectId/users/:userId
/projects/:projectId/users/:userId/profile
/projects/:projectId/users/new
从应用程序路由派生出的组件层次结构ios
Vue-router 在每个组件里注入当前路由对象。每一个组件里能够经过 this.$route
访问它。但关于这个对象有两点须要注意的事项。git
路由对象是不可改变的。github
若是你使用 $router.push()
,$router.replace()
或者连接导航到任何路由上,则会建立 $route
对象的新副本。已有的(路由)对象是不会被修改的。因为它(路由对象)是不可变的,因此你不须要设置 deep 属性监听这个 $route
对象:web
Vue.component('app-component', {
watch: {
$route: {
handler() {},
deep: true // <-- 并不须要
}
}
});
复制代码
路由对象是共享的。正则表达式
不可变性带来了进一步的优点。路由在全部组件内部共享同一个 $route
对象实例。因此下面这些内容都将生效:vue-router
// 父组件
Vue.component('app-component', {
mounted() { window.obj1 = this.$route; }
});
// 子组件
Vue.component('user-list', {
mounted() { window.obj2 = this.$route; }
});
// 一旦 App 实例化
window.obj1 === window.obj2; // <-- 返回 true
复制代码
理论上来讲,路由是分解大型网络应用程序的第一级抽象。状态管理更晚一些。shell
有两种关于分解网络应用程序的思考方式。一种是把应用程序分解成一系列的页面(例如,每一个页面都根据 URL 边界进行拆分),另外一种是把应用程序理解成已经定义好的一组状态(可选择让每一个状态都有一个 URL)。
state-router 会把应用程序拆解成一组状态。url-router 会把应用程序拆解成一组页面。
Vue-router 是 url-router。Vue 没有官方 state-router。有 Angular 背景的人员立刻会意识到它们的区别。状态路由器(state-router)相较于 URL 路由器(url-router)方式的区别:
即使不是状态路由器,在转变过程当中,你仍然能够把复杂数据从一个路径传递到另外一个上,而不用将数据做为 URL 的一部分。
当使用 vue-router 从一个路由导航到另外一个路由时,你能够传递隐式数据或状态。
这在哪里有用呢?主要是优化的时候。考虑下面的例子:
/users/:userId
简介页 —— /users/:userId/profile
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
对象注入在每一个组件中且是共享不可变的。否则会很难办。
若是你有嵌套配置,那么任何子组件上的保护都有可能阻塞父组件的渲染。例如:
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 的一些概念以后,是时候讨论关于编写优雅代码的模式了。
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,
}
]
}];
复制代码
基于树结构配置有一些优势:
使用基于树结构配置的细微差异在于建立中间组件,它们可能只包含一个 router-view
组件。Vue-router 没有将 RouterView
组件直接暴露给最终开发者。可是一个包装 router-view
的小技巧能够极大地帮助减小中间组件:
const RouterViewWrapper = Vue.extend({
template: `<router-view></router-view>`
});
// 如今,能够在路由配置树的任何位置
// 使用 RouterViewWrapper 组件。
复制代码
注意:Trie 是一种搜索树数据结构的类型(译者注:前缀树)。基于前缀的路由是可预见的,而且无论路由的定义顺序。在 Nodejs 生态环境里,存在不少基于前缀或者相似的路由。Hapi.js 和 Fastify.js 使用的是基于前缀的路由。
简而言之:
树结构配置优于扁平结构配置。
当你使用导航保护的时候,你可能在这些保护函数里须要一些依赖。大多数常见的例子是 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: []
})
}
复制代码
设想你在一个异步组件里使用路由配置。异步组件是经过懒加载方式引入的。这一般是使用像 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();
});
}
}
复制代码
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、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。