乐趣区

history和hashvuerouter

history

window.history(可直接写成 history)指向 History 对象,它表示当前窗口的浏览历史。History 对象保存了当前窗口访问过的所有页面网址

  1. history 对象的常见属性和方法
    go()
    接受一个整数为参数,移动到该整数指定的页面,比如 history.go(1) 相当于 history.forward(),history.go(-1)相当于 history.back(),history.go(0)相当于刷新当前页面
    back()
    移动到上一个访问页面,等同于浏览器的后退键, 常见的返回上一页就可以用 back(),是从浏览器缓存中加载,而不是重新要求服务器发送新的网页
    forward()
    移动到下一个访问页面,等同于浏览器的前进键
    pushState()
    在浏览器历史中添加记录,方法接受三个参数,以此为:

    history.pushstate(state,title,url)
    
    if(!!(window.hostory && history.pushState)) {// 支持 History API} else {// 不支持}

    state: 一个与指定网址相关的状态对象,popState 事件触发时,该对象会传入回调函数,如果不需要这个对象,此处可填 null
    title: 新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填 null
    url: 新的网址,必须与当前页面处在同一个域,浏览器的地址栏将显示这个网址

    history.pushState({a:1},'page 2','2.html')

    用上面代码添加 2.html 后,浏览器地址栏立刻显示 2.html,但不会跳到 2.html, 只会更新浏览器历史记录,此时点击后退按钮则会回到原网页, 但是会改变 history 的 length 属性;
    如果 pushState 的 url 参数,设置了一个新的锚点值(即 hash),并不会触发 hashChange 事件,如果设置了一个跨域网址,则会报错。

    replaceState()
    history.replaceState()方法的参数和 pushState()方法一摸一样,区别是它修改浏览器历史当中的记录
    两者的区别在于
    push
    此时执行 history.back() 返回 /about

    replace
    此时执行 history.back()返回 /blog

    length
    history.length 属性保存着历史记录的 url 数量,初始时该值为 1,如果当前窗口先后访问了三个网址,那么 history 对象就包括 3 项,history.length=3
    state
    返回当前页面的 state 对象。可以通过 replaceState()和 pushState()改变 state,可以存储很多数据
    scrollRestoration
    history.scrollRestoration = ‘manual’; 关闭浏览器自动滚动行为
    history.scrollRestoration = ‘auto’; 打开浏览器自动滚动行为(默认)
    popState 事件
    每当同一个文档的浏览历史(即 history)出现变化时,就会触发 popState 事件
    需要注意:仅仅调用 pushState 方法或 replaceState 方法,并不会触发该事件,只有用户点击浏览器后退和前进按钮时,或者使用 js 调用 back、forward、go 方法时才会触发。另外该事件只针对同一个文档,如果浏览历史的切换,导致加载不同的文档,该事件不会被触发
    使用的时候,可以为 popState 事件指定回调函数

    window.onpopstate = function (event) {console.log('location:' + document.location);
          console.log('state:' +JSON.stringify(event.state));
        };
        
        // 或者
        
        window.addEventListener('popstate', function(event) {console.log('location:' + document.location);
          console.log('state:' + JSON.stringify(event.state));
        });

    回调函数的参数是一个 event 事件对象,它的 state 属性指向 pushState 和 replaceState 方法为当前 url 所提供的状态对象(即这两个方法的第一个参数)。上边代码中的 event.state 就是通过 pushState 和 replaceState 方法为当前 url 绑定的 state 对象
    这个 state 也可以直接通过 history 对象读取
    history.state
    注意:页面第一次加载的时候,浏览器不会触发 popState 事件

hash

hash 就是指 url 尾巴后的 # 号以及后面的字符。这里的 # 和 css 里的 # 是一个意思。hash 也 称作 锚点,本身是用来做页面定位的,她可以使对应 id 的元素显示在可视区域内。

通过 window.location.hash 获取 hash 值

延伸:
window.location 对象里面
hash:设置或返回从 (#) 开始的 URL(锚)。
host:设置或返回主机名和当前 URL 的端口号。
hostname:设置或返回当前 URL 的主机名。
href:设置或返回完整的 URL。
pathname:设置或返回当前 URL 的路径部分。
port:设置或返回当前 URL 的端口号。
search:设置或返回从问号 (?) 开始的 URL(查询部分)。
assign():加载新的文档。
reload():重新加载当前文档。
replace():用新的文档替换当前文档。
hashchange

当 hash 值改变时会触发这个事件,
if('onhashchange' in window) {window.addEventListener('hashchange',function(e){console.log(e.newURL,e.oldURL)
},false)
}

vue-router

在 vue-router 中,它提供 mode 参数来决定采用哪一种方式;
默认是 hash,可以配置 mode:history, 选择 history 模式;
选好 mode 后 vueRouter 中会创建 history 对象(HashHistory 或 HTML5History,这两种类都是继承 History 类,这个类定义了一些公共方法)

// 根据 mode 确定 history 实际的类并实例化
    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if (process.env.NODE_ENV !== 'production') {assert(false, `invalid mode: ${mode}`)
        }
    }
  }

现在我们来看当我们在代码中执行了 this.$router.push()之后具体的流程

