前端知识点——VueRouter

写在前面

笔记内容大多出自于拉勾教育大前端高薪训练营的教程,因此也许会和其他文章雷同较多,请担待。

hash <------> history

  • hash 兼容性好,但是链接有#
  • history 链接美观,没有多余的符号等,但只能 IE10 以后使用,因为其利用了 H5 的 historyAPI。而且需要配置服务器参数,例如 Nginx 需要在 location / {} 中加入 try_files $uri $uri/ /index.html;

hash

  • 将/#/后的内容作为路径地址,可以直接通过 location.url 来切换浏览器中的地址,如果只改变了/#/后面的内容,是不会向服务器请求这个地址的,但是会将这个地址记录到浏览器的访问历史中
  • 当 hash 改变后,需要监听 hash 的变换( hash 一旦改变就会触发 hashchange 事件 )并做相应的处理,可以在 hashchange 中获取到当前的路由地址,并且找到该路径对应的组件然后进行渲染

history

  • 通过 history.pushState 方法改变地址栏,并将这个地址记录到浏览器的访问历史中,但不会真正地跳转到指定的路径
  • 通过监听 popState 事件可以知道浏览器操作地址的变化,然后找到该路径对应的组件然后进行渲染

abstract

  • 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式

使用

  • Vue.use 注册 vue-router 插件
import VueRouter from 'vue-router'
Vue.use(VueRouter)
  • 实例化 VueRouter 并传入路由数组
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') // 懒加载动态导入
  }
]
const router = new VueRouter({
  mode: 'history', // 默认 hash,如果使用 history 需要指定 mode
  routes
})

原理

  • 使用混入给每一个 Vue 实例注册 router
  • Vue.observable() 响应式 router,在页面路由切换时可以自动渲染页面
  • Vue.component + 插槽 + render函数

VueRouter 构造函数

  • Vue.use() 调用插件其实就是调用模块的 install 方法,但由于 VueRouter是一个类,所以需要将 install 方法变成静态方法,这样就可以在不被实例化的时候也可以调用到
class VueRouter {
  options
  data
  routerMap
  constructor(options) {}
  static install(Vue) {}
  init() {}
  initEvent() {}
  createRouterMap() {}
  initComponents(Vue) {}
}

手写 VueRouter

constructor

  • 从*const router = new VueRouter({ mode, routes})*中可以得知,constructor需要接收一个以对象形式的参数,并且内部有 options,data,routeMap 三个成员变量
  • 从*new Vue({ router })*中可以得知,router 被放入了 Vue 实例的 $options 中
let _Vue = null
constructor(options) {
  this.options = options // 内含 mode 和 routes 信息
  this.routeMap = {} // 记录路由地址,以键值对存在,键是route.path,值是route.component
  this.data = _Vue.observable({
    current: '/'
  }) // 记录当前路由地址
  // 我们在调用插件的时候是先使用 Vue.use(VueRouter) 调用 install 方法,然后再生成 VueRouter 实例的,所以这时候 _Vue 已经获取到 Vue 的构造函数了
}
static install(Vue) {
  _Vue = Vue
}

install

let _Vue = null
static install(Vue) {
  // 判断是否已经加载过该插件
  if (VueRouter.install.installed) return
  VueRouter.install.installed = true
  // 挂载 Vue 构造函数到 全局
  _Vue = Vue
  // 将 Vue 实例的 $options.router 取出挂载到 Vue 构造函数的 $router 上,这样就可以在页面文件中通过 this.$router 来进行调用了
  _Vue.mixin({
    beforeCreate() {
      // 因为无法通过 this 找到 Vue 实例,所以通过使用 mixin 找到当前的 Vue 实例
      // 因为只有入口 Vue 实例身上挂在了 router 对象,所以要判断当前 Vue 实例是不是入口生成的 Vue实例
      if (this.$options.router) {
        _Vue.prototype.$router = this.$options.router
        // 将 routes 以键值对缓存到 routeMap 上
        // 创建 router-link 和 router-view 标签
      }
    }
  })
}

