vue-router原理

筅森魡賤 提交于 2020-01-27 07:01:05

用hash、history实现单页面

hash

hash原理:hash router 有一个明显的标志是url 中带有#, 我们可以通过onhashchange监听url中的hash来进行路由跳转
# 后面的 fragment 发生改变时,页面不会重新请求,其他参数发生变化,都会引起页面的重新请求

Onhashchange事件触发条件:
这是一个HTML 5新增的事件,当#值发生变化时,就会触发这个事件。IE8+、Firefox 3.6+、Chrome 5+、Safari 4.0+支持该事件。

  • 直接更改浏览器地址,在最后面增加或改变#path;
  • 通过改变location.href或locaion.hash的值
  • 通过触发点击带锚点的连接
  • 浏览器前进后退可能导致hash的变化,前提是两个网页地址中的hash值不同(会留下一个历史记录)

它的使用方法有三种:

window.onhashchange = func;
<body onhashchange="func();">
window.addEventListener("hashchange", func, false);

对于不支持onhashchange的浏览器,可以用setInterval监控location.hash的变化。
参考:
url中#(hash)的含义

<a href="#/home">首页</a>
<a href="#/about">关于</a>
  <script>
// hash原理
window.addEventListener('load',()=>{
  html.innerHTML = location.hash.slice(1) // 在页面进来时初始化html
})

window.addEventListener('hashchange',()=>{ // 判断网页状态是否改变
  html.innerHTML = location.hash.slice(1) // 根据hash变化,替换html内容
})
  </script>

history

history原理:利用pushState改变路径,利用popstate事件,实现history路由拦截,监听页面返回事件

1)history api
在这里插入图片描述

$router.go(-1) === history.go(-1)   // 浏览器的后退操作

2)History.pushState(stateObj,title,url)
pushState不会刷新页面,只是网站(history对象)发生变化,只有触发前进后退事件back()和forward()时浏览器才会刷新
这里的url是受同源策略限制,防止恶意脚本模仿其他网站url用来欺骗用户,所以当违背同源策略时将会报错

3)popstate事件:实现history路由拦截,监听页面返回事件
同一个文档的浏览历史(history对象)出现变化时,就会触发popstate事件
注意:仅仅调用pushState方法或replaceState方法,并不会触发该事件,只有用户点击浏览器倒退前进或back、forward、go才会触发。

<a onclick="go('/home')">首页</a>
<a onclick="go('/about')">关于</a>
  <script>
// history 浏览器提供的
// 可以做到改变网址却不需要刷新页面
function go(pathname){
 history.pushState({name:'xxx'},null,pathname)
  html.innerHTML = pathname
}
window.addEventListener('popstate',()=>{ // 有前进或后退就监听go()
  go(location.pathname)
})
  </script>

启动本地服务器看

vue-router

三种模式

hash模式 和 history模式 abstract模式
1)abstract模式:
适用于所有JavaScript环境,例如服务器端和Node.js. 如果没有浏览器API,路由器将自动强制进入此模式。

2)hash模式:hash router 有一个明显的标志是url 中带有#, 我们可以通过监听url中的hash来进行路由跳转
①就是指 url 尾巴后的 # 号以及后面的字符, 请求的时候不会被包含在 http 请求中 只会携带#之前的,所以每次改变hash不会重新请求加载页面
②hash 改变会触发 hashchange 事件
③hash变化会被浏览器记录,浏览器的前进和后退都能用。
④能兼容到ie8

3)history模式:history 为 HTML5 Api,提供了丰富的router 相关属性, 比如history.back() 就能轻松的做到页面
①页面请求时会带上整个链接,所以后台需要做相对处理,不然返回404
②window.history.pushState(state, title, url) // 改变网址
需要注意的是调用 history.pushState() 或 history.replaceState() 用来在浏览历史中添加或修改记录,不会触发popstate事件
③window.history.back(); // 后退
window.history.forward(); // 前进
window.history.go(-3); // 后退三个页面
④history 只能兼容到 IE10

hash和history的区别

在hash模式下,前端路由修改的是#中的信息,而浏览器请求时是不带它玩的,所以没有问题.但是在history下,你可以自由的修改path,当刷新时,如果服务器中没有相应的响应或者资源,会分分钟刷出一个404来。

hash模式url中会带有#号,破坏url整体的美观性, history 需要服务端支持rewrite, 否则刷新会出现404现象

hashHistory:简单 浏览器支持好 不会和服务端路由耦合
缺点:http请求没有前端路由信息、锚点功能失效、对seo不友好

