关于前端:SPA-路由三部曲之实践演练

8次阅读

共计 19287 个字符,预计需要花费 49 分钟才能阅读完成。

回顾

《SPA 路由三部曲之外围原理》的姊妹篇《SPA 路由三部曲之实际演练》终于要跟大家见面了,在开篇之前,咱们先来回顾一下,在上一篇中都理解到了哪些常识:前端路由风行的模式有两种 hash 模式和 history 模式,两者别离利用浏览器自有个性实现单页面导航。

  • hash 模式:window.location 或 a 标签扭转锚点值,window.hashchange() 监听锚点变动
  • history 模式:history.pushState()、history.repalceState() 定义指标路由,window.onpopstate() 监听浏览器操作导致的 URL 变动

那《SPA 路由三部曲之实际演练》会带来哪些内容呢,利用之前学到的外围原理,加上前端路由在 Vue 技术栈的应用形式,入手实现属于本人的前端路由管理器 myRouter。小编将应用原生 JS、Vue 别离实现 hash 模式路由与 history 模式路由。

原生 JS 实现路由

在基于技术栈 Vue 实现 myRouter 之前,先用原生 JS 小试牛刀。

实现 hash 路由

在应用原生 JS 实现 hash 路由,咱们须要先明确,触发路由导航的形式都有哪些:

  • 利用 a 标签,触发锚点扭转,监听 window.onhashchange() 进行路由导航
  • JS 动静扭转 hash 值,window.location.hash 赋值,进行路由导航

那咱们就一步步来,先来实现 HTML 的设计:

index.html

<body>
  <div>
      <h3>Hash 模式路由跳转 </h3>
      <ul class="tab">
          <!-- 定义路由 -->
          <li><a href="#/home"> a 标签点击跳转 home</a></li>
          <li><a href="#/about"> a 标签点击跳转 about</a></li>
      </ul>
      <!-- JS 动静扭转 hash 值,实现跳转 -->
      <div id="handleToCart"> JS 动静点击跳转 cart</div>  
      <!-- 渲染路由对应的 UI -->
      <div id="routeViewHash" class="routeView"></div> 
  </div>
</body>

接下来,将对 URL 的操作定义成 JSHashRouter 类

class JSHashRouter {constructor(routerview){this.routerView = routerview}
    init(){
        // 首次渲染如果不存在 hash 值,那么重定向到 #/, 若存在 hash 值,就渲染对应的 UI
        if(!location.hash){location.hash="#/"}else{this.routerView.innerHTML = '以后路由:'+ location.hash}
        // 监听 hash 值扭转
        window.addEventListener('hashchange', ()=>{this.routerView.innerHTML = '以后路由:'+ location.hash})
    }
    push(path){window.location.hash = path}
}

JSHashRouter 类本身定义了 routerView 属性,接管渲染路由 UI 的容器。JSHashRouter 类定义了 2 个办法:init() 和 push()

  • init()
    首先页面渲染时,不会触发 window.onhashchange(),依据以后 hash 值,渲染 routerView。监听 window.onhashchange() 事件,一旦事件触发,从新渲染 routerView。
  • push()
    在理论开发过程中,进行路由跳转须要应用 JS 动静设置。通过为 window.location.hash 设置值,实现路由跳转。这里须要留神,window.location.hash 扭转 hash 值,也会触发 window.onhashchange() 事件。

所以不论是 a 标签扭转 hash,还是 window.location.hash 扭转 hash,对立在 window.onhashchange() 事件中,从新渲染 routerView。

接下来只有 JSHashRouter 实例化,调用就实现了 hash 路由:

index.html

<script type="text/javascript" src="./hash/hash.js"></script>
<script>
window.onload = function () {let routerview = document.getElementById('routeViewHash')
    // HashRouter 实例化
    let hashRouter = new JSHashRouter(routerview)  
    hashRouter.init()
    // 点击 handleToCart,JS 动静扭转 hash 值
    var handleToCart = document.getElementById('handleToCart');  
    handleToCart.addEventListener('click', function(){hashRouter.push('/cart')
    }, false); 
}
</script>

ok,来看一下成果:

实现 history 路由

顺利实现了 hash 路由,依照雷同的思路实现 History 路由前,明确触发 history 路由跳转的形式有哪些:

  • 动静触发 pushState()、replaceState()
  • 拦挡 a 标签默认事件,检测 URL 变动,应用 pushState() 进行跳转。

