【W(wǎng)eb技術(shù)】913- 談?wù)勀銓?duì)前端路由的理解

來(lái)自:掘金
作者:尼克陳
鏈接:https://juejin.cn/post/6917523941435113486
一篇文章,不可能做的面面俱到,全部受眾。希望大家?guī)еl(fā)散思維去看文章,將文章涉及的知識(shí)點(diǎn),吸收為己所用。這樣看完一篇文章,才能有所收獲。
前言
好了不裝了,今天我就化身性感面試官在線問(wèn)大家一個(gè)問(wèn)題,“談?wù)勀銓?duì)前端路由的理解”。看到這個(gè)問(wèn)題,那回答可多了去了。但是換位思考一下,你問(wèn)候選人這個(gè)問(wèn)題的時(shí)候,你想要得到什么答案?以我個(gè)人拙見(jiàn),我希望候選人能從全局解讀這個(gè)問(wèn)題,大致以下三點(diǎn)。
1、為什么會(huì)出現(xiàn)前端路由。
2、前端路由解決了什么問(wèn)題。
3、前端路由實(shí)現(xiàn)的原理是什么。
我們帶著這三個(gè)問(wèn)題,繼續(xù)往下看,閱讀的過(guò)程中如果同學(xué)們有自己的見(jiàn)解,可以評(píng)論區(qū)發(fā)表自己的看法。如果覺(jué)得講的內(nèi)容讓你有了新的見(jiàn)解,請(qǐng)獻(xiàn)上你寶貴的一贊??,這將是我繼續(xù)寫作的動(dòng)力。
傳統(tǒng)頁(yè)面
這里不糾結(jié)叫法,凡是整個(gè)項(xiàng)目都是 DOM 直出的頁(yè)面,我們都稱它為“傳統(tǒng)頁(yè)面”(SSR 屬于首屏直出,這里我不認(rèn)為是傳統(tǒng)頁(yè)面的范疇)。那么什么是 DOM 直出呢?簡(jiǎn)單說(shuō)就是在瀏覽器輸入網(wǎng)址后發(fā)起請(qǐng)求,返回來(lái)的 HTML 頁(yè)面是最終呈現(xiàn)的效果,那就是 DOM 直出。并且每次點(diǎn)擊頁(yè)面跳轉(zhuǎn),都會(huì)重新請(qǐng)求 HTML 資源。耳聽(tīng)為虛,眼見(jiàn)為實(shí)。我們以這個(gè)地址為例,驗(yàn)證以下上述說(shuō)法。
www.cnblogs.com/han-1034683…

定眼一看,就能明白上圖在描述什么。沒(méi)錯(cuò),博客園就是一個(gè)傳統(tǒng)頁(yè)面搭建而成的網(wǎng)站,每次加載頁(yè)面,都會(huì)返回 HTML 資源以及里面的 CSS 等靜態(tài)資源,組合成一個(gè)新的頁(yè)面。
“瞎了”的同學(xué),我再教一個(gè)方法,就是在瀏覽器頁(yè)面右鍵,點(diǎn)擊“顯示網(wǎng)頁(yè)源代碼”,打開(kāi)后如下所示:

網(wǎng)頁(yè)上能看到什么圖片或文字,你能在上述圖片中找到相應(yīng)的 HTML 結(jié)構(gòu),那也屬于傳統(tǒng)頁(yè)面,也就是 DOM 直出。
單頁(yè)面
時(shí)代在進(jìn)步,科技在發(fā)展,面對(duì)日益增長(zhǎng)的網(wǎng)頁(yè)需求,網(wǎng)頁(yè)開(kāi)始走向模塊化、組件化的道路。隨之而來(lái)的是代碼的難以維護(hù)、不可控、迭代艱難等現(xiàn)象。面臨這種情況,催生出不少優(yōu)秀的現(xiàn)代前端框架,首當(dāng)其沖的便是 React 、 Vue 、 Angular 等著名單頁(yè)面應(yīng)用框架。而這些框架有一個(gè)共同的特點(diǎn),便是“通過(guò) JS 渲染頁(yè)面”。
舉個(gè)例子,以前我們直出 DOM ,而現(xiàn)在運(yùn)用這些單頁(yè)面框架之后, HTML 頁(yè)面基本上只有一個(gè) DOM 入口,大致如下所示:

