后端的路由就是URL与对应页面的映射关系,从用户的角度来说,就是不同的URL地址对应不同的页面展示。前端路由就是把这块工作放到前端来,不同的URL对应不同的前端渲染的页面。
现在的前端路由一般都采用HTML5的History模式了,即可以真正的切换URL,而不向过去一样采用锚点。
Vue官方提供了路由组件叫做Vue Router,由于这个路由是以插件的形式提供的,所以在模块化的开发,也就是前端工程里,必须在源码里启用路由功能,看一下具体操作吧。
这里依然使用之前Webpack搭建的Vue环境。
- 安装Vue Router
- 初步使用路由
- 路由应用
- 高级应用
安装Vue Router
安装分为两步:
- 安装
Vue-router
本身:npm install vue-router
- 在
main.js
中启用路由插件:
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
不管是前端路由还是后端路由,其本质没有改变,依然要匹配访问的URL和对应的组件。现在就来简单配置一下,做一个简单的例子。
初步使用路由
所谓展示不同的页面,在Vue中,其实就是挂载不同的组件。所以必须要设置一组规则,让URL和要挂载的组件建立映射关系。
这里可以把原来试验性质的单文件组件都删除掉,只保留作为入口的index.js
文件和app.vue
,然后在src
目录里创建两个新的组件index.vue
和about.vue
:
//index.vue
<template>
<div>欢迎来到首页</div>
</template>
<script>
export default {
name: "about"
}
</script>
<style scoped>
div {
font-size: 2rem;
text-align: center;
}
</style>
//about.vue
<template>
<div>关于本站</div>
</template>
<script>
export default {
name: "about"
}
</script>
<style scoped>
div {
font-size: 2rem;
text-align: center;
}
</style>
其实是两个再简单不过的组件,只有一个标题,没有其他内容。
然后需要在index.js
设置路由,主要有以下步骤:
- 一般创建一个数组作为路由表,每一个元素是一个路由匹配规则对象。这个对象
path
属性对应URL
,而component
属性对应组件对象。
- 使用这个路由表数组,使用
new VueRouter({routes:路由表数组})
创建路由对象。
- 根实例渲染的组件需要编写特殊的渲染路由的标签
- 将路由对象设置为根实例
router
属性的值,然后挂载根实例。
- 如果要使用Webpack-dev-server,需要加上一个参数
--history-api-fallback
,所有的路由都会指向index.html
。
根据这个顺序,来编写一下index.js
文件:
import Vue from "vue";
import VueRouter from "vue-router"
import App from "./app.vue";
import Index from "./index.vue";
import About from "./about.vue";
Vue.use(VueRouter);
//1 创建路由表数组
const routes = [
{
path: "/index",
component: Index
},
{
path: "/about",
component: About
}
];
//2 使用路由表数组创建路由对象
const router = new VueRouter({
mode: "history",
routes
});
注意标红的这一行,是指定路由模式为HTML5
的HISTORY
模式,如果不指定,会变成锚点。现代开发一般都是使用HISTORY
模式。
在挂载根实例之前还需要修改app.vue
:
//3 根实例组件使用特殊的标签router-view,根据URL加载不同组件。
<template>
<div>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: "app"
}
</script>
<style scoped>
</style>
之后需要挂载根实例,继续在index.js
中编写:
//4 创建Vue根实例并挂载app,路由动态渲染的标签写在app.vue中。
const app = new Vue({
router,
render: h => h(App)
}).$mount("#app");
还有最后一步,前端路由需要所有的页面都要指向index.html
,也就是让挂载的根实例发生作用才能解析,所以需要让DevServer
把路由全部指向index.html
,可以在package.json
里加上一条命令:
//5 配置Webpack-dev-server
"scripts": {
"dev": "webpack-dev-server --open --history-api-fallback"
},
之后使用npm run dev
就可以启动DevServer
了。
访问首页,然后在地址栏中输入http://localhost:8080/index
和http://localhost:8080/about
,可以看到在会根据不同的URL挂载不同的组件,页面显示出不同的文字和样式。
这是因为<router-view>
会根据当前路由动态渲染配置好的组件。网页上一些不需要每次重新加载和渲染的内容比如导航条,可以写在app.vue里与<router-view>
同级,就比较好看了。
路由应用
来看一下路由的一些其他应用。
路径重定向
首先是在路由列表里可以增加通配符,如果找不到,就重定向到首页或者其他路径,有点像Django里最后找一个通配符路径接着其他所有路径,也可以增加具体的重定向:
const routes = [
{
path: "/index",
component: Index
},
{
path: "/about",
component: About
},
{
path: "/query",
redirect: "/about"
},
{
path: "*",
redirect: "/index"
}
];
一般在找不到页面的时候会返回一个自制的404页面。所以一般在匹配规则里列出所有可能的URL,然后使用通配符,匹配其他路径到自己编写的404组件中。
动态路由匹配
这个也是Web开发的重头戏,所谓动态路由匹配就是获取URL变量以便操作。这个需要两部分来配合:
- 路由对象中设置动态路由参数,类似
path: '/user/:id'
- 匹配的参数会被传递到
this.$route.params
,可以在每个组件中使用。在组件中访问this.$route
得到的是当前这个组件的路由对象。
注意,我们的项目里有一个路由器对象,使用this.$router
访问,不同的组件内访问路由器,都是同一个路由器对象。
而this.$route
是每个组件不同的,是组件自己的路由对象,不是路由器对象,这个要区分开。
此外还有$route.query
,hash
等用于获取URL查询和锚点的信息。
用一个例子来说明,在路由表数组内添加一个新匹配规则:
{
path: "/user/:id",
component: User
},
然后编写一个对应的user.vue
组件:
<template>
<div>
<p>当前用户的id是 {{$route.params.id}}</p>
</div>
</template>
<script>
export default {
name: "user",
mounted() {
console.log("获取了用户ID");
console.log(this.$route.params.id);
}
}
</script>
<style scoped>
div p {
color: brown;
font-size: 1.5rem;
}
</style>
URL中的id
匹配到的值就会存储在this.$route.params.id
同名的属性中,取出来就可以进行操作,比如在加载组件的时候根据用户ID发送AJAX请求获取用户信息等。
还可以使用多个参数匹配,比如:
{
path: "/post/:year/:month",
component: Post
},
Vue的路由匹配规则使用了path-to-regexp插件,使用正则也可以,进一步学习可以看这里。
还需要注意的是这个很类似Django的规则,写在前边的路由先匹配,匹配成功就不会继续匹配后边的路由,所以路由表数组的顺序很重要。
导航
所谓导航,就是点击链接跳转。一般有两种方式:
- 使用
router-link
标签生成链接
- 使用JavaScript来捕获点击事件然后处理
先来看router-link
标签,这个标签实际上会默认生成A标签
,依照标签的属性生成对应的链接和文字。
修改app.vue
的模板部分和绑定一个v-model
用来拼接URL:
<template>
<div>
<h1>柚子小站</h1>
<p>
<router-link to="/index">Go to index</router-link>
<router-link to="/about">Go to about</router-link>
<label>输入用户ID
<input type="number" v-model="url"><router-link :to="'/user/'+url">跳转到用户ID界面</router-link>
</label>
</p>
<div>
<router-view></router-view>
</div>
</div>
</template>
<script>
export default {
name: "app",
data:function () {
return {
url: ""
}
}
}
</script>
<style>
h1 {
text-align: center;
background-color: black;
color: orange;
margin-top: 0;
}
a {
display: block;
}
body{
margin: 0;
padding: 0;
}
</style>
浏览器中三个router-link
被渲染成了:
<a href="/index" class="router-link-exact-active router-link-active">Go to index</a>
<a href="/about" class="">Go to about</a>
<a href="/user/" class="">跳转到用户ID界面</a>
其中第三个链接没有输入,就不会拼接字符串。在input中输入用户id,再点击链接就可以跳转到对应的URL了。
router-link
有很多属性,默认情况下会被渲染为A标签
,其他常用的有:
tag
,指定渲染为什么标签,比如tag="li"就会渲染成LI标签
replace
,一个属性,加上就可以生效,不会在HISTORY里留下记录,所以无法回退
active-class
,如果点击的链接的路由匹配成功,会自动给当前对应的这个标签添加一个router-link-active
,如果精确匹配,还会添加一个router-link-exact-active
。可以修改这个样式名称,但一般不推荐修改。
A标签浏览器默认就有跳转功能,再来看看使用JavaScript跳转,这种方式经常使用在BUTTON
元素上,用于自定义需要跳转的内容。
给about.vue添加一个按钮跳转到66号用户:
<template>
<div>
<p>关于本站</p>
<button @click="handleClick">跳转到66号用户</button>
</div>
</template>
<script>
export default {
name: "about",
methods: {
handleClick() {
this.$router.push("/user/66");
}
}
}
</script>
<style scoped>
div {
font-size: 2rem;
text-align: center;
}
</style>
这里使用了路由器对象:this.$router.push("/user/66");
,调用其push
方法,就会“推动”路由器转到其中的路径。
这个push()
方法。实际上是向HISTORY栈里加入一条数据,所以页面会转向最新的URL,还可以使用回退来返回上一个路径。实际上<router-link to="/about">Go to about</router-link>
内部就是调用这个方法。
$router
路由器对象还有其他功能:
replace
,与路由对象的replace
方法类似,不在HISTORY中留下记录。
go(Number)
,和window.history.go()
类似,指定回退的步数。
嵌套路由
一个URL实际上可以认为是一个目录,所以可能会有嵌套的路由用于结构化配置路由。类似/users/admin/team1/66
这种URL,直接写成一个单独的路由,不如使用嵌套路由更加方便修改和扩展。
嵌套的路由使用起来其实很简单,就是在组件中再嵌入<router-view></router-view>,然后在路由表数组中给对应的一级路由对象使用children
配置。children
属性的内容,也是一个路由表数组。
举个简单的例子,现在想匹配/index/home
,采用嵌套路由的方式:
const routes = [
{
path: "/index",
component: Index,
children: [
{
path: "home",
component: Home
}
]
},
然后在index
组件内增加一个路由出口,创建home.vue
组件:
// index.vue
<template>
<div>
<p>首页内容</p>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: "about"
}
</script>
<style scoped>
div {
font-size: 3rem;
text-align: center;
}
</style>
//home.vue
<template>
<div>HOME组件的内容</div>
</template>
<script>
export default {
name: "home"
}
</script>
<style scoped>
</style>
这样配置之后,访问/index
,不会渲染出home.vue
组件,访问/index/home
,就可以渲染出home.vue
组件。
命名路由和命名视图
这两个提供了一些使用的便利,直接可以看官方文档,提供了命名的一大好处就是可以动态的解析导航的链接。
高级应用
在页面中跳转,由于我们实际上并没有在HTML文件中跳转,所以页面的HEAD结构始终是同一个,那么如何改变标题呢。
可以在每个组件加载的时候调用mounted
方法来修改,但是组件多了需要反复写这个方法。
vue-router
提供了导航守卫的功能,就是路由发生变化的时候,执行的功能,这种功能很方便对页面进行一些预处理。
导航守卫何时出发,分为全局的,路由级别的和组件级别的,先来看全局的。
全局导航守卫
全局导航守卫需要在index.js
中注册,需要先生成router对象,然后在router对象上注册:
这里还需要了解元数据,也是每个路由规则对象可以添加的属性之一,把标题内容存到每个路由规则对象的meta中,然后进行注册。
Vue-router
提供了beforeEach
和afterEach
,它们会在路由即将改变前和改变后触发。先来实际注册一个前置的钩子:
// index.js
......
const routes = [
{
path: "/index",
component: Index,
children: [
{
path: "home",
component: Home
}
],
meta:{
title: "网站首页"
}
},
{
path: "/about",
component: About,
meta:{
title: "关于本站"
}
},
{
path: "/user/:id",
component: User,
meta:{
title: "用户信息"
}
},
{
path: "/post/:year/:month",
component: Post,
meta:{
title: "文章信息"
}
},
{
path: "*",
redirect: "index",
meta:{
title: "无法匹配URL"
}
},
];
......
// 注册全局前置守卫
router.beforeEach(function(to, from, next){
window.document.title = to.meta.title;
next();
});
每个前置守卫方法都接受3个参数,to
是即将要进入的目标路由对象(就是路由表数组里的一个元素),from
是即将要离开的路由对象,next
调用之后,才能完成这个钩子,进入下一个钩子。类似于Gulp里边任务函数的的done()
。
next
还可以加一些参数:
next(false)
,中止导航,即跳转不生效。如果用户手动按了后退,会变成from
参数的地址
next(URL)
,可以跳转到具体的地址,这个地址可以是写死的,也可以是路由的名称,此时需要传入一个对象:{ path: '/' }
,凡是用在router-link中的属性都可以使用。
next(error)
,抛出一个错误给router.onError()
注册过的回调函数。
设置好之后就会发现页面的标题已经正常变化了。后置守卫afterEach
方法只有to
和from
两个参数,没有next
参数。
这两个钩子的用处非常多。比如每次跳转都默认回到页面顶端:
router.beforeEach(function(to, from, next){
window.document.title = to.meta.title;
window.scrollTo(0, 0);
next();
});
还有重要的应用就是跳转页面的时候,通过后端鉴权,如果成功再跳转,不成功就跳转到指定页面,现在由于没有后端支持,先用伪代码看个意思:
router.beforeEach(function (to, from, next) {
var token = sendajax(username, password);
if (validate(token)) {
next();
}else{
next('/login');
}
});
大概就是每次跳转的时候,都去鉴权,成功的话就继续,否则就跳转到登录页。
全局导航顺序
可以注册多个全局守卫:
router.beforeEach(function(to, from, next){
console.log("第一个守卫");
window.document.title = to.meta.title;
next();
});
router.beforeEach(function(to, from, next){
console.log("第二个守卫");
window.scrollTo(0, 0);
next();
});
router.beforeEach(function (to, from, next) {
console.log("第三个守卫");
next();
});
在执行的时候,是按照注册的顺序进行执行的,在上一个守卫执行完next()
正常resolve后,下一个才会执行。如果某一个next(false)
或者跳转走了,其后的守卫也不会被执行。
路由级别的导航守卫
路由的守卫直接写在路由对象里即可:
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
组件级别的导航守卫
组件内的守卫可以直接写在组件的属性里,可以看官方文档:
const Foo = {
template: `...`,
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}
官网还有一个完整的导航解析流程可以参考。
在使用Vue-CLI创建的项目中,只需要把所有的组件复制到/src/components目录下,app.vue重命名为App.vue替代原来的文件,index.js中的内容复制到main.js里。由于相对路径有变化,修改一下导入即可,启动项目,即可完美将Webpack项目弄过来,自动会配置好路由。
感叹一下技术确实在进步,在一年前刚开始学编程和Web开发的时候,IDE里都是单独的HTML,JS,CSS文件,一个一个学着用。到了现在自学了一圈基础技术,使用了前端工程化,打开的就是一个一个Vue文件了。变化确实快。