VueRouter

前端路由原理

监听 url 的变化,然后匹配路由规则,显示相应页面,无需刷新。
目前前端路由只有两种实现方式

  1. hash 模式

监听 hash 值变化(url 后的,从#号开始)
window.addEventListener('hashchange',()=>{})

1
2
3
4
5
6
//监听hash变化
window.onhashchange = function (event) {
console.log(event.oldURL, event.newURL);
let hash = loaction.hash; //通过location对象来获取hash地址
console.log(hash); // "#/notebooks/260827/list" 从#号开始
};

因为 hash 发生变化的 url 都会被浏览器记录下来,从而你会发现浏览器的前进后退都可以用.
特点: hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面。

  1. history 模式

通过此模式改变 url 同样不会引起页面刷新,只会更新浏览器的历史记录’
修改为 history 模式:在 router 中添加mode: 'history'.

history.pushState
替换当前历史记录
history.replaceState
点击后退触发 popState 事件
window.addEventListener('popState',e=>{})

切换历史状态

包括 back,forward,go 三个方法,对应浏览器的前进,后退,跳转操作.

1
2
3
4
history.go(-2); //后退两次
history.go(2); //前进两次
history.back(); //后退
hsitory.forward(); //前进

修改历史状态

包括了 pushState,replaceState 两个方法,这两个方法接收三个参数:stateObj,title,url.

1
2
3
history.pushState({color:'red'}, 'red', 'red'})
history.back();
history.forward();

通过 pushstate 把页面的状态保存在 state 对象中,当页面的 url 再变回这个 url 时,可以通过 event.state 取到这个 state 对象.

history 模式缺点

通过 history api,我们丢掉了丑陋的#,但是它也有个毛病:
不怕前进,不怕后退,就怕刷新,f5(如果后端没有准备的话),因为刷新是实实在在地去请求服务器的,不玩虚的。

在 hash 模式下,前端路由修改的是#中的信息,而浏览器请求时是不带它玩的,所以没有问题.但是在 history 下,你可以自由的修改 path,当刷新时,如果服务器中没有相应的响应或者资源,会请求失败.

所以,如果你想在 github.io 上搭一个单页博客,就应该选择 hash 模式

vue-router 路由

  1. 安装 vue-router
    npm install --save vue-router
  2. 引用
1
2
import router from "vue-router";
Vue.use(router);
  1. 配置路由文件,并在 Vue 实例中注入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import Vue from "vue";
import App from "./App";
import router from "vue-router";
import HelloWorld from "./components/HelloWorld";

Vue.use(router); //引入并且使用

var rt = new VueRouter({
//下面这个routes是数组,用[]
routes: [
{
path: "/hello", //指定要跳转的路径
component: HelloWorld, //指定要跳转的组件
},
],
});

new Vue({
el: "#app",
router: rt, //把路由实例rt写到Vue实例中就注入了
components: { App }, //组件直接注入.分开写便于模块化
template: "<App/>", //同上
});
  1. 确定视图加载的位置
1
2
<router-view></router-view>
//把这个写到想注入的位置就可以了

路由跳转

在路由文件中,一般是router/index.js.就是把路由单独写一个文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Vue from "vue";
import router from "vue-router";
import HelloWorld from "./components/HelloWorld";
import HelloEarth from "./components/Helloearth";

Vue.use(router); //引入并且使用

export default new router({
routes: [
{
path: "/helloworld", //指定要跳转的路径
component: HelloWorld, //指定要跳转的组件
},
{
path: "/helloearth", //指定要跳转的路径
component: HelloEarth, //指定要跳转的组件
},
],
});

然后在页面组件中

1
2
3
4
5
6
7
8
9
10
<template>
<ul>
<li>
<router-link to="/helloworld">HelloWorld</router-link>
</li>
<li>
<router-link to="/helloearth">HelloEarth</router-link>
</li>
</ul>
</template>

vue-router 路由参数传递(传参)

  1. 必须在路由内加入路由的name
  2. 必须在path后加/:加传递的参数
  3. 传递参数(传值)和接收参数(具体看下面两种方法)

传递参数方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//params:
this.$router.push({
name: "xxx",
params: {
id: id,
},
});

//params读取参数: this.$route.params.id

//query
this.$router.push({
path: "/xxx",
query: {
id: id,
},
});

//query读取参数: this.$route.query.id

注意: params 传参,push 里只能是 name:’xxx’,不能是 path:’/xxx’,因为 params 只能用 name 来引入路由,如果这个写成 path,接收参数页面会是 undefined.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//两种方式:
<router-link :to="{name: helloworld,params:{worldmsg: '你好世界' }}">
helloworld</router-link>
//接收参数:$route.params.xxx
//这种方式等同于./helloworld/你好世界

