你觉得的不是你觉得的 History


title: 你觉得的不是你觉得的 History date: 2019-05-13 17:00:00 tags: -JavaScript
categories: JavaScript

Window.history是一个只读属性,用来获取History 对象的引用,History 对象提供了操做浏览器会话历史(浏览器地址栏中访问的页面,以及当前页面中经过框架加载的页面)的接口,容许你在用户浏览历史中向前和向后跳转。javascript

history

Window.history是一个只读属性,用来获取History 对象的引用,History 对象提供了操做浏览器会话历史(浏览器地址栏中访问的页面,以及当前页面中经过框架加载的页面)的接口,容许你在用户浏览历史中向前和向后跳转。html

同时,从HTML5开始提供了对history栈中内容的操做。 History 对象是 window 对象的一部分,可经过 window.history 属性对其进行访问。vue

History对象属性

属性 描述
length 返回浏览器历史列表中的 URL 数量。
state 返回一个表示历史堆栈顶部的状态的值。这是一种能够没必要等待popstate 事件而查看状态而的方式

History 对象方法

方法 属性
back() 加载 history 列表中的前一个 URL。
forward() 加载 history 列表中的下一个 URL。
go() 加载 history 列表中的某个具体页面。

History 对象最初设计来表示窗口的浏览历史。但出于隐私方面的缘由,History 对象再也不容许脚本访问已经访问过的实际 URL。惟一保持使用的功能只有 back()、forward() 和 go() 方法。html5

<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>history test</title>
    <script type="text/javascript" src="jquery.min.js"></script>
</head>

<body>
	<button onclick="count()">count</button>
    <button onclick="doForWard()">forward</button>        
    <button onclick="doBack()">back</button>          
    
    <script> var index=1; function count(){ console.log(`window.history.length = ${history.length}`); } function doForWard(){ history.forward(); } function doBack(){ history.back(); } </script>
</body>

</html>

复制代码

html5新特性:利用history的pushState等方法来解决使用ajax致使页面后退和前进的问题

使用ajax,能够实现不须要刷新整个页面就能够进行局部页面的更新。这样能够开发交互性很强的富客户端程序,减小网络传输的内容。但长期以来存在一个问题,就是没法利用浏览器自己提供的前进和后退按钮进行操做。好比在页面执行某个动做,该动做利用ajax请求到服务器获取数据,更新了当前页面的某些内容,这时想回到操做前的界面,用户就会习惯点击浏览器的后退按钮,实际这里是无效的。 HTML5引入了histtory.pushState()和history.replaceState()这两个方法,它们会更新history对象的内容。同时,结合window.onpostate事件,就能够解决这个问题。java

方法 属性
pushState() 按指定的名称和URL(若是提供该参数)将数据push进会话历史栈
replaceState() 按指定的数据,名称和URL(若是提供该参数),更新历史栈上最新的入口
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>history test</title>
    <script type="text/javascript" src="jquery.min.js"></script>
</head>

<body>
	<button onclick="count()">count</button>
    <button onclick="doForWard()">forward</button>        
    <button onclick="doBack()">back</button>        
    <button onclick="doPushState()">pushState</button>        
    <button onclick="doReplaceState()">replaceState</button>        
    
    <script> var index=1; function count(){ console.log(`window.history.length = ${history.length}`); } function doForWard(){ history.forward(); } function doBack(){ history.back(); } function doPushState(){ history.pushState({page:index++}, null,'?page='+index); } function doReplaceState(){ history.replaceState({page:index++}, null,'?page='+index); } </script>
</body>

</html>
复制代码

pushState接受3个参数 1)第一个参数是个js对象,能够听任何的内容,能够在onpostate事件中获取到便于作相应的处理。jquery

2)第二个参数是个字符串,目前任何浏览器都没有实现,但将来可能会用到,能够传个空串。ajax

3)第三个参数是个字符串,就是保存到history中的url。vue-router

调用 pushState() 后浏览器并不会当即加载这个URL,但可能会在稍后某些状况下加载这个URL,好比在用户从新打开浏览器时。新URL没必要须为绝对路径。若是新URL是相对路径,那么它将被做为相对于当前URL处理。新URL必须与当前URL同源,不然 pushState() 会抛出一个异常。该参数是可选的,缺省为当前URL。 pushState的做用是修改history历史,当调用方法时,将当前url存入history栈中,并在地址栏中显示参数中的url,可是并不会加载新的url。 replaceState的参数和pushState相同,可是调用时并不会将旧url存入history,而是将history中的url替换为参数中的url。chrome

