路由的概念
路由是一个比较广义和抽象的概念,路由的本质就是对应关系;
在开发中,路由分为前端路由和后端路由
后端路由
- 概念:根据不同用户的URl请求,返回不同的内容;
- 本质:URL请求地址和服务器资源之间的对应关系
前端路由
- 概念: 根据不同的用户时间,显示不同的页面内容
- 本质:用户事件与时间处理函数之间的对应关系
Vue-Router是前端路由,当路径切换时,在浏览器判断当前路径,加载对应的组件。
Hash模式和History模式
前端路由中,不管是什么实现方式,都是客户端的一种实现方式,也就是当路径发生变化的时候,是不会向服务器发送请求的。
如果需要向服务器发送请求,就要用到ajax方式
两种模式的区别
表现形式的差异
Hash模式
- 带有
#,#后内容左右路由地址,可以通过?携带参数; - 这种模式相对来说比较丑,路径中带有与数据无关的符号,例如#和?
History模式
- 正常的路径模式,需要服务端的相应支持(后面会解释原因)
原理上的差别
Hash模式
- 基于锚点,以及onhashchange事件
- 通过锚点的值作用路径地址,当地址发生变化后触发onhashchange事件
History
- 基于HTML5中的History API
- history.pushState() // IE10以后才支持
- history.repalceState()
History模式为何需要服务器的支持???
因为在单页面中,只有一个页面,也就是index.html页面。服务端不存在www.test.com/lohin 这样的地址。也就是说如果刷新浏览器,请求服务器,是找不到login这个页面的。所以就会出现404的错误。 所以说在服务端应该除了静态资源外都返回单页面SPA应用的index.html
vue-router的使用
是官方的路由管理器,可以非常方便的用于SPA应用程序的开发
基本使用步骤
引入相关的库文件(vue、vue-router,使window挂在Vue和VueRouter这两个构造函数)添加路由链接
// router-link是vue中提供的标签,默认会被渲染为a标签;
// to属性默认会被渲染为href属性,其值会被渲染为#开头的hash地址
<router-link to="/user">User</router-link>
添加路由填充位/路由占位符
// 将来通过路由规则匹配到的组件,将会被渲染到router-view所在的位置
// to属性默认会被渲染为href属性,其值会被渲染为#开头的hash地址
<router-view></router-view>
定义路由组建
const UserComp = {
template: '<div>这里是用户主页</div?'
}
配置路由规则并创建路由实例
const router = new VueRouter({
routes: [
{ path: '/user', component: UserComp },
]
})
把路由挂载到Vue根实例中
new Vue({
el: "#app",
router,
})
vueRouter模拟实现
回顾使用VueRouter的核心代码
// 注册组件Vue.use支持传入函数和对象,如果传入的是对象,会调用对象中的install方法
Vue.use(VueRouter)
// 创建路由对象 VueRouter 是个类 里面应该有一个静态的install方法
const router = new VueRouter({
routes: [
{ name: 'home', path: '/', component: homeComponent }
]
})
// main.js
// 创建vue实例,注册router兑现
new Vue({
router,
render: h => h(App)
}).$mount('#app')
画一个类图
| 三个属性 | 用处 |
|---|---|
| options | 记录构造函数中传入的对象 |
| data | current记录当前的地址的,data对象是响应式对象,可以调用Vue.observal方法 |
| routeMap | 记录路由地址和组件的对应关系 |
| 6个方法 | 解释 |
|---|---|
| Constructor(Options) | VueRouter 构造函数 |
| install(Vue):void | 用来实现vue的插件机制 静态方法 |
| init() | void 注册popstate |
| initEvent(): void | 处理前进后退 |
| createRouteMap():void | 初始化routeMap属性,转成map形式 |
| initCOmponents(Vue): void | 创建route-link 和route-view这两个组件的 |
创建VueRouter的静态函数注册install方法
先看install需要做哪些事情:
- 判断当前插件是否已经被安装,如果已经安装,不需要重复安装
- 把Vue构造函数记录到全局变量中,静态方法install,这个方法接收了一个参数-vue的构造函数,而将来我们在vue实例中的一些方法中,还要使用vue的构造函数,比如创建组件时,需要使用Vue.component()
- 把创建Vue实例时传入的router对象注入到Vue实例上
export default class VueRouter {
static install(Vue) {
// 1. 判断当前插件是否已经被安装
if (VueRouter.install.installed) {
return
}
VueRouter.install.installed = true
// 2. vue的构造函数记录到全局变量中
_Vue = vue
// 3. 把创建Vue实例时传入的router对象注入到Vue实例上
// _Vue.prototype.$router = this.$options.router(x)
// 因为只有在new Vue()的时候,才会有$options这个属性
// 混入
_Vue.mixin({
beforeCreate() {
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router
}
}
})
}
}
实现构造哦函数
在介绍VueRouter的类图时,我们说过,Cinstructor是一个构造函数,该猴枣函数方法中会初始化options,data,routeMap这三个属性
export default class VueRouter {
constructor(options) {
this.options = options;
this.routeMap = {}
this.data = _Vue.observable({
currnet: "/", // current记录当前的地址的,data对象是响应式对象,可以调用Vue.observal方法
})
}
}
实现cerareRouteMap方法
cerareRouteMap方法,会把构造中传入进来的options中的路由规则,转换成键值对的形式存储到routeMap中。键就是路由地址,值就是对应的组建
export default class VueRouter {
cerareRouteMap() {
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
})
}
}
实现initComponents方法
initComponents方法,主要作用是用来创建router-link和router-view这两个组件的。
<router-link to="/user">用户主页</route-link>
我们知道router-link这个组件最终会被渲染成a标签,同时to作为一个属性,其值会作为a标签中的href属性的值。同时还要获取route-link这个组件中的文本,作为最终超链接的文本;
export default class VueRouter {
initComponents() {
Vue.component('route-link', {
props: {
to: String,
},
template: '<a :href="to"><slot></slot></a>'
})
}
}
现在我们已经通过Vue.component来创建了router-link这个组建;
现在我们开始对上述代码进行测试,要进行测试先将cerareRouteMa和initComponents进行调用,那什么时候调用呢? 我们应该在VueRouter对象创建成功之后,并且将VueRouter对象注册到Vue实例上的时候,也就是在beforeCreate这个钩子函数中
export default class VueRouter {
static install(Vue) {
// 1. 判断当前插件是否已经被安装
if (VueRouter.install.installed) {
return
}
VueRouter.install.installed = true
// 2. vue的构造函数记录到全局变量中
_Vue = vue
// 3. 把创建Vue实例时传入的router对象注入到Vue实例上
// _Vue.prototype.$router = this.$options.router(x)
// 因为只有在new Vue()的时候,才会有$options这个属性
// 混入
_Vue.mixin({
beforeCreate() {
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router
}
// *** 在这里调用
this.$options.router.init()
}
})
},
constructor(options) {
this.options = options;
this.routeMap = {}
this.data = _Vue.observable({
currnet: "/", // current记录当前的地址的,data对象是响应式对象,可以调用Vue.observal方法
})
},
init() {
this.cerareRouteMap()
this.initComponents(_Vue)
}
cerareRouteMap() {
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
})
},
initComponents(_Vue) {
_Vue.component('route-link', {
props: {
to: String,
},
template: '<a :href="to"><slot></slot></a>'
})
}
}
this.$options.router.init() 含义
this表示的就是Vue实例,$options表示的就是创建Vue实例的时候传递进来的选项,如下所示:
const vm = new Vue({
el: "#app",
router,
})
看$options中有el属性,也有router属性,router是什么呢,是const router = new VueRouter() ,init就是这个类上的实例的方法;
在我们引用我们自己创建的VueRouter后,
import VueRouter from './vurRouter'
// 注册组件Vue.use支持传入函数和对象,如果传入的是对象,会调用对象中的install方法
Vue.use(VueRouter)
// 创建路由对象 VueRouter 是个类 里面应该有一个静态的install方法
const router = new VueRouter({
routes: [
{ name: 'home', path: '/', component: homeComponent }
]
})
// main.js
// 创建vue实例,注册router兑现
new Vue({
router,
render: h => h(App)
}).$mount('#app')
我们得到这样一个错误: You are using the runtime-only build of Vue where the template compiler is not available ……
该错误的含义是,我妈使用的是运行时版本的Vue,编译模板不可用;
你可以使用预编译模板把模板编译成render函数,或者是哟哦弄个包含编译版本的Vue;
以上错误说明了Vue的构建版本有两个,分别是运行时版和完整版
运行时版: 不支持template模版,需要打包的时候提前编译;
完整版:包含运行时和编译器,体积比运行时大10k左右,程序运行的时候把模板转换成render函数。性能低于运行时版本;
使用vue-cli创建的项目默认使用运行时版,而我们创建的VueRouter中有template模板,所以才会出现那个错误
解决的方案:在项目的根目录创建vue.config.js文件,在该文件中添加runtimeCompiler配置项,该配置项表示的是,是否使用包含运行时编译器的Vue构建
moudle.exports = {
runtimeCompiler: true,
}
render函数
如果不想做上述的修改,我们可以将template改成render函数
export default class VueRouter {
initComponents() {
Vue.component('route-link', {
props: {
to: String,
},
render(h) {
return h(
"a",
{
attrs: {
href: this..to
},
},
[this.$slots.default]
)
}
})
}
}
在测试之前一定要将根目录下的vue.config.js文件删除掉,这样当前的环境为“运行时”环境
创建router-view组件
router-view组件就是一个占位符。当根据路由规则找到组件后,会渲染到router-view的位置
export default class VueRouter {
initComponents() {
Vue.component('route-link', {
props: {
to: String,
},
render(h) {
return h(
"a",
{
attrs: {
href: this..to
},
},
[this.$slots.default]
)
}
}),
const self = this
Vue.component('route-view', {
render(h) {
const component = self.routeMap[self.data.current]
return h(component)
})
}
}
可以测试一下效果
当我们单击链接的时候,发现了浏览器进行了刷新操作。表明向服务器发送了请求,而我们单页面应用中是不希望向服务器发送请求。
修改后的initComponents方法如下:
export default class VueRouter {
initComponents() {
Vue.component('route-link', {
props: {
to: String,
},
render(h) {
return h(
"a",
{
attrs: {
href: this..to
},
on: {
click: this.clickHandler()
}
},
[this.$slots.default]
)
}
}),
methods: {
clickHandler(e) {
history.pushState({},"", this.to);
this.$router.data.current = this.to
// 阻止向服务器发送
e.preventDefault()
}
},
const self = this
Vue.component('route-view', {
render(h) {
const component = self.routeMap[self.data.current]
return h(component)
})
}
}
给a标签添加了单击事件
initEvent方法的实现
现在有一个问题就是,当点击浏览器中的前进和后退按钮的时候,地址栏中的地址放生了变化,但是对应的组建没有发生变化
这时候就要解决这个问题,就需要用到popstate事件;
popstate事件,可以发现浏览器历史操作的变化,记录改变后的地址,单击前进后者后退按钮的时候触发该事件
initEvetn() {
window.addEventListener('popstate', () => {
this.data.current = window.localtion.pathname
})
}
// 针对initEvent方法的调用如下
init() {
this.createRouteMap();
this.initComponents(_vue)
this.initEvent()
}