export default new router({
routes: [{
name: 'helloworld',
path: '/helloworld/:id', //动态绑定,各个不同id就都使用HelloWorld组件了
component: HelloWorld //指定要跳转的组件
},
{
name: 'helloearth',
path: '/helloearth/:earthmsg', //指定要跳转的路径
component: HelloEarth //指定要跳转的组件
},
]
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  <router-link :to="{path: '/helloearth',query:{msg: 只有一个地球}}">
</router-link>
//方式等同于./helloworld?name=xxx&count=xxx
//接收参数this.$route.query.xxx

coust couter = new VueRouter({
routes:[{
path: '/search',
component: SearchUser,
props: (route) =>({
query: route.query.q
})
}
]
})

导航守卫

导航守卫就是路由跳转过程中的一些钩子函数.

1
2
3
4
5
6
7
8
9
10
11
//钩子函数执行后输出的顺序
全局前置守卫: beforeEach;
路由独享守卫: beforeEnter;
组件路由守卫: beforeRouteEnter, 此时this并不指向该组件实例;
全局解析守卫: beforeResolve;
全局后置守卫: aferEach;
组件生命周期: beforeCreate;
组件生命周期: created;
组件生命周期: borforeMount;
组件生命周期: mounted;
组件路由守卫: beforeRouteEnter的next回调;

导航守卫分为:全局的、单个路由独享的、组件内的三种.

全局的

分别是beforeEach,beforeResolve,afterEach

1
2
3
4
5
const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
// ...
})

beforeEach: 全局前置守卫.在路由跳转前触发,参数:to,from,next.这个钩子函数主要用于登录验证.
beforeResolve: 全局解析守卫.和beforeEach类似.区别是在beforeEach和组件内beforeRouteEnter之后,afterEach之前调用.
afterEach: 全局后置钩子.和beforeEach相反,在路由跳转完成后触发,参数: to,from.他发生在beforeEachbeforeResolve之后.

三个参数(to,from,next)

  1. to: Route: 即将要进入的目标路由对象
  2. from: Route: 当前导航正要离开的路由
  3. next: Function: 必须调用该方法来 resolve 这个钩子.否则不能进入路由.执行效果依赖 next 方法的调用函数.
  4. next(false): 中断当前导航.如果浏览器的 URL 改变,那么 URL 会重置到 from 路由对应的地址.
  5. next('/'): 跳转到一个不同的地址.
  6. next(error): 如果传入的 next 的参数是一个 Error 实例.则导航会被终止且该错误会被传递给router.onerror()注册过的回调.

确保要调用next方法,否则钩子就不会被resolved.

路由独享的守卫 beforeEnter

只在进入路由时调用.

可以在路由配置上直接定义 beforeEnter 守卫:

1
2
3
4
5
6
7
8
9
10
11
12
const router = new VueRouter({
routes: [
{
path: "/foo",
component: Foo,
brforeEnter: (to, from, next) => {
//参数用法什么的都一样,调用顺序在全局前置守卫后面,所以不会被全局守卫覆盖
//...
},
},
],
});

组件内的守卫

beforeRouteEnter 进入路由前
beforeRouteUpdate 路由复用同一个组件时
beforeRouteLeave 离开当前路由时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
},
};

beforeRouteEnter 守卫不能访问 this

因为钩子在组件实例还没被创建的时候调用,可以通过传一个回调给next来访问组件实例 。
不过,可以通过传一个回调给 next 来访问组件实例.在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数.
但是回调的执行时机在 mounted 后面,所以在我看来这里对 this 的访问意义不太大,可以放在created或者mounted里面。

1
2
3
4
5
beforeRouteEnter(to, from, next){
next(vm => {
//通过`vm`访问组件实例
})
}

beforeRouteLeave

导航离开该组件的对应路由时调用,我们用它来禁止用户离开,比如还未保存草稿,或者在用户离开前,将 setInterval销毁,防止离开之后,定时器还在调用。

1
2
3
4
5
6
7
beforeRouteLeave (to, from , next) {
if (文章保存) {
next(); // 允许离开或者可以跳到别的路由 上面讲过了
} else {
next(false); // 取消离开
}
}

路由钩子函数的错误捕获

如果在导航守卫的钩子函数中有错误,可以这样捕获:

1
2
3
router.onError((callback) => {
console.log(callback, "callback");
});

假设是从 a 组件离开,第一次进入 b 组件

完整的路由导航解析流程:

  1. 触发进入其他路由。
  2. 调用要离开路由的组件守卫beforeRouteLeave
  3. 调用全局前置守卫: beforeEach
  4. 在重用的组件里调用beforeRouteUpdate
  5. 调用路由独享守卫beforeEnter
  6. 解析异步路由组件
  7. 在将要进入的路由组件中调用beforeRouteEnter
  8. 调用全局解析守卫beforeResolve
  9. 导航被确认
  10. 调用全局后置钩子afterEach
  11. 触发 DOM 更新(mounted)
  12. 执行beforeRouteEnter守卫中传给 next 的回调函数

疑难杂症

监听物理返回键或页面返回

1
2
3
4
5
6
7
8
9
//unit.js
//存储当前历史记录点,实现控制手机物理返回键的按钮事件
export const pushHistory = () => {
let state = {
title: "",
url: "",
};
window.history.pushState(state, state.title, state.url);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//index.vue
import { pushHistory } from '@/misc/utils' //引入工具
export default {
mounted(){
pushHistory()
//监听历史记录点,添加返回事件监听
window.onpopstate = ()=>{
this.$router.push(...)
//输入要返回的上一级路由地址
}
},
destroyed(){
window.removeEventListener('popstate', this.fun, false);//false阻止默认事件
}
//页面销毁时,取消监听。否则其他vue路由页面也会被监听
}