前言:现在 Vue 的路由已经开始大规模应用在单页面应用上了。比较常见的就是路由网址中的 URL 里面的hash(#) ,这个 hash(#)来源于哪里那?没错就来自HTML 的锚点技术。大概如下原理:
一图胜千言。
源代码(省略好多h6标签):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>#的作用</title>
</head>
<body>
<h1><a href="#title">跳转到标记一 </a></h1>
<h6>一行车马向东疾驰,行不数里,便有数骑马迎来,驰到车前,翻身下马,高声向令狐冲致意,言语礼数,甚是恭敬。</h6>
<h1><a id="title">我是标记一</a></h1>
<h6>一行车马向东疾驰,行不数里,便有数骑马迎来,驰到车前,翻身下马,高声向令狐冲致意,言语礼数,甚是恭敬。</h6>
</body>
</html>
除了使用锚点技术之外,很明显无论 React 还是 Vue 的路由大量使用了 H5 里面的,window.history
,最常用的就包含window.pushState({},"",url)
=>添加一条历史记录。window.history.replaceState()
=>替换当前URL, window.history.forward()
=> 浏览器的前进按钮,window.history.back()
=> 浏览器的后退按钮,window.history.go(number)
=>浏览器前进后退几步,还有一个 hash 路由事件onhashchange
。
一、配置一个路由
Vue 官网:https://router.vuejs.org/zh/
项目安装路由:
npm install --save vue-router
项目目录的结构:这次目录的结构做的真好看,制作方法是使用搜狗输入法的(ctr+shift+z)制表符。
┏ components
┃ ┣ HaHa.vue
┃ ┗ XiXi.vue
┣ router
┃ ┗ router.js
┣ App.vue
┣ main.js
┗ index.html
最终的路由效果:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue 学习</title>
</head>
<body>
<div id="app"></div>
<script src = "./virtual/bundle.js"></script>
</body>
</html>
main.js 里 vue-router 配置四步走
import Vue from "vue";
import App from "./App.vue";
// ①
import VueRouter from "vue-router";
import routes from "./router/router.js";
// 使用路由②
Vue.use(VueRouter);
// ③
const router = new VueRouter({
routes
})
new Vue({
el:"#app",
render(h){
return h(App);
},
// ④
router
});
router.js
// 引入组件
import XiXi from "../components/XiXi.vue";
import HaHa from "../components/HaHa.vue";
// 暴露路由需要的数组
export default [
{path : "/XiXi",component : XiXi},
{path : "/HaHa",component : HaHa}
];
App.vue
<template>
<div>
<h1>路由测试</h1>
<div class="box">
<p>我是一个盒子组件将会在这里显示</p>
<!-- 使用 router-link 组件来导航. -->
<!-- 通过传入 `to` 属性指定链接. -->
<!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
<router-link to="/HaHa">Go to HaHa</router-link>
<router-link to="/XiXi">Go to XiXi</router-link>
<router-link to="/">Go to home</router-link>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</div>
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
.box{
width: 500px;
height: 200px;
border:20px solid #5e5ed9;
}
</style>
components 里面就是单纯的两个组件。
二、子路由
我们在子组件 HaHa 里面再放入孙子组件 Bar 。URL 变成http://127.0.0.1:8080/#/HaHa/bar
演示效果大约如下:
我们需要改动的地方有:
router.js 里面在组件 HaHa 下面加入 childrren 书写子路由。与 react 不同的是子路由 不需要在前面加上 /
。
// 引入组件
import XiXi from "../components/XiXi.vue";
import HaHa from "../components/HaHa.vue";
import Bar from "../components/Bar.vue";
// 暴露路由需要的数组
export default [
{path : "/XiXi",component : XiXi},
{
path : "/HaHa",
component : HaHa,
children:[
{path : "Bar",component:Bar}
]
}
];
除此之外还需要在 HaHa.vue 组件里面放上标签<router-view></router-view>
,用来显示孙子组件 Bar 。
嵌套路由如何指定默认路由:
{
path:"user",
name:user,
component: ()=>import("@/components/user.vue"),
children:[
{
path:"/",//path:"/"就是默认路由,同时path:""也是,自动访问
name:index,
component: ()=>import("@/components/index.vue")
}
]
}
三、两种路由跳转方式
- 第一种 HTMl 方法
就是我们上面使用的<router-link to="/HaHa">Go to HaHa</router-link>
标签,这个标签的页面效果就类似HTML里面的a
标签。 - JS 方法
<a @click="$router.push('/HaHa/bar')">Go to HaHa</a>
我们在 a 标签添加事件,因为配置过路由,$router
就像vuex的$store
一样可以在任何子组件使用,我们只需要调用push
就能指定跳转目标。
路由跳转路径可为分为两种:字符串和对象
- 字符串上面我们使用的就是字符串
- 对象字面量方法,使用对象的时候我们可以:
-
{path:'/HaHa'}
,还可以传参分为 params和query的方式,{path:'/HaHa',params:{userId:123}}
,{path:'/HaHa',query:{name:'hero'}}
-
{name:'haha'}
命名路由,也可以传参也分为 params和query的方式,{path:'/HaHa',params:{userId:123}}
,{path:'/HaHa',query:{name:'hero'}}
-
四、使用路由进行页面布局
有时候想同时 (同级) 展示多个视图,而不是嵌套展示,就例如我们的有一个网页页面的头部是一个 Header
组件,页面底部是一个Footer
组件,页面中间左边是一个侧边栏 Slider
组件。右边是页面内容 Content
组件。而我们要求输入的网址必须为:http://127.0.0.1:8080/#/index.html
原型图如下:
首先我们要是用路由的重定向功能,让网址一打开
http://127.0.0.1:8080/#/
就跳转到http://127.0.0.1:8080/#/index.html
。路由文件添加代码:
{ path: '/', redirect: "/index.html"}
插题路由重定向是可以重定向到自己内部的。
{
path:"/home",
redertec:"/index",
children:[
{
path:"/index",
name:"index",
children:[
]
}
]
}
下面来讲讲如何实现,默认已经完成基本布局HTML大致布局代码:
<div class="box">
<!-- 网页头部 -->
<header>我是Header组件</header>
<!-- 网页主体 -->
<main>
<aside>我是Slider侧边栏组件</aside>
<section>我是Content组件</section>
</main>
<!-- 网页的底部 -->
<footer>我是Footer组件</footer>
</div>
我们只需要把每部分单独提出来变成组件然后使用路由进行显示。
- 命名视图法
<header><router-view></router-view></header>
组件router-view
不加 name 属性在路由文件里面组件将会默认填充到这里,如果写了name属性那么相应的组件会映射到相应的name
标签。
我们现在把内容给提成四个组件:App.vue 配上路由就变成了
<!-- 网页头部 -->
<header><router-view></router-view></header>
<!-- 网页主体 -->
<main>
<aside><router-view name="Slider"></router-view></aside>
<section><router-view name="Content"></router-view></section>
</main>
<!-- 网页的底部 -->
<footer><router-view name="Footer"></router-view></footer>
然后我们只需要改变路由文件 router.js 文件,让不同的路由视图,显示显示对应的组件,只需要采用组件组的形式:
// 引入组件
import Header from "../components/Header.vue";
import Footer from "../components/Footer.vue";
import Slider from "../components/Slider.vue";
import Content from "../components/Content.vue";
// 暴露路由需要的数组
export default [
{ path: '/', redirect: "/index.html"},
{ path: '/index.html', components:{
default:Header,
Footer,
Slider,
Content
}}
];
五、面包屑导航
左边的 Slider 组件作用使用 HTML 进行路由跳转。代码为:
<template>
<div>
<aside>
<div>
<router-link class="cur" to="/">首页</router-link>
</div>
<div>
<router-link class="cur" to="/index.html/yule">娱乐</router-link>
</div>
<div>
<router-link class="cur" to="/index.html/news">新闻</router-link>
</div>
</aside>
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
.cur{
display:inline-block;
margin:20px 30px;
text-decoration: none;
color:#666;
font-size: 16px;
}
</style>
然后去更改路由文件router.js:
{
path: '/index.html',
components : {
default:Header,
Footer,
Slider,
Content
},
meta:{chinese:"首页"},
children : [
{ path : "news" ,component : News,meta:{chinese:"新闻"}},
{ path : "yule" ,component : Yule,meta:{chinese:"娱乐"}}
]
}
路由元信息(meta)?
定义路由的时候可以配置 meta 字段:一个路由匹配到的所有路由记录会暴露为$route
对象的$route.matched
数组。因此,我们需要遍历$route.matched
来检查路由记录中的 meta 字段。
我们增加了两个子组件,news 和 yule(娱乐)。
再回到 Content 组件里面。进行配置。
<div>
当前所在位置:
<router-link to="/">首页</router-link>
{{$router.currentRoute.matched[1] ? "→ "+$router.currentRoute.matched[1].meta.chinese : null}}
<router-view></router-view>
</div>
猛一看可能比较懵,我们来看看 $router
身上携带了什么参数,我们直接在生命周期beforeCreate 里面打印出来$router
:
就一个 API 有用:
currentRoute
,在把currentRoute
打印出来console.log(this.$router.currentRoute);
各个参数用处大约如下:
// 路由模式
console.log(this.$router.mode);
//当前路由的路径
console.log(this.$router.currentRoute.fullPath);
console.log(this.$router.currentRoute.path);
// URL路径的查询结果index.html?name=hero后面的东西{name: "hero"}
console.log(this.$router.currentRoute.query);
// 动态参数/:id,用户输入的id格式{id: "876543"}
console.log(this.$router.currentRoute.params);
// <router-view></router-view> 上的name属性
console.log(this.$router.currentRoute.name);
// meta自定义传入的参数,可以被我们使用
console.log(this.$router.currentRoute.meta.chinese);
// matched是一个数组,里面存放着满足当前路径的所有路由组件的路径
console.log(this.$router.currentRoute.matched);
需要注意的是用到路由参数的时候,可能会失效,例如: /index.html/news 导航到 /index.html/yule,原来的组件实例会被复用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。重要的是生命周期钩子不会再被调用。在视图中如果使用路由参数(参数是对象或数组)插入,造成结果是视图不更新,所以声明式写法必须没有引用类型。除此之外解决办法就只能使用官方推荐的方法:
使用 watch (监测变化) $router 对象:
watch: {
'$router' (to, from) {
// 对路由变化作出响应...
}
}
或者使用生命周期的 updated 来实现。亲测可行,Vue 就是乱,官网上讲不行,结果还是的以实际为准。
六、导航守卫(路由钩子)
1. 组件内的守卫
- beforeRouteEnter 进入组件触发
- beforeRouteUpdate (2.2 新增) 组件复用触发
- beforeRouteLeave 离开组件触发
接下来以 XiXi 组件为例,修改代码加入路由进入和路由离开:
<template>
<div>
<h1>我是嘻嘻组件</h1>
</div>
</template>
<script>
export default {
// 组件内的守卫
// 导航离开该组件的对应路由时调用
beforeRouteLeave (to, from , next) {
// 这个离开守卫通常用来禁止用户在还未保存修改前突然离开
const answer = window.confirm("你确定要离开当前页面!");
if (answer) {
next();//确定跳转
} else {
next(false);//导航可以通过 next(false) 来取消
}
},
beforeRouteEnter (to, from, next) {
// 进入该组件触发
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
const answer = window.confirm("你确定进入该页面!");
if (answer) {
next(vm => {
// 通过 `vm` 访问本组件的实例
console.log(vm)
});//确定进入
} else {
next(false);//不进入
}
}
}
</script>
<style lang="scss" scoped>
</style>
演示结果:
最最重要的就是
beforeRouteLeave
(将要离开当前路由),这个离开守卫通常用来禁止用户在还未保存修改前突然离开,简书写文章时未保存,或者平时填写一些表单退出时给出提示用到的就是这个。除了进入路由和离开路由还有个特殊的路由。为此特地新建一个组件 Foo,并配置路由为:{path : "/foo/:id",component : Foo}
。Foo组件的内容为:
<template>
<div>
<h1>被复用的组件</h1>
</div>
</template>
<script>
export default {
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
const answer = window.confirm("你确定从/foo/1 => /foo/2 !");
if (answer) {
next(vm => {
// 通过 `vm` 访问本组件的实例
console.log(vm)
});//确定进入
} else {
next(false);//不进入
}
}
}
</script>
<style lang="scss" scoped>
</style>
在 APP 组件在添加两个跳转按钮:
<router-link to="/foo/1">/foo/1</router-link>
<router-link to="/foo/2">/foo/2</router-link>
查看演示效果:
每个守卫方法接收三个参数:
-
to: Route
: 即将要进入的目标 路由对象 -
from: Route:
当前导航正要离开的路由。 -
next: Function:
next(): 进行管道中的下一个钩子。next(false): 中断当前的导航。next('/') 或者 next({ path: '/' }): 跳转到一个不同的地址。
2. 路由独享的守卫
可以在路由配置上直接定义 beforeEnter 守卫:此处以 HaHa 组件为例
beforeEnter: (to, from, next) => {
// 路由独享的守卫
const answer = window.confirm("路由独享的守卫!");
if (answer) {
next();//确定进入
} else {
next(false);//不进入
}
}
查看演示:
3. 全局前置守卫
当一个导航触发时,全局前置守卫按照创建顺序调用。
官网代码案例:
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
// ...
})
太简单不演示了。
七、路由懒加载
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
结合 Vue 的异步组件和 Webpack 的代码分割功能,轻松实现路由组件的懒加载。
使用的形式其实就是组件的函数化:
以前:
component:About
现在:
component: () => import('./About.vue')
这是 Vue 的路由懒加载,还有 webpack 的懒加载方式,我同学项目中也有人用。基本没变,看了秒懂:
component: r => require.ensure([], () => r(require('../page/home')), 'home')
详情请去查看这个博客,里面最重要的话就是:
当然其实讲了那么长的 require.ensure并没有什么用,因为这个函数已经被 import() 取代了,但是考虑到之前的版本应该有很多人都是用 require.ensure 方法去加载的,所以还是讲一下,而且其实 import 的执行过程跟 require.ensure 是一样的,只不过用了更友好的语法而已,所以关于 import 的执行流程我也没啥好讲的了,感兴趣的人看一下两者的 API介绍就好了。
八、404 路由设置
常规参数只会匹配被/
分隔的 URL 片段中的字符。如果想匹配任意路径,我们可以使用通配符 (*):
{
// 会匹配所有路径
path: '*'
}
{
// 会匹配以 `/user-` 开头的任意路径
path: '/user-*'
}
当使用通配符路由时,请确保路由的顺序是正确的,也就是说含有通配符的路由应该放在最后。路由 { path: '*' } 通常用于客户端 404 错误。当使用一个通配符时,$router.currentRoute.params
内会自动添加一个名为 pathMatch
参数。它包含了 URL 通过通配符被匹配的部分:
例如输入错误的网址为:http://127.0.0.1:8080/#/index
$router.currentRoute.params.pathMatch
匹配输出为:/index
八、滚动行为
滚动行为:使用前端路由,当切换到新路由时,想要页面滚到自己指定位置,或者是保持原先的滚动位置,就像重新加载页面那样。
const router = new VueRouter({
routes,
scrollBehavior (to, from, savedPosition) {
// return 期望滚动到哪个的位置切换滚动到
return { x: 0, y: 1000 }
}
});