browserHistory:单点登录时能带上前端路由,能统计到前端路由访问情况,对seo友好
缺点:分不清前端与后端、需要服务端支持

vue-router的实现

在这里插入图片描述

/**
 * 1、配置hash、history路由,传进来的参数有哪些
 * 2、匹配到对应的路由表
 * 3、渲染视图
 */
class HistoryRoute {
  constructor () {
    this.current = null
    console.log(this.current)
  }
}

class VueRouter {
  constructor (options) {
    this.mode = options.mode || 'hash'
    this.routes = options.routes || []
    // 把数组变成下面的对象
    // 你传递的这个路由表是一个数组 {'/home':Home , '/about':About}
    this.routesMap = this.createMap(this.routes)
    console.log(this.routesMap)
    // 路由中需要存放当前的路径 需要状态
    // this.history = { current: null } // 每次切换,把里面的值变成 {current:'/home'} {current:'/about'}
    this.history = new HistoryRoute()
    // 把内容渲染到页面
    this.init() // 开始初始化操作
  }
  init () {
    if (this.mode === 'hash') {
      // 先判断用户打开时有没有hash 没有就跳转到 #/
      location.hash ? '' : location.hash = '/'
      window.addEventListener('load', () => {
        // 在页面进来时初始化html
        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
      })
    }
  }
  go () {
  }
  back () {
  }
  push () {
  }
  createMap (routes) {
    return routes.reduce((memo, current) => {
      memo[current.path] = current.component
      return memo
    }, {})
  }
}

VueRouter.install = function (Vue, opts) {
  // 每个组件的实例都有this.$router / this.roue这两个属性
  // 所有组件中怎么拿到同一个路由的实例
  Vue.mixin({
    beforeCreate () {
      // 获取组件的属性名字
      // 给当前实例定义$router属性
      if (this.$options && this.$options.router) { //  定位根组件
        // this._root = this // 把当前实例挂载在_root上   this==> vue
        Object.defineProperty(this, '_root', { // Router的实例
          get () {
            return this
          }
        })
        this._router = this.$options.router // 把router实例挂载在_router上
        // observer方法 深度劫持
        /**
         * 如果history中的current属性变化 也会刷新视图
         * this.xxx = this._router.history
         */
        Vue.util.defineReactive(this, 'xxx', this._router.history)
      } else {
        // vue组件的渲染顺序 父->子->孙子
        // this._root = this.$parent._root // 如果想获取唯一的路由实例this._root._router
        Object.defineProperty(this, '_root', { // Router的实例
          get () {
            return this.$parent._root
          }
        })
      }
      Object.defineProperty(this, '$router', { // Router的实例
        get () {
          return this._root._router
        }
      })
      Object.defineProperty(this, '$route', {
        get () {
          return {
            // 当前的路由所在的状态
            current: this._root._router.history.current
          }
        }
      })
    }
  })
  Vue.component('router-link', {
    props: {
      to: String,
      tag: String
    },
    methods: {
      handleClick () {
        // mode === 'hash' ? `#${this.to}` : this.to
        // 如果是hash怎么跳转 如果是history怎么跳转
        let mode = this._self._root._router.mode
        // let current = this._self.$router.history.current
        if (mode === 'hash') {
          // this.href = `#${this.to}`
          this._self.$router.history.current = `#${this.to}`
        } else {
          this._self.$router.history.current = this.to
        }
      }
    },
    // jxs用法
    render (h) {
      // return h('a', {}, '首页') // react createElement
      let mode = this._self._root._router.mode
      let tag = this.tag || 'a'
      // 深度渲染
      return <tag on-click={this.handleClick} href={mode === 'hash' ? `#${this.to}` : this.to}>{this.$slots.default}</tag>
    }
  })
  // 根据current(当前路径)找出对应的组件,通过渲染函数render渲染出来
  Vue.component('router-view', { // 根据当前的状态 current路由表{'/about':About}
    render (h) {
      console.log(this)
      // 如何将current变成动态的 current变化应该会影响视图刷新
      // vue事项双向绑定 Object.defineProperty set get
      let current = this._self.$router.history.current
      let routesMap = this._self.$router.routesMap
      console.log(routesMap)
      return h(routesMap[current])
    }
  })
}

export default VueRouter

代码参考

其他问题

1、什么是单页应用,原理?

整个webapp就一个HTML文件,里面的各个功能页面是JavaScript通过hash或者history api来进行路由,并通过ajax 拉取数据实现响应功能。

