XingYun blog
  • JS基础

    • 图解js原型链
    • JS Event Loop
    • 对象的底层数据结构
    • 让你的JavaScript代码简单又高效
    • 函数参数按值传递
    • 判断数据类型
    • 浮点数精度问题和解决办法
    • 常用方法snippet
    • 实现Promise
    • 防抖和节流
    • 巧用sort排序
  • CSS && HTML

    • CSS也需要性能优化
    • class命名规范
    • em、px、rem、vh、vw 区别
    • CSS揭秘阅读笔记
  • 浏览器

    • 浏览器是如何渲染页面的
    • 重排和重绘
    • BOM浏览器对象模型
    • DOM事件
    • 浏览器存储
  • 数据结构

    • JS实现链表
    • JS实现栈与栈应用
    • JS实现常见排序
    • 哈夫曼编码
    • MD5算法
  • vue原理浅析

    • Vue虚拟dom与Diff算法
    • 前端打包文件的缓存机制
    • vue数组为什么不是响应式
    • v-for为什么不能用index做key
  • 前端工程化

    • 浏览器是如何渲染页面的
    • 前端打包需要gzip压缩吗
    • 前端打包文件的缓存机制
    • webpack loader和plugin
  • 轮子&&组件库

    • 实现水波浪进度球
  • 文字转语音mp3文件
  • 文件上传前后端实现
  • moment.js给定时间获取自然月、周的时间轴
  • 实现文件上传功能
  • 批量下载照片
  • leaflet改变坐标原点
  • 网络

    • 有了MAC地址 为什么还需要IP地址
    • 为什么IP地址老是变
    • 我们为什么需要IPV6
    • TCP与UDP
  • 计算机组成原理

    • ASCII、Unicode、UTF-8和UTF-16
  • VSCode

    • VSCode图片预览插件 Image preview
    • rsync:linux间的高效传输工具

XingYun

