Vue Router 实现原理
Vue Router使用步骤
基础路由
接下来我会通过代码的形式为大家展现路由的基本用法
- 创建router对象,router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
// 路由组件
import index from '@/views/index'
// 组成插件
Vue.use(VueRouter)
// 路由规则
const routes = [
{
name: 'index',
path: '/',
component: index
}
]
// 路由对象
const router = new VueRouter({
routes
})
export default router
- 注册注册router对象,在main.js文件内操作
import router from './router'
new Vue({
render: h => h(App),
router
}).$mount('#app')
- 创建路由占位, 在需要用到路由的文件中,例如App.vue文件
<router-view></router-view>
- 最后一步创建链接
<router-link to="./">首页</router-link>
<router-link :to="{name: 'index'}">首页</router-link>
动态路由
<template>
<div>
// 方式一: 通过路由规则获取的数据
通过当前路由规则匹配获取: {{$route.params.id}}
// 方式二: 不仅可以通过路由,还有通过父组件传递(推荐)
通过开启 props 获取: {{id}}
</div>
</template>
<script>
export default {
name: 'Detail',
props: ['id']
}
</script>
const routes = [
{
name: 'Detail',
path: '/detail/:id', // :id是占位符,传入对应的id,
// 开启props会把url中的参数传递给组件,在组件内通过props来接收url参数
props: true,
// 路由懒加载,当需要访问该路由时加载组件
// webpackChunkName 这个参数在我 前端模块打包工具Webpack中有介绍(魔法注释,自定义打包名)
component: () => import(/* webpackChunkName: "detail" */ '../views/Detail.vue')
}
]
嵌套路由
const routes = [
{
name: 'Login',
path: '/login',
component: () => import('@/views/Login.vue')
},
// 嵌套路由
{
path: '/',
component: () => import('@/components/Layout.vue'), // 公共组件
children: [
{
name: 'index',
path: '', // 如果为空则访问父路径('/')时会默认将父组件合并加载该组件
component: () => import('@/views/Index.vue'), // 跟上边公共组件合并
},
{
name: 'detail',
path: 'detail/:id', // 相对路径
props: true,
component: () => import('@/view/Detail.vue'), // 跟上边公共组件合并
}
]
}
]
编程式导航
-
跳转路由但不记录历史
this.$router.replace('/login')
-
会记录历史跳转
this.$router.push('/')
-
会记录历史跳转切传递参数id为1
this.$router.push({name: 'Detail', params: { id: 1 }})
-
后退到上上一次页面,后退2次
this.$router.go(-2)
Hash模式和History模式
表现形式区别
-
Hash模式
https://xuanhe.com/#/detail?age=18
-
History模式
https://xuanhe.com/detail/18
原理的区别
- Hash模式是基于锚点,以及onhashchange事件,通过锚点的值作为路由地址,当地址发生变化后,发生onhashchange事件
- History模式是基于HTML5中的History API
- history.pushState() 不会向服务端发送请求,只会改变浏览器地址,并且将地址记录在历史记录中
- history.replaceState() 同上,但是不会保存记录
- 监听popstate事件监听浏览器的变化,但是调用history.pushState()和history.replaceState()不会触发该事件,只有在点击浏览器的回退按钮(或者在Javascript代码中调用history.back()或者history.forward()方法)才会出发
关于popstate的用法详情 popstate
注意: History模式需要服务器的支持
-
单页应用中,服务端不存在的地址会返回找不到该页面(刷新页面的操作会向服务器发送请求)
-
在服务器端要配置除了静态资源外都返回单页应用的index.html
-
node中使用一个插件处理history模式
connect-history-api-fallback
-
nginx中配置
server { listen 80; # 端口号 server_name localhost; # 域名(一般是线上地址) #charset koi8-r; #access_log logs/host.access.log main; location / { root html; # 根目录 index index.html index.htm; # 默认首页(从根目录里找) try_files $uri $uri/ /index.html; # $uri就是请求的路径,会去找($url这个文件, 或者是$uri目录下的index配置的页面,或者直接访问根文件的index.html,可以根据实际情况进行配置) } }
模拟实现Vue Router
回顾核心代码
// router/index.js
// 注册插件
// 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')
install 方法的创建
- 判断插件是否已经安装
- 把vue的构造函数记录的全局变量中
- 把创建vue实例时候传入的router对象注入的vue实例上
let _vue = null; 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实例都会有 _vue.mixin({ beforeCreate() { // 原型上挂在操作只需要执行一次,如果是组件不执行,如果是vue实例才执行 if (this.$options.router) { _vue.prototype.$router = this.$optins.router; } }, }); } }
构造函数实现
构造函数主要初始化三个参数.
- options
- data (响应式对象)
- routeMap (键值对形式存储,键是路由地址,值是对应的组件)
constructor(options) {
this.options = options;
this.data = _vue.observable({
current: '/'
});
// 解析routes解析路由规则,键值对形式
this.routeMap = {};
}
createRouteMap方法实现
- 把构造函数中选项内传递过来的routes转换成键值对的形式,存储到routeMap中
// 把构造函数中选项内传递过来的routes转换成键值对的形式,存储到routeMap中
createRouteMap () {
// 遍历routes把path和component存到routerMap对象中;
this.options.routes.forEach((route) => {
this.routeMap[route.path] = route.component
})
}
initComponents的router-link组件
// 创建组件
initComponents () {
// 创建router-link组件, 两个参数,1组件名, 2组件参数
_vue.component('router-link', {
props: {
to: String
},
template: '<a :href="to"><slot></slot></a>'
})
}
初步router插件已经完成,但是这样有一个问题,就是运行时版本不支持tempalte选项,必须使用render函数来创建组件,如果直接使用会报错
Vue 构建版本
- 运行时版: 不支持template模板,需要打包的时候提前编译
- 完整版: 包含运行时和编译器,体积比运行时版大10K左右,程序运行的时候把模板转换成render函数
要开启完整版只需要在vue.config.js
文件中设置runtimeCompiler: true
module.exports = {
runtimeCompiler: true
}
运行时版不带编译器,不支持template选项
单文件组件中的template在打包时编译成了render函数,所以单文件中的template在运行时版可以生效
- 不使用编译器来解决组件template的方法
将tempalte替换为render函数,initComponents
方法
initComponents的router-view组件和router-link组件的修改
- router-view相当于一个占位符,在该组件内部根据当前路由地址获取到对应的路由组件,渲染到router-view中
- 其他细节均在代码注释中体现
// 创建组件
initComponents () {
// 创建router-link组件, 两个参数,1组件名, 2组件参数
_vue.component('router-link', {
props: {
to: String
},
render (h) {
// 第一个参数选择器,第二个参数设置的属性, 第三个参数设置子元素
return h('a', {
// 添加dom对象的属性写在attrs中
attrs: {
href: this.to
},
// 注册dom对象的事件
on: {
click: this.clickHandler
}
}, [this.$slots.default]) // 获取子元素默认插槽
},
methods: {
// 事件处理函数
clickHandler(e) {
// 1.调用pushState方法改变浏览器的地址
// pushState接收三个参数,1.触发pushState传给事件对象的参数,2. 网页标题 3. 地址
history.pushState({}, '', this.to)
// 设置路由地址,之后才能根据这个地址加载对应的组件(这里的this是vue实例),因为这个属性是响应式的所以当这个属性值改变时会重新加载组件,(走下面的router-view组件)并时时渲染试图
this.$router.data.current = this.to // 下面方式使用self也行,这个也行
// 阻止默认行为
e.preventDefault()
}
}
// 运行时版本不支持template,只支持render函数
// template: '<a :href="to"><slot></slot></a>'
})
// 创建router-view组件
// 组件内部的this是vue实例,这里需要用到router实例
const self = this
_vue.component('router-view',{
// 利用h参数创建虚拟dom
render(h) {
// 获取当前路由地址(data.current)之后值是在router-link组件内设置的 找到对应的组件
const component = self.routeMap[self.data.current]
// h函数可以接收一个组件转换成一个虚拟dom
return h(component)
}
})
}
initEvent解决浏览器前进后退
现在基本上已经完成了一个基础的router了,但是会有一个小问题,在点击浏览器的前进后退按钮时地址栏发生变化,但是组件没有发生变化,没有重新加载地址对应的组件
- 解决浏览器前进后退渲染不更新的情况
// 解决浏览器前进后退不重新加载组件的方法
initEvent(){
window.addEventListener('popstate', () => {
// 将响应式的保存路径的值设置成 浏览器地址中路径部分的值即可
this.data.current = window.location.pathname
})
}
init方法封装
- 主要就是封装方法的调用
// 封装初始化的一些方法
init() {
// 调用保存地址和组件映射的方法
this.createRouteMap()
// 调用创建router-link和router-view组件的方法
this.initComponents()
// 调用解决浏览器前进后退不重新加载组件的方法
this.initEvent()
}
简版的完整代码
那么现在,我们就实现了一个简单的Vue Router,完整代码如下:
let _vue = null
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实例都会有
_vue.mixin({
beforeCreate () {
// 原型上挂在操作只需要执行一次,如果是组件不执行,如果是vue实例才执行
if (this.$options.router) {
_vue.prototype.$router = this.$options.router
}
}
})
}
constructor (options) {
this.options = options
this.data = _vue.observable({
current: '/'
})
// 解析routes解析路由规则,键值对形式
this.routeMap = {}
this.init()
}
// 封装初始化的一些方法
init () {
// 调用保存地址和组件映射的方法
this.createRouteMap()
// 调用创建router-link和router-view组件的方法
this.initComponents()
// 调用解决浏览器前进后退不重新加载组件的方法
this.initEvent()
}
// 把构造函数中选项内传递过来的routes转换成键值对的形式,存储到routeMap中
createRouteMap () {
// 遍历routes把path和component存到routerMap对象中;
this.options.routes.forEach((route) => {
this.routeMap[route.path] = route.component
})
}
// 创建组件
initComponents () {
// 创建router-link组件, 两个参数,1组件名, 2组件参数
_vue.component('router-link', {
props: {
to: String
},
render (h) {
// 第一个参数选择器,第二个参数设置的属性, 第三个参数设置子元素
return h('a', {
// 添加dom对象的属性写在attrs中
attrs: {
href: this.to
},
// 注册dom对象的事件
on: {
click: this.clickHandler
}
}, [this.$slots.default]) // 获取子元素默认插槽
},
methods: {
// 事件处理函数
clickHandler (e) {
// 1.调用pushState方法改变浏览器的地址,pushState接收三个参数
// 1.触发pushState传给事件对象的参数,2. 网页标题 3. 地址
history.pushState({}, '', this.to)
// 设置路由地址,之后才能根据这个地址加载对应的组件(这里的this是vue实例)
// 因为这个属性是响应式的所以当这个属性值改变时会重新加载组件,(走下面的router-view组件)并时时渲染试图
this.$router.data.current = this.to
e.preventDefault()
}
}
// 运行时版本不支持template,只支持render函数
// template: '<a :href="to"><slot></slot></a>'
})
// 创建router-view组件
const self = this
_vue.component('router-view', {
// 利用h参数创建虚拟dom
render (h) {
// 获取当前路由地址(data.current)之后值是在router-link组件内设置的 找到对应的组件
const component = self.routeMap[self.data.current]
// h函数可以接收一个组件转换成一个虚拟dom
return h(component)
}
})
}
// 解决浏览器前进后退不重新加载组件的方法
initEvent () {
window.addEventListener('popstate', () => {
// 将响应式的保存路径的值设置成浏览器地址中路径部分的值即可
this.data.current = window.location.pathname
})
}
}
希望大家对Router的理解更加深刻,开发中更加效率!