History 模式触发路由跳转的形式与 Hash 模式稍有不同。通过《SPA 路由三部曲之外围原理》咱们理解到,pushState()、replaceState()、a 标签扭转 URL 都不会触发 window.onpopstate() 事件。那该怎么办,在理论开发过程中,咱们是须要在 HTML 中进行跳转的。好在能够拦挡 a 标签的点击事件,阻止 a 标签默认事件,检测 URL,应用 pushState() 进行跳转。

index.html

<div>
    <ul class="tab">
        <li><a href="/home"> 点击跳转 /home</a></li>
        <li><a href="/about"> 点击跳转 /about</a></li>
    </ul>
    <!-- JS 动静扭转 URL 值,实现跳转 -->
    <div id="handlePush" class="btn"> JS 动静 pushState 跳转 list</div>
    <div id="handleReplace" class="btn"> JS 动静 replaceState 跳转 item</div>
    <!-- 渲染路由对应的 UI -->
    <div id="routeViewHistory" class="routeView"></div>
</div>

history 路由与 hash 路由定义的 html 模板大同小异,区别在于 a 标签 href 属性的定义形式不同,history 模式下的 URL 中是不存在 # 号的。接下来,到了定义 JSHistoryRouter 类的时候。

class JSHistoryRouter {constructor(routerview){this.routerView = routerview}
    init(){
        let that = this
        let linkList = document.querySelectorAll('a[href]')
        linkList.forEach(el => el.addEventListener('click', function (e) {e.preventDefault()  // 阻止 <a> 默认跳转事件
            history.pushState(null, '', el.getAttribute('href')) // 获取 URL,跳转
            that.routerView.innerHTML = '以后路由:' + location.pathname
        }))
        // 监听 URL 扭转
        window.addEventListener('popstate', ()=>{this.routerView.innerHTML = '以后路由:' + location.pathname})
    }
    push(path){history.pushState(null, '', path)
        this.routerView.innerHTML = '以后路由:' + path
    }
    replace(path){history.replaceState(null, '', path)
        this.routerView.innerHTML = '以后路由:' + path
    }
}

与 JSHashRouter 类一样,JSHistoryRouter 类本身定义了 routerView 属性,接管渲染路由 UI 的容器。JSHistoryRouter 类同样定义了三个办法:init()、push()、replace()。

  • init() 中次要做了两件事:

    1. 定义 window.onpopstate() 事件,用于监听 history.go()、history.back()、history.forword() 事件。
    2. 点击 a 标签,浏览器 URL 默认更新为 href 赋值的 url,应用 e.preventDefault() 阻止默认事件。将 href 属性赋值的 url 通过 pushState() 更新浏览器 URL,从新渲染 routerView。
  • push() 函数,通过 history.pushState() 新增浏览器 URL,从新渲染 routerView。
  • replace() 函数,通过 history.replaceState() 替换浏览器 URL,从新渲染 routerView。

ok,当初只有实例化 JSHistoryRouter 类,调用办法就实现了 history 路由!
index.html

<script type="text/javascript" src="./history/history.js"></script>
<script>
window.onload = function () {let routerview = document.getElementById('routeViewHistory')
    let historyRouter = new JSHistoryRouter(routerview)  // HistoryRouter 实例化
    historyRouter.init()
    // JS 动静扭转 URL 值
    document.getElementById('handlePush').addEventListener('click', function(){historyRouter.push('/list')
    }, false); 
    document.getElementById('handleReplace').addEventListener('click', function(){historyRouter.replace('/item')
    }, false); 
}
</script>

来来来,展现成果啦!

仔细的同学应该发现了,在 HashRouter 类的 init() 办法中,解决了页面首次渲染的状况,但在 HistoryRouter 类中却没有。这是为什么呢?Hash 模式下,扭转的是 URL 的 hash 值,浏览器申请是不携带 hash 值的,所以由 http://localhost:8080/#/home 到 http://localhost:8080/#/cart,浏览器是不发送申请。History 模式下,扭转的则是 URL 除锚点外的其余局部,所以由 http://localhost:8080/cart 到 http://localhost:8080/home,浏览器会从新发送申请。这也就解释了 vue-router 的 Hash 模式,后端不须要做任何解决,而 History 模式后端须要将域名下匹配不到的动态资源,重定向到同一个 index.html 页面。

