在前端快速发展的今天,如果不能时刻保持学习就会很快被淘汰。分享一下Vue-Router 原理实现的相关知识。每天进步一点点。
一、Vue-Router 基础知识
**1、Vue.use()方法:**接收一个参数,如果传入的参数是一个函数,直接调用这个函数;如果是一个对象,就调用这个对象的install方法。
2、 在初始化Vue实例的时候传入 router,Vue实例中会生成 r o u t e 和 route和 route和router两个属性: r o u t e 中 存 储 了 当 前 的 路 由 规 则 , route 中存储了当前的路由规则, route中存储了当前的路由规则,router中存储了路由实例对象,可以调用一些和路由相关的方法。
**3、Vue-Router 的使用步骤:**创建视图,注册路由插件,创建路由对象并配置路由规则,注册Router 对象,在页面中使用 占位,通过创建链接。
4、动态路由:
cosnt routes = [
{
// 使用动态路由
path: '/detail/:id',
name: 'Detail',
// 开启 props,会把 URL 中的参数传递给组件
// 在组建中通过 props 来接收 URL 参数
props: true,
// 路由懒加载
component: () => import('../views/Detail.vue')
}
]
5、在组件中获取路由中参数的方式:
① 通过当前路由规则,获取数据 {{ $route.params.id }}
,这种方式强依赖与路由,在使用组件的时候必须有路由给传递参数。
② 路由规则中开启 props ,路由会把 URL 中的参数传递给相应的组件,在组件接收这个props就可以了,这和父子组件传值的方式是一样的,不再依赖路由。 props: ['id']
6、嵌套路由:
cosnt routes = [
// 嵌套路由
{
path: '/',
component: Layout,
children: [
{
path: '',
name: 'Index',
component: Index
},
{
// path 可以使用相对路径也可以使用绝对路径
// path: '/detail/:id',
path: 'detail/:id',
name: 'Detail',
props: true,
component: () => import('../views/Detail.vue')
}
]
}
]
7、编程式导航:replace/push/go
this.$router.replace('/login')
, 不会记录本次历史
this.$router.push({ name: 'Detail', params: { id: 1 } })
this.$router.go(-2)
,如果传入-1,等同于 this.$router.back()
二、Hash 模式 和 History 模式
1、Hash 模式 和 History 模式 的区别:
表现形式的区别:
Hash 模式带有#,井号后面是路由地址
History 模式 就是普通的URL,需要服务端支持
原理的区别:
Hash 模式 是基于锚点,以及 onhashchange 事件
History 模式 是基于 HTML5 中的 History API
history.pushState(), IE10 以后才支持(不会发送请求,只是改变路由地址,并将这个地址记入历史记录)
history.replaceState()
2、History 模式
在服务端应该除了静态资源外都返回单页应用的 index.html
点击超链接的时候:
点击超链接,调用 history.pushState(),改变浏览器地址栏中的地址,但是不会发送请求,并将地址存入历史记录中,这些都是在客户端完成的。
刷新浏览器的时候:
浏览器向服务器发送请求,请求的地址是地址栏中的地址,如果服务器没有处理这个地址就会出现错误;如果服务器开启对 History 支持,当服务器判断当前路由地址不存在时,将单页应用的首页 index.html 返回给浏览器,浏览器接收到页面后再去判断路由地址,再去加载相应的组件内容进行渲染
三、模拟实现 Vue Router
1、Vue 前置知识:
插件、混入、Vue.observable()、插槽、render 函数、运行时和完整版的 Vue
2、Vue Router 实现原理
Hash 模式:
① URL 中 # 后面的内容作为路径地址,如果只改变#后面的地址,浏览器不会发送请求,会把这个地址记录到浏览器访问历史中;
② 监听 hashchange 事件,当hash改变后会触发hashchange事件,并且记录当前的路由地址;
③ 根据当前路由地址找到对应组件重新渲染。
History 模式:
① 通过 history.pushState() 方法改变地址栏,只改变地址并记录路由地址,不发送请求
② 监听 popstate 事件,可以监听到浏览器操作的变化,记录改变后的地址,调用pushState 或者 replaceState 的时候不会触发,浏览器的前进后退按钮 或者 back 和 forward 方法会触发
③ 根据当前路由地址找到相应组件重新渲染
3、Vue Router 核心代码
// 注册插件
// Vue.use() 内部调用传入对象的 install 方法
Vue.use(VueRouter)
// 创建路由对象
const router = new VueRouter({
routes: [
{ name: 'home', path: '/', component: homeComponent }
]
})
// 创建 Vue 实例,注册 router 对象
new Vue({
router,
render: h => h(App)
}).$mount('#app')
4、实现思路
① 创建 VueRouter 插件,静态方法 install
判断插件是否已经被加载
当 Vue 加载的时候把传入的 router 对象挂载到 Vue 实例上(注意:只执行一次)
② 创建 VueRouter 类
初始化,options、routeMap、data(简化操作,创建 Vue 实例作为响应式数据记录当前路径)
创建路由地图,遍历所有路由信息,把组件和路由的映射记录到 routeMap 对象中
创建 router-link 和 router-view 组件
当路径改变的时候通过当前路径在 routerMap 对象中找到对应的组件,渲染 router-view
注册 popstate、hashchange、load 事件,当路由地址发生变化,重新记录当前的路径
5、代码实现
① 创建 VueRouter 插件
static install (Vue) { // 调用此方法的时候传入 Vue 构造函数
// 1、判断当前插件是否已经被安装
if (VueRouter.install.installed) return
VueRouter.install.installed = true
// 2、把Vue构造函数记录到全局变量
_Vue = Vue
// 3、把创建Vue实例时候传入的 router 对象注入到 Vue 实例上
// 混入,所有的 Vue 实例 和 组件 都会执行此方法
_Vue.mixin({
beforeCreate() {
if (this.$options.router) { // 只有 Vue 实例选项中才有此属性,组件没有
_Vue.prototype.$router = this.$options.router
// 初始化组件、路由地图和事件注册 在 第3、4、5中实现
this.$options.router.init()
}
}
})
}
② 实现 构造函数
constructor (options) { // 初始化 三个属性
this.options = options
this.routeMap = {}
this.data = _Vue.observable({ // data 属性是响应式的,当路由变化时自动更新视图
current: '/'
})
}
③ 实现 creatRouteMa()
creatRouteMap () { // 创建路由地图
// 遍历所有的路由规则,把路由规则解析成键值对的形式存储到 routeMap 中
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
})
}
④ 实现 router-link 和 router-view 组件
// Vue 的构建版本 包括 运行时版本 和 完整版本
// 运行时版本: 不支持 template 模板,需要打包的时候提前编译
// 完整版本: 包含运行时和编译器,体积比运行时版本大 10KB 左右,程序运行的时候把模板转换成 render 函数
initComponents () { // 创建 router-link 和 router-view 两个组件
_Vue.component('router-link', {
props: {
to: String
},
// 1-完整版本的 Vue -- 需要在vue.config.js 中 配置 runtimeCompiler: true,自动将模板解析为render函数
// template: "<a :href='to'><slot></slot></a>"
// 2-运行时的 Vue -- 直接使用render函数
render (h) {
// h 函数接收3个参数,第一个是生成dom元素的标, 第二个是一些选项设置,第三个是内容
return h('a', {
attrs: {
href: this.to
},
on: {
click: this.handleClick
}
}, [this.$slots.default])
},
methods: {
handleClick(e) {
// pushState 接收三个参数,1-事件对象 2-网页标题 3-超链接跳入的地址
history.pushState({}, '', this.to)
this.$router.data.current = this.to
// 取消a标签的默认行为,防止页面刷新向服务器发起请求
e.preventDefault()
}
}
})
const that = this
_Vue.component('router-view', {
render (h) {
// 根据当前的路径找到对应的的组件, 注意 this 问题
const component = that.routeMap[that.data.current]
return h(component)
}
})
}
⑤ 实现 注册事件
当页面刷新,后退,前进,路径变化的时候,还存在一些问题
initEvents() {
// 路径变化的问题 重新获取当前路径并记录到 data 中的 current
window.addEventListener('hashchange', this.onHashChange.bind(this))
// 刷新按钮的问题
window.addEventListener('load', this.onHashChange.bind(this))
// 前进后退按钮的问题
window.addEventListener('popstate', this.onHashChange.bind(this))
}
⑥ 实现 init()
// 在混入的时候方便进行初始化
init() {
this.creatRouteMap()
this.initComponents()
this.initEvents()
}
注意:
vue-cli 创建的项目默认使用的是运行时版本的 Vue.js
如果想切换成带编译器版本的 Vue.js,需要修改 vue-cli 配置
项目根目录创建 vue.config.js 文件,添加 runtimeCompiler
module.exports = {
runtimeCompiler: true
}
6、具体源码
let _Vue = null
export default class VueRouter {
static install (Vue) { // 调用此方法的时候传入 Vue 构造函数
// 1、判断当前插件是否已经被安装
if (VueRouter.install.installed) return
VueRouter.install.installed = true
// 2、把Vue构造函数记录到全局变量
_Vue = Vue
// 3、把创建Vue实例时候传入的 router 对象注入到 Vue 实例上
// 混入,所有的 Vue 实例 和 组件 都会执行此方法
_Vue.mixin({
beforeCreate() {
if (this.$options.router) { // 只有 Vue 实例选项中才有此属性,组件没有
_Vue.prototype.$router = this.$options.router
this.$options.router.init()
}
}
})
}
constructor (options) { // 初始化 三个属性
this.options = options
this.routeMap = {}
this.data = _Vue.observable({ // data 属性是响应式的
current: '/'
})
}
init() {
this.creatRouteMap()
this.initComponents()
this.initEvents()
}
creatRouteMap () { // 创建路由地图
// 遍历所有的路由规则,把路由规则解析成键值对的形式存储到 routeMap 中
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
})
}
// Vue 的构建版本
// 运行时版本: 不支持 template 模板,需要打包的时候提前编译
// 完整版本: 包含运行时和编译器,体积比运行时版本大 10KB 左右,程序运行的时候把模板转换成 render 函数
initComponents () { // 创建 router-link 和 router-view 两个组件
_Vue.component('router-link', {
props: {
to: String
},
// 1-完整版本的 Vue -- 需要在vue.config.js 中 配置 runtimeCompiler: true,自动将模板解析为render函数
// template: "<a :href='to'><slot></slot></a>"
// 2-运行时的 Vue -- 直接使用render函数
render (h) {
// h 函数接收3个参数,第一个是生成dom元素的标, 第二个是一些选项设置,第三个是内容
return h('a', {
attrs: {
href: this.to
},
on: {
click: this.handleClick
}
}, [this.$slots.default])
},
methods: {
handleClick(e) {
// pushState 接收三个参数,1-事件对象 2-网页标题 3-超链接跳入的地址
history.pushState({}, '', this.to)
this.$router.data.current = this.to
// 取消a标签的默认行为,防止页面刷新向服务器发起请求
e.preventDefault()
}
}
})
const that = this
_Vue.component('router-view', {
render (h) {
// 根据当前的路径找到对应的的组件, 注意 this 问题
const component = that.routeMap[that.data.current]
return h(component)
}
})
}
initEvents() {
// 路径变化的问题 重新获取当前路径并记录到 data 中的 current
window.addEventListener('hashchange', this.onHashChange.bind(this))
// 刷新按钮的问题
window.addEventListener('load', this.onHashChange.bind(this))
// 前进后退按钮的问题
window.addEventListener('popstate', this.onHashChange.bind(this))
}
// 重新获取当前路径并记录到 data 中的 current
onHashChange() {
this.data.current = window.location.pathname || '/'
}
}