首先看 HashHistory
1 $router.push() // 显式调用方法
2 HashHistory.push() // 我们来看下 push 方法

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  this.transitionTo(location, route => {pushHash(route.fullPath)
    onComplete && onComplete(route)
  }, onAbort)
}

function pushHash (path) {window.location.hash = path}

transitionTo()方法是父类中定义的是用来处理路由变化中的基础逻辑的,push()方法最主要的是对 window 的 hash 进行了直接赋值:hash 的改变会自动添加到浏览器的访问历史记录中。

window.location.hash = route.fullPath // 类似 /thunder/bless_sort/1?fromType=homeTap

那么视图的更新是怎么实现的呢,我们来看父类 History 中 transitionTo()方法的这么一段:

transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {const route = this.router.match(location, this.current)
  this.confirmTransition(route, () => {this.updateRoute(route)
    ...
  })
}

updateRoute (route: Route) {this.cb && this.cb(route)
  
}

listen (cb: Function) {this.cb = cb}

路由变化后会执行 updateRoute(),其实是执行 this.cb,而 this.cb 是在 listen 函数中被执行的,那么在那里调用 listen 函数呢

init (app: any /* Vue component instance */) {this.apps.push(app)

  history.listen(route => {this.apps.forEach((app) => {app._route = route})
  })
}

app 为 vue 组件实例,vue 本身是没有 vue-routerd 的,需要在组件中挂载这个属性

export function install (Vue) {
  
  Vue.mixin({beforeCreate () {if (isDef(this.$options.router)) {
        this._router = this.$options.router
        this._router.init(this)
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      }
      registerInstance(this, this)
    },
  })
}

通过 Vue.mixin()方法,全局注册一个混合,影响注册之后所有创建的每个 Vue 实例,该混合在 beforeCreate 钩子中通过 Vue.util.defineReactive()定义了响应式的_route 属性(当前的路由)。即当_route 值改变时,会自动调用 Vue 实例的 render()方法,更新视图。
总结一下,从设置路由改变到视图更新的流程如下:

$router.push() --> HashHistory.push() --> History.transitionTo() --> History.updateRoute() --> {app._route = route} --> vm.render()

replace 方法
功能: 替换当前路由并更新视图,常用情况是地址栏直接输入新地址
流程与 push 基本一致
但流程 2 变为替换当前 hash(window.location.replace= XXX)
replace 和 hash 的区别在于它并不是将新路由添加到浏览器访问历史的栈顶,而是替换掉当前的路由:如上图

监听地址栏
以上讨论的 VueRouter.push()和 VueRouter.replace()是可以在 vue 组件的逻辑代码中直接调用的,除此之外在浏览器中,用户还可以直接在浏览器地址栏中输入改变路由,因此 VueRouter 还需要能监听浏览器地址栏中路由的变化,并具有与通过代码调用相同的响应行为。在 HashHistory 中这一功能通过 setupListeners 实现:

setupListeners () {window.addEventListener('hashchange', () => {if (!ensureSlash()) {return}
    this.transitionTo(getHash(), route => {replaceHash(route.fullPath)
    })
  })
}

该方法设置监听了浏览器事件 hashchange,调用的函数为 replaceHash,即在浏览器地址栏中直接输入路由相当于代码调用了 replace()方法

而在 HTML5History 具体又是怎样的呢
代码结构以及更新视图的逻辑与 hash 模式基本类似,只不过将对 window.location.hash 直接进行赋值 window.location.replace()改为了调用 history.pushState()和 history.replaceState()方法。
在 HTML5History 中添加对修改浏览器地址栏 URL 的监听是直接在构造函数中执行的:监听 popState 事件(地址栏变化触发 window.onpopstate),调用 repalce 方法

 constructor (router: Router, base: ?string) {
      
      window.addEventListener('popstate', e => {
        const current = this.current
        this.transitionTo(getLocation(this.base), route => {if (expectScroll) {handleScroll(router, route, current, true)
          }
        })
      })
    }

除此之外 vue-router 还为非浏览器环境准备了一个 abstract 模式,其原理为用一个数组 stack 模拟出浏览器历史记录栈的功能。以上是 vue-router 的核心逻辑;

两种模式对比
History 模式的优点:
1.History 模式的地址栏更美观。。。
2.History 模式的 pushState、replaceState 参数中的新 URL 可为同源的任意 URL(可为不同的 html 文件),而 hash 只能是同一文档
3.History 模式的 pushState、replaceState 参数中的 state 可为 js 对象,能携带更多数据
4.History 模式的 pushState、replaceState 参数中的 title 能携带字符串数据(当然,部分浏览器,例如 firefox 不支持 title,一般 title 设为 null,不建议使用)
缺点:
不过这种模式需要后端配置,因为我们这个页面是单页面应用,如果用户直接访问 http://oursite.com/user/id
后台没有正确的配置,则就会返回 404,
这个时候需要后台配置一个能够覆盖所有情况的候选资源,如果 url 匹配不到任何静态资源时,则要返回同一个 index.html;

注:该篇文章参考了 https://zhuanlan.zhihu.com/p/…

退出移动版