createRouteMap

  • 以键值对将路由缓存到 routeMap 上
createRouteMap() {
  this.options.routes.forEach(route => {
    this.routeMap[route.path] = route.component
  })
}

initComponents

  • 创建 router-link 和 router-view 标签
initComponents() {
  const self = this // 设置变量,让 this 指向 VueRouter 实例
  // 因为该方法在 install 内部调用,所以 _Vue 已经获取到 Vue 的构造函数了
  // 调用 Vue.component 全局 API 创建 router-link 标签,其本质就是一个 a 标签
  // <router-link to="/">Home</router-link> ------> to & slot
  // to 属性可以是对象,也可以是字符串,这里先只使用字符串编写
  // 因为 router-link 内部可以拥有 html-DOM 结构,所以需要使用到 slot,在 Vue.component 中使用 slot 就需要调用 Vue 实例的 $slot.default
  _Vue.component('router-link', {
    props: {
      to: String
    },
    render(h) {
      return h('a', {
        attrs: {
          href: this.to
        },
        on: {
          click: this.handleClick // 因为 a 标签会自动跳转,但这里并不需要,所以要改写 a 标签的点击方法
        }
      }, [this.$slots.default])
    },
    methods: {
      handleClick(e) {
        history.pushState({}, '', this.to) // 监听 history.pushState 方法在浏览器历史中记录路径
        self.data.current = this.to // 因为 current 是一个响应式数据,所以在它改变时会引起页面的重绘
        e.preventDefault() // 取消 a 标签的默认方法
      }
    }
  })
  _Vue.component('router-view', {
    render(h) {
      return h(self.routeMap[self.data.current])
    }
  })
}

initEvent

  • 当浏览器左上角的前进后退改变时存放在内存里的数据并不会改变,所以需要监听 popstate 方法
initEvent() {
  window.addEventListener('popstate', () => {
    this.data.current = window.location.pathname
  })
}

init

init() {
  this.createRouteMap()
  this.initComponents()
  this.initEvent()
}

VueRouter 简易构造函数

let _Vue = null
export default class VueRouter {
  constructor(options) {
    this.options = options
    this.routeMap = {}
    this.data = _Vue.observable({
      current: '/'
    })
  }
  static install(Vue) {
    if (VueRouter.install.installed) return
    VueRouter.install.installed = true
    _Vue = Vue
    _Vue.mixin({
      beforeCreate() {
        if (this.$options.router) {
          _Vue.prototype.$router = this.$options.router
          this.$options.router.init() // 这时候的 this 依然指向 Vue 实例,所以无法通过 this.init 进行调用
        }
      }
    })
  }
  init() {
    this.createRouteMap()
    this.initComponents()
    this.initEvent()
  }
  createRouteMap() {
    this.options.routes.forEach(route => {
      this.routeMap[route.path] = route.component
    })
  }
  initComponents() {
    const self = this
    _Vue.component('router-link', {
      props: {
        to: String
      },
      render(h) {
        return h('a', {
          attrs: {
            href: this.to
          },
          on: {
            click: this.handleClick
          }
        }, [this.$slots.default])
      },
      methods: {
        handleClick(e) {
          history.pushState({}, '', this.to)
          self.data.current = this.to
          e.preventDefault()
        }
      }
    })
    _Vue.component('router-view', {
      render(h) {
        return h(self.routeMap[self.data.current])
      }
    })
  }
  initEvent() {
    window.addEventListener('popstate', () => {
      this.data.current = window.location.pathname
    })
  }
}

VueRouter 构造函数(支持 to 对象和 mode)

import { isString, isObject } from '@/utils/isType'
let _Vue = null