优点:
分离前后端关注点,前端负责界面显示,后端负责数据存储和计算,各司其职,不会把前后端的逻辑混杂在一起;
减轻服务器压力,服务器只用出数据就可以,不用管展示逻辑和页面合成,吞吐能力会提高几倍;
同一套后端程序代码,不用修改就可以用于Web界面、手机、平板等多种客户端;
缺点:
SEO问题,现在可以通过Prerender等技术解决一部分;
前进、后退、地址栏等,需要程序进行管理;
书签,需要程序来提供支持;

document.getElementById("main").innerHTML = "404";
window.onhashchange = function() {
  let page = location.hash;
  if (page === "#home") {
    document.getElementById("main").innerHTML = "这是首页";
    return;
  } else if (page === "#help") {
    document.getElementById("main").innerHTML = "这是帮助页面";
    return;
  }
};

2、单页面和多页面的区别?

在这里插入图片描述

3、Vue-router有哪些钩子?使用场景?

1)全局守卫
router.beforeEach 路由前置时触发

const router = new VueRouter({ ... })
// to 要进入的目标路由对象
// from 当前的路由对象
// next resolve 这个钩子,next执行效果由next方法的参数决定
// next() 进入管道中的下一个钩子
// next(false) 中断当前导航
// next({path}) 当前导航会中断,跳转到指定path
// next(error) 中断导航且错误传递给router.onErr回调
// 确保前置守卫要调用next,否然钩子不会进入下一个管道
router.beforeEach((to, from, next) => {
  // ...
})

router.afterEach 路由后置时触发

// 与前置守卫基本相同,不同是没有next参数
router.afterEach((to, from) => {
  // ...
})

router.beforeResolve 跟router.beforeEach类似,在所有组件内守卫及异步路由组件解析后触发

2)路由独享守卫
参数及意义同全局守卫,只是定义的位置不同

const router = new VueRouter({
  routes: [
    {
      path: '/',
      component: Demo,
      beforeEnter: (to, from, next) => {
        // ...
      },
      afterEnter: (to, from, next) => {
        // ...
      },
    }
  ]
})

3)组件独享守卫
组件内新一个守卫, beforeRouteUpdate,在组件可以被复用的情况下触发,如 /demo/:id, 在/demo/1 跳转/demo/2的时候,/demo 可以被复用,这时会触发beforeRouteUpdate

const Demo = {
  template: `...`,
  beforeRouteEnter (to, from, next) {
    ...
  },
  // 在当前路由改变,但是该组件被复用时调用
  beforeRouteUpdate (to, from, next) {
    ...
  },
  beforeRouteLeave (to, from, next) {
    ...
  }
}

注意:注意在beforeRouteEnter前不能拿到当前组件的this,因为组件还有被创建,我们可以通过next(vm => {console.log(vm)}) 回传当前组件的this进行一些逻辑操作

4、完整的路由导航解析流程

1)触发进入其他路由。
2)调用要离开路由的组件守卫beforeRouteLeave
3)调用局前置守卫:beforeEach
4)在重用的组件里调用 beforeRouteUpdate
5)调用路由独享守卫 beforeEnter。
6)解析异步路由组件。
7)在将要进入的路由组件中调用beforeRouteEnter
8)调用全局解析守卫 beforeResolve
9)导航被确认。
10)调用全局后置钩子的 afterEach 钩子。
11)触发DOM更新(mounted)。
12)执行beforeRouteEnter 守卫中传给 next 的回调函数
参考

5、vue-router的三种跳转方式

1)router-link     
<router-link :to="{name:'home', params: {id:1}}">  //  path:'/home'  不配置path ,第一次可请求,刷新页面id会消失
<router-link :to="{name:'home', query: {id:1}}">  // query传参数 (类似get,url后面会显示参数)

2)this.$router.push() 函数里面调用
this.$router.push({name:'home',query: {id:'1'}})
this.$router.push({path:'/home',query: {id:'1'}})
this.$router.push({name:'home',params: {id:'1'}})  // 只能用 name


3)this.$router.replace() 用法同上
4)this.$router.go(n) 

扩展:
1、区别?
this.router.pushurlhistory退this.router.push 跳转到指定url路径,并想history栈中添加一个记录,点击后退会返回到上一个页面 this.router.replace
跳转到指定url路径,但是history栈中不会有记录,点击返回会跳转到上上个页面 (就是直接替换了当前页面)

this.$router.go(n)
向前或者向后跳转n个页面,n可为正整数或负整数

2、query和params区别
query类似 get, 跳转之后页面 url后面会拼接参数,类似?id=1, 非重要性的可以这样传, 密码之类还是用params刷新页面id还在
params类似 post, 跳转之后页面 url后面不会拼接参数 , 但是刷新页面id 会消失

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!