【面試題】2055- 談?wù)勄岸寺酚傻膶崿F(xiàn)原理【hash&history】
共 13946字,需瀏覽 28分鐘
·
2024-05-23 09:35
當談到前端路由時,指的是在前端應(yīng)用中管理頁面導(dǎo)航和URL的機制。前端路由使得單頁應(yīng)用(Single-Page Application,SPA)能夠在用戶與應(yīng)用交互時動態(tài)地加載不同的視圖,而無需每次都重新加載整個頁面。
在前端開發(fā)中,常用的前端路由庫有很多,比如React Router、Vue Router和Angular Router等。這些庫提供了一組API和組件,用于定義路由規(guī)則、處理導(dǎo)航事件和渲染相應(yīng)的視圖。
簡單了解前端路由后,那么前端路由實現(xiàn)的原理是什么呢?
請看今天的分享:
?vue-router是前端路由,但是前端路由不是vue-router,這是個包含關(guān)系
?
路由
路由一詞最早來自服務(wù)器,和前端沒有關(guān)系。當你想要從服務(wù)器中讀取某個盤的文件,這個文件的路徑就是路由。也就是說「路由是服務(wù)器端用來描述路徑的,或者是說url和文件的映射關(guān)系」
后來因為前端的SPA單頁應(yīng)用,前端也借鑒了路由這個概念。瀏覽器的url變了需要映射到頁面的某個組件,url變了需要展示某個組件。/home和Home.vue,/about和About.vue就是一一映射的關(guān)系。「前端借鑒路由的稱呼來描述url和組件的映射關(guān)系」。這個時候你就想起來router中index.js文件中,一個path對應(yīng)一個component,也就是一個路徑對應(yīng)一個組件
實現(xiàn)路由需要解決的問題
-
如何修改url還不引起頁面的刷新 -
如何知道url變化了
若是能解決這兩個問題就可以實現(xiàn)前端路由了。
哈希Hash
哈希是一種值,按照某種規(guī)則生成的一串值,用來代表一個唯一的文件,文件名后加一個哈希值,可以看到文件是否被修改過。
在瀏覽器中也有hash這個概念,url中接一個#,#后的值就是哈希值,按道理url變了,頁面一定會刷新,但是哈希是個特例,放個哈希值就是不會刷新頁面,這樣,我們就解決了第一個問題,修改url不引起頁面的刷新
「在瀏覽器url后加個哈希值,哈希值的變更不會引起瀏覽器頁面的刷新」
下面利用哈希模式實現(xiàn)路由
哈希手搓一個路由
我們新建一個hash.html文件,放兩個a標簽,但是a標簽有個機制,就是點擊必定會引起頁面的刷新。但瀏覽器的機制是哈希值的變更不會引起頁面刷新,所以地址放哈希值可以解決這一問題
<ul>
<li><a href="#/home">首頁</a></li>
<li><a href="#/about">關(guān)于</a></li>
</ul>
<div id="routeView">
<!-- 放一個代碼片段 點擊首頁首頁代碼片段生效,反之關(guān)于生效-->
</div>
現(xiàn)在模擬一個場景,如果點擊首頁,routeView容器展示首頁的內(nèi)容,點擊關(guān)于routeView容器展示關(guān)于頁面的內(nèi)容,如果能夠?qū)崿F(xiàn),路由就可以實現(xiàn)了
自行封裝一個路由,先寫一個路由的映射關(guān)系
<script>
const routes = [
{
path: '#/home',
component: '首頁內(nèi)容'
},
{
path: '#/about',
component: '關(guān)于頁面內(nèi)容'
}
]
</script>
點擊首頁,展示首頁內(nèi)容,點擊關(guān)于,展示關(guān)于首頁內(nèi)容。
接下來的事情就是點擊url,我們需要知道url的變化。我們不可能給按鈕添加一個點擊事件,如果項目大起來,按鈕很多,每次點擊一個按鈕都判斷一次url的變化,會非常的不優(yōu)雅。
js自帶一個hashchange事件,它可以自動監(jiān)聽hash值的變更。當我們點擊首頁的時候,下面的代碼都會執(zhí)行一次,因為hash值變了
window.addEventListener('hashchange', () => {
console.log('changed')
})
這樣,第二個問題我們已經(jīng)解決了。非常之簡單!
我們現(xiàn)在把監(jiān)聽器的回調(diào)函數(shù)寫出來,拿到當前的哈希值,去對應(yīng)component。
window.addEventListener('hashchange', onHashChange)
function onHashChange() {
console.log(location)
}
location代表window窗口的url,我們運行打印這個location看看
看到?jīng)]有,里面剛好有個hash值,我們可以把這個拿出來去對應(yīng)component!
這個時候直接去數(shù)組中匹配就可以,forEach遍歷
function onHashChange() {
console.log(location)
routes.forEach((item, index) => {
if(item.path === location.hash) {
routeView.innerHTML = item.component
}
})
}
當然,記得拿到routeView的dom結(jié)構(gòu)
這樣寫會有個問題,就是頁面剛加載完畢的時候不會去加載當前的路由,想要hashchange在頁面初次加載的時候觸發(fā)一次,那就給一個監(jiān)聽dom結(jié)構(gòu)的事件,dom一出來就會執(zhí)行,也就是說頁面加載完畢就調(diào)用一次hashchange
window.addEventListener('DOMContentLoaded', onHashChange)
好了,最終的hash.html如下
<body>
<!-- 模擬單頁頁面應(yīng)用 -->
<ul>
<li><a href="#/home">首頁</a></li>
<li><a href="#/about">關(guān)于</a></li>
<!-- 判斷url的變化,綁定點擊事件不好,頁面過多就很累贅,有個hashchange的官方方法 -->
</ul>
<div id="routeView">
<!-- 放一個代碼片段 點擊首頁首頁代碼片段生效,反之關(guān)于生效-->
</div>
<script>
const routes = [
{
path: '#/home',
component: '首 容'
},
{
path: '#/about',
component: '關(guān)于頁面內(nèi)容'
}
]
const routeView = document.getElementById('routeView')
window.addEventListener('DOMContentLoaded', onHashChange) // 與vue的聲明周期一個道理,dom一加載完畢就觸發(fā)
window.addEventListener('hashchange', onHashChange)
function onHashChange() {
console.log(location) // url詳情,里面就有個hash值 liveserver可以幫你把html跑成服務(wù)器
routes.forEach((item, index) => {
if(item.path === location.hash) {
routeView.innerHTML = item.component
}
})
}
</script>
</body>
其實這就是vue-router中兩種模式之一哈希模式,哈希模式就是這樣是實現(xiàn)的。
修改地址欄
-
a標簽 -
瀏覽器前進后退 -
window.location
以上方式導(dǎo)致url變更都會觸發(fā)hashchange事件。
那問題來了,history模式?jīng)]有哈希是如何實現(xiàn)的呢?沒有哈希值a標簽一定會引起頁面的刷新,如何解決?我們繼續(xù)看下去
?history用得更多,二者沒有本質(zhì)區(qū)別,僅僅是因為哈希模式的url多了個#很丑,所以用的少
?
history手搓一個路由
我們先看下history在mdn中的介紹
History - Web API 接口參考 | MDN (mozilla.org)(https://developer.mozilla.org/zh-CN/docs/Web/API/History)
?文檔中介紹:history接口允許操作瀏覽器的曾經(jīng)在標簽頁或者框架里訪問的會話歷史記錄
?
我們重點看一個history自帶的方法pushState
它可以修改url且不引起頁面的刷新
瀏覽器中有個會話歷史棧,它可以維護你的訪問路徑,有了這個你返回就可以按照棧的順序進行前進回退。
pushState提到了popState,他是靠popState監(jiān)聽url的改變的,并且僅當瀏覽器前進后退時生效
既然如此,我們現(xiàn)在開始手搓
同樣是上面的情景,兩個a標簽,一個首頁,一個關(guān)于頁面。
<ul>
<li><a href="/home">首頁</a></li>
<li><a href="/about">關(guān)于</a></li>
</ul>
<div id="routeView">
給個url和組件的對應(yīng)關(guān)系數(shù)組,已經(jīng)不用哈希了
<script>
const routes = [
{
path: '/home',
component: '首頁內(nèi)容'
},
{
path: '/about',
component: '<h1>關(guān)于頁面內(nèi)容</h1>'
}
]
</script>
a標簽有個默認的頁面跳轉(zhuǎn)效果,既然現(xiàn)在不用哈希,我們就需要自己把a標簽的頁面跳轉(zhuǎn)刷新效果干掉
先拿到所有的a標簽
const links = document.querySelectorAll('li a')
再去禁用掉默認的跳轉(zhuǎn)行為,它跳轉(zhuǎn)一定會帶來刷新,要干掉它
links.forEach(a => {
a.addEventListener('click', (e) => {
console.log(e)
e.preventDefault() // 阻止a的跳轉(zhuǎn)行為
})
})
我們可以打印看看這個事件參數(shù),順著原型鏈找到event對象,里面有個preventDefault,這個就是禁用a標簽?zāi)J的跳轉(zhuǎn)行為
接下來添加一個可以修改url又不引起頁面刷新的方法,就是pushState,具體用法查看mdn
他有三個參數(shù),第一個參數(shù)是JavaScript對象,一般不需要,給個null就好,第二個參數(shù)由于歷史原因,寫個空字符,不寫會有問題,第三個參數(shù)是新的url
新的url肯定是點了什么放什么url,所以我需要讀取到a標簽的href值
a.getAttribute('href')
以上方法是核心,這里已經(jīng)實現(xiàn)了哈希一樣的效果,并且沒有難看的#,pushState的核心原理就是它會往瀏覽器的歷史棧中塞一個值進去,讓瀏覽器顯示新的值,并且不引起頁面的刷新
接下來就是要去感知到url的變化,去一一對應(yīng)組件的展示
我們寫一個函數(shù),來實現(xiàn)這個功能。還是一樣的,先拿到當前的瀏覽器地址
location.pathname
然后再進行遍歷,去添加組件
routes.forEach((item) => {
if(item.path === location.pathname) {
routeView.innerHTML = item.component
}
})
同樣的,我們需要在頁面初次加載的時候調(diào)用函數(shù)
但是瀏覽器的前進后退沒有觸發(fā)上面的遍歷函數(shù),popState剛好填補這個空缺
window.addEventListener('popState', onPopState)
好了,最終的history.html如下
<body>
<ul>
<li><a href="/home">首頁</a></li>
<li><a href="/about">關(guān)于</a></li>
</ul>
<div id="routeView">
</div>
<script>
const routes = [
{
path: '/home',
component: '首頁內(nèi)容'
},
{
path: '/about',
component: '<h1>關(guān)于頁面內(nèi)容</h1>'
}
]
const routeView = document.getElementById('routeView')
window.addEventListener('DOMContentLoaded', onLoad)
window.addEventListener('popstate', onPopState)
function onLoad() {
const links = document.querySelectorAll('li a') // 獲取所有的li下的a標簽
// console.log(links)
links.forEach((a) => {
// 禁用a標簽的默認跳轉(zhuǎn)行為
a.addEventListener('click', (e) => {
console.log(e)
e.preventDefault() // 阻止a的跳轉(zhuǎn)行為
history.pushState(null, '', a.getAttribute('href')) // 核心方法 a.getAttribute('href')獲取a標簽下的href屬性
// 映射對應(yīng)的dom
onPopState()
})
})
}
function onPopState() {
console.log(location.pathname)
routes.forEach((item) => {
if(item.path === location.pathname) {
routeView.innerHTML = item.component
}
})
}
</script>
</body>
效果如下
最后
本期文章主要介紹了路由這個概念,以及重點講前端實現(xiàn)路由的兩種模式,哈希剛好就是瀏覽器承認他,接了哈希值改變url不會引起頁面的刷新,然后通過location.hash得知哈希值;history就是有個方法,可以改變url不引起頁面刷新的pushState,通過location.pathname得知url,這個模式下前進回退需要通過popState事件來觸發(fā)。
?本次學(xué)習(xí)代碼已上傳至GitHub學(xué)習(xí)倉庫:(https://github.com/DolphinFeng/CodeSpace)
?
?原文地址:https://juejin.cn/post/7321049431489544229
作者:Dolphin_海豚
?
