Vue-Router 原理实现

在前端快速发展的今天,如果不能时刻保持学习就会很快被淘汰。分享一下Vue-Router 原理实现的相关知识。每天进步一点点。

一、Vue-Router 基础知识

**1、Vue.use()方法:**接收一个参数,如果传入的参数是一个函数,直接调用这个函数;如果是一个对象,就调用这个对象的install方法。

2、 在初始化Vue实例的时候传入 router,Vue实例中会生成 r o u t e 和 route和 routerouter两个属性: r o u t e 中 存 储 了 当 前 的 路 由 规 则 , route 中存储了当前的路由规则, routerouter中存储了路由实例对象,可以调用一些和路由相关的方法。

**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 || '/'
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值