一口气拿下vue-router所有知识点,薪资暴涨3000
一 vue路由基础使用【固定路由的配置和使用】
首先我们展示一下router使用后的效果。
首先启动服务后,输入路由http://localhost:8080/#/A ,到达页面A,展示页面A内容,点击页面上按钮或者将url切换为http://localhost:8080/#/B ,到达页面B,展示页面B内容。
怎样配置
- 创建两个文件src/views/a.vue和src/views/b.vue,用来展示路由切换效果。
// src/views/a.vue
<template>
<div>
<h1>我是页面a</h1>
</div>
</template>
// src/views/b.vue
<template>
<div>
<h1>我是页面b</h1>
</div>
</template>
- 创建src/router/index.js文件,在src/router/index.js中引入router必要文件,导出router对象。
src/router/index.js文件代码示例如下。
import Vue from "vue";
import VueRouter from "vue-router";
import a from "../views/a.vue";
import b from "../views/b.vue";
Vue.use(VueRouter);
const routes = [
{
path: "/B",
name: "A",
component: a,
},
{
path: "/B",
name: "B",
component: b,
},
];
const router = new VueRouter({
routes,
});
export default router;
- 在src/main.js中,在全局vue实例创建的时候,注入router对象。
src/main.js代码示例如下。
...
import router from "./router";
...
new Vue({
router,
...
render: (h) => h(App)
}).$mount("#app");
这一步做了什么呢,我们可以将vue实例打印出来,看一下router写入前后,vue实例有什么变化。
const vm1 = new Vue({
router,
render: (h) => h(App),
}).$mount("#app");
const vm2 = new Vue({
render: (h) => h(App),
}).$mount("#app");
console.log(vm1, vm2);
我们写了两个不同的Vue实例,两者唯一的区别是有无注入router,将它们打印出来后对比一下,可以看到,注入router的vue实例中增加了几个router相关的属性,
r
o
u
t
e
r
和
router和
router和route属性被赋值。
点击打开后可以看到,
r
o
u
t
e
r
中
有
一
些
路
由
相
关
的
方
法
,
router中有一些路由相关的方法,
router中有一些路由相关的方法,route中有当前路由的信息。
这些信息已经挂载到了全局vue实例上,在页面中就可以直接使用了。
- 在App.vue中对应位置添加
<router-view/> 标签,该标签为路由组件的占位符,路由切换时,该标签会被替换成对应的路由组件。就达到了我们想要的效果。
在切换路由时,我们可以通过点击<router-link /> 标签或者手动更改url的方式进行页面切换,下面是一个带有<router-view/> 标签和<router-link /> 标签的代码示例。
这里我们为router-link标签写上了一些样式,使展示更加美观。
<template>
<div id="app">
<router-link class="a" to='A'>跳往页面A</router-link>
<router-link class="b" to='B'>跳往页面B</router-link>
<router-view />
</div>
</template>
<style>
.a,.b {
border: 1px solid #42b983;
padding: 5px 10px;
margin: 5px;
margin-top: 300px;
color: #42b983;
}</style>
怎样控制路由跳转
刚刚介绍了路由在html中使用标签进行跳转,但有时我们希望在js中跳转,该怎样操作呢?这就用到上面挂载在vue实例上的$router。代码示例如下。
this.$router.push('/b')
this.$route.query
this.$router.push({ name: 'B' })
this.$router.replace('/b')
this.$router.go(-2)
this.$router.go(-1) === this.$router.back()
二 如何在路由中携带参数【动态路由的配置和使用】
刚才我们对于不同页面配置了不同固定路由。
不知你有没有注意到csdn的个人主页,小明的个人主页链接为https://blog.csdn.net/123 ,而小米的个人主页链接为https://blog.csdn.net/456 ,相同的页面对应的路由却不一样,似乎路由中携带了一个参数,用来区分不同用户,而路由指向的又是同一个页面,这是怎么回事呢?
怎样携带参数
我们再看上面配置的src/router/index.js文件route对象。
const routes = [
{
path: "/a",
name: "A",
component: a,
},
{
path: "/b",
name: "B",
component: b,
},
];
这时我们希望将b页面的路由达到上面携带不同参数,有指向同一个页面的效果,我们将这种路由叫做动态路由,动态路由的配置也很简单,对应位置前加冒号就可以,代码示例如下。
const routes = [
{
path: "/a",
name: "A",
component: a,
},
{
path: "/b/:id",
name: "B",
component: b,
},
];
这时我们可以看到,原来的路由http://localhost:8080/#/b 已经访问不到该页面了,将路由变更为http://localhost:8080/#/b/1 可以正常访问页面,将b/后面更改为任意值都可以访问到页面。
带有参数的动态路由也可以通过js指定name跳转。
this.$router.push({ name: 'B', params: { id: 1 } })
现在参数已经写入动态路由中了,怎么获取该参数呢?
怎样获取参数
- 使用
r
o
u
t
e
.
p
a
r
a
m
s
.
i
d
获
取
,
动
态
路
由
中
的
参
数
都
可
以
通
过
v
u
e
全
局
属
性
route.params.id获取,动态路由中的参数都可以通过vue全局属性
route.params.id获取,动态路由中的参数都可以通过vue全局属性route下的params对象获取,代码示例如下。
<template>
<div>
<h1>我是页面b</h1>
通过当前路由获取:{{ $route.params.id }}
</div>
</template>
<script>
export default {
name: B
};
</script>
- 使用props获取,使用该方法需要在route路由中配置props为true,代码示例如下。
const routes = [
{
path: "/a",
name: "A",
component: a,
},
{
path: "/b/:id",
name: "B",
component: b,
props:true,
},
];
<template>
<div>
<h1>我是页面b</h1>
通过props获取: {{ id }}
</div>
</template>
<script>
export default {
name: B,
props: ["id"],
};
</script>
三 如何提取页面相同内容【嵌套路由的配置和使用】
刚刚我们在app.vue中写入了标签,该标签会被路由文件中配置的组件替换。
当前页面是这样的。
现在我们有了新的需求,在b页面中,我们希望可以再开辟一块自由区域,可以随着路由变化。
这时我们的b页面就是一个layout页面(布局页面),它里面包含一部分可以分化的内容,可以分化出不同页面。
具体到页面上,我们将实现下面的效果。
首先我们新建三个文件,将layout页面和内部的两个分化页面layoutA和layoutB准备好。
// src/view/LayoutA.vue
<template>
<div>AAAAAAAA</div>
</template>
// src/view/LayoutB.vue
<template>
<div>AAAAAAAA</div>
</template>
// src/component/Layout.vue
<template>
<div>
<div class="bar">header</div>
<div class="content">
<router-view />
</div>
<div class="bar">footer</div>
</div>
</template>
<style lang="scss" scoped>
.bar {
width: 100px;
padding: 10px;
text-align: center;
background-color: green;
padding:20px 0;
color: #fff;
width: 100%;
}
.content {
background-color: khaki;
height: 300px;
color: green;
line-height: 300px;
width: 100%;
font-size: 40px;
}
</style>
然后我们在src/router/index.js中修改router配置如下。
const routes = [
... ...
{
path: "/b",
name: "B",
component: layout,
children: [
{
name: "LayoutA",
path: "/b/a",
component: LayoutA,
},
{
name: "LayoutB",
path: "/b/b",
component: LayoutB,
},
],
},
];
当路由/b/a或者/b/b的时候,组件layout会替换app.vue中的标签,组件LayoutA或者LayoutB会替换组件layout中的标签。
就实现了上面的效果。
四 路由的两种模式有什么不同【hash和history的区别】
表现形式有什么区别
hash模式带有#号,#号后面的内容是我们的路由地址,history模式是正常的路由。
hash:http://localhost:8080/#/b
history:http://localhost:8080/b
实现原理有什么区别
hash模式:基于锚点也就是#号,当#后面的路由发生变化后,触发onhashchange事件,在事件中对页面进行操作。
history模式:基于h5中的historyAPI。
history.pushState() 在浏览器记录中添加一个新纪录,不向后端发送请求。
history.replaceState() 修改浏览器历史中当前历史记录,不向后端发送请求。
由于浏览器的url栏中始终显示最新的url记录,所以就好像触发了页面更新一样,其实并没有。
五 history模式的使用和问题
history模式配置和使用
history的配置很简单,在src/router/index.js中,创建路由实例时设置属性mode为history即可。
import Vue from "vue";
import VueRouter from "vue-router";
...
Vue.use(VueRouter);
const routes = [...];
const router = new VueRouter({
mode: "history",
routes,
});
export default router;
现在我们再次访问刚才的页面。
现在使用http://localhost:8080/#/b/b 已经访问不到啦,我们将#去掉,url更改为http://localhost:8080/b/b ,页面出现了。
history浏览器请求问题
history模式中,在触发页面跳转时,我们使用history.pushState() 在浏览器记录中添加一个新纪录,不向后端发送请求。
但如果我们使用浏览器触发跳转会怎样呢,比如直接向浏览器中输入url后,或者点击页面刷新。
有的同学有讲,我在开发过程中经常直接使用浏览器跳转呀,没遇到问题,这是因为我们使用的vue脚手架已经帮我们解决了这个问题,但是当代码打包,上线后,问题就出现了,浏览器会告诉我们,找不到该页面。
这是因为我们使用代码去进行跳转/b/b,实际上不是真实的跳转,不会向服务端发送请求,而浏览器跳转会向后端发送请求,后端一看,没有/b/b这个请求路径呀,就会报错。
所以使用history模式,需要在服务端配置一下。
如果我们使用的是node做服务端时,可以通过配置中间件/插件(connect-history-api-fallback)来实现对history的支持。把这个插件在node.js服务端引入后注册就好了。
如果我们使用的是nginx服务,在nginx的配置文件中配置当找不到对应页面的时候返回首页就好啦。
因为我们主要是讲vue-router,这里就不展开讲解node服务和nginx服务啦。
六 浅析vue-router实现原理【面试必备】
hash模式实现原理
- url中#后面的内容作为路径地址
- 监听hashchange事件
- 根据当前路由地址找到对应组件重新渲染
history模式实现原理
- 通过history.pushState()方法改变地址栏
- 监听popstate事件
- 根据当前路由地址找到对应组件重新渲染
七 怎样实现自己的vue-router
vue-router是什么
vue-router是什么?实现vue-router从何下手,首先我们来分析它的使用,在src/router/index.js中,我们引入vue-router,使用vue.use()注册了它,又new了它的实例,最后导出。
import Vue from "vue";
import VueRouter from "vue-router";
Vue.use(VueRouter);
const routes = [...];
const router = new VueRouter({
mode: "history",
routes,
});
export default router;
Vue.use方法里面的参数可以是一个对象或者一个函数,当它是一个函数的时候它会被执行,当它是一个对象的时候,会执行对象内部的install函数,那么vue-router是什么呢。
下面我们使用new创建了一个vue-router实例,这里可以看出,vue-router是一个类(类也是对象哦)。
我们现在对vue-router有了一定的认知,它是一个类,里面包含install方法,它有一个构造函数,可以对new时传入的参数进行处理。
知道了它是什么,我们就可以向着这个方向开发啦。
实现一个vue插件【install】
实现一个插件需要几步?首先我们写一个含有install方法的类。
export default class VueRouter {
static install(Vue) { }
}
该类作为插件,被vue调用install方法的时候,会传入一个参数,就是vue构造函数。在install中我们可以保存vue构造函数和对vue构造函数做改动。
首先我们可以在install中对插件是否已安装做判断,已安装状态下直接返回。我们可以通过设置一个变量来记录。
然后将vue构造函数记录在一个变量中,以备使用。
let _Vue = null;
export default class VueRouter {
static install(Vue) {
if (VueRouter.install.installed) {
return;
}
VueRouter.install.installed = true;
_Vue = Vue;
}
}
最后对vue构造函数做一定的修改,我们刚才有提到,我们创建vue实例的时候,将返回的router传入了vue实例中。
new Vue({
router,
render: (h) => h(App),
}).$mount("#app");
传入之后vue做了什么呢,要知道router对于vue来说只是一个插件而已,vue原本的逻辑是不包括对router的处理的,所以router处理的这一步应该是写在vue-router的install中的。
在创建vue实例前,vue会调用钩子函数beforeCreate,这部分逻辑可以使用混入/mixin写入。
let _Vue = null;
export default class VueRouter {
static install(Vue) {
...
_Vue.mixin({
beforeCreate() {
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router;
}
},
});
}
}
我们使用混入,为vue实例的beforeCreate钩子函数添加了一段逻辑,这里的this.$options指的就是我们在创建vue实例时传入的对象。比如下面的代码中,option就是含有router和render属性的对象。
new Vue({
router,
render: (h) => h(App),
}).$mount("#app");
还记得前面我们有将vue实例打印出来看吗,option中添加了router后,vue实例的$router就有值啦。
现在install函数就写好了,我们的vuerouter已经是一个合格的插件了,我们盘点一下一共做了几步。
- 判断当前插件是否被安装
- 将vue构造函数记录到全局变量
- 为vue添加beforeCreate逻辑,增加对router信息的处理。
实现构造函数
接下来我们看router/index的第二步,它传入了一个对象,new了一个实例。
const router = new VueRouter({
routes,
});
接下来我们就要写VueRouter的构造函数,在构造函数中,对传入的对象做处理。
构造函数的用处主要是初始化一些内容,最终构造的实例是要挂在vue.
r
o
u
t
e
r
上
的
,
所
以
通
过
对
v
u
e
实
例
的
打
印
,
可
以
看
到
router上的,所以通过对vue实例的打印,可以看到
router上的,所以通过对vue实例的打印,可以看到router也就是VueRouter实例到底生成了什么。
我们实现一个简单的vue,就初始化几个主要的属性,首先将传入的options存储起来,然后设置一个双向绑定的变量data,用来记录当下的路由信息。
由于当下的路由信息是可以变化的,当它变化的时候,我们需要在页面可以观察到它的变化,而通过Vue.observable定义的变量,是可观测的,可以直接在computed和watch中使用,所以当下的路由信息变量采用Vue.observable定义。
let _Vue = null;
export default class VueRouter {
...
constructor(options) {
this.options = options;
this.routeMap = {};
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
})
this.data = _Vue.observable({
current: '/a'
})
}
}
上面的代码中,我们定义了options存储变量this.options,和当前路由信息变量data以外,还有一个变量this.routeMap,这个变量是整理后的router规则,它的key是路由地址,value是组件。后面我们要根据它进行渲染。
实现router-link
刚刚我们有讲,router-link是router自带的一个组件,可以点击跳转到指定路由,这个组件我们的router也要有,那什么时间去注册这个组件呢?怎么注册呢?
这里我们使用vue的一个函数render函数,render函数可以创建vue组件,render函数创建vue组件后,可以自动注册到vue上。我们在模板中就可以使用注册的组件了。
我们首先使用html标签实现一下router-link组件。
<a :href="to"><slot></slot></a>
render函数是写在Vue.component()中的,Vue.component()接收两个参数,第一个参数为这次要生成/注册的组件名称,第二个参数为组件的配置,组件的配置中可以写props等使用js定义的内容和负责生成html标签的render函数。
代码实现如下。
Vue.component("router-link", {
props: {
to: String,
},
render(h) {
return h(
"a",
{
attrs: {
href: this.to,
},
},
[this.$slots.default]
);
},
});
render函数有一个参数,该参数是创建节点的函数,该函数接受三个参数,html标签名称、属性对象列表(可选)和子节点数组(可选)。这里我们实现的组件结构为<a :href="to"><slot></slot></a> ,它包含一个子节点,所以第三个参数只有一项[this.$slots.default] ,子节点是默认插槽,可以使用this.$slots.default获取,如果子节点是其他,可以在这里嵌套Vue.component函数去进行子节点的创建。
我们把注册过程写在我们的router类中的方法initComponents中。
因为这一步属于对vue的处理,所以我们放在install上去执行,放在mixin混入这步执行。
代码如下。
let _Vue = null;
export default class VueRouter {
static install(Vue) {
...
_Vue.mixin({
beforeCreate() {
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router;
this.$options.router.init();
}
},
});
}
init() {
this.initComponents(_Vue);
}
initComponents(Vue) {
Vue.component("router-link", {
props: {
to: String,
},
render(h) {
...
}
});
}
}
现在我们实验一下成果。
在router/index.js文件中,我们把引入的vue-router改成我们自己的router,然后修改一下路由配置。
import VueRouter from "../vuerouter";
const routes = [
{
path: "/a",
name: "A",
component: a,
},
{
path: "/b",
name: "B",
component: b,
},
];
我们去到app.vue中,代码修改如下。
<template>
<div id="app">
<router-link class="a" to='A'>跳往页面A</router-link>
<router-link class="b" to='B'>跳往页面B</router-link>
<router-view />
</template>
打开页面,输入路由http://localhost:8080/a,点击标签,成功触发跳转。
注意看图中的url已经改变了,但是页面始终没有变化,页面的变化逻辑在router-view组件中,我们接下来实现一下router-view组件。
实现router-view
我们仍然使用render函数去实现,刚刚我们准备了一个key是路由地址,value是组件的变量routeMap,和记载当前路由的变量data.current,我们就用self.routeMap[self.data.current] 找到对应的组件名称,而render函数是可以通过传入已经存在的组件名称,注册组件的。
我们直接把这段代码放置在刚刚router-link代码的下面。
let _Vue = null;
export default class VueRouter {
...
initComponents(Vue) {
...
const self = this;
Vue.component("router-view", {
render(h) {
const component = self.routeMap[self.data.current];
return h(component)
},
});
}
}
加入这段代码后,我们再看页面,这次页面渲染成功了,点击按钮,url改变了,但是我们发现了问题。
- url改变后,页面没有跟随变化,始终是初始页面
从下面可以看到,无论路由是/A还是/B,都展示我是页面a。
- url改变的时候,页面刷新了
刚刚我们是使用a标签实现的router-link,点击a标签触发了默认行为跳转,接下来我们给这个a标签添加点击事件,在这个点击事件中做三件事。
- pushState触发跳转,pushState函数在history模式的使用和问题一章有提到哦,这里就不做重复说明了。
- 为记载当前路由的变量data.current赋值,由于这个变量是通过Vue.observable定义的响应式变量,被赋值后会触发依赖该变量的Vue.component函数的重渲染。
- 阻止a标签的默认行为。
Vue.component("router-link", {
props: {
to: String,
},
methods: {
clickHander(e) {
history.pushState({}, "", this.to);
this.$router.data.current = '/' + this.to;
e.preventDefault();
},
},
render(h) {
return h(
"a",
{
attrs: {
href: this.to,
},
on: {
click: this.clickHander,
},
},
[this.$slots.default]
);
}
});
现在我们的router-link已经可以正常使用啦。
|