好了,应用 JS 原生实现了根底的路由跳转,是不是更加期待基于 Vue 技术栈实现本人的路由了呢,马上安顿!

源码直通车:https://github.com/yangxiaolu1993/jsForRouter

基于 Vue 实现路由

vue-router 与 react-router 是当初风行的单页面路由管理器。尽管二者的应用形式有些差异,但外围原理是大同小异的,只有把握了其中一个,另一个也就不难理解了。咱们参照 vue-router 的应用形式与性能,实现基于 Vue 技术栈的 myRouter。首先通过 vue-cli 搭建简略的 Vue 开发环境,搭建过程在这里就不赘述了,调整之后的目录如下:

├── config                   
├── index.html               
├── package.json
├── src
│   ├── App.vue
│   ├── main.js
│   ├── assets
│   │   ├── css
│   │   ├── image
│   │   └── js
│   ├── components
│   ├── plugins                  // 插件
│   │   └── myRouter             
│   │       ├── index.js         // MyRouter 类
│   │       └── install.js       // MyRouter 对象的 install 函数
│   ├── router
│   │   └── myRouter.js
│   ├── util
│   └── view
│       └── home.vue
├── static
└── vue.config.js

Vue Router 的实质

在实现 myRouter 之前,咱们先来回顾一下 Vue 中是如何引入 Vue Router 的。

  1. 通过 NPM 装置 vue-router 依赖包

    import VueRouter from vue-router
  2. 定义路由变量 routes,每个路由映射一个组件

    const routes = [{ path: '/foo', component: { template: '<div>foo</div>'} }, // 能够通过 import 等形式引入
        {path: '/bar', component: { template: '<div>bar</div>'} }
    ]
  3. router 实例化,将路由变量 routes 作为配置项

    const router = new VueRouter({
        mode:'history',
        routes // (缩写) 相当于 routes: routes
    })
  4. 通过 Vue.use(router) 使得 vue 我的项目中的每个组件都能够领有 router 实例与以后路由信息
  5. 将 router 挂载在根实例上。

    export default new Vue({
        el: '#app',
        router
    })

通过这五步,就顺利完成了路由的配置工作,在任何组件内能够通过 this.&dollar;router 取得 router 实例,也能够通过 this.&dollar;route 取得以后路由信息。在配置路由的过程中,咱们取得了哪些信息呢?

  • router 实例是通过 new VueRouter({…}) 创立的,也就是说,咱们引入的 Vue Router 其实就是 VueRouter 类。
  • router 实例化的配置项是一个对象,也就是,VueRouter 类的 contractor 办法接管一个对象参数,routes、mode 是该对象的属性。
  • 通过 Vue.use(…) 为每个组件注入 router 实例,而 Vue.use() 其实是执行对象的 install 办法。

通过这三点信息,咱们基本上能够将 MyRouter 的根本结构设计进去:

class MyRouter{constructor(options){this.routes = options.routes || []
        this.mode = options.mode || 'hash' // 模式 hash || history
        ......
    }
}
MyRouter.install = function(Vue,options){......}
export default MyRouter

将我的项目中 Vue Router 替换成定义好的 MyRouter,运行我的项目,页面空白,没有报错。

依照本人的开发习惯,将 MyRouter 的根本构造拆分成两个 js 文件 install.js 与 index.js。install.js 文件用于实现 install 办法,index.js 用于实现 MyRouter 类。

Vue.use()

为了能更好的了解 Vue Router 的实现原理,简略理解一下 Vue.use() 在定义插件时,都能够做哪些事件。如果你曾经把握了 Vue.use() 的应用,能够间接跳过此大节。

Vue 官网形容:如果插件是一个对象,必须提供 install 办法。如果插件是一个函数,它会被作为 install 办法。install 办法调用时,会将 Vue、vue 实例化时传入的选项 options 作为参数传入,利用传入的 Vue 咱们便能够定义一些全局的变量、办法、组件等。

  • Vue.component() 全局组件注册

    MyRouter.install = function(Vue,options){Vue.component("my-router-view", {template: '<div> 渲染路由 UI</div>'})
    }

    我的项目中的任何组件都能够间接应用 <my-router-view>,不须要引入。

  • Vue.mixin() 全局注册一个混入

    MyRouter.install = function(Vue,options){
        Vue.mixin({beforeCreate(){
                Object.defineProperty(this,'$location',{get(){return  window.location}
                })
            }
        })
    }

    应用全局混入,它将影响每个之后独自创立的 vue 实例。通过 Object.defineProperty() 为注入的 vue 实例,增加新的属性 &dollar;location,运行我的项目,在每一个 vue 实例(组件)中,都能够通过 this.&dollar;location 取得 location 对象。

