路由是任何单页应用(SPA的重要组成部分。本章重点介绍如何最大化 Vue 路由,并介绍从页面之间的用户路由到参数,再到最佳配置的所有内容。
本章结束时,我们将介绍以下内容:
- 在 Vue.js 应用中实现路由
- 使用动态路线匹配创建路线参数
- 将管线参数作为组件道具传递
现代 JavaScript 应用实现了一种称为 SPA 的模式。在其最简单的形式中,可以将其视为基于 URL 显示组件的应用。由于模板映射到路由,因此不需要重新加载页面,因为它们可以根据用户导航的位置进行注入。
这是路由的工作。
通过这种方式创建应用,我们能够提高感知速度和实际速度,因为我们的应用更加动态。
让我们启动一个游乐场项目并安装vue-router
库。这使我们能够利用应用内部的路由,并提供现代 SPA 的强大功能。
在终端中运行以下命令:
# Create a new Vue project
$ vue init webpack-simple vue-router-basics
# Navigate to directory
$ cd vue-router-basics
# Install dependencies
$ npm install
# Install Vue Router
$ npm install vue-router
# Run application
$ npm run dev
由于我们使用 webpack 作为构建系统的一部分,我们已经安装了带有npm
的路由。然后我们可以在src/main.js
内初始化路由:
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';
Vue.use(VueRouter);
new Vue({
el: '#app',
render: h => h(App)
});
这有效地将VueRouter
注册为一个全局插件。插件只是一个函数,它接收Vue
和options
作为参数,并允许VueRouter
等库向我们的 Vue 应用添加功能。
然后,我们可以在main.js
文件中定义两个小组件,它们只是有一个显示h1
的模板,其中包含一些文本:
const Hello = { template: `<h1>Hello</h1>` };
const World = { template: `<h1>World</h1>`};
然后,为了在屏幕上特定 URL(如/hello
和/world
处显示这些组件,我们可以在应用内定义路由:
const routes = [
{ path: '/hello', component: Hello },
{ path: '/world', component: World }
];
现在我们已经定义了我们想要使用的组件以及应用内部的路由,我们需要创建一个新的VueRouter
实例并沿着路由传递。
虽然我们已经使用了Vue.use(VueRouter)
,但仍然需要创建VueRouter
的新实例并初始化路由。这是因为只要将VueRouter
注册为插件,我们就可以访问 Vue 实例中的路由选项:
const router = new VueRouter({
routes
});
然后我们需要将router
传递给我们的根 Vue 实例:
new Vue({
el: '#app',
router,
render: h => h(App)
});
最后,要在App.vue
组件中显示我们的路由组件,我们需要在template
中添加router-view
组件:
<template>
<div id="app">
<router-view/>
</div>
</template>
如果我们随后导航到/#/hello/
或/#/world
,将显示相应的组件:
我们还可以根据特定参数动态匹配路由。这是通过在参数名称前指定带有冒号的路由来完成的。下面是一个使用类似问候语组件的示例:
// Components
const Hello = { template: `<h1>Hello</h1>` };
const HelloName = { template: `<h1>Hello {{ $route.params.name}}` }
// Routes
const routes = [
{ path: '/hello', component: Hello },
{ path: '/hello/:name', component: HelloName },
]
如果我们的用户导航到/hello
,他们将看到带有文本Hello
的h1
。否则,如果他们导航到/hello/{name}
(即 Paul),他们将看到h1
和文本Hello Paul
。
我们已经取得了很大的进展,但重要的是要知道,当我们导航到参数化 URL 时,如果参数发生变化(即从/hello/paul
到/hello/katie
),组件生命周期挂钩不会再次触发。我们很快就会看到的!
让我们改变我们的/hello/name
路线,将name
参数作为component
道具传递,这可以通过向路线添加props: true
标志来实现:
const routes = [
{ path: '/hello', component: Hello },
{ path: '/hello/:name', component: HelloName, props: true},
]
然后,我们可以更新我们的组件,以接收一个名为id
的道具,并将其记录到生命周期挂钩中的控制台:
const HelloName = {
props: ['name'],
template: `<h1>Hello {{ name }}</h1>`,
created() {
console.log(`Hello ${this.name}`)
}
}
如果我们尝试导航到不同的动态路由,我们将看到创建的钩子只触发一次(除非刷新页面),即使页面显示正确的名称:
我们如何解决生命周期挂钩问题?在这种情况下,我们可以使用所谓的导航卫士。这使我们能够连接到路由的不同生命周期,例如beforeRouteEnter
、beforeRouteUpdate
和beforeRouteLeave
方法。
让我们使用beforeRouteUpdate
方法访问有关路线更改的信息:
const HelloName = {
props: ['name'],
template: `<h1>Hello {{ name }}</h1>`,
beforeRouteUpdate(to, from, next) {
console.log(to);
console.log(from);
console.log(`Hello ${to.params.name}`)
},
}
如果我们在导航到/hello/{name}
下的另一条路线后检查 JavaScript 控制台,我们将能够看到用户要去哪条路线以及他们来自哪里。to
和from
对象还提供了对params
的访问、查询、完整路径等。
虽然我们正确地获得了 log 语句,但如果我们尝试在路由之间导航,您会注意到我们的应用不会使用参数name
prop 进行更新。这是因为在我们完成了守卫中的任何计算之后,我们还没有使用next
函数。我们再加上:
beforeRouteUpdate(to, from, next) {
console.log(to);
console.log(from);
console.log(`Hello ${to.params.name}`)
next();
},
我们还可以利用beforeRouteEnter
在进入组件路径之前执行操作。下面是一个例子:
beforeRouteEnter(to, from, next) {
console.log(`I'm called before entering the route!`)
next();
}
我们仍然需要调用next
将堆栈向下传递给下一个路由处理程序。
我们还可以在离开路线时挂接beforeRouteLeave
执行操作。因为我们已经在这个钩子的上下文中走上了这条路线,所以我们可以访问组件实例。让我们看一个例子:
beforeRouteLeave(to, from, next) {
console.log(`I'm called before leaving the route!`)
console.log(`I have access to the component instance, here's proof!
Name: ${this.name}`);
next();
}
在这种情况下,我们必须再次调用next
。
我们已经研究了组件导航卫士,虽然这些卫士是逐个组件工作的,但您可能希望建立侦听导航事件的全局钩子。
我们可以使用router.beforeEach
在应用中全局侦听路由事件。如果您有身份验证检查或其他应该在每个路由中使用的功能,那么这是值得使用的。
下面是一个简单的例子,它可以注销用户要去和要去的路由。以下示例中的每一个都假设路由存在于与以下类似的范围内:
const router = new VueRouter({
routes
})
router.beforeEach((to, from, next) => {
console.log(`Route to`, to)
console.log(`Route from`, from)
next();
});
再一次,我们必须呼叫next()
触发下一个路线守卫。
beforeResolve
全局路由保护是在确认导航之前触发的,但重要的是要知道,只有在解决了所有特定于组件的保护和异步组件之后才会触发。
下面是一个例子:
router.beforeResolve((to, from, next) => {
console.log(`Before resolve:`)
console.log(`Route to`, to)
console.log(`Route from`, from)
next();
});
我们还可以连接到全局afterEach
功能,该功能允许我们执行操作,但我们不会影响导航,因此只能访问to
和from
参数:
router.afterEach((to, from) => {
console.log(`After each:`)
console.log(`Route to`, to)
console.log(`Route from`, from)
});
现在,我们已经熟悉了提供的各种不同的路由生命周期挂钩,每当我们尝试导航到另一个路由时,都有必要研究整个分辨率堆栈:
- 触发路线变更:这是任何路线生命周期的第一阶段,并且在我们尝试导航到新路线时触发。例如从
/hello/Paul
到/hello/Katie
。此时未触发导航防护。 - 触发组件离开防护装置:接下来,在加载的组件上触发任何离开防护装置,例如
beforeRouteLeave
。 - 触发全局 beforeach guards:由于可以使用
beforeEach
创建全局路由中间件,所以在任何路由更新之前都会调用这些函数。 - 触发本地 beforeRouteUpdate****重用组件中的防护:正如我们前面看到的,每当我们使用不同参数导航到同一路线时,生命周期挂钩不会触发两次。相反,我们使用
beforeRouteUpdate
触发生命周期更改。 - 组件:中的路由前触发器每次导航到任何路由之前都会调用该触发器。在这个阶段,组件没有呈现,因此它没有访问
this
组件实例的权限。 - 解析异步路由组件:然后尝试解析项目中的任何异步组件。下面是一个例子:
const MyAsyncComponent = () => ({
component: import ('./LazyComponent.vue'),
loading: LoadingComponent,
error: ErrorComponent,
delay: 150,
timeout: 3000
})
-
路由前触发成功激活组件: 我们现在可以访问
beforeRouteEnter
钩子,并可以在解析路由之前执行任何操作。 -
Trigger global beforeResolve hooks:提供的组件内保护和异步路由组件已经解决,我们现在可以挂接到全局
router.beforeResolve
方法中,该方法允许我们在此阶段执行操作。 -
导航:之前的所有导航防护都已被触发,用户现在成功导航到一条路线。
-
每次钩住后触发:虽然用户已经导航到了路线,但没有停在那里。接下来,路由触发一个全局
afterEach
钩子,该钩子可以访问to
和from
参数。由于此阶段已解决路线问题,因此没有下一个参数,因此不会影响导航。 -
触发 DOM 更新:路由已解析,Vue 可以适当触发 DOM 更新。
-
beforeRouteEnter:中的在 next 内触发回调,由于
beforeRouteEnter
无权访问组件的this
上下文,next
参数接受一个回调,该回调在导航时解析到组件实例。这里可以看到一个例子:
beforeRouteEnter (to, from, next) {
next(comp => {
// 'comp' inside this closure is equal to the component instance
})
我们不限于使用router-link
进行模板导航;我们还可以通过编程将用户导航到 JavaScript 中的不同路径。在我们的App.vue
中,让我们展示<router-view>
并让用户能够选择一个按钮,将他们导航到/hello
或/hello/:name
路线:
<template>
<div id="app">
<nav>
<button @click="navigateToRoute('/hello')">/Hello</button>
<button
@click="navigateToRoute('/hello/Paul')">/Hello/Name</button>
</nav>
<router-view></router-view>
</div>
</template>
然后,我们可以添加一个方法,将新路由推送到路由堆栈*:*上
<script>
export default {
methods: {
navigateToRoute(routeName) {
this.$router.push({ path: routeName });
},
},
};
</script>
在这一点上,任何时候我们选择一个按钮,它应该随后导航用户到适当的路线。$router.push()
函数可以接受各种不同的参数,具体取决于路由的设置方式。以下是一些例子:
// Navigate with string literal
this.$router.push('hello')
// Navigate with object options
this.$router.push({ path: 'hello' })
// Add parameters
this.$router.push({ name: 'hello', params: { name: 'Paul' }})
// Using query parameters /hello?name=paul
this.$router.push({ path: 'hello', query: { name: 'Paul' }})
我们也可以用router.replace
替换当前历史堆栈,而不是在堆栈上推送导航项。下面是一个例子:
this.$router.replace({ path: routeName });
如果我们想向后或向前导航用户,可以使用router.go
;这本质上是对window.history
API 的抽象。让我们来看看一些例子:
// Navigate forward one record
this.$router.go(1);
// Navigate backward one record
this.$router.go(-1);
// Navigate forward three records
this.$router.go(3);
// Navigate backward three records
this.$router.go(-3);
我们还可以延迟加载路由,以利用 webpack 的代码拆分。这使我们的性能比急切加载路线时更高。为此,我们可以创建一个小型游乐场项目。在终端中运行以下命令:
# Create a new Vue project
$ vue init webpack-simple vue-lazy-loading
# Navigate to directory
$ cd vue-lazy-loading
# Install dependencies
$ npm install
# Install Vue Router
$ npm install vue-router
# Run application
$ npm run dev
首先,我们在src/components
内部创建两个组件,分别命名为Hello.vue
和World.vue
:
// Hello.vue
<template>
<div>
<h1>Hello</h1>
<router-link to="/world">Next</router-link>
</div>
</template>
<script>
export default {};
</script>
现在我们已经创建了我们的Hello.vue
组件,让我们像这样创建第二个World.vue
:
// World.vue
<template>
<div>
<h1>World</h1>
<router-link to="/hello">Back</router-link>
</div>
</template>
<script>
export default {};
</script>
然后我们可以像往常一样在main.js
内初始化路由:
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter);
主要的区别在于我们进口组件的方式。这需要使用syntax-dynamic-import
巴别塔插件。通过运行以下项目将其安装到终端:
$ npm install --save-dev babel-plugin-syntax-dynamic-import
然后我们可以更新.babelrc
以使用新插件:
{
"presets": [["env", { "modules": false }], "stage-3"],
"plugins": ["syntax-dynamic-import"]
}
最后,这允许我们异步导入组件,如下所示:
const Hello = () => import('./components/Hello');
const World = () => import('./components/World');
然后,我们可以定义路由并初始化路由,这次引用异步导入:
const routes = [
{ path: '/', redirect: '/hello' },
{ path: '/hello', component: Hello },
{ path: '/World', component: World },
];
const router = new VueRouter({
routes,
});
new Vue({
el: '#app',
router,
render: h => h(App),
});
然后,在应用中导航时,我们可以通过开发者工具|网络选项卡在 Chrome 中查看结果:
每个路由都会添加到其自己的捆绑包文件中,并随后提高了性能,因为初始捆绑包要小得多:
让我们创建一个使用 RESTful API 和我们刚刚学习的路由概念的项目。通过在终端中运行以下命令来创建新项目:
# Create a new Vue project
$ vue init webpack-simple vue-spa
# Navigate to directory
$ cd vue-spa
# Install dependencies
$ npm install
# Install Vue Router and Axios
$ npm install vue-router axios
# Run application
$ npm run dev
我们可以从在应用中启用VueRouter
插件开始。为此,我们可以在src/router
中创建一个名为index.js
的新文件。我们将使用此文件包含所有特定于路由的配置,但我们将根据基础功能将每个路由分离为不同的文件。
让我们导入并添加路由插件:
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter)
为了在我们的应用中将路由划分为不同的文件,我们可以首先在名为user.routes.js
的src/components/user
下创建一个文件。每次我们有一个不同的功能集(需要路由),我们可以创建自己的*.routes.js
文件,该文件可以导入路由的index.js
。
现在,我们只需导出一个新的空数组:
export const userRoutes = [];
然后我们可以将路线添加到我们的index.js
(尽管我们尚未定义):
import { userRoutes } from '../components/user/user.routes';
const routes = [...userRoutes];
我们使用的是 ES2015+spread 操作符,它允许我们使用数组中的每个对象,而不是数组本身。
为了初始化路由,我们可以创建一个新的VueRouter
并沿着路由传递,如下所示:
const router = new VueRouter({
// This is ES2015+ shorthand for routes: routes
routes,
});
最后,让我们导出路由,以便它可以在我们的主 Vue 实例中使用:
export default router;
在main.js
中,我们导入路由并将其添加到实例中,如图所示:
import Vue from 'vue';
import App from './App.vue';
import router from './router';
new Vue({
el: '#app',
router,
render: h => h(App),
});
我们应用的第一部分将是一个主页,其中显示来自 API 的用户列表。我们在过去使用过这个示例,因此您应该熟悉所涉及的步骤。让我们在src/components/user
下创建一个名为UserList.vue
的新组件。
组件的外观如下所示:
<template>
<ul>
<li v-for="user in users" :key="user.id">
{{user.name}}
</li>
</ul>
</template>
<script>
export default {
data() {
return {
users: [
{
id: 1,
name: 'Leanne Graham',
}
],
};
},
};
</script>
现在可以随意添加您自己的测试数据。我们将立即从 API 请求这些数据。
在我们创建组件之后,我们可以向user.routes.js
添加一条路由,每当'/'
(或您选择的路径)被激活时,该路由就会显示该组件:
import UserList from './UserList';
export const userRoutes = [{ path: '/', component: UserList }];
为了显示此路由,我们需要更新App.vue
,以便随后将内容注入router-view
节点。让我们更新App.vue
来处理这个问题:
<template>
<div>
<router-view></router-view>
</div>
</template>
<script>
export default {};
</script>
<style>
</style>
然后,我们的应用应该显示一个用户。让我们创建一个 HTTP 实用程序来从 API 获取数据。
在名为api.js
的src/utils
下创建一个新文件。这将用于创建Axios
的基本实例,然后我们可以对其执行 HTTP 请求:
import axios from 'axios';
export const API = axios.create({
baseURL: `https://jsonplaceholder.typicode.com/`
})
当有人导航到'/'
路线时,我们可以使用beforeRouteEnter
导航卫士获取用户数据:
<template>
<ul>
<li v-for="user in users" :key="user.id">
{{user.name}}
</li>
</ul>
</template>
<script>
import { API } from '../../utils/api';
export default {
data() {
return {
users: [],
};
},
beforeRouteEnter(to, from, next) {
API.get(`users`)
.then(response => next(vm => (vm.users = response.data)))
.catch(error => next(error));
},
};
</script>
然后我们发现屏幕上有一个用户列表,如下面的屏幕截图所示,每个用户都表示为不同的列表项。下一步是创建detail
组件,注册详细路线,并找到链接到该路线的方法:
为了创建一个详细页面,我们可以创建UserDetail.vue
并遵循与前面组件类似的步骤:
<template>
<div class="container">
<div class="user">
<div class="user__name">
<h1>{{userInfo.name}}</h1>
<p>Person ID {{$route.params.userId}}</p>
<p>Username: {{userInfo.username}}</p>
<p>Email: {{userInfo.email}}</p>
</div>
<div class="user__address" v-if="userInfo && userInfo.address">
<h1>Address</h1>
<p>Street: {{userInfo.address.street}}</p>
<p>Suite: {{userInfo.address.suite}}</p>
<p>City: {{userInfo.address.city}}</p>
<p>Zipcode: {{userInfo.address.zipcode}}</p>
<p>Lat: {{userInfo.address.geo.lat}} Lng:
{{userInfo.address.geo.lng}} </p>
</div>
<div class="user__other" >
<h1>Other</h1>
<p>Phone: {{userInfo.phone}}</p>
<p>Website: {{userInfo.website}}</p>
<p v-if="userInfo && userInfo.company">Company:
{{userInfo.company.name}}</p>
</div>
</div>
</div>
</template>
<script>
import { API } from '../../utils/api';
export default {
data() {
return {
userInfo: {},
};
},
beforeRouteEnter(to, from, next) {
next(vm =>
API.get(`users/${to.params.userId}`)
.then(response => (vm.userInfo = response.data))
.catch(err => console.error(err))
)
},
};
</script>
<style>
.container {
line-height: 2.5em;
text-align: center;
}
</style>
由于我们的详细信息页面中不应该有多个用户,userInfo
变量被创建为 JavaScript 对象,而不是数组。
然后我们可以将新组件添加到我们的user.routes.js
:
import UserList from './UserList';
import UserDetail from './UserDetail';
export const userRoutes = [
{ path: '/', component: UserList },
{ path: '/:userId', component: UserDetail },
];
为了链接到此组件,我们可以在我们的UserList
组件中添加router-link
:
<template>
<ul>
<li v-for="user in users" :key="user.id">
<router-link :to="{ path: `/${user.id}` }">
{{user.name}}
</router-link>
</li>
</ul>
</template>
如果我们在浏览器中查看,我们可以看到只有一个用户列出,下面的信息来自链接到该用户的用户详细信息:
我们还可以从 API 访问帖子,因此,我们可以在用户信息旁边显示这两篇帖子的信息。让我们创建一个名为UserPosts.vue
的新组件:
<template>
<div>
<ul>
<li v-for="post in posts" :key="post.id">{{post.title}}</li>
</ul>
</div>
</template>
<script>
import { API } from '../../utils/api';
export default {
data() {
return {
posts: [],
};
},
beforeRouteEnter(to, from, next) {
next(vm =>
API.get(`posts?userId=${to.params.userId}`)
.then(response => (vm.posts = response.data))
.catch(err => console.error(err))
)
},
};
</script>
这允许我们根据userId
路线参数获取帖子。为了将此组件显示为子视图,我们需要在user.routes.js
中注册它:
import UserList from './UserList';
import UserDetail from './UserDetail';
import UserPosts from './UserPosts';
export const userRoutes = [
{ path: '/', component: UserList },
{
path: '/:userId',
component: UserDetail,
children: [{ path: '/:userId', component: UserPosts }],
},
];
然后,我们可以在UserDetail.vue
组件中添加另一个<router-view>
标记来显示子路由。该模板现在如下所示:
<template>
<div class="container">
<div class="user">
// Omitted
</div>
<div class="posts">
<h1>Posts</h1>
<router-view></router-view>
</div>
</div>
</template>
此外,我们还添加了一些样式,在左侧显示用户信息,在右侧显示帖子:
<style>
.container {
line-height: 2.5em;
text-align: center;
}
.user {
display: inline-block;
width: 49%;
}
.posts {
vertical-align: top;
display: inline-block;
width: 49%;
}
ul {
list-style-type: none;
}
</style>
然后,如果我们转到浏览器,我们可以看到数据的显示方式与我们计划的一样,用户信息显示在左侧,帖子显示在右侧:
塔达!我们现在已经创建了一个具有多个路由、子路由、参数等的 Vue 应用!
在本节中,我们了解了 Vue 路由以及如何使用它创建单页应用。因此,我们涵盖了从初始化路由插件到定义路由、组件、导航保护等所有内容。现在,我们具备了创建可扩展到单个组件的 Vue 应用的必要知识。
现在我们已经扩展了我们的知识并了解了如何使用 Vue 路由,我们可以在下一章中继续使用Vuex
处理状态管理。