你覺得的不是你覺得的 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
})()
複製代碼
相關文章
相關標籤/搜索