popState

当活动历史记录条目更改时,将触发popstate事件。若是被激活的历史记录条目是经过对history.pushState()的调用建立的,或者受到对history.replaceState()的调用的影响。 popstate事件的state属性包含历史条目的状态对象的副本,也是pushState以及replaceState函数跳转到当前页面时传递的第一个参数。浏览器

popstate事件在浏览器前进、后退时会触发,配合pushState和replaceState,能够解决使用ajax的页面前进后退的问题。

具体解决方案是在触发ajax事件时同时调用pushState事件修改url信息,当popState 被触发时,同时调用history事件,根据当前url的不一样,调用ajax事件恢复对应的状态。

history+popstate 应用

  1. 网页不是你想走,想走就能走 只需添加几行代码
history.pushState(null, null, document.URL);
window.addEventListener('popstate', function () {
    history.pushState(null, null, document.URL);
});
复制代码

这段代码会把当前url先加入url历史中,此时history栈顶端有两个相同的url,当点击后退事件时,退回到前一个url,可是页面会监听后退事件并再次push一个相同的url进入栈中。 对于用户来讲点击后退按钮后,url永远不变,页面也没有从新加载(出于道德伦理公约,此方法慎用)

  1. 路由的history模式 详见路由实现

chrome在考虑新的history实现 baijiahao.baidu.com/s?id=163318…

location

history是HTML5新方法,在旧版本的IE浏览器中有兼容性问题,而且history在更改url的时候不会发送请求刷新页面, 若是是想在切换url的同时加载url,则可使用location.assign和location.replace方法

  • window.location.assign(url) : 加载 URL 指定的新的 HTML 文档。 就至关于一个连接,跳转到指定的url,当前页面会转为新页面内容,能够点击后退返回上一个页面。
  • window.location.replace(url) : 经过加载 URL 指定的文档来替换当前文档 ,这个方法是替换当前窗口页面,先后两个页面共用一个

用history和location实现简单的路由

路由要实现的功能:

  • 点击按钮,url更新,不从新加载资源,仅动态执行js
  • 刷新页面保持当前状态
  • push方法把路由历史加入页面历史记录中,replace方法替换历史记录中的页面,点击返回
  • 监听地址栏,用户手动输入时,显示对应内容
  • 分为hash模式和history模式

hash

/** * hash 模式 */
function Route(){
    this.routes = {};// 存放路由path及callback
    this.currentUrl = '';

    window.addEventListener('hashchange', this.refresh);
}

// 切换路由后执行
Route.prototype.refresh = function(){
    console.log(location.hash +' dom is refresh')
}
// 事件和路由绑定
Route.prototype.route = function(path, callback){
    this.routes[path] = callback;
}
// 执行事件
Route.prototype.push = function(path){
    //更新视图 dosomething
    location.hash = path;
    this.currentUrl = location.hash.slice(1) || '/';
    this.routes[this.currentUrl] && this.routes[this.currentUrl]();
}

Route.prototype.replace = function(path){
    //更新视图 dosomething
    const i = location.href.indexOf('#');
    location.replace(location.href.slice(0, i >= 0 ? i : 0) + '#' + path);
    this.routes[path] && this.routes[path]();
}

var myRouter = new Route();

// 定义路由事件
myRouter.route('my', () => {
    console.log('page1');
})
myRouter.route('home', () => {
    console.log('page2');
})

// 使用路由
myRouter.push('my');
myRouter.replace('home');
复制代码

history

function Route(){
    this.routes = {};// 存放路由path及callback

    window.addEventListener('popState', this.refresh)
}

Route.prototype.refresh = function(path){
    console.log('dom has refresh');
}
// 事件和路由绑定
Route.prototype.route = function(path, callback){
    this.routes[path] = callback;
}

Route.prototype.replace = function (path) {
    //更新视图
    history.replaceState({path: path}, null, path);
    this.routes[path] && this.routes[path]();
}
Route.prototype.push = function (path) {
    //更新视图
    history.pushState({path: path}, null, path);
    this.routes[path] && this.routes[path]();
}

var myRouter = new Route();

// 定义路由事件
myRouter.route('/my', () => {
    console.log('page1');
})
myRouter.route('/detail', () => {
    console.log('page2');
})

// 使用路由
myRouter.push('/detail')
复制代码

附录

vue-router源码导读 1-1 VueRouter构造函数