export default class VueRouter {
  constructor(options) {
    this.mode = options.mode
    this.options = options
    this.routeMap = {}
    this.data = _Vue.observable({
      current: this.mode === 'history' ? '/' : '/#'
    }) // 记录当前路由
  }
  static install(Vue) {
    // 判断插件是否已经安装
    if (VueRouter.install.isInstalled) return
    VueRouter.install.isInstalled = true
    // 将 Vue 构造函数记录到全局变量
    _Vue = Vue
    // 将创建 Vue 实例时传入的 router 对象注入到 Vue 实例上
    /*
      _Vue.propertype.$router = this.$options.router
      因为 install 是静态方法,左移下面的方式中的 this 指向并非 Vue 实例,而是 VueRouter 类
      所以无法获取 this.$options 中的 router,因此采用全局API mixin 的方式找到当前 Vue 的实例
    */
    _Vue.mixin({
      beforeCreate() {
        // 在所有 Vue 实例中混入一个 $router 对象,但是所有组件都属于一个新的 Vue 实例,所以这种方式会加载很多次,其实没必要。
        // _Vue.propertype.$router = this.$options.router
        /*
          new Vue(options) => options -> $options
          new Vue({
            router, // $options.router
            store, // $options.store
            render: h => h(App)
          }).$mount('#app')
          main.js 中可以发现将 router 作为了主 Vue 实例的属性,因此只有这个 Vue 实例上有 router 对象
        */
        if (this.$options.router) {
          _Vue.prototype.$router = this.$options.router
          _Vue.prototype.$router.init()
        }
      }
    })
  }
  init() {
    this.createRouteMap()
    this.initComponents()
    this.initEvent()
  }
  createRouteMap() {
    // 遍历路由规则,以键值对方法存放到 routeMap
    this.options.routes.forEach(route => {
      this.routeMap[route.path] = route.component
    })
  }
  initComponents() {
    const self = this
    _Vue.component('router-link', {
      props: {
        to: String | Object
      },
      data() {
        return {
          url: '',
          pathname: '',
          params: {},
          query: {}
        }
      },
      render(h) {
        return h('a', {
          attrs: {
            href: this.pathname
          },
          on: {
            click: this.handleClick
          }
        }, [this.$slots.default])
      },
      methods: {
        initRouter() {
          this.params = {}
          this.query = {}
        },
        initParams() {
          const to = this.to
          if (isObject(to)) {
            if (to.name) { // 使用 name 则说明使用的是 params 传参
              const thisRoute = self.options.routes.filter(route => route.name === to.name)[0]
              this.url = thisRoute.path
              this.pathname = thisRoute.path
              this.params = to.params
            } else { // 否则是使用 path 来确定路由,说明使用的是 query 传参
              let query = ''
              for (const param in to.query) {
                query += `${param}=${to.query[param]}&`
              }
              query = query.substring(0, query.length - 1)
              this.url = to.path
              this.pathname = `${to.path}?${query}`
              this.query = to.query
            }
          } else if (isString(to)) {
            this.url = to
            this.pathname = to
          } else {
            this.url = '/'
            this.pathname = '/'
          }
        },
        handleClick(e) {
          this.initRouter()
          this.initParams()
          if (self.mode === 'history') {
            history.pushState({}, '', this.pathname)
          } else {
            window.location.hash = '#' + this.pathname
          }
          self.data.current = this.url
          e.preventDefault()
        }
      }
    })
    _Vue.component('router-view', {
      render(h) {
        if (self.mode === 'history') {
          self.data.current = window.location.pathname
        } else {
          self.data.current = window.location.hash.replace('#', '').split('?')[0]
        }
        return h(self.routeMap[self.data.current])
      }
    })
  }
  initEvent() {
    if (this.mode === 'history') {
      // 因为浏览器的返回前进按钮不会改变 this.data.current,所以需要监听 popstate 来修改当前页面的渲染
      window.addEventListener('popstate', () => {
        this.data.current = window.location.pathname
      })
    } else {
      window.addEventListener('hashchange', () => {
        this.data.current = window.location.hash.replace('#', '').split('?')[0]
      })
    }
  }
}

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值