自定义插件中定义的全局组件、办法、过滤器、解决自定义选项注入逻辑等都是在 install 办法中实现的。

&dollar;myRouter 与 &dollar;myRoute

我的项目中应用了 vue-router 插件后,在每个 vue 实例中都会蕴含两个对象 &dollar;router 和 &dollar;route。

  • &dollar;router 是 Vue Router 的实例化对象,是全局对象,蕴含了所有 route 对象。
  • &dollar;route 是以后路由对象,每一个路由都是一个 route 对象,是部分对象。

&dollar;myRouter

在注册 vue-router 时,最初一步是将 router 挂载在根实例上,在实例化根组件的时候,将 router 作为其参数的一部分。也就是目前只有根组件有 router 实例,其余子组件都没有。问题来了,如何将 router 实例放到每个组件呢?

通过之前对 Vue.use() 的根本理解,咱们晓得:Vue.mixin() 能够全局注册一个混入,之后的每个组件都会执行。混入对象的选项将被“混合”进入该组件自身的选项,也就是每个组件在实例化时,install.js 中 Vue.mixin() 内定义的办法,会与组件中的办法合并。利用这个个性,就能够将 router 实例传递到每一个子组件。先来看一下代码:

install.js

MyRouter.install = function(Vue,options){
    Vue.mixin({beforeCreate(){if (this.$options && this.$options.myRouter){  // 根组件
                this._myRouter = this.$options.myRouter;
            }else {this._myRouter= this.$parent && this.$parent._myRouter // 子组件}
            // 以后实例增加 $router 实例
            Object.defineProperty(this,'$myRouter',{get(){return this._myRouter}
            })
        }
    })  
}

上述代码中,是在组件 beforeCreate() 阶段混入的路由信息,是有什么非凡的含意吗?在 Vue 初始化 beforeCreate 阶段,会将 Vue 之前定义的原型办法 (Vue.prototype)、全局 API 属性、全局 Vue.mixin() 内的参数,合并成一个新的 options,并挂载到 &dollar;options 上。在根组件上,&dollar;options 能够取得到 myRouter 对象。this 指向的是以后 Vue 实例,即根实例,myRouter 对象通过自定义的 _myRouter 变量,挂载到根实例上。子组件在初始化 beforeCreate 阶段,除了合并 install 办法中定义的 Vue.mixin() 的内容外,还会将父组件的参数进行合并,比方父组件定义在子组件上的 props,当然还包含在根组件上自定义的 _myRouter 属性。

不晓得大家有没有思考一个问题,当晓得组件为子组件时,为什么就能够间接拿父组件的 _myRouter 对象呢?想要解释这个问题,咱们先来思考一下,父组件与子组件生命周期的执行程序:

父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 create -> 子 beforeMount -> 子 mounted -> 父 mounted

问题的答案是不是很分明了,子组件 beforeCreate 执行时,父组件 beforeCreate 曾经执行完了,_myRouter 曾经挂载到父组件的实例上了。

最初,咱们通过 Object.defineProperty 将 &dollar;myRouter 挂载到组件实例上。

&dollar;myRoute

&dollar;myRoute 用于存储以后门路。参照 vue-router 的 &dollar;route,&dollar;myRoute 的属性包含:path、name、hash、meta、fullPath、query、params,那如何获取这些值呢?不论在哪种路由模式下,MyRouter 类必定会蕴含通过 URL 与配置的路由表做匹配,取得到指标路由信息的逻辑解决,&dollar;myRouter 又能够拿到 MyRouter 类的所有信息,是不是很完满。在 MyRouter 类中定义 current 属性,用于存储指标路由信息,current 会依据路由的扭转而从新赋值,也就保障了 &dollar;myRouter 中始终指标路由的信息。

install.js

MyRouter.install = function(Vue,options){
    Vue.mixin({beforeCreate(){
            ......
            // 为以后实例增加 $route 属性
            Object.defineProperty(this,'$myRoute',{get(){return this._myRouter.current}
            })
        }
    })  
}

current 属性的赋值过程会在 MyRouter 类实现时解说。

MyRouterLink 组件与 MyRouterView 组件

好了,回到咱们之前的话题,咱们通过回顾 Vue Router 的引入,实现了 MyRouter 类的根本构造。接下来,咱们在看看 Vue Router 是如何应用的。

Vue Router 定义了两个组件:<router-link>、<router-view>

  • <router-link> 用来路由导航,默认会被渲染成 a 标签,通过传入 to 属性定义指标路由
  • <router-view> 用来渲染匹配到的路由组件
<div id="app">
  <p>
    <router-link to="/foo">Go to Foo</router-link>
    <router-link to="/bar">Go to Bar</router-link>
  </p>
  <router-view></router-view>
</div>

仿照 Vue Router,在 myRouter 中同样增加两个全局组件 <my-router-view>、<my-router-link>。通过上面对 Vue.use() 定义插件的介绍,置信大家曾经晓得该如何在插件中定义组件了,那就先来定义 <my-router-link> 组件吧。

<router-link> 默认会被渲染成 a 标签,<router-link to=”/foo”>Go to Foo</router-link> 在最终在浏览器中渲染的后果是 <a href=”/foo”>Go to Foo</a>。当然,能够通过 <router-link> 的 tag prop 指定标签类型。小伙伴们有没有发现问题,依照惯例的定义组件形式,在 <my-router-link> 组件中放的是 a 标签,<solt/> 代替模板内容:

<template>
  <a :href="to"><slot/></a>
</template>
<script>
export default {
  name: 'MyRouterLink',
  props:['to'],
  data () {return {}}
}
</script>

咱们该如何定义 tag prop 呢?如果路由导航的触发条件不是 click,是 mouseover,又如何定义呢?还记得 Vue 中提供的 render 渲染函数吗,render 函数联合数据生成 Virtual DOM 树、Diff 算法和 Patch 后生成新的组件。<my-router-link> 就应用 render 函数定义。

link.js

export default {
    name: 'MyRouterLink',
    functional: true,
    props: {
        to: {   // 指标导航
            type: [String, Object],
            required: true
        },
        tag:{   // 定义导航标签
            type: String,
            default: 'a'
        },
        event: {  // 触发事件
            type: String,
            default: 'click'
        }
    },
    render: (createElement, {props,parent}) => {let toRoute = parent.$myRoute.mode == 'hash'?`#/${props.to}`:`/${props.to}`
        return createElement(props.tag,{
            attrs: {href: toRoute},
            on:{[event]:function(){}
            }
        },context.children)
    }
};

ok,援用组件来看一下成果!

App.vue

<div class="footer">
    <my-router-link to="home" tag="p"> 首页 </my-router-link>
    <my-router-link to='classify'> 分类 </my-router-link>
</div>


失常渲染,没有报错,应用 p 标签胜利渲染,但问题也就接踵而至。<my-router-link> 默认渲染成 a 标签,history 路由模式下,a 标签默认的跳转事件,不仅会跳出以后的 document,还不能触发 popstate 事件。如果应用的是自定义标签,触发导航的时候,须要用 pushState 或 window.location.hash 更新 URL,难道这里还要写 if 判断?No,为何不阻止 a 标签的默认事件呢,这样一来,a 标签与自定义标签就没有差异了,导航标签都通过 pushState 或 window.location.hash 进行路由导航。

link.js

export default {
    ......
    render: (createElement, {props,parent,children}) => {let toRoute = parent.$myRoute.mode == 'hash'?`#/${props.to}`:`/${props.to}`
        let on = {'click':guardEvent} 
        on[props.event] = e=>{guardEvent(e)  // 阻止导航标签的默认事件
            parent.$myRouter.push(props.to)   // props.to 的值传到 router.push()}
        return createElement(props.tag,{attrs: { href: toRoute},
            on,
        },children)
    }
};
function guardEvent(e){if (e.preventDefault) {e.preventDefault()
    }
}

太不容易了,单实现 MyRouterLink 组件就死了有数的脑细胞。接下来,让大脑放松一下!依照 MyRouterLink 组件的思路,实现 MyRouterView 组件。MyRouterView 的性能绝对简略,将以后路由匹配到的组件进行渲染就行。还记得下面提到的 &dollar;myRoute 对象吗,将以后路由信息注入到了每个 vue 实例中,在 &dollar;myRoute 对象上新增 component 属性,存储路由对应的组件。其实,在 &dollar;route 的 matched 数组中,同样记录着组件信息,包含嵌套路由组件、动静路由匹配 regx 等等,小编只是将其简化。

view.js

export default {
    ......
    render: (createElement, {props, children, parent, data}) => {
        let temp = parent.$myRoute && parent.$myRoute.component?parent.$myRoute.component.default:''
        return createElement(temp)
    }
}

install.js

import MyRouterView from './view'
MyRouter.install = function(Vue,options){Vue.component(MyRouterView.name, MyRouterView)
}

又到了激动人心的时刻了,增加上 <my-router-view>,看看能不能如愿以偿!

App.vue

<template>
  <div id="app">
    <my-router-view/>
    <div class="footer">
        <my-router-link to="home" tag="p"> 首页 </my-router-link>
        <my-router-link to='classify'> 分类 </my-router-link>
        <my-router-link to='cart'> 购物车 </my-router-link>
    </div>
  </div>
</template>

赞,MyRouter 插件已将初见雏形了,太不容易了!做好筹备,要害内容来喽~~

外围类 MyRouter

终于到了欠缺 MyRouter 类了,在欠缺之前,咱们先来确定一下,MyRouter 类须要做哪些事件。

  1. 接管 MyRouter 实例化时传入的选项 options,数组类型的路由表 routes、代表以后路由模式的 mode
  2. vue-router 应用时,不仅能够应用 path 匹配,还能够通过 name 匹配,路由表 routes 不不便路由匹配,将路由表转换成 key:value 的模式,key 为路由表 routes 配置的 path 或 name
  3. 为了辨别路由的两种模式,创立 HashRouter 类、HistoryRouter 类,合称为 Router 类,新增 history 属性存储实例化的 Router 类
  4. 定义 current 属性,存储指标路由信息,通过 &dollar;myRoute 挂载到每一个实例上
  5. 定义 push()、replace()、go() 办法

MyRouter 类做的事件还是挺多的,对于程序猿来说,只有逻辑分明,再多都不怕。那就一点一点的来实现吧!先来将 MyRouter 类的根本构造搭起来。

index.js

class MyRouter {constructor(options){this.routes = options.routes || [] // 路由表
        this.mode = options.mode || 'hash' // 模式 hash || history
        this.routesMap = Utils.createMap(this.routes) // 路由表装换成 key:value 模式  
        this.history = null  // 存储实例化 HashRouter 或 HistoryRouter
        this.current= {
            name: '',
            meta: {},
            path: '/',
            hash: '',
            query:{},
            params: {},
            fullPath: '',
            component:null
        } // 记录以后路由
        // 依据路由模式,实例化 HashRouter 类、HistoryRouter 类
        switch (options.mode) {
            case 'hash':
                this.history = new HashRouter(this)
            case 'history':
                this.history = new HistoryRouter(this)
            default:
                this.history = new HashRouter(this)
        }
    }
    init(){this.history.init()
    }
    push(params){this.history.push(params)
    }
    replace(params){this.history.replace(params)
    }
    go(n){this.history.go(n)
    }
}
export default MyRouter

MyRouter 类初始化的时候,咱们实例化的 Router 类放到了 history 属性中,在应用 MyRouter 类的 init()、push()、replace()、go() 办法,就是在调用 HashRouter 类或 HistoryRouter 类的办法。所以在 HashRouter 类与 HistoryRouter 类中同样须要蕴含这 4 个办法。

Utils.createMap(routes)

createMap(routes){let nameMap = {} // 以每个路由的名称创立 key value 对象
    let pathMap = {} // 以每个路由的门路创立 key value 对象
    routes.forEach((route)=>{         
        let record = {
            path:route.path || '/',
            component:route.component,
            meta:route.meta || {},
            name:route.name || ''
        }
        if(route.name){nameMap[route.name] = record
        }
        if(route.path){pathMap[route.path] = record
        }
    })
    return {
        nameMap,
        pathMap
    }
}

将路由表 routes 中的每一个路由对象,重新组合,别离调整为以 route.path、route.name 为关键字,value 值为 record 对象的模式。这里只是一个简略的解决,并没有思考嵌套路由、父组件路由、重定向、props 等状况。

咱们在回过头来看看之前说的 MyRouter 类须要做的 5 件事件,当初还须要实现的就是定义 HashRouter 类与 HistoryRouter 类,并在其中为 current 赋值。废话不多说,开整!

HashRouter 类

顾名思义,HashRouter 类用来实现 Hash 模式下的路由的跳转。通过定义 MyRouter 类,能够明确 HashRouter 类须要定义 4 个函数。同时 HashRouter 类为 MyRouter 类中的 current 属性赋值,须要接管 MyRouter 类的参数。那么,HashRouter 类的根本框架就进去了。

hash.js

export default class HashRouter {constructor(router){this.router = router // 存储 MyRouter 对象}
    init(){}
    push(params){}
    replace(params){}
    go(n){}}

实现 hash 模式路由跳转的要害就是监听 URL Hash 值的变动。还记得之前原生 JS 实现的 Hash 路由吗,原理是截然不同的,代码间接拿来用都能够。

export default class HashRouter {
    ......
    init(){this.createRoute()   // 页面首次加载时,判断以后路由 
        window.addEventListener('hashchange', this.handleHashChange.bind(this))  // 监听 hashchange
    }
    handleHashChange(){this.createRoute()
    }
    // 更新以后路由 current
    createRoute(){let path = location.hash.slice(1)  
        let route = this.router.routesMap.pathMap[path] 
        // 更新以后路由
        this.router.current = {
            name: route.name || '',
            meta: route.meta || {},
            path: route.path || '/',
            hash: route.hash || '',
            query:location.query || {},
            params: location.params || {},
            fullPath: location.href,
            component: route.component
        }  
    }
    ......
}

HashRouter 类与 JSHashRouter 类实现思路是一样的,稍有不同的是对 UI 渲染的形式。HashRouter 类是通过 <my-router-view> 渲染组件的。

HashRouter 类中的 createRoute() 获取 URL 的 hash 值,即 path 值,通过 MyRouter 类中的 routesMap.pathMap 对象,获取到以后路由匹配的路由配置选项,将路由配置选项合并到 myRouter.current 对象中,myRouter.current 通过 &dollar;myRoute 挂载到了每个实例中。也就是说,&dollar;myRoute.component 就是咱们匹配到的路由组件,<my-router-view> 也是通过它确定要渲染的组件。

代码实现到这里,应该能够实现根本的跳转了,来看一下成果,验证一下!

页面初始渲染显示失常,点击底部导航也能失常跳转,然而为什么页面不更新呢?尽管咱们通过 myRouter.current 将 hash 值变动与 <my-router-view> 建设了分割,但没有对 myRouter.current 进行双向绑定,说白了,<my-router-view> 并不知道 myRouter.current 的产生了扭转。难道要实现双向绑定,当然不必!小编给大家安利一个 Vue 暗藏的 API:Vue.util.defineReactive(),理解过 Vue 源码的童鞋应该晓得这个 API,用于定义一个对象的响应属性。那就简略了,在 MyRouter 插件初始化的时候,利用 Vue.util.defineReactive(),使 myRouter.current 失去监听。

install.js

MyRouter.install = function(Vue,options){
    Vue.mixin({beforeCreate(){
            ......
            Object.defineProperty(this,'$myRoute',{ ....})
            // 新增代码 利用 Vue defineReactive 监听以后路由的变动
            Vue.util.defineReactive(this._myRouter,'current')
        }
    })
}

当初来看看,<my-router-view> 的内容有没有更新。

太不容易了,终于失去咱们想要的成果了。HashRouter 类的 push()、replace()、go() 办法,间接 copy JSHashRouter 类的代码就实现了。

export default class HashRouter {
    ......
    push(params){window.location.href = this.getUrl(params)
    }
    replace(params){window.location.replace(this.getUrl(params))
    }
    go(n){window.history.go(n)
    }
    // 获取以后 URL 
    getUrl(path){let path = ''if(Utils.getProperty(params) =='string'){path = params} else if(params.name || params.path){path = params.name?params.name:params.path}
        const fullPath = window.location.href
        const pos = fullPath.indexOf('#')
        const p = pos > 0?fullPath.slice(0,pos):fullPath
        return `${p}#/${path}`
    }
}

参照 vue-router,动静导航办法能够是字符串,也能够是形容地址的对象,getUrl 函数解决 params 的各种状况。

HashRouter 类基本功能实现完了,是不是挺简略的,对 HistoryRouter 类的实现充斥了信念,那就趁热打铁,实现 HistoryRouter 类走起!!

çççHistoryRouter 类 **

HistoryRouter 类与 Hash 类一样,同样须要 push()、replace()、go() 动静设置导航,一个自有参数接管 MyRouter 类。惟一不同的就是解决监听 URL 变动的形式不一样。

history.js

export default class HistoryRouter {constructor(router){this.router = router}
    init(){window.addEventListener('popstate', ()=>{// 路由扭转})
    }
    push(params){}
    replace(params){}
    go(n){}}

history 路由模式导航是通过 history.pushState()、history.replaceState() 配合 window.onpopstate() 实现的。与 HashRouter 类一样,HistoryRouter 类的实现仍然能够将原生 JS 实现的 JSHistoryRouter 类的代码间接拿过去。

history.js

export default class HistoryRouter {constructor(router){this.router = router}
    init(){
        // 监听 popstate
        window.addEventListener('popstate', ()=>{this.createRoute(this.getLocation())  // 导航 UI 渲染
        })
    }
    push(params){history.pushState(null, '', params.path)
        this.createRoute(params)  // 导航 UI 渲染
    }
    replace(params){history.replaceState(null, '', params.path)
        this.createRoute(params)  // 导航 UI 渲染
    }
    go(n){window.history.go(n)}
    getLocation () {let path = decodeURI(window.location.pathname)
        return (path || '/') + window.location.search + window.location.hash
    }
}

因为 history.pushState 与 history.replaceState 对浏览器历史状态进行批改并不会触发 popstate,只有浏览器的后退、后退键才会触发 popstate 事件。所以在通过 history.pushState、history.replaceState 对历史记录进行批改、监听 popstate 时,都要依据以后 URL 进行导航 UI 渲染。除了进行 UI 渲染,还有十分重要的一点,对 router.current 的更新,只有将其更新,<my-router-view> 才会更新。

history.js

export default class HistoryRouter {
    ......
    createRoute(params){let route = {}
        if(params.name){route = this.router.routesMap.nameMap[params.name]
        }else{let path = Utils.getProperty(params) == 'String'?params:params.path
            route = this.router.routesMap.pathMap[path]
        }
        // 更新路由
        this.router.current = {
            name: route.name || '',
            meta: route.meta || {},
            path: route.path || '/',
            hash: route.hash || '',
            query:location.query || {},
            params: location.params || {},
            fullPath: location.href,
            component: route.component
        }  
    }
}

仔细的同学可能发现了问题,在原生 JS JSHistoryRouter 类中,将 a 标签的点击事件进行了革新,阻止了默认事件,应用 pushState 实现路由导航。HistoryRouter 类中为什么没有增加这个逻辑呢?还记得 <my-router-link> 的实现吗,其阻止了 a 标签的默认事件,对立应用 MyRouter 类的 push 办法,进行路由导航。

HistoryRouter 类的基本功能也实现完了,曾经急不可待想要验证代码的正确性了!

完满实现,有点小小的拜服本人!竟然呈现了能够参加编写 vue-router 的错觉。与 vue-router 相比,还有很多的性能没有实现,比方嵌套路由、路由导航守卫、过渡动画等等,myRouter 插件只是实现了简略的路由导航和页面渲染。往往一件事件最难的是第一步,myRouter 曾经开了一个不错的头,置信欠缺之后的性能也不是问题。感兴趣的小伙伴能够跟小编一起持续开发上来!

源码直通车:https://github.com/yangxiaolu1993/my-router

总结

vue-router 实现思路不难,源码逻辑却是相当的简单,MyRouter 相比 vue-router 尽管简化了很多,但整体思路是统一的。小编置信,MyRouter 的实现肯定能帮忙小伙伴更加疾速的把握 vue-router 的源码。小伙伴们在编写代码时,有没有跟小编一样的经验,须要不停地批改、欠缺之前的代码,哪怕过后想的很全面,也仍然会亲手把代码删除,单单是 <my-router-link> 组件的实现,小编就批改了不下五遍。过程尽管苦楚,但后果却是播种却满满,成就感爆棚。由衷拜服 vue-router 的开发者,不晓得他们是进行多少遍的批改,能力做到现在的境地!

欢送大家期待之后《SPA 路由三部曲》的最初一篇,在这篇文章中,小编将率领大家进入 vue-router 的源码世界。置信在理解了 vue-router 的实现思路后,大家就都能够真正实现本人的前端路由了。

正文完
 0