// src/index.js

export default class VueRouter{
  mode: string; // 传入的字符串参数,指示history类别
  history: HashHistory | HTML5History | AbstractHistory; // 实际起做用的对象属性,必须是以上三个类的枚举
  fallback: boolean; // 如浏览器不支持,'history'模式需回滚为'hash'模式
  
  constructor (options: RouterOptions = {}) {
    
    let mode = options.mode || 'hash' // 默认为'hash'模式
    this.fallback = mode === 'history' && !supportsPushState // 经过supportsPushState判断浏览器是否支持'history'模式
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {
      mode = 'abstract' // 不在浏览器环境下运行需强制为'abstract'模式
    }
    this.mode = mode

    // 根据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}`)
        }
    }
  }

  init (app: any /* Vue component instance */) {
    
    const history = this.history

    // 根据history的类别执行相应的初始化操做和监听
    if (history instanceof HTML5History) {
      history.transitionTo(history.getCurrentLocation())
    } else if (history instanceof HashHistory) {
      const setupHashListener = () => {
        history.setupListeners()
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener,
        setupHashListener
      )
    }

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

  // VueRouter类暴露的如下方法实际是调用具体history对象的方法
  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    this.history.push(location, onComplete, onAbort)
  }

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    this.history.replace(location, onComplete, onAbort)
  }
}
复制代码
  1. 在初始化对应的history以前,会对mode作一些校验:若浏览器不支持HTML5History方式(经过supportsPushState变量判断),则mode设为hash;若不是在浏览器环境下运行,则mode设为abstract;
  2. VueRouter类中的onReady(),push()等方法只是一个代理,实际是调用的具体history对象的对应方法,在init()方法中初始化时,也是根据history对象具体的类别执行不一样操做;

1-2 HashHistory 流程:

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

HashHistory.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 //对hash进行直接赋值
}
复制代码

HashHistory.replace()

replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  this.transitionTo(location, route => {
    replaceHash(route.fullPath)
    onComplete && onComplete(route)
  }, onAbort)
}
  
function replaceHash (path) {
  const i = window.location.href.indexOf('#')
  window.location.replace(
    window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path //调用location.replace()直接替换hash
  )
}
复制代码

1-3 transitionTo()

//父类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
}

//VueRouter类中定义了listen方法
init (app: any /* Vue component instance */) {
    
  this.apps.push(app)

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

//install.js在插件加载的地方混入了_route
// 经过Vue.mixin()方法,全局注册一个混合,影响注册以后全部建立的每一个Vue实例,
// 该混合在beforeCreate钩子中经过Vue.util.defineReactive()定义了响应式的_route属性。
// 所谓响应式属性,即当_route值改变时,会自动调用Vue实例的render()方法,更新视图
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)
    },
  })
}
复制代码

1-4 监听地址栏

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

1-5 HTML5History

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  const { current: fromRoute } = this
  this.transitionTo(location, route => {
    pushState(cleanPath(this.base + route.fullPath))
    handleScroll(this.router, route, fromRoute, false)
    onComplete && onComplete(route)
  }, onAbort)
}

replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  const { current: fromRoute } = this
  this.transitionTo(location, route => {
    replaceState(cleanPath(this.base + route.fullPath))
    handleScroll(this.router, route, fromRoute, false)
    onComplete && onComplete(route)
  }, onAbort)
}

// src/util/push-state.js
export function pushState (url?: string, replace?: boolean) {
  saveScrollPosition()
  // try...catch the pushState call to get around Safari
  // DOM Exception 18 where it limits to 100 pushState calls
  const history = window.history
  try {
    if (replace) {
      history.replaceState({ key: _key }, '', url)
    } else {
      _key = genKey()
      history.pushState({ key: _key }, '', url)
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url)
  }
}

export function replaceState (url?: string) {
  pushState(url, true)
}
复制代码

popState监听地址栏变化

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)
      }
    })
  })
}
复制代码

supportsPushState检查浏览器是否支持HTML5的history

// src/util/push-state.js

export const supportsPushState = inBrowser && (function () {
  const ua = window.navigator.userAgent

  if (
    (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
    ua.indexOf('Mobile Safari') !== -1 &&
    ua.indexOf('Chrome') === -1 &&
    ua.indexOf('Windows Phone') === -1
  ) {
    return false
  }

  return window.history && 'pushState' in window.history
})()
复制代码
相关文章
相关标签/搜索