【W(wǎng)eb技術(shù)】1254- 深度剖析:一文搞懂前端路由原理

前言
前端三大框架?Angular、React、Vue?,它們的路由解決方案?angular/router、react-router、vue-router?都是基于前端路由原理進(jìn)行封裝實(shí)現(xiàn)的,因此將前端路由原理進(jìn)行了解和掌握是很有必要的,因?yàn)槲覀冊(cè)偈褂玫倪^程中也難免會(huì)遇到一些坑,一旦我們掌握了它的實(shí)現(xiàn)原理,那么就能在開發(fā)中對(duì)路由的使用更加游刃有余。
一、什么是路由?
路由的概念起源于服務(wù)端,在以前前后端不分離的時(shí)候,由后端來控制路由,當(dāng)接收到客戶端發(fā)來的?HTTP?請(qǐng)求,就會(huì)根據(jù)所請(qǐng)求的相應(yīng)?URL,來找到相應(yīng)的映射函數(shù),然后執(zhí)行該函數(shù),并將函數(shù)的返回值發(fā)送給客戶端。對(duì)于最簡(jiǎn)單的靜態(tài)資源服務(wù)器,可以認(rèn)為,所有?URL?的映射函數(shù)就是一個(gè)文件讀取操作。對(duì)于動(dòng)態(tài)資源,映射函數(shù)可能是一個(gè)數(shù)據(jù)庫讀取操作,也可能是進(jìn)行一些數(shù)據(jù)的處理等等。然后根據(jù)這些讀取的數(shù)據(jù),在服務(wù)器端就使用相應(yīng)的模板來對(duì)頁面進(jìn)行渲染后,再返回渲染完畢的頁面。它的好處與缺點(diǎn)非常明顯:
好處:安全性好,
SEO?好;缺點(diǎn):加大服務(wù)器的壓力,不利于用戶體驗(yàn),代碼冗合不好維護(hù);
也正是由于后端路由還存在著自己的不足,前端路由才有了自己的發(fā)展空間。對(duì)于前端路由來說,路由的映射函數(shù)通常是進(jìn)行一些?DOM?的顯示和隱藏操作。這樣,當(dāng)訪問不同的路徑的時(shí)候,會(huì)顯示不同的頁面組件。前端路由主要有以下兩種實(shí)現(xiàn)方案:
HashHistory
當(dāng)然,前端路由也存在缺陷:使用瀏覽器的前進(jìn),后退鍵時(shí)會(huì)重新發(fā)送請(qǐng)求,來獲取數(shù)據(jù),沒有合理地利用緩存。但總的來說,現(xiàn)在前端路由已經(jīng)是實(shí)現(xiàn)路由的主要方式了,前端三大框架?Angular、React、Vue?,它們的路由解決方案?angular/router、react-router、vue-router?都是基于前端路由進(jìn)行開發(fā)的,因此將前端路由進(jìn)行了解和 掌握是很有必要的,下面我們分別對(duì)兩種常見的前端路由模式?Hash?和?History?進(jìn)行講解。
二、前端路由的兩種實(shí)現(xiàn)
2.1、Hash 模式
2.1.1、原理
早期的前端路由的實(shí)現(xiàn)就是基于?location.hash?來實(shí)現(xiàn)的。其實(shí)現(xiàn)原理也很簡(jiǎn)單,location.hash?的值就是?URL?中 # 后面的內(nèi)容。比如下面這個(gè)網(wǎng)站,它的?location.hash?的值為?'#search':
https://www.word.com#search
此外,hash?也存在下面幾個(gè)特性:
URL?中?hash?值只是客戶端的一種狀態(tài),也就是說當(dāng)向服務(wù)器端發(fā)出請(qǐng)求時(shí),hash?部分不會(huì)被發(fā)送。hash?值的改變,都會(huì)在瀏覽器的訪問歷史中增加一個(gè)記錄。因此我們能通過瀏覽器的回退、前進(jìn)按鈕控制hash?的切換。我們可以使用?
hashchange?事件來監(jiān)聽?hash?的變化。
我們可以通過兩種方式觸發(fā)?hash?變化,一種是通過?a?標(biāo)簽,并設(shè)置?href?屬性,當(dāng)用戶點(diǎn)擊這個(gè)標(biāo)簽后,URL?就會(huì)發(fā)生改變,也就會(huì)觸發(fā)?hashchange?事件了:
<a href="#search">searcha>
還有一種方式就是直接使用?JavaScript來對(duì)?loaction.hash?進(jìn)行賦值,從而改變?URL,觸發(fā)?hashchange?事件:
location.hash="#search"
以下實(shí)現(xiàn)我們采用第2種方式來實(shí)現(xiàn)。
2.1.2、實(shí)現(xiàn)
我們先定義一個(gè)父類?BaseRouter,用于實(shí)現(xiàn)?Hash?路由和?History?路由的一些共有方法;
export class BaseRouter {// list 表示路由表constructor(list) {this.list = list;}// 頁面渲染函數(shù)render(state) {let ele = this.list.find(ele => ele.path === state);ele = ele ? ele : this.list.find(ele => ele.path === '*');ELEMENT.innerText = ele.component;}}
我們簡(jiǎn)單實(shí)現(xiàn)了?push?壓入功能、go?前進(jìn)/后退功能,相關(guān)代碼的注釋都已經(jīng)標(biāo)上,簡(jiǎn)單易懂,就不在一 一介紹,參見如下:
export class HashRouter extends BaseRouter {constructor(list) {super(list);this.handler();// 監(jiān)聽 hashchange 事件window.addEventListener('hashchange', e => {this.handler();});}// hash 改變時(shí),重新渲染頁面handler() {this.render(this.getState());}// 獲取 hash 值getState() {const hash = window.location.hash;return hash ? hash.slice(1) : '/';}// push 新的頁面push(path) {window.location.hash = path;}// 獲取 默認(rèn)頁 urlgetUrl(path) {const href = window.location.href;const i = href.indexOf('#');const base = i >= 0 ? href.slice(0, i) : href;return base +'#'+ path;}// 替換頁面replace(path) {window.location.replace(this.getUrl(path));}// 前進(jìn) or 后退瀏覽歷史go(n) {window.history.go(n);}}
2.1.3、效果圖
Hash?模式的路由實(shí)現(xiàn)例子的效果圖如下所示:

2.2、History 模式
2.2.1、原理
前面的?hash?雖然也很不錯(cuò),但使用時(shí)都需要加上 #,并不是很美觀。因此到了?HTML5,又提供了?History API?來實(shí)現(xiàn)?URL?的變化。其中做最主要的?API?有以下兩個(gè):history.pushState()?和?history.repalceState()。這兩個(gè)?API可以在不進(jìn)行刷新的情況下,操作瀏覽器的歷史紀(jì)錄。唯一不同的是,前者是新增一個(gè)歷史記錄,后者是直接替換當(dāng)前的歷史記錄,如下所示:
window.history.pushState(null, null, path);window.history.replaceState(null, null, path);
此外,history?存在下面幾個(gè)特性:
pushState?和?repalceState?的標(biāo)題(title):一般瀏覽器會(huì)忽略,最好傳入?null?;我們可以使用?
popstate? 事件來監(jiān)聽?url?的變化;history.pushState()?或?history.replaceState()?不會(huì)觸發(fā)?popstate?事件,這時(shí)我們需要手動(dòng)觸發(fā)頁面渲染;
2.2.2、實(shí)現(xiàn)
我們同樣簡(jiǎn)單實(shí)現(xiàn)了?push?壓入功能、go?前進(jìn)/后退功能,相關(guān)代碼的注釋都已經(jīng)標(biāo)上,簡(jiǎn)單易懂,就不在一 一介紹,參見如下:
export class HistoryRouter extends BaseRouter {constructor(list) {super(list);this.handler();// 監(jiān)聽 popstate 事件window.addEventListener('popstate', e => {console.log('觸發(fā) popstate。。。');this.handler();});}// 渲染頁面handler() {this.render(this.getState());}// 獲取 urlgetState() {const path = window.location.pathname;return path ? path : '/';}// push 頁面push(path) {history.pushState(null, null, path);this.handler();}// replace 頁面replace(path) {history.replaceState(null, null, path);this.handler();}// 前進(jìn) or 后退瀏覽歷史go(n) {window.history.go(n);}}
2.2.3、效果圖
History?模式的路由實(shí)現(xiàn)例子的效果圖如下所示:

2.3、兩種路由模式的對(duì)比
| 對(duì)比點(diǎn) | Hash 模式 | History 模式 |
| 美觀性 | 帶著 # 字符,較丑 | 簡(jiǎn)潔美觀 |
| 兼容性 | >= ie 8,其它主流瀏覽器 | >= ie 10,其它主流瀏覽器 |
| 實(shí)用性 | 不需要對(duì)服務(wù)端做改動(dòng) | 需要服務(wù)端對(duì)路由進(jìn)行相應(yīng)配合設(shè)置 |
三、總結(jié)
本文我們大致介紹了什么是路由、前端路由的源起、以及分析了兩種前端路由:Hash?模式和?History?模式的原理以及簡(jiǎn)單功能實(shí)現(xiàn)。?通過本文對(duì)前端路由原理的掌握,這時(shí)你就可以基于原理基礎(chǔ)去閱讀?vue-router?和?react-router?的源碼實(shí)現(xiàn)了。
