History API

https://developer.mozilla.org/zh-CN/docs/Web/API/History
https://developer.mozilla.org/en-US/docs/Web/API/History_API

  1. window.history.back();
  2. window.history.go(-1);
  3. window.history.forward();
  4. window.history.go(1);
  5. // refresh current page
  6. window.history.go(0);//the same like:
  7. window.history.go();

pushState( ) replaceState( )
不会触发 popstate, 不会触发 hashchange
不会发起请求

go( ) back( ) forward( ) 会触发 popstate
浏览器上的前进后退按钮 会触发popstate
location.hash=xxx location.href=xxx 即使只改变 hash 也会触发 popstate

go( ) back( ) forward( ) pushState( ) replaceState( ) 都会影响 history 记录
hash的变化也会产生history记录

pushState()

  1. history.pushState(state, title[, url])

state_object: 任何可被序列化的数据, 在firefox中限制序列化后大小不超过64K
title: 暂时被忽略
URL: 可选, 浏览器不会尝试载入这个URL, 但是下一次访问这个历史时会载入

在window.history中插入一条记录, 并改变地址栏展示, 但实际不会刷新页面
Note that pushState() never causes a hashchange event to be fired, even if the new URL differs from the old URL only in its hash.


The popstate event
A popstate event is dispatched to the window every time the active history entry changes.
If the history entry being activated was created by a call to pushState or affected by a call to replaceState, the popstate event’s state property contains a copy of the history entry’s state object.
See window.onpopstate for sample usage.

Calling history.pushState() or history.replaceState() won’t trigger a popstate event.
The popstate event is only triggered by performing a browser action,
such as clicking on the back button (or calling history.back() in JavaScript),
when navigating between two history entries for the same document.

Vue Router

不同路由类型:

  1. switch (mode) {
  2. case 'history':
  3. this.history = new HTML5History(this, options.base)
  4. break
  5. case 'hash':
  6. this.history = new HashHistory(this, options.base, this.fallback)
  7. break
  8. case 'abstract':
  9. this.history = new AbstractHistory(this, options.base)
  10. break
  11. default:
  12. if (process.env.NODE_ENV !== 'production') {
  13. assert(false, `invalid mode: ${mode}`)
  14. }
  15. }

Hash Mode

Hash Mode监听路由变化:

  1. const handleRoutingEvent = () => {
  2. const current = this.current
  3. if (!ensureSlash()) {
  4. return
  5. }
  6. this.transitionTo(getHash(), route => {
  7. if (supportsScroll) {
  8. handleScroll(this.router, route, current, true)
  9. }
  10. if (!supportsPushState) {
  11. replaceHash(route.fullPath)
  12. }
  13. })
  14. }
  15. const eventType = supportsPushState ? 'popstate' : 'hashchange'
  16. window.addEventListener(
  17. eventType,
  18. handleRoutingEvent
  19. )

实际页面渲染, 调用导航守护等操作是在 this.transitionTo()
编程式导航同样用了this.transitionTo()

编程式的导航

  1. export class HashHistory extends History {
  2. ...
  3. push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  4. const { current: fromRoute } = this
  5. this.transitionTo(
  6. location,
  7. route => {
  8. pushHash(route.fullPath)
  9. handleScroll(this.router, route, fromRoute, false)
  10. onComplete && onComplete(route)
  11. },
  12. onAbort
  13. )
  14. }
  15. replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  16. const { current: fromRoute } = this
  17. this.transitionTo(
  18. location,
  19. route => {
  20. replaceHash(route.fullPath)
  21. handleScroll(this.router, route, fromRoute, false)
  22. onComplete && onComplete(route)
  23. },
  24. onAbort
  25. )
  26. }
  27. ...
  28. }

pushHash 更新历史记录

  1. transitionTo (
  2. location: RawLocation,
  3. onComplete?: Function,
  4. onAbort?: Function
  5. ) {
  6. const route = this.router.match(location, this.current)
  7. this.confirmTransition(
  8. route,
  9. () => {...},
  10. () => {...},
  11. )
  12. }
  1. confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
  2. ...
  3. const queue: Array<?NavigationGuard> = [].concat(
  4. // in-component leave guards
  5. extractLeaveGuards(deactivated),
  6. // global before hooks
  7. this.router.beforeHooks,
  8. // in-component update hooks
  9. extractUpdateHooks(updated),
  10. // in-config enter guards
  11. activated.map(m => m.beforeEnter),
  12. // async components
  13. resolveAsyncComponents(activated)
  14. )
  15. ...
  16. const iterator = (hook: NavigationGuard, next) => { ... }
  17. runQueue(queue, iterator, () => { ... })
  18. function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) { ... }
  19. }

页面跳转后的下一步:

  1. function pushHash (path) {
  2. if (supportsPushState) {
  3. pushState(getUrl(path))
  4. } else {
  5. window.location.hash = path
  6. }
  7. }
  8. function replaceHash (path) {
  9. if (supportsPushState) {
  10. replaceState(getUrl(path))
  11. } else {
  12. window.location.replace(getUrl(path))
  13. }
  14. }
  1. export function pushState (url?: string, replace?: boolean) {
  2. saveScrollPosition()
  3. // try...catch the pushState call to get around Safari
  4. // DOM Exception 18 where it limits to 100 pushState calls
  5. const history = window.history
  6. try {
  7. if (replace) {
  8. history.replaceState({ key: _key }, '', url)
  9. } else {
  10. _key = genKey()
  11. history.pushState({ key: _key }, '', url)
  12. }
  13. } catch (e) {
  14. window.location[replace ? 'replace' : 'assign'](url)
  15. }
  16. }

History Mode

  1. export class HTML5History extends History {
  2. ...
  3. push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  4. const { current: fromRoute } = this
  5. this.transitionTo(location, route => {
  6. pushState(cleanPath(this.base + route.fullPath))
  7. handleScroll(this.router, route, fromRoute, false)
  8. onComplete && onComplete(route)
  9. }, onAbort)
  10. }
  11. replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  12. const { current: fromRoute } = this
  13. this.transitionTo(location, route => {
  14. replaceState(cleanPath(this.base + route.fullPath))
  15. handleScroll(this.router, route, fromRoute, false)
  16. onComplete && onComplete(route)
  17. }, onAbort)
  18. }
  19. ...
  20. }

history mode 只监听了 popstate

不过这种模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问 http://oursite.com/user/id 就会返回 404,这就不好看了。 所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。 link

tips: pushState 不会发起请求, 只有 “直接访问” 会发起请求需要这样处理

完整的导航解析流程

link

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫 (2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。

手动实现HashRouter

https://www.xiabingbao.com/post/fe/hash-history-router.html
文章用react讲的 不过都一样