冲!
  • JS基础

    • 图解js原型链
    • JS Event Loop
    • 对象的底层数据结构
    • 让你的JavaScript代码简单又高效
    • 函数参数按值传递
    • 判断数据类型
    • 浮点数精度问题和解决办法
    • 常用方法snippet
    • 实现Promise
    • 防抖和节流
    • 巧用sort排序
  • CSS && HTML

    • CSS也需要性能优化
    • class命名规范
    • em、px、rem、vh、vw 区别
    • CSS揭秘阅读笔记
  • 浏览器

    • 浏览器是如何渲染页面的
    • 重排和重绘
    • BOM浏览器对象模型
    • DOM事件
    • 浏览器存储
  • 数据结构

    • JS实现链表
    • JS实现栈与栈应用
    • JS实现常见排序
    • 哈夫曼编码
    • MD5算法
  • vue原理浅析

    • Vue虚拟dom与Diff算法
    • 前端打包文件的缓存机制
    • vue数组为什么不是响应式
    • v-for为什么不能用index做key
  • 前端工程化

    • 浏览器是如何渲染页面的
    • 前端打包需要gzip压缩吗
    • 前端打包文件的缓存机制
    • webpack loader和plugin
  • 轮子&&组件库

    • 实现水波浪进度球
  • 文字转语音mp3文件
  • 文件上传前后端实现
  • moment.js给定时间获取自然月、周的时间轴
  • 实现文件上传功能
  • 批量下载照片
  • leaflet改变坐标原点
  • 网络

    • 有了MAC地址 为什么还需要IP地址
    • 为什么IP地址老是变
    • 我们为什么需要IPV6
    • TCP与UDP
  • 计算机组成原理

    • ASCII、Unicode、UTF-8和UTF-16
  • VSCode

    • VSCode图片预览插件 Image preview
    • rsync:linux间的高效传输工具
  • 3个提升Vue性能的写法
  • 重读Vue文档
  • moment.js给定时间获取自然月、周的时间轴
  • Vue虚拟dom与Diff算法
  • 深入响应式原理
  • Echart样例
  • 我的Vue指令库
  • 滚动到底部加载更多
  • 实现一键换肤
  • 手写Vue数据劫持
  • vue-router核心原理与手写实现
    • computed和watch
    • vue数组为什么不是响应式
    • v-for为什么不能用index做key
    • webpack loader和plugin
    • keep-alive组件原理
    • vue插槽进化
    • Vue多层嵌套组件
    • vue生命周期hook
    • vue监听dom元素的resize事件
    • 前端打包需要gzip压缩吗
    • 实现水波浪进度球
    • Vue
    XingYun
    2022-04-13
    目录

    vue-router核心原理与手写实现

    # 一、核心原理

    1.什么是前端路由?

    在 Web 前端单页应用 SPA(Single Page Application)中,路由描述的是 URL 与 UI 之间的映射关系,这种映射是单向的,即 URL 变化引起 UI 更新(无需刷新页面)。

    2.如何实现前端路由?

    要实现前端路由,需要解决两个核心:

    • 如何改变 URL 却不引起页面刷新?
    • 如何检测 URL 变化了?

    下面分别使用 hash 和 history 两种实现方式回答上面的两个核心问题。

    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 方法去触发该事件

    所以监听 URL 变化可以实现,只是没有 hashchange 那么方便。

    # JS 简单实现

    <!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>
        let routerView = routeView
        window.addEventListener('hashchange', () => {
          let hash = location.hash
          routerView.innerHTML = hash
        })
        window.addEventListener('DOMContentLoaded', () => {
          if (!location.hash) {
            //如果不存在hash值,那么重定向到#/
            location.hash = '/'
          } else {
            //如果存在hash值,那就渲染对应UI
            let hash = location.hash
            routerView.innerHTML = hash
          }
        })
      </script>
    </html>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32

    上面代码中 我们监听 hashchange 事件。一旦事件触发,就改变 routerView 的内容,若是在 vue 中,这改变的应当是 router-view 这个组件的内容

    同理 history 模式实现

    let routerView = routeView
    window.addEventListener('DOMContentLoaded', onLoad)
    window.addEventListener('popstate', () => {
      routerView.innerHTML = location.pathname
    })
    function onLoad() {
      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
        })
      )
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    两种模式都可以利用 history.go,back,forward 来触发 hashchange 事件

    # 二、vue-router 实现

    我们先看一下项目中是如何引入 VueRouter 的

    import VueRouter from 'vue-router'
    Vue.use(VueRouter)
    
    1
    2

    添加 VueRouter 后发生了什么变化?

    • 通过 Vue.use(VueRouter) 使得每个组件都可以访问 this.$router 并拥有 this.$route 实例
    • 增加了 router-view 与 router-link 两个组件

    然后我们会 new 一个 VueRouter 对象

    const router = new VueRouter({
      mode: 'hash',
      routes: [
        {
          path: '/page1',
          name: 'page1-1',
          component: 'page1.vue',
          children: [
            {
              path: 'page1-1',
              name: 'page1-1',
              component: 'page1-1.vue'
            }
          ]
        },
        {
          path: '/login',
          name: 'Login',
          component: 'page3.vue',
        }
      ]
    })
    
    // main.js中挂载
    new Vue({
      router
      render: (h) => h(App)
    }).$mount('#app')
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28

    按照上面的流程,我们先搭一下 VueRouter 类的框架

    class VueRouter {
       constructor(options) {
          his.mode = options.mode || "hash"
          this.routes = options.routes || []
        }
    
      install(Vue) {
        Vue.$router = ...
        Vue.$route = ...
        Vue.component('router-link', {})
        Vue.component('router-view', {})
      }
    }
    
    export default VueRouter
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    为了方便处理, 我把 routes 的数组类型处理为 map 类型

    class VueRouter {
      constructor(options) {
        this.routes = options.routes || []
        this.routesMap = this.createRoutesMap(this.routes)
      }
    
     function createRoutesMap(routes, prefix = '') {
      return routes.reduce((pre, current) => {
        pre[prefix + current.path] = current.component
        // 递归解析children
        if (current.children && current.children.length > 0) {
          pre = Object.assign(pre, createRoutesMap(current.children, prefix + current.path + '/')) // children的key为 父+ /
        }
        return pre
        }, {})
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    处理后我们的映射关系为

    {
      '/page1': 'page1.vue',
      '/page1/page1-1': 'page1-1.vue',
      '/login': 'page3.vue'
    }
    
    1
    2
    3
    4
    5

    有了映射表,我们来监听地址栏的 url 变化, 并映射到对应的页面

    class VueRouter {
      constructor(options) {
        this.mode = options.mode || 'hash'
        this.history = { current: null }
        this.init()
      }
    
      init() {
        if (this.mode === 'hash') {
          // 先判断用户打开时有没有hash值,没有的话跳转到 /
          location.hash ? '' : (location.hash = '/')
          window.addEventListener('load', () => {
            this.history.current = location.hash.slice(1)
          })
          window.addEventListener('hashchange', () => {
            this.history.current = location.hash.slice(1)
          })
        } else {
          location.pathname ? '' : (location.pathname = '/')
          window.addEventListener('load', () => {
            this.history.current = location.pathname
          })
          window.addEventListener('popstate', () => {
            this.history.current = location.pathname
          })
        }
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28

    现在我们拿到了当前路由的信息了, 可以实现$router 与 Vue.$route 了

    class VueRouter {
      install(Vue) {
        Object.defineProperty(Vue, '$router', {
          get() {
            return Vue.router
          }
        })
        Object.defineProperty(Vue, '$route', {
          get() {
            return Vue.router.history.current
          }
        })
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    现在我们已经保存了当前路径,我们可以根据当前路径从路由表中获取对应的组件进行渲染

    接下来定义 router-view 和 router-link 组件

    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])
      }
    })
    
    Vue.component('router-link', {
      props: {
        to: String
        // 也可以通过 name 方式, 此处省略
      },
      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)
      }
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    到这里 VueRouter 我们就全部实现了,看一下完整代码

    class VueRouter {
      constructor(options) {
        this.mode = options.mode || 'hash'
        this.routes = options.routes || []
        this.routesMap = this.createRoutesMap(this.routes)
        this.history = { current: null }
        this.init()
      }
    
      init() {
        if (this.mode === 'hash') {
          // 先判断用户打开时有没有hash值,没有的话跳转到 /
          location.hash ? '' : (location.hash = '/')
          window.addEventListener('load', () => {
            this.history.current = location.hash.slice(1)
          })
          window.addEventListener('hashchange', () => {
            this.history.current = location.hash.slice(1)
          })
        } else {
          location.pathname ? '' : (location.pathname = '/')
          window.addEventListener('load', () => {
            this.history.current = location.pathname
          })
          window.addEventListener('popstate', () => {
            this.history.current = location.pathname
          })
        }
      }
    
      install(Vue) {
        Object.defineProperty(Vue, '$router', {
          get() {
            return Vue.router
          }
        })
        Object.defineProperty(Vue, '$route', {
          get() {
            return Vue.router.history.current
          }
        })
        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])
          }
        })
    
        Vue.component('router-link', {
          props: {
            to: String
            // 也可以通过 name 方式, 此处省略
          },
          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)
          }
        })
      }
    
      createRoutesMap(routes, prefix = '') {
        return routes.reduce((pre, current) => {
          pre[prefix + current.path] = current.component
          // 递归解析children
          if (current.children && current.children.length > 0) {
            pre = Object.assign(
              pre,
              this.createRoutesMap(current.children, prefix + current.path + '/') // children的key为 父+ /
            )
          }
          return pre
        }, {})
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76

    以上代码和源码不一样但原理基本相同

    这里 URL 变化并不会引起视图的更新, 我们可以在 install 方法里用 Vue.util.defineReactive() 来监听 history

    install(Vue){
      Vue.util.defineReactive(this,"myRouter",this.history)
    }
    
    1
    2
    3

    这样视图就可以正常更新了

    # hash 模式和 history 模式的区别

    形式上:hash 模式 url 里面永远带着#号,开发当中默认使用这个模式。如果用户考虑 url 的规范那么就需要使用 history 模式,因为 history 模式没有#号,是个正常的 url,适合推广宣传;

    功能上:比如我们在开发 app 的时候有分享页面,那么这个分享出去的页面就是用 vue 或是 react 做的,咱们把这个页面分享到第三方的 app 里,有的 app 里面 url 是不允许带有#号的,所以要将#号去除那么就要使用 history 模式,但是使用 history 模式还有一个问题就是,在访问二级页面的时候,做刷新操作,会出现 404 错误,那么就需要和后端人配合,让他配置一下 apache 或是 nginx 的 url 重定向,重定向到 app 的首页路由上就 ok 了。

    使用场景

    一般场景下,hash 和 history 都可以,除非你更在意颜值,# 符号夹杂在 URL 里看起来确实有些突兀。

    如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。 Vue-router 里调用 history.pushState() 相比于直接修改 hash,存在以下优势:

    • pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,因此只能设置与当前 URL 同文档的 URL
    • pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中;而 hash 设置的新值必须与原来不一样才会触发动作将记录添加到栈中
    • pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中;而 hash 只可添加短字符串

    # 总结

    1. 路由就是 URL 与 UI 之间的映射关系,处理好映射关系就实现了路由
    2. 路由分为 hash 和 history两种模式, 通过监听相应的浏览器对象事件监测 URL 变化。
    3. 当 URL 发生变化,用新 URL 与处理好的路由表比对,找到对应的 vue 组件进行渲染
    4. 不在意 URL 内容时推荐使用 hash 模式,配置少,更省事, 需要标准的 URL 时使用 history,需注意配置 ng 防止白屏
    #Vue
    上次更新: 2023/04/05, 09:41:10
    手写Vue数据劫持
    computed和watch

    ← 手写Vue数据劫持 computed和watch→

    最近更新
    01
    JavaScript-test
    07-20
    02
    二维码的原理
    07-20
    03
    利用ChatGPT优化代码
    07-20
    更多文章>
    Theme by Vdoing | Copyright © 2021-2023 XingYun | MIT License
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式