VueRouter实现

前端路由

  • 如何改变 URL 却不引起页面刷新?

  • 如何检测 URL 变化了?

hash 实现
hash 是 URL 中 hash (#) 及后面的那部分,常用作锚点在页面内进行导航,改变 URL 中的 hash 部分不会引起页面刷新

通过 hashchange 事件监听 URL 的变化,改变 URL 的方式只有这几种:

  • 通过浏览器前进后退改变 URL
  • 通过<a>标签改变 URL
  • 通过window.location改变URL

history 实现
history 提供了 pushState 和 replaceState 两个方法,这两个方法改变 URL 的 path 部分不会引起页面刷新

history 提供类似 hashchange 事件的 popstate 事件,但 popstate 事件有些不同:

  • 通过浏览器前进后退改变 URL 时会触发 popstate 事件
  • 通过pushState/replaceState或<a>标签改变 URL 不会触发 popstate 事件。
  • 好在我们可以拦截 pushState/replaceState的调用和<a>标签的点击事件来检测 URL 变化
  • 通过js 调用history的back,go,forward方法课触发该事件

js实现基础前端路由
基于 hash 实现

<!DOCTYPE html>
<html lang="en">
<body>
<ul>
    <ul>
        <!-- 定义路由 -->
        <li><a href="#/home">home</a></li>
        <li><a href="#/about">about</a></li>

        <!-- 渲染路由对应的 UI -->
        <div id="routeView"></div>
    </ul>
</ul>
</body>
<script>
	//获取渲染路由对应的UI对象
    let routerView = routeView
	
	//监听hash改变,渲染对应内容
    window.addEventListener('hashchange', ()=>{
        let hash = location.hash;
        routerView.innerHTML = hash
    })
    
    //页面第一次加载完不会触发 hashchange,因而用load事件来监听hash值,再将视图渲染成对应的内容。
    window.addEventListener('DOMContentLoaded', ()=>{
        if(!location.hash){//如果不存在hash值,那么重定向到#/
            location.hash="/"
        }else{//如果存在hash值,那就渲染对应UI
            let hash = location.hash;
            routerView.innerHTML = hash
        }
    })
</script>
</html>

基于 history 实现

<!DOCTYPE html>
<html lang="en">
<body>
<ul>
    <ul>
        <li><a href='/home'>home</a></li>
        <li><a href='/about'>about</a></li>

        <div id="routeView"></div>
    </ul>
</ul>
</body>
<script>
	//获取渲染的ui对象
    let routerView = routeView
    
    window.addEventListener('DOMContentLoaded', onl oad)
    window.addEventListener('popstate', ()=>{
        routerView.innerHTML = location.pathname
    })
    
    //劫持a标签,通过history.pushState改变url,然后渲染对应内容
    function onl oad () {
        routerView.innerHTML = location.pathname
        var linkList = document.querySelectorAll('a[href]')
        linkList.forEach(el => el.addEventListener('click', function (e) {
            e.preventDefault()
            history.pushState(null, '', el.getAttribute('href'))
            routerView.innerHTML = location.pathname
        }))
    }
   	
   	//直接通过history.pushState改变url的劫持,查看history、hash路由文章

</script>
</html>

实现VueRouter
基本框架

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/home">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view/>
  </div>
</template>

==================================================================
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import About from "../views/About.vue"
Vue.use(VueRouter)
  const routes = [
  {
    path: '/home',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  }
]
const router = new VueRouter({
  mode:"history",
  routes
})
export default router

==========================================
<template>
  <div class="home">
    <h1>这是Home组件</h1>
  </div>
</template>
==========================================
<template>
  <div class="about">
    <h1>这是about组件</h1>
  </div>
</template>

  • 先 const router = new VueRouter({…}),再把router作为参数的一个属性值,new Vue({router})
  • 通过Vue.use(VueRouter) 使得每个组件都可以拥有store实例

Vue.use实现
查找插件是否已安装,安装退出,否则调用插件,具体插件Vue.install文章

VueRouter.install实现

let Vue = null;
class VueRouter{

}
VueRouter.install = function (v) {
    Vue = v;
};

export default VueRouter

插件中注册router-link、router-view全局组件

let Vue = null;
class VueRouter{

}
VueRouter.install = function (vm) {
    Vue = vm;

    //新增代码
    Vue.component('router-link',{
        render(h){
            return h('a',{},'首页')
        }
    })
    Vue.component('router-view',{
        render(h){
            return h('h1',{},'首页视图')
        }
    })
};

export default VueRouter

给每个组件添加$route和$router
目前只有根组件有这个router值,而其他组件是还没有的,所以我们需要让其他组件也拥有这个router。

let Vue = null;
class VueRouter{

}
VueRouter.install = function (v) {
    Vue = v;
    // 新增代码
    Vue.mixin({
        beforeCreate(){
            if (this.$options && this.$options.router){ // 如果是根组件
                this._root = this; //把当前实例挂载到_root上
                this._router = this.$options.router; //将VueRouter实例挂在_router上
            }else { //如果是子组件
                this._root= this.$parent && this.$parent._root	//将根实例挂在子组件的_root上
            }
        }
    })

    Vue.component('router-link',{
        render(h){
            return h('a',{},'首页')
        }
    })
    Vue.component('router-view',{
        render(h){
            return h('h1',{},'首页视图')
        }
    })
};

export default VueRouter

监听每一个实例调用$router方法

let Vue = null;
class VueRouter{

}
VueRouter.install = function (v) {
    Vue = v;
    
    Vue.mixin({
        beforeCreate(){
            if (this.$options && this.$options.router){ // 如果是根组件
                this._root = this; //把当前实例挂载到_root上
                this._router = this.$options.router;
            }else { //如果是子组件
                this._root= this.$parent && this.$parent._root	//将根实例挂在子组件的_root上
            }
            
            // 新增代码
            Object.defineProperty(this,'$router',{	//劫持调用当前实例的$router,指向根实例挂载的VueRouter实例
                get(){
                    return this._root._router
                }
            })
        }
    })

    Vue.component('router-link',{
        render(h){
            return h('a',{},'首页')
        }
    })
    Vue.component('router-view',{
        render(h){
            return h('h1',{},'首页视图')
        }
    })
};

export default VueRouter

完善VueRouter接受的路由配置参数

class VueRouter{
    constructor(options) {
        this.mode = options.mode || "hash" //判断路由模式
        this.routes = options.routes || [] //你传递的这个路由是一个数组表
        this.routesMap = this.createMap(this.routes) //将数组-对象映射成对象形式
    }
    createMap(routes){
        return routes.reduce((pre,current)=>{
            pre[current.path] = current.component
            return pre;
        },{})
    }
}

格式映射

const routes = [
  {
    path: '/home',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  }

值就是component
VueRouter实现

新增类管理路由属性

let Vue = null;
//新增代码
class HistoryRoute {
    constructor(){
        this.current = null
    }
}
class VueRouter{
    constructor(options) {
        this.mode = options.mode || "hash"
        this.routes = options.routes || [] //你传递的这个路由是一个数组表
        this.routesMap = this.createMap(this.routes)
        //新增代码
        this.history = new HistoryRoute();
        this.init()
    }

    createMap(routes){
        return routes.reduce((pre,current)=>{
            pre[current.path] = current.component
            return pre;
        },{})
    }
    
	//新增代码
    init(){
       if (this.mode === "hash"){	//判断路由模式
           // 先判断用户打开时有没有hash值,没有的话跳转到#/
           location.hash? '':location.hash = "/";
           window.addEventListener("load",()=>{
               this.history.current = location.hash.slice(1) //在HistoryRoute类中管理当前url的hash路径
           })
           window.addEventListener("hashchange",()=>{
               this.history.current = location.hash.slice(1)//在HistoryRoute类中管理当前url的hash路径
           })
       } else{
           location.pathname? '':location.pathname = "/";
           window.addEventListener('load',()=>{
               this.history.current = location.pathname		//在HistoryRoute类中管理当前url的history路径
           })
           window.addEventListener("popstate",()=>{
               this.history.current = location.pathname		//在HistoryRoute类中管理当前url的history路径
           })
       }
   }

}

通过上一步能管理路由的相关属性,实现$route的访问

VueRouter.install = function (v) {
    Vue = v;
    Vue.mixin({
        beforeCreate(){
            if (this.$options && this.$options.router){ // 如果是根组件
                this._root = this; //把当前实例挂载到_root上
                this._router = this.$options.router;
            }else { //如果是子组件
                this._root= this.$parent && this.$parent._root	//将根实例挂在子组件的_root上
            }
            Object.defineProperty(this,'$router',{	//劫持调用当前实例的$router,指向根实例挂载的VueRouter实例
                get(){
                    return this._root._router
                }
            });
            
            //新增代码
            Object.defineProperty(this,'$route',{	//劫持调用当前实例的$route,指向根实例挂载的VueRouter实例的$route
                get(){
                    return this._root._router.history.current	//返回路由当前路径相关信息
                }
            })
        }
    })
    Vue.component('router-link',{
        render(h){
            return h('a',{},'首页')
        }
    })
    Vue.component('router-view',{
        render(h){
            return h('h1',{},'首页视图')
        }
    })
};

完善router-view组件,根据上一步实现的管理当前路由的路径,渲染对应组件

Vue.component('router-view',{
    render(h){
        let current = this._self._root._router.history.current	//获取路径
        let routeMap = this._self._root._router.routesMap;		//获取映射对象
        return h(routeMap[current])				//根据路径渲染对应组件
    }
})

因为router-view是一个组件,所以当路径改变时,需要重新渲染组件
组件中的this._self._root._router.history.current并不是响应式的,所以现在改变不能引起界面刷新

将_router.history进行响应式化

Vue.mixin({
    beforeCreate(){
        if (this.$options && this.$options.router){ // 如果是根组件
            this._root = this; //把当前实例挂载到_root上
            this._router = this.$options.router;
            
            //新增代码,通过提供的defineReactive将history对象变成响应式,使得改变对象时,对象收集的dep会通知router-view的组件依赖的wacther执行update()
            Vue.util.defineReactive(this,"",this._router.history)
            
        }else { //如果是子组件
            this._root= this.$parent && this.$parent._root
        }
        Object.defineProperty(this,'$router',{
            get(){
                return this._root._router
            }
        });
        Object.defineProperty(this,'$route',{
            get(){
                return this._root._router.history.current
            }
        })
    }
})

完善router-link组件,使得能够改变界面路由路径

<router-link to="/home">Home</router-link> 
<router-link to="/about">About</router-link>

================================================

Vue.component('router-link',{
    props:{
        to:String
    },
    render(h){
        let mode = this._self._root._router.mode;		//获取路由模式
        let to = mode === "hash"?"#"+this.to:this.to	//进行对应模式的路径拼接
        return h('a',{attrs:{href:to}},this.$slots.default)	//返回a标签
    }
})

至此通过点击router-link标签就可以实现url上路径的切换以及路由的更新
至于通过history.push等动态改变方式,通过在HistoryRoute类中,添加对应的方法使用history.pushState等方式实现,只要能使得history.current的路径和界面路径相同,则就能通过响应式改变router-view路由组件渲染对应的组件视图

上一篇:base::strftime


下一篇:vue-router详解