这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战
一.什么是vue-router
- Vue Router官方文档定义它为 Vue.js 官方的路由管理器。
- 最主要功能:根据页面地址的不同,展示不同的组件。起到管理组件,分发页面的作用。
- 其他功能:
- 既然是管理组件切换,那么肯定要考虑组件切换的用户体验,于是就有了过渡动效。
- 组件切换可能会遇到进入页面前要执行一些逻辑,或者从该路由离开要执行一些逻辑,于是就有了路由守卫,数据获取。
- 我们还可以通过页面地址传递变量,那就是路由组件传参数,动态路由等等。
- 那么,如何在代码中方便的控制路由跳转呢,这就是编程式导航。
- 还要考虑路由过多的情况下,部分路由可能重用一部分组件,所以有了路由嵌套。可以在用到某个路由的情况下去加载它,就有了路由懒加载。
- 当然,还得考虑异常和失败的情况,这就是导航故障
二.vue-router的使用
为了最方便的开始使用vue-router,可以直接使用vue-cli。
- 安装
npm install -g @vue/cli
- 验证安装成功
vue --version
- 创建vue-router项目
vue create vue-router
选择最后一个
选择router
后续选择 vue2.x
项目创建完毕,如下
- 在router目录中添加代码
// src/router/index.js
import Vue from 'vue';
import VueRouter from 'vue-router';
// 1.首先导入Vue和VueRouter
import Home from '../views/Home.vue';
// 2.调用Vue.use注册组件,调用传入对象的install方法
Vue.use(VueRouter);
// 3.编写路由规则
const routes = [
{
path: '/',
name: 'Home',
component: Home,
// 首页是进页面之后,就需要进行加载
},
{
path: '/about',
name: 'About',
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
// 而about页面初始化的时候不需要加载,只有用到的时候才需要加载,这个时候就可以使用路由懒加载
// 用户访问的时候才加载,用户不访问的时候不加载
},
{
path: '/blog',
name: 'Blog',
component: () => import(/* webpackChunkName: "blog" */ '../views/Blog.vue'),
},
];
// 4.创建路由对象,传入路由规则
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes,
});
// 5.导出路由对象,接下来看main.js
export default router;
- views目录中添加代码
<template>
<div>这是blog页面</div>
</template>
<script>
export default {
name: 'Blog',
};
</script>
<style>
</style>
- 创建完路由并导出后,还需要在main.js中注册一下
// src/main.js
import Vue from 'vue';
import App from './App.vue';
import router from './router';
// 6.引入router对象
Vue.config.productionTip = false;
new Vue({
// 7.注册router对象
router,
render: (h) => h(App),
}).$mount('#app');
- 确定在哪个组件中展示,还要放置占位标签
// src/App.vue
<template>
<div id="app">
<div id="nav">
<!-- 9.通过router-link创建一些链接 -->
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<!-- 8.创建路由组件占位符 -->
<!-- 当我们的路径匹配到一个组件之后,会把这个组件加载进来,并最后替换到router-view这个位置 -->
<router-view/>
</div>
</template>
- 至此,vue-router使用完毕
三.Vue实例中的router
- 我们可以在main.js中先注释router,打印出Vue。再加上router,再打印出Vue,看看对比
import Vue from 'vue'
import App from './App.vue'
// import router from './router'
// 用vm来接受这个实例,并打印出来
const vm = new Vue({
// 3. 注册路由对象
// router,
render:h=>h(App)
}).$mount('#app')
console.log(vm)
- 没有加载router的情况
- 加载了router的情况
- 当我们给Vue实例注册router的时候,会给Vue实例创建
_route和_router这两个属性。 _route: 表示规则,存储了当前路由的地址,路径。如果路径中有参数,会在这里展示。_router: 就是VueRouter的实例,这个路由对象会提供一些和路由相关的方法。比如常用的router.push,go等等,在_proto__上可以找到。还有路由的一些相关信息,比如mode模式。还有currentRoute,在不方便获取$route或者_route的时候,可以通过currentRoute获取当前的路由规则。
四.动态路由
- router
// src/router/index.js
import Vue from 'vue';
import VueRouter from 'vue-router';
// 1.首先导入Vue和VueRouter
import Home from '../views/Home.vue';
// 2.调用Vue.use注册组件,调用传入对象的install方法
Vue.use(VueRouter);
// 3.编写路由规则
const routes = [
{
path: '/',
name: 'Home',
component: Home,
// 首页是进页面之后,就需要进行加载
},
{
path: '/about',
name: 'About',
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
// 而about页面初始化的时候不需要加载,只有用到的时候才需要加载,这个时候就可以使用路由懒加载
// 用户访问的时候才加载,用户不访问的时候不加载
},
{
// 动态路由
// 通过一个占位,动态的匹配变化的位置
// 获取id的形式有两种,看photo页面
path: '/photo/:id',
name: 'Photo',
props: true,
// 开启props,会把URL中的参数传递给组件
// 在组件中通过props来接受URL参数
component: () => import(/* webpackChunkName: "Photo" */ '../views/Photo.vue'),
},
];
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes,
});
export default router;
- views添加代码
<template>
<div>
<div>这是Photo页面</div>
<!-- 方式一:通过当前的路由规则获取 {{ $route.params.id }} -->
<div>{{ $route.params.id }}</div>
<br />
<!-- 方式二:开启props后,通过props获取 -->
<div>{{ id }}</div>
</div>
</template>
<script>
export default {
name: 'Photo',
props: ['id'],
};
</script>
<style>
</style>
- 需要注意的是,虽然在Vue实例上观察到的属性是
_route,但实际在页面中使用并获取_route上的属性的还是$route。
五.嵌套路由
- 路由页面代码
import Vue from 'vue';
import VueRouter from 'vue-router';
// 1.首先导入Vue和VueRouter
import Home from '../views/Home.vue';
// 2.调用Vue.use注册组件,调用传入对象的install方法
Vue.use(VueRouter);
// 3.编写路由规则
const routes = [
{
path: '/',
name: 'Home',
component: Home,
// 首页是进页面之后,就需要进行加载
},
{
path: '/about',
name: 'About',
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
// 而about页面初始化的时候不需要加载,只有用到的时候才需要加载,这个时候就可以使用路由懒加载
// 用户访问的时候才加载,用户不访问的时候不加载
},
{
// 动态路由
// 通过一个占位,动态的匹配变化的位置
// 获取id的形式有两种,看photo页面
path: '/photo/:id',
name: 'Photo',
props: true,
// 开启props,会把URL中的参数传递给组件
// 在组件中通过props来接受URL参数
component: () => import(/* webpackChunkName: "Photo" */ '../views/Photo.vue'),
},
{
path: '/home',
component: Home,
// 如果需要嵌套路由,那么Home组件内需要插入<router-view />
children: [
{
// 注意,这里的地址不能带有/符号,带有/符号的都是根路径
path: 'blog',
name: 'Blog',
component: () => import(/* webpackChunkName: "Blog" */ '../views/Blog.vue'),
},
],
},
];
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes,
});
export default router;
- Home页面代码
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'Home',
};
</script>
- 记得在父路由中加入router-view,确定子组件的插入位置,否则子组件不会渲染
- 子组件blog代码
<template>
<div>这是blog页面</div>
</template>
六.编程式导航
- 编程式导航中的常用方法
push方法的路由跳转:this.$router.push('/')可以传入字符串,表示跳转的路径。也可以传入对象,表示跳转的名字this.$router.push({ name: 'Home' })push方法的路由传参:this.$router.push()replace方法:this.$router.replace('/login'),和push方法类似,跳转到登陆页,但是不会记录历史,跳转后的浏览器后退按钮依然是不可点击的。go方法:跳转到历史的某一次,可以是负数,this.$router.go(-2),-1的情况和back一样
七.hash模式和history模式
1.Hash模式:
- 举例:www.example.com/#/list?id=3…
- 路径中会带着#符号,#后面的作为请求的地址,可以通过?携带参数
hash模式是基于锚点,以及onhashchange事件。通过锚点的值作为路由地址,当地址发生变化后,触发onhanshchange事件。根据路径,决定页面上呈现的内容。
- 修改route/index.js文件为hash模式,并在html中添加监听事件
<script>
window.addEventListener('hashchange', function () {
console.log('The hash has changed!')
}, false);
</script>
- 监听hash模式的变化
- 如果我们手动修改地址拦的地址,或者点击浏览器的后退,前进就会触发console.log
- 如果我们点击router-link的按钮,则不会出发hashchange
- 添加popstate监听事件
window.addEventListener('popstate', (event) => {
console.log("location: " + document.location + ", state: " + JSON.stringify(event.state));
});
- 行为与
hanshchange一致
2. History模式:
- www.example.com/list?id=321…
- 就是一个正常的地址,如果需要配置
history模式,还需要服务端的支持 history模式是基于HTML5中的History APIhistory.pushState()不会发起请求,只会改变浏览器端的地址,并把这个地址记录到历史记录里面。
history.replaceState()与history.pushState()非常相似,区别在于replaceState()是修改了当前的历史记录项而不是新建一个。 注意这并不会阻止其在全局浏览器历史记录中创建一个新的历史记录项。
- history模式需要服务器的支持,
- 单页应用中,服务端不存在
http://www.example.com/list这样的地址,会返回找不到页面。比如刷新操作。所以服务端需要配置,除了静态资源,其他的所有路径,都应该返回单页应用的index.html
八.Vue Router的实现原理
- Vue的前置知识
- 插件
- 混入
- Vue.observable()
- 插槽
- render函数
- 运行时和完整版的Vue
- Vue-router是前端路由,当浏览器路径发生变化的时候,去加载对应的组件
- Hash模式
- URL中#后面的内容作为路径的地址,改变这部分地址,浏览器不会向服务器请求这个地址,但会把这个地址记录到浏览器的访问历史中
- 监听hashchange事件,记录路由改变的地址
- 根据当前路由地址找到对应的组件进行重新渲染
- History模式
- 通过history.pushState()方法改变地址栏,pushState仅仅只是改变地址栏,并不会真正的发起请求
- 监听popstate事件,可以监听到浏览器历史操作的变化,在popstate处理函数中,可以找到改变后的地址。要注意的是,调用pushState和replace的时候,不会调用该事件。当使用浏览器的前进和后退,forward和back的时候,才触发
- 根据当前路由地址找到对应的组件重新渲染
- 类图:描述这个类中的所有成员
- 有了类图,就方便去实现这个类
- 类图分为三部分,第一部分是类的名字,第二部分是类的属性,第三部分是类的方法
- options的作用是记录构造函数传入的对象,在new router中传入路由规则
- routeMap是一个对象,用来记录组件和路由地址的对应关系,会把路由规则解析到routeMap里面
- data是一个对象,里面有个属性current,会记录当前的路由。data作为对象是因为需要一个响应式的对象,路由发生变化后,对应的组件要自动更新。可以调用Vue.observe方法
- VueRouter方法中,+对应的是公开的方法,-对应的是静态方法
- 其中install就是静态方法,实现插件的功能。
- constructor构造函数方法
- init方法是用来调用下面三个方法 ,把不同的功能,分割到不同的方法中实现。
- initEvent方法,是用来注册popstate事件,来监听浏览器历史的变化
- createRouteMap是用来初始化routeMap属性,把构造函数中的路由规则,转化成routeMap里面的键值对的形式。
- initComponents方法,是用来创建和这两个组件的。
classDiagram
VueRouter <|--
VueRouter: Constructor
VueRouter: +option
VueRouter: +data
VueRouter: +routeMap
VueRouter: +Constructor(option)
VueRouter: _install(Vue)
VueRouter: +init()
VueRouter: +initEvent()
VueRouter: +createRouteMap()
VueRouter: +initComponents()
九.实现一个Vue Router
- 替换router/index.js里导入的vue-router
/* eslint-disable */
let _Vue = null;
export default class VueRouter {
// 其中有一个静态方法,install,同时要接受两个参数,一个是构造函数,一个是可选的选项对象
// 考虑下在install方法中,要去做那些事情,然后 再 一个一个去实现
// 1.判断下当前插件是否被安装,如果已经被安装,就不用去重复安装了
// 2.把Vue的构造函数,记录到全局变量中去。因为这只是个静态方法,且后面会使用到这个构造函数,比如构建routelink组件
// 3.把创建Vue实例时,注入的route对象,传递到所有vue实例中上。
static install(Vue) {
// 1.判断下当前插件是否被安装
if (VueRouter.install.installed) {
return;
}
VueRouter.install.installed = true;
// 2.把Vue的构造函数,记录到全局变量中去。
// 在文件最顶层设置一个全局的变量,进行保存
_Vue = Vue;
// 3.把创建Vue实例时,注入的route对象,传递到所有vue实例中上。
// 让所有实例都共享一个属性,Vue的组件也是一个实例,那么很明显就是放到构造函数的原型上
// 因此,获取vue构造时的选项router,进行赋值,带有$符号的时vue自带的属性,以便与用户自定义的区分
// 因此,需要获取Vue实例上的选项options,因此需要用到混入,可以给 所有的 Vue实例混入一个选项,在选择中设置一个beforeCreate,在这个钩子中,就可以通过this获取Vue的实例选项
_Vue.mixin({
beforeCreate() {
// 因为混入后,所有的Vue实例都会执行,但只需要执行一次,所以需要加个判断
if (this.$options.router) {
// 因为只有Vue的构造函数选择中有router这个属性,其他组件中的Vue.$options中并没有router
_Vue.prototype.$router = this.$options.router;
// 先找到option中的router对象,执行下面的初始化函数
this.$options.router.init();
}
},
});
}
// 构造函数中,初始化我们需要的属性
constructor(options) {
// 记录下传入的参数
this.options = options;
// 记录下传入的参数options中,路由和地址的映射关系
this.routeMap = {};
// data是一个响应式的对象,里面有个current。记录当前的地址。可以通过Vue.observable创建响应式对象
this.data = _Vue.observable({
current: "/about",
});
}
// 遍历所有的路由规则,在constructor中传入的对象参数里面,去解析成键值对的形式,储存到routeMap这个对象里面
createRouteMap() {
this.options.routes.forEach((route) => {
this.routeMap[route.path] = route.component;
});
}
// 渲染router-link和router-view组件,
initComponents(Vue) {
// route-link组件最后就是超链接,<router-link to="/">Home</router-link>,超链接的地址就在to里面,内容在标签之间
Vue.component("router-link", {
props: {
to: String,
},
// 注意,vue的运行时版本不支持template
// template:'<a :href="to"><slot></slot></a>
// 因此可以用render函数
render(h) {
// h是Vue传递的,用来创建虚拟DOM
// h函数可以传递三个参数,第一个是标签选择器,第二个是给标签增加属性,第三个参数是生成元素的子元素,是一个数组
return h(
"a",
{
attr: {
href: this.to,
},
// 注册一个click事件
on: {
click: this.clickHander,
},
},
[this.$slots.default]
); // 设置默认插槽
},
methods: {
// 这样会存在问题, 点击a超链接以后,浏览器会向服务端发起请求,并刷新,但我们只需要改变浏览器的地址栏,同时又不发起请求,所以需要一个click事件,同时阻止后续的操作
clickHander(e) {
// pushState有三个参数,第一个data,传给popState事件的一个事件参数,第二个网页的标题,第三个是网页的地址
// 改变地址栏了
history.pushState({}, "", this.to);
// 改变地址栏之后,需要去加载对应的组件
this.$router.data.current = this.to; // 因为是响应式对象,所以改变current后,会重新加载组件
e.preventDefault();
},
},
});
// 实现router-view组件
const self = this;
Vue.component("router-view", {
render(h) {
// 注意,这种方式只能接收按需加载的组件,即 component: () => import(/* webpackChunkName: "Blog" */ '../views/Blog.vue'),
// 且不能是动态路由,之后再补充
const component = self.routeMap[self.data.current];
// console.log(self.routeMap[self.data.current])
return h(component);
},
});
}
init() {
// 把初始化的方法集中一起执行,最后放到静态方法install里面
this.createRouteMap();
this.initComponents(_Vue);
this.initEvent();
}
// 这个方法是用来解决,当浏览器前进和后退时的去加载组件,因为目前还没有处理
// popState是当浏览器历史发生变化的时候触发的,但如果是pushState和replaceState是不会触发的
initEvent() {
// 这个函数是用来注册popstate事件
window.addEventListener("popstate", () => {
this.data.current = window.location.pathname;
});
}
}
- 未实现动态路由和直接导入的情况...(待续)