所有的頁(yè)面組件,都是通過(guò)運(yùn)行上圖底部的 app.js 腳本,掛載到 <div id="root"></div> 這個(gè)節(jié)點(diǎn)下面。用一個(gè)極其簡(jiǎn)單的 JS 展示掛載這一個(gè)步驟:
<body>
<div id="root"></div>
<script>
const root = document.getElementById('root') // 獲取根節(jié)點(diǎn)
const divNode = document.createElement('div') // 創(chuàng)建 div 節(jié)點(diǎn)
divNode.innerText = '你媽貴姓?' // 插入內(nèi)容
root.appendChild(divNode) // 插入根節(jié)點(diǎn)
</script>
</body>

脫去所有的凡塵世俗,最本真的單頁(yè)項(xiàng)目運(yùn)行形式便是如此。注意,我要點(diǎn)題了啊?。?!

既然單頁(yè)面是這樣渲染的,那如果我有十幾個(gè)頁(yè)面要互相跳轉(zhuǎn)切換,咋整?。???這時(shí)候 前端路由 應(yīng)運(yùn)而生,它的出現(xiàn)就是為了解決單頁(yè)面網(wǎng)站,通過(guò)切換瀏覽器地址路徑,來(lái)匹配相對(duì)應(yīng)的頁(yè)面組件。我們通過(guò)一張丑陋的圖片來(lái)理解這個(gè)過(guò)程:

前端路由 會(huì)根據(jù)瀏覽器地址欄 pathname 的變化,去匹配相應(yīng)的頁(yè)面組件。然后將其通過(guò)創(chuàng)建 DOM 節(jié)點(diǎn)的形式,塞入根節(jié)點(diǎn) <div id="root"></div> 。這就達(dá)到了無(wú)刷新頁(yè)面切換的效果,從側(cè)面也能說(shuō)明正因?yàn)闊o(wú)刷新,所以 React 、 Vue 、 Angular 等現(xiàn)代框架在創(chuàng)建頁(yè)面組件的時(shí)候,每個(gè)組件都有自己的 生命周期 。
原理
前端路由 插件比較火的倆框架對(duì)應(yīng)的就是 Vue-Router 和 React-Router ,但是它們的邏輯,歸根結(jié)底還是一樣的,用殊途同歸四個(gè)字,再合適不過(guò)。
通過(guò)分析哈希模式和歷史模式的實(shí)現(xiàn)原理,讓大家對(duì)前端路由的原理有一個(gè)更深刻的理解。
哈希模式
a 標(biāo)簽錨點(diǎn)大家應(yīng)該不陌生,而瀏覽器地址上 # 后面的變化,是可以被監(jiān)聽(tīng)的,瀏覽器為我們提供了原生監(jiān)聽(tīng)事件 hashchange ,它可以監(jiān)聽(tīng)到如下的變化:
點(diǎn)擊 a標(biāo)簽,改變了瀏覽器地址瀏覽器的前進(jìn)后退行為 通過(guò) window.location方法,改變?yōu)g覽器地址
接下來(lái)我們利用這些特點(diǎn),去實(shí)現(xiàn)一個(gè) hash 模式的簡(jiǎn)易路由:在線運(yùn)行
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hash 模式</title>
</head>
<body>
<div>
<ul>
<li><a href="#/page1">page1</a></li>
<li><a href="#/page2">page2</a></li>
</ul>
<!--渲染對(duì)應(yīng)組件的地方-->
<div id="route-view"></div>
</div>
<script type="text/javascript">
// 第一次加載的時(shí)候,不會(huì)執(zhí)行 hashchange 監(jiān)聽(tīng)事件,默認(rèn)執(zhí)行一次
// DOMContentLoaded 為瀏覽器 DOM 加載完成時(shí)觸發(fā)
window.addEventListener('DOMContentLoaded', Load)
window.addEventListener('hashchange', HashChange)
// 展示頁(yè)面組件的節(jié)點(diǎn)
var routeView = null
function Load() {
routeView = document.getElementById('route-view')
HashChange()
}
function HashChange() {
// 每次觸發(fā) hashchange 事件,通過(guò) location.hash 拿到當(dāng)前瀏覽器地址的 hash 值
// 根據(jù)不同的路徑展示不同的內(nèi)容
switch(location.hash) {
case '#/page1':
routeView.innerHTML = 'page1'
return
case '#/page2':
routeView.innerHTML = 'page2'
return
default:
routeView.innerHTML = 'page1'
return
}
}
</script>
</body>
</html>
當(dāng)然,這是很簡(jiǎn)單的實(shí)現(xiàn),真正的 hash 模式,還要考慮到很多復(fù)雜的情況,大家有興趣就去看看源碼。
瀏覽器展示效果如下:
歷史模式
history 模式會(huì)比 hash 模式稍麻煩一些,因?yàn)?nbsp;history 模式依賴的是原生事件 popstate ,下面是來(lái)自 MDN 的解釋:

小知識(shí):pushState 和 replaceState 都是 HTML5 的新 API,他們的作用很強(qiáng)大,可以做到改變?yōu)g覽器地址卻不刷新頁(yè)面。這是實(shí)現(xiàn)改變地址欄卻不刷新頁(yè)面的重要方法。
包括 a 標(biāo)簽的點(diǎn)擊事件也是不會(huì)被 popstate 監(jiān)聽(tīng)。我們需要想個(gè)辦法解決這個(gè)問(wèn)題,才能實(shí)現(xiàn) history 模式。

**解決思路:**我們可以通過(guò)遍歷頁(yè)面上的所有 a 標(biāo)簽,阻止 a 標(biāo)簽的默認(rèn)事件的同時(shí),加上點(diǎn)擊事件的回調(diào)函數(shù),在回調(diào)函數(shù)內(nèi)獲取 a 標(biāo)簽的 href 屬性值,再通過(guò) pushState 去改變?yōu)g覽器的 location.pathname 屬性值。然后手動(dòng)執(zhí)行 popstate 事件的回調(diào)函數(shù),去匹配相應(yīng)的路由。邏輯上可能有些饒,我們用代碼來(lái)解釋一下:在線地址
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>History 模式</title>
</head>
<body>
<div>
<ul>
<li><a href="/page1">page1</a></li>
<li><a href="/page2">page2</a></li>
</ul>
<div id="route-view"></div>
</div>
<script type="text/javascript">
window.addEventListener('DOMContentLoaded', Load)
window.addEventListener('popstate', PopChange)
var routeView = null
function Load() {
routeView = document.getElementById('route-view')
// 默認(rèn)執(zhí)行一次 popstate 的回調(diào)函數(shù),匹配一次頁(yè)面組件
PopChange()
// 獲取所有帶 href 屬性的 a 標(biāo)簽節(jié)點(diǎn)
var aList = document.querySelectorAll('a[href]')
// 遍歷 a 標(biāo)簽節(jié)點(diǎn)數(shù)組,阻止默認(rèn)事件,添加點(diǎn)擊事件回調(diào)函數(shù)
aList.forEach(aNode => aNode.addEventListener('click', function(e) {
e.preventDefault() //阻止a標(biāo)簽的默認(rèn)事件
var href = aNode.getAttribute('href')
// 手動(dòng)修改瀏覽器的地址欄
history.pushState(null, '', href)
// 通過(guò) history.pushState 手動(dòng)修改地址欄,
// popstate 是監(jiān)聽(tīng)不到地址欄的變化,所以此處需要手動(dòng)執(zhí)行回調(diào)函數(shù) PopChange
PopChange()
}))
}
function PopChange() {
console.log('location', location)
switch(location.pathname) {
case '/page1':
routeView.innerHTML = 'page1'
return
case '/page2':
routeView.innerHTML = 'page2'
return
default:
routeView.innerHTML = 'page1'
return
}
}
</script>
</body>
</html>
這里注意,不能在瀏覽器直接打開(kāi)靜態(tài)文件,需要通過(guò) web 服務(wù),啟動(dòng)端口去瀏覽網(wǎng)址。
總結(jié)
這篇文章主要知識(shí)點(diǎn)集中在前端路由這塊,能完全看完,并且把實(shí)現(xiàn)原理捋一遍,我想你應(yīng)該對(duì)現(xiàn)代前端框架會(huì)有一個(gè)新的理解。沒(méi)有新的理解的同學(xué),來(lái)杭州打我,我不還手。

回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
點(diǎn)擊“閱讀原文”查看 120+ 篇原創(chuàng)文章
