一爸业、前言
理論上說,每個有效的 URL 都指向一個唯一的資源亏镰。這個資源可以是一個 HTML 頁面扯旷,一個 CSS 文檔,一幅圖像等索抓。在地址欄鍵入完整的 URL 地址薄霜,瀏覽器就會將對應(yīng)資源展示出來。
為了在多個 URL 之間往返纸兔,瀏覽器廠商定義了一種可存儲瀏覽器會話歷史(下稱“歷史記錄”)的機(jī)制,每訪問新的 URL 就會在歷史記錄中增加一個新的歷史記錄條目否副。當(dāng)前“歷史條目”可通過 History 對象(即 window.history
)獲取汉矿,該對象包括了 back()
、forward()
备禀、go()
等方法洲拇。
在很早以前奈揍,不同 URL 之間進(jìn)行切換,都是需要重新加載資源的赋续。直到 Ajax 的出現(xiàn)男翰,打破了這種限制。Ajax 技術(shù)允許通過 JavaScript 腳本向服務(wù)器發(fā)起請求纽乱,服務(wù)器接收到請求蛾绎,將數(shù)據(jù)返回客戶端(瀏覽器),然后根據(jù)響應(yīng)數(shù)據(jù)按需操作 DOM 以實現(xiàn)局部刷新鸦列。這個過程頁面并不會重新加載租冠,只會更新 DOM 的局部,因此 URL 并沒有發(fā)生變化薯嗤。但是 Ajax 局部刷新的能力顽爹,似乎與一個 URL 對應(yīng)一個資源相悖。于是......就出現(xiàn)了一種解決方案骆姐,既可以實現(xiàn)頁面局部刷新镜粤,也會修改 URL。
那就是 URL 中的 #
模式玻褪,例如:
http://www.example.com/index.html#user
#
號表示網(wǎng)頁的一個位置肉渴,跟在 #
號后面的字符串稱為“錨”。當(dāng)錨發(fā)生變化归园,若頁面中存在這樣一個位置(可通過錨點或標(biāo)簽元素 id
屬性設(shè)置)黄虱,瀏覽器會使頁面自動滾動至對應(yīng)位置。這種機(jī)制的好處是庸诱,僅用于指導(dǎo)瀏覽器的動作捻浦,而對服務(wù)器是完全無用的。例如桥爽,請求上述網(wǎng)址朱灿,HTTP 請求的服務(wù)器地址是:http://www.example.com/index.html
(不會包含 #user
)。
相比 http://www.example.com/index.html/user
這種形式钠四,URL 上帶 #
號除了看著不順眼之外盗扒,對于分享 URL 或 SEO 來說也是一個問題(對此 Google 還提出了一種優(yōu)化 SEO 的方案,即 URL 中帶上 "#!"
缀去,詳見)侣灶。后來 HTML5 中提供了另外一種解決方案。它同樣是可以修改 URL 且不觸發(fā)頁面重載缕碎,而且可以修改 URL 中 Origin
后面的任意路徑(即 /index.html/user
)褥影,這點 #
模式是做不到的。他們將這種能力內(nèi)置在 History 對象下咏雌,包含 history.pushState()
凡怎、history.replaceState()
方法校焦。
上面提到了一些詞語,有必要說明一下:
歷史記錄
是指在瀏覽器中每個標(biāo)簽(窗口)的會話歷史(下稱“歷史記錄”)统倒。它由瀏覽器某個線程維護(hù)著寨典,而且標(biāo)簽之間的歷史記錄是相互獨立的,且無法通過 JavaScript 腳本讀取房匆。當(dāng)標(biāo)簽關(guān)閉或者退出瀏覽器耸成,會話結(jié)束,歷史記錄也隨之被銷毀(沒錯坛缕,這里的“歷史記錄”墓猎,并不是指瀏覽器應(yīng)用的“歷史記錄”功能)。
歷史條目
瀏覽器每訪問一個新的 URL赚楚,就會產(chǎn)生一條記錄(下稱“記錄”)毙沾,并保存至“歷史記錄”。這條記錄宠页,僅能在當(dāng)前頁面的window.history
對象讀取到左胞。舉個例子,假設(shè)當(dāng)前歷史記錄里有 3 條不同頁面的記錄(假設(shè)用數(shù)組
[A, B, C]
表示举户,真正如何表示不去深究烤宙,非本文討論范圍),若當(dāng)前處于C
頁面俭嘁,那么通過window.history
讀取到數(shù)據(jù)躺枕,是指C
頁面的記錄信息。而A
供填、B
頁面的信息是獲取不對的拐云,除非后退并在對應(yīng)頁面內(nèi)執(zhí)行腳本。新的 URL
請注意近她,這個“新”是相對的叉瘩。由于下文經(jīng)常提到,因此有必要說明一下粘捎。假設(shè)在
A
頁面跳轉(zhuǎn)到B
頁面薇缅,這個B
就是“新的 URL”。若在B
中也有一個鏈接指向A
頁面攒磨,點擊的時候泳桦,這個A
也是“新的 URL”,因為它是相較于當(dāng)前頁面 URL 所得出來的結(jié)論娩缰。因此蓬痒,這個過程會產(chǎn)生 3 條記錄,所以歷史記錄將會是[A, B, A]
。
下面將按歷史順序一一介紹...
二梧奢、URL 的 # 號
其實前面剛提到,#
表示頁面中的一個位置演痒。比如:
https://github.com/toFrankie/csscomb-mini#usage
上述 URL 中亲轨,#usage
表示 https://github.com/toFrankie/csscomb-mini
頁面的 usage
位置。
URL 上跟在 #
后面的所有字符串鸟顺,被稱為 Fragment(錨惦蚊,或片段標(biāo)識符),所以此 URL 的錨為 usage
讯嫂。
1. location.hash 屬性
打印 window.location
結(jié)果如下:
{
hash: '#usage'
host: 'github.com'
hostname: 'github.com'
href: 'https://github.com/toFrankie/csscomb-mini#usage'
origin: 'https://github.com'
pathname: '/toFrankie/csscomb-mini'
port: ''
protocol: 'https:'
search: ''
}
其中 location.hash
值為 #usage
蹦锋,它是由 # + Fragment
組成的字符串。
如果 URL 中不存在 Fragment欧芽,
location.hash
會返回一個空字符串(''
)莉掂。
// 1. https://github.com/toFrankie/csscomb-mini
window.location.hash // ""
// 2. https://github.com/toFrankie/csscomb-mini#
window.location.hash // ""
// 3. https://github.com/toFrankie/csscomb-mini#/
window.location.hash // "#/"
// 4. https://github.com/toFrankie/csscomb-mini#usage
window.location.hash // "#usage"
2. 修改 URL hash 值
修改 hash
值就會直接體現(xiàn)在地址欄上,并且在歷史記錄中會產(chǎn)生一條新記錄千扔。比如憎妙,執(zhí)行 history.length
可以看到 length
的變化。history.length
表示歷史記錄中的記錄個數(shù)曲楚。
可以通過以下幾種方式去修改:
// 1. 直接給該屬性賦值
window.location.hash = '#usage' // # 號可省略
// 2. 給 window.location 賦值厘唾,請注意 # 是不能省略,否則不僅是修改 Fragment 了
window.location = '#usage'
window.location.href = '#usage'
// 3. 請注意龙誊,只修改 Fragment 部分抚垃,否則會重新加載頁面。類似 history.replaceState 作用
window.location.replace('https://github.com/toFrankie/csscomb-mini#/usage')
// 4. 通過 <a> 標(biāo)簽設(shè)置 href 屬性趟大,且不能省略 # 號
<a href="#usage"></a>
請注意鹤树,多次設(shè)置同一個 Fragment 時,僅首次有效护昧,重復(fù)的部分可以理解為是無效的魂迄。
3. location.hash、location.href 與 location.replace()
前面兩個方法都可讀可寫惋耙,其中 location.hash
絕對不會重載頁面捣炬。這跟它的設(shè)計初衷有關(guān),前面提過了绽榛,不再贅述湿酸。而 location.href
和 location.replace()
若只是 URL 的 Fragment 部分發(fā)生,也不會重載頁面灭美,而其他情況總會重載頁面推溃。
通過 location.href
、location.hash
方式去“修改” URL届腐,歷史記錄都會新增一條新記錄铁坎。由于 history.length
是歷史記錄數(shù)量的體現(xiàn)蜂奸,因此也會隨之改變。而 location.replace()
則是用新記錄覆蓋當(dāng)前記錄硬萍,因此 history.length
不會發(fā)生變化扩所。
注意點:
以上三種方式(包括
<a>
標(biāo)簽形式)去修改 URL,只有在新舊 URL 不相同的情況下朴乖,才會新增一條記錄祖屏。其中
location.href
與location.replace()
方法,若 URL 中包含 Fragment 部分买羞,且新舊 URL 之間僅 Fragment 部分發(fā)生變化袁勺,也不會重載頁面。不管新舊 URL 是否一致(URL 不含 Fragment 時)畜普,
location.href
總會重載頁面期丰。當(dāng)新舊 URL 相同時,
location.href
作用等同于location.reload()
漠嵌、history.go(0)
咐汞。雖說是重新加載頁面,但多數(shù)是從瀏覽器緩存中加載儒鹿,除非頁面緩存失效或過期了化撕。對于
location.href
我們通常會賦予一個完整的 URL 地址,但它是支持“相對路徑”形式的 URL 的约炎。(詳見:絕對 URL 和相對 URL)上面是指寫操作植阴,并不是讀操作哈。
一句話總結(jié):若新舊 URL 之間僅僅 Fragment 部分發(fā)生改變圾浅,以上幾種方法都會在歷史記錄新增一條記錄掠手,且不會重載頁面。
4. Fragment 的位置
前面提到狸捕,# + Fragment
表示網(wǎng)頁的一個位置喷鸽,用于指導(dǎo)瀏覽器的行為。當(dāng) Fragment 的值發(fā)生改變灸拍,頁面會滾動至對應(yīng)位置做祝。當(dāng)然,前提是這個位置存在于頁面中鸡岗,否則也是不會發(fā)生滾動的混槐。
那么這個“位置”,如何設(shè)置呢轩性?
講真的声登,天天用框架寫頁面,最原始的反而忘了。有兩種方式:
- 使用錨點悯嗓,即利用
<a>
標(biāo)簽的name
屬性(不推薦)- 使用標(biāo)簽
id
屬性(推薦)
請注意件舵,<a>
標(biāo)簽的 name
屬性在 HTML5 中已廢棄,請使用 HTML 全局屬性 id
來代替绅作。后者在整個 DOM 中必須是唯一的芦圾。常用于查詢節(jié)點、樣式選擇器俄认、作為頁面 Fragment 的位置。
<!-- 1. 錨點 -->
<a name="usage"></a>
<!-- 2. 設(shè)置 id 屬性 -->
<div id="usage"></div>
<!-- 這種也是可以的洪乍,但這種不稱為錨點 -->
<a id="usage"></a>
再看一例子:
<!-- 1. 在點擊 a 標(biāo)簽時眯杏,會修改 hash 屬性為 #usage,但不會滾動至 a 標(biāo)簽 -->
<a href="#usage"></a>
<!-- 2. 以下情況壳澳,除了修改 hash 值岂贩,頁面也會隨之滾動至 a 標(biāo)簽 -->
<a name="usage" href="#usage"></a>
<a id="usage" href="#usage"></a>
上述示例,作者本人會經(jīng)诚锊ǎ混淆(希望你們不會)萎津,順道提一下。簡單來說抹镊,href="usage"
是為了修改 URL锉屈,當(dāng) URL 的 hash
變成 #usage
,瀏覽器就會滾動至對應(yīng)位置(即錨點為 usage
或 id
屬性為 usage
的元素所在位置)垮耳。
5. hashchange 事件
若在全局注冊了 hashchange
事件監(jiān)聽器颈渊,只要 URL 的 Fragment 發(fā)生變化,將會被事件處理程序捕獲到终佛,事件對象包含了 newURL
和 oldURL
等該事件特有的屬性俊嗽。其余的,在下文對比 popstate
事件時再詳細(xì)介紹铃彰。
三绍豁、History 對象
前面提到,每個標(biāo)簽都有一個獨立的歷史記錄牙捉,里面維護(hù)著一條或多條記錄竹揍。每條記錄保存了對應(yīng) URL 的一些狀態(tài),僅能在當(dāng)前頁面的 window.history
對象讀取到鹃共。(這里不再贅述鬼佣,若概念有混淆的,請回到開頭再看一遍)
在 HTML5 之前霜浴,History 對象主要包含以下屬性和方法:
history.length
history.back()
history.forward()
history.go()
1. history.length
只讀晶衷,該屬性返回當(dāng)前會話的歷史記錄個數(shù)。由于 history.length
是歷史記錄數(shù)量的體現(xiàn),那么當(dāng)歷史記錄發(fā)生變化時晌纫,它才會隨之改變税迷。
注意以下幾點:
若“主動”打開瀏覽器的新標(biāo)簽,就會產(chǎn)生一條記錄锹漱,盡管它可能是一個空標(biāo)簽頁箭养,即
history.length
為1
。當(dāng)鍵入新 URL 并回車哥牍,此時history.length
就會變?yōu)?2
毕泌。若瀏覽器的標(biāo)簽是通過類似
<a target="_blank">
形式自動創(chuàng)建的話,新標(biāo)簽的history.length
是1
(不是2
哦)嗅辣。此時原標(biāo)簽的歷史記錄不會受到影響撼泛,它們是相互獨立的。這種情況就類似于在微信里打開一個鏈接澡谭,進(jìn)入頁面的history.length
為1
愿题。不管以任何方式刷新頁面,歷史記錄和
history.length
都不會改變蛙奖。在地址欄鍵入新的 URL潘酗,歷史記錄會增加
1
。一般情況下雁仲,若新舊 URL 相同仔夺,此時歷史記錄不會發(fā)生變化,
history.length
也不會伯顶。特殊情況是囚灼,history.pushState()
和history.replaceState()
方法總會產(chǎn)生一條新記錄,即使新舊 URL 相同也會祭衩。點擊瀏覽器前進(jìn)/后退/刷新按鈕灶体,或者調(diào)用
history.back()
、history.forward()
掐暮、history.go()
方法蝎抽,不會使歷史記錄和history.length
值發(fā)生變化。這些操作只會退回/前往歷史記錄中某個具體的頁面路克。但會觸發(fā)popstate
或hashchange
事件(若有注冊的話)樟结。
這里描述的場景很多,原因是此前對某些場景沒有完全弄清楚(如果你沒有這個困擾精算,簡單略過即可)瓢宦。
既然 history.length
是只讀的,換句話說灰羽,就是我們無法“直接”操作歷史記錄(比如刪除某個歷史記錄)驮履,事實上我們也訪問不到鱼辙。
2. history.back()
它的作用同瀏覽器的后退按鈕,通俗地講就是后退至上一頁玫镐。等價于 history.go(-1)
倒戏。
若當(dāng)前頁面是歷史記錄的第一個頁時,調(diào)用此方法不執(zhí)行任何操作恐似。此時瀏覽器后退按鈕也是置灰的杜跷,是不可操作的。換句話說矫夷,此方法僅在
history.length > 1
時有效葛闷。
3. history.forward()
它的作用同瀏覽器的前進(jìn)按鈕,通俗地講就是前往下一頁双藕。等價于 history.go(1)
孵运。
若當(dāng)前頁面是歷史記錄里最頂端的頁面時,調(diào)用此方法不執(zhí)行任何操作蔓彩。此時瀏覽器前進(jìn)按鈕也是置灰的,是不可操作的驳概。
4. history.go()
該方法接受一個 delta
參數(shù)(可選)赤嚼,通過當(dāng)前頁面的相對位置加載某個頁面。
window.history.go(delta)
一般來說顺又,參數(shù)可缺省更卒、為 0
、為負(fù)整數(shù)(表示后退)稚照、正整數(shù)(表示前進(jìn))蹂空。
比如說,
history.go(-2)
會歷史記錄里后退兩個頁面果录。相應(yīng)地上枕,history.go(2)
會前進(jìn)兩個頁面。其中
history.go(1)
作用同history.forward()
弱恒,history.go(-1)
作用同history.back()
辨萍。若缺省
delta
或者delta
為0
,會重新加載當(dāng)前頁面返弹。此時作用同location.reload()
或者瀏覽器的刷新按鈕锈玉。若
delta
數(shù)值部分超出了歷史記錄的范圍,不會執(zhí)行任何操作义起。既不會后退至歷史記錄的第一個頁面拉背,也不會前往歷史記錄里最頂端的頁面。它會默默地失敗默终,且不會報錯椅棺。假設(shè)歷史記錄只有 5 條犁罩,然后你試圖后退/前進(jìn) 10 個頁面,這就屬于超出范圍土陪。若
delta
不是Number
類型炉爆,內(nèi)部先進(jìn)行隱式類型轉(zhuǎn)換成對應(yīng)的Number
值,再執(zhí)行go()
方法甥材。比如常挚,history.go(true)
相當(dāng)于history.go(1)
,history.go(NaN)
相當(dāng)于history.go(0)
源哩。
5. 小結(jié)
就 back()
鞋吉、forward()
、go()
三個方法励烦,簡單總結(jié)一下:
僅調(diào)用以上三個方法谓着,不會使得歷史記錄或
history.length
發(fā)生改變。調(diào)用以上三個方法坛掠,通常是從瀏覽器緩存中加載頁面赊锚。在 Network 選項卡中往往可以看到類似
from disk cache
的字樣。當(dāng)超出了當(dāng)前標(biāo)簽的歷史記錄范圍屉栓,調(diào)用以上三個方法都不會執(zhí)行任何操作舷蒲,默默地失敗且不報錯。
- 請注意友多,若后退/前進(jìn)時牲平,只是錨點發(fā)生變化,是不會重新加載頁面域滥。
四纵柿、HTML5 History API
History API 作為 HTML5 的新特性之一,解決了 Fragment 的一些痛點启绰,包括 URL 分享昂儒,SEO 優(yōu)化等都得到了很好的解決。這些新特性都內(nèi)置于 History 對象之中:
history.state
history.scrollRestoration
history.pushState()
history.replaceState()
1. history.state
只讀酬土,該屬性返回當(dāng)前頁面的狀態(tài)值荆忍。
const currentState = history.state
只有通過 pushState()
和 replaceState()
方法產(chǎn)生的歷史記錄,這個屬性才會有相應(yīng)的值撤缴,否則為 null
刹枉。
請注意,
history.state
的返回值是一份拷貝值屈呕。
2. history.scrollRestoration
可讀寫微宝,該屬性允許 Web 應(yīng)用程序在歷史導(dǎo)航上顯式地設(shè)置默認(rèn)滾動恢復(fù)行為。此屬性可以是自動的(auto
)或者手動的(manual
)虎眨。
3. history.pushState()
在當(dāng)前位置蟋软,總會產(chǎn)生一條新的記錄镶摘,并保存在歷史記錄里面,而且 history.length
也會增加岳守。若新舊 URL 不相同的情況下凄敢,也伴隨著 URL 的變化。
請注意湿痢,它并不會重載頁面涝缝。同樣的還有
history.pushState()
方法。
偽代碼:
// 假設(shè)歷史記錄(稱為 histories)有 5 個頁面譬重,當(dāng)前處于最后一個頁面拒逮,即 5 位置。
const histories = [1, 2, 3, 4, 5]
// 若后退 2 頁
history.go(-2) // 此時臀规,我們的頁面處于歷史記錄中的 3 位置滩援。
// 插入一個新記錄,假設(shè)新記錄稱為 6
history.pushState(
{ state: 'new' }, // 通常是對象塔嬉,可通過 history.state 獲取
'custom title', // 幾乎所有瀏覽器都會忽略此參數(shù)玩徊,所以是沒用的
'https://xxx.com' // 該 URL 必須跟當(dāng)前網(wǎng)頁是同源的,否則會報錯谨究。
)
// 執(zhí)行 pushState() 方法后佣赖,不會加載頁面
window.location.href // "https://xxx.com"
window.document.URL // "https://xxx.com"
window.document.title // 這還是原來的標(biāo)題,而不是 "custom title"
window.history.state // { state: 'new' }
window.history.length // 4
histories // [1, 2, 3, 6]
語法
history.pushState(state, title[, url])
state
- 可以是任意值记盒,通常為(可序列化)對象。它可以通過history.state
獲取到外傅,或者在popstate
事件的事件對象中體現(xiàn)纪吮。title
- 請忽視該參數(shù)的作用,它幾乎被所有瀏覽器所忽略萎胰,但不得不傳碾盟。通常,會傳遞''
技竟、null
或undefined
冰肴。url
- (可選)新 URL,它最終體現(xiàn)在地址欄的 URL 上榔组。請注意熙尉,新 URL 與當(dāng)前頁面 URL 必須是同源的(即location.origin
相同),否則將會拋出錯誤搓扯。
注意點
參數(shù) state
是可序列化對象检痰,怎么理解?
個人猜測是那些可作用域 JSON.stringify()
方法的原始值或引用值锨推,具體沒去深究铅歼。舉個例子公壤,下面這個將會拋出錯誤:
history.pushState(
{ fn: function () {} },
'',
location.href + 'abc'
)
// DOMException: Failed to execute 'pushState' on 'History':
// function() {} could not be cloned.
較為冷門的東西,參數(shù) url
也支持 絕對 URL 和相對 URL椎椰。舉些例子:
// 假設(shè)當(dāng)前 URL 如下厦幅,它的 Origin 是 https://developer.mozilla.org
// https://developer.mozilla.org/zh-CN/docs/Web/API/History/pushState
// 1?? 完整網(wǎng)站,可理解為絕對路徑慨飘,將會變成:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript
history.pushState({}, '', 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript')
// 2?? 含 / 可理解為相對路徑确憨,相對于當(dāng)前 Origin,將會變成 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript
history.pushState({}, '', '/zh-CN/docs/Web/JavaScript')
// 3?? 若為 ../xxx 形式套媚,相對于當(dāng)前 URL缚态,將會變成 https://developer.mozilla.org/zh-CN/docs/Web/API/History/go
history.pushState({}, '', '../History/go')
// 4?? 若為字符串,將會變成 https://developer.mozilla.org/zh-CN/docs/Web/API/History/hhh
history.pushState({}, '', 'hhh')
另外堤瘤,使用 history.pushState()
可以改變 referrer
玫芦。
4. history.replaceState()
參數(shù)約定與 pushState()
完全一致,語法如下:
history.replaceState(stateObj, title[, url])
replaceState()
也總會產(chǎn)生一條新記錄本辐,并用新記錄替換掉當(dāng)前頁面對應(yīng)的歷史記錄桥帆。
偽代碼...
const histories = [1, 2, 3, 4, 5] // 當(dāng)前處于 5 位置
history.replaceState({}, '', 'new-url') // 創(chuàng)建一個新記錄,假設(shè)稱為 6
// 新記錄 6 會替換記錄 5
histories // 歷史記錄慎皱,將會變?yōu)?[1, 2, 3, 4, 6]
history.length // 5老虫,未發(fā)生變化
5. pushState 與 replaceState 區(qū)別
還是偽代碼哈:
// 假設(shè)歷史記錄里,有 5 條記錄茫多,并處于歷史記錄的頂端祈匙,即第五個位置
const histories = [1, 2, 3, 4, 5]
// 后退 2 個頁面,即當(dāng)前處于第三個位置
history.go(-2)
// 使用 replaceState 產(chǎn)生一條新記錄(假設(shè)稱為 6)天揖,
// 它的作用是用新記錄替換當(dāng)前記錄夺欲,因此記錄 3 被新記錄 6 所替換
// 但仍處于歷史記錄的第三個位置
history.replace('6', '', 'new-url-6')
histories // [1, 2, 6, 4, 5]
history.length // 5
// 使用 pushState 產(chǎn)生一條新記錄(假設(shè)稱為 7),
// 它的作用是在當(dāng)前記錄后面添加一條新記錄今膊,
// 它會刪除當(dāng)前記錄后面的所有記錄些阅,然后再往后追加一條,
// 同時斑唬,它的位置也會前往至歷史記錄頂端市埋,即第四個位置。
history.replace('7', '', 'new-url-7')
histories // [1, 2, 6, 7]
history.length // 4
如果用 Array.prototype.splice()
來類比的話恕刘,可以這樣:
const arr = [1, 2, 3, 4, 5]
// pushState 類似于
arr.splice(curIndex + 1, 1000, newItem)
// replaceState 類似于
arr.splice(curIndex, 1, newItem)
// 注釋:
// arr 表示歷史記錄
// curIndex 表示當(dāng)前記錄的位置缤谎,
// 1000 只是為了表達(dá)刪除完 curIndex + 1 后面的所有項,可用 arr.length 等替代
// newItem 表示新記錄
簡單來說褐着,pushState()
和 replaceState()
區(qū)別如下:
- 兩者都會產(chǎn)生新的記錄弓千。
- 前者會先移除當(dāng)前記錄后面的所有記錄,并將新記錄追加到歷史記錄頂端献起。而后者僅會用新記錄替換當(dāng)前記錄洋访,后面的記錄并不受影響(若有)镣陕。
- 兩者都會使得歷史記錄發(fā)生變化。后者不會使得
history.length
發(fā)生改變姻政。
另外呆抑,對于歷史記錄及其數(shù)量,history.replaceState()
與 location.replace()
表現(xiàn)是一致的汁展,只是后者有可能會重載頁面鹊碍。
6. popstate 事件
調(diào)用 pushState()
、replaceState()
方法的話食绿,既不會觸發(fā) popstate
事件監(jiān)聽器侈咕,也不會觸發(fā) hashchange
事件監(jiān)聽器(即使新舊 URL 只是 Fragment 部分不同)。這個也是 History API 的優(yōu)點之一器紧。
其余的下一節(jié)介紹...
五耀销、hashchange 和 popstate 事件
1. hashchange 事件
IE8 及以上瀏覽器都支持 hashchange
事件。注冊事件監(jiān)聽器铲汪,如下:
function listener(e) {
// 可通過 e.newURL 和 e.oldURL 獲取完整的新舊 URL 值(只讀)
// do something...
}
// 通過 DOM2 注冊(更推薦)
window.addEventListener('hashchange', listener)
// 通過 DOM0 注冊
window.onhashchange = listener
對于事件監(jiān)聽器的兼容性熊尉,可看:細(xì)讀 JavaScript 事件詳解。
除了通過調(diào)用 pushState()
掌腰、replaceState()
使 URL 的 Fragment 部分發(fā)生變化狰住,不會觸發(fā) hashchange
事件之外,其他任何方式致使 Fragment 發(fā)生改變齿梁,都會觸發(fā)該事件催植,包括 history.forward()
、history.back()
勺择、location.hash
查邢、<a href="#anchor">
、操作瀏覽器后退/前進(jìn)按鈕酵幕、修改地址欄 Fragment 值等方式。
本文提到的 Fragment 均指 URL 上跟在
#
后面的所有字符串缓苛。
2. popstate 事件
需要注意的是芳撒,調(diào)用
history.pushState()
或history.replaceState()
不會觸發(fā)popstate
事件。
只有通過點擊瀏覽器后退/前進(jìn)按鈕未桥,或者通過腳本調(diào)用 history.back()
笔刹、history.forward()
、history.go()
(go(0)
除外)方法冬耿,popstate
事件才會被觸發(fā)舌菜。
function listener(e) {
// 通過 e.state 可以獲取當(dāng)前記錄的狀態(tài)對象對應(yīng)的拷貝值。
// 非 pushState亦镶、replaceState 產(chǎn)生的記錄日月,該屬性值都為 null袱瓮。
}
// 通過 DOM2 注冊(更推薦)
window.addEventListener('popstate', listener)
// 通過 DOM0 注冊
window.onpopstate = listener
另外,不同瀏覽器在加載頁面時處理 popstate
事件的形式可能存在差異爱咬。
3. 小結(jié)
下面總結(jié)了很多條尺借,很大可能會記不住,沒關(guān)系:
通過
back()
精拟、forward()
燎斩、go()
或瀏覽器后退/前進(jìn)按鈕切換的過程,一定會觸發(fā)popstate
事件蜂绎。若伴隨著 Fragment 的變化栅表,也會觸發(fā)hashchange
事件。(與記錄產(chǎn)生的方式無關(guān))在調(diào)用
pushState()
师枣、replaceState()
時怪瓶,既不會觸發(fā)popstate
事件,也不會觸發(fā)hashchange
事件(即使包括 Fragment 發(fā)生改變)坛吁。除了
pushState()
劳殖、replaceState()
,其他任何方式致使 Fragment 發(fā)生改變拨脉,都會觸發(fā)hashchange
事件哆姻。通過
location.hash = 'foo'
方式致使 Fragment 發(fā)生改變,會觸發(fā)hashchange
事件玫膀,而不會觸發(fā)popstate
事件矛缨。而通過
window.location = '#foo'
或<a href="#foo">
形式致使 Fragment 發(fā)生改變,同時觸發(fā)hashchange
和popstate
事件帖旨。
簡化記憶:
其實常用的方法只有三個:history.pushState()
箕昭、history.replaceState()
、location.hash
解阅。最重要的是落竹,通常一個項目不會兩者混用,不然得多亂啊货抄。例如 React 述召、Vue 提供的路由系統(tǒng)只能二選一:
- History 模式:使用 HTML5 History API,更符合未來發(fā)展的方向
- Hash 模式:利用
location.hash
和hashchange
事件實現(xiàn)蟹地,兼容性較好积暖,且服務(wù)端無需額外的配置。
所以怪与,就簡化成兩條:
- 調(diào)用
pushState()
夺刑、replaceState()
時,不會觸發(fā)popstate
事件。其他 URL 的變化都會觸發(fā)此事件遍愿。 - 當(dāng) URL 的 Fragment 部分發(fā)生改變存淫,都會觸發(fā)
hashchange
事件。
六错览、比較
History 模式和 Hash 模式纫雁,在不重載頁面的前提下,實現(xiàn)了局部刷新的能力倾哺。
從某種程度來說, 調(diào)用 pushState()
和 window.location= "#foo"
基本上一樣, 他們都會在當(dāng)前的歷史記錄中創(chuàng)建和激活一個新的歷史條目轧邪。但是 pushState()
有以下優(yōu)勢:
新的 URL 可以是任何和當(dāng)前 URL 同源的 URL。但是設(shè)置
window.location
只會在你只設(shè)置 Fragment 的時候才會使當(dāng)前的 URL羞海。非強制修改 URL忌愚。相反,設(shè)置
window.location = '#foo'
僅僅會在錨的值不是#foo
情況下創(chuàng)建一條新的歷史記錄却邓。可以在新的歷史記錄中關(guān)聯(lián)任何數(shù)據(jù)硕糊。
window.location = "#foo"
形式的操作,你只可以將所需數(shù)據(jù)寫入錨的字符串中腊徙。
注意: pushState()
不會造成 hashchange
事件調(diào)用简十,即使新舊 URL 只是 Fragment 不同。
七撬腾、React Router
在 React 的路由系統(tǒng)中螟蝙,修改路由、監(jiān)聽路由實際上是由 history 庫中 createBrowserHistory()
或 createHashHistory()
方法所構(gòu)造的 history
對象(有別于 window.history
對象)去操作的民傻。
在 React 中胰默,路由操作有這幾種方法。
props.history.push()
- 新增一條歷史記錄
props.history.replace()
- 新增一條記錄漓踢,并替換當(dāng)前記錄
props.history.go()
- 后退/前進(jìn)
props.history.goBack()
- 即props.history.go(-1)
props.history.goForward()
- 即props.history.go(1)
其中牵署,props.history.go()
實際上就是調(diào)用了 window.history.go()
方法。前面兩個方法喧半,在不同路由模式下奴迅,調(diào)用的能力是不一樣的。
在
BrowserRouter
模式下挺据,對應(yīng)window.history.pushState()
和window.history.replaceState()
方法取具。在
HashRouter
模式下,對應(yīng)window.location.hash
和window.location.replace()
方法吴菠。
在 React Router 中,路由更新以加載不同的組件浩村,是通過 React Context
實現(xiàn)的做葵,即 Provider/Consumer
的模式。當(dāng)路由更新時心墅,Provider
的 value
屬性會發(fā)生變化酿矢,使得對應(yīng)消費 Consumer
的組件得以更新榨乎。
前面我們提到過,調(diào)用 history.pushState()
或 history.replaceState()
并不會觸發(fā) popstate 事件監(jiān)聽函數(shù)瘫筐。那么 React Router 是怎么知道 URL 發(fā)生變化的呢蜜暑?
首先在選擇使用 <BrowserRouter>
或 <HashHistory>
組件時,它內(nèi)部設(shè)置了一個監(jiān)聽器策肝,這個監(jiān)聽器的回調(diào)函數(shù)里面有一個 setState()
方法肛捍。當(dāng)我們在 React 組件中使用 props.history.push()
方法去跳轉(zhuǎn)頁面時,它除了會執(zhí)行 window.history.pushState()
使得 URL 發(fā)生改變之外之众,還會執(zhí)行前面提到的監(jiān)聽器拙毫,那么監(jiān)聽器的回調(diào)函數(shù)也會被執(zhí)行,既然里面有 setState()
操作棺禾,就會使得 <BrowserRouter>
或 <HashHistory>
組件執(zhí)行一次更新缀蹄,那么該組件的 Provider
就會更新,React Router 的 Consumer
們根據(jù) URL 來匹配對應(yīng)的路由膘婶,以加載相應(yīng)的組件缺前。因此,我們就能在瀏覽器中看到 URL 的變化以及頁面的跳轉(zhuǎn)悬襟。
未完待續(xù)...