1. 程式人生 > 實用技巧 >手動實現前端路由

手動實現前端路由

1. 網頁應用的發展

早在 ajax 技術出現以前,網頁的執行模式就是在伺服器端寫好所有的靜態頁面,使用者通過在瀏覽器輸入不同的 url,瀏覽器負責向對應的 url 地址傳送請求,伺服器端響應請求,將對應的靜態頁面返回給瀏覽器,瀏覽器收到靜態頁面負責解析後顯示給使用者。此時的網頁應用,使用者想要與網頁應用進行互動或者進行頁面跳轉,就必須傳送不同的 url 請求得到,也就是說必須重新整理瀏覽器。傳送請求和響應請求的頻繁操作讓使用者體驗很不好。(頁面互動和頁面跳轉都需要頁面重新整理)

後來,隨著 ajax 技術的出現,做到了區域性非同步資料請求而不重新整理瀏覽器。節省了很多的靜態頁面的頻繁請求,而且 ajax

請求的傳送和響應速度比靜態頁面的要快得多。這樣一來,ajax 做到了使用者在與頁面互動時是無重新整理的。(頁面互動無重新整理,頁面跳轉有重新整理)

ajax 技術雖然極大地改善了使用者與網頁的互動性,但是,傳統的網頁應用還是需要向伺服器端請求多個靜態頁面資源,傳送請求和響應請求也都需要時間,在網路不好的情況下,使用者體驗還是很差。因此就有了後來的 SPA(single page application)單頁面應用。單頁面應用的設計思想就是,整個網頁應用就只有一個 index.html 靜態頁面,頁面顯示內容的切換時通過更新 index.html 頁面的 div 容器內的內容來實現的。這樣一來,檢視內容之間的切換就變成了類似於讓 div

容器內的上一個內容隱藏,顯示下一個內容這樣的模式。當然了,肯定不是顯示 / 隱藏的設計思想,而是通過 JavaScript 語言全權操作 DOM 元素 的模式更新檢視(view = render(model))。把所有的元素都變成了 JS 控制的動態生成的方式,也就不用再頻繁地向伺服器傳送請求了,網頁與使用者的互動性更強了,訪問速度更快了。(頁面互動和頁面跳轉都不需要頁面重新整理)

SPA 的核心思想就是:更新檢視而不重新請求頁面

瀏覽器向伺服器傳送請求的方式就是重新整理頁面

2. SPA與前端路由

SPA 單頁面應用雖然是用內容切換的方式代替了頁面跳轉,但是 SPA 依舊是根據 url 的改變來模擬頁面跳轉的。

那麼如何用 url 模擬頁面跳轉而不讓瀏覽器重新整理呢?

這時前端路由就出現了,專門用於 SPA 單頁面應用的,確保改變 url 的情況下,不讓頁面重新整理。

無論是 Vue 還是 React,只要是 SPA 單頁面應用,都需要前端路由。

3. 手動實現一個前端路由管理器

根據 url 的構成可知,當 url 中含 # 時,會被認為是 # 及其後面的內容是用於定位頁面內的某一元素的,因此 # 及其後面的內容不會被髮送到伺服器,也就是說,當 url# 後面的內容改變時,瀏覽器不會發送請求,因此就不會進行頁面重新整理。

因此可以用 url# 後面內容的改變來讓 url 改變,從而模擬跳轉頁面。

手動實現一個前端路由器的思路就是匹配不同的 url路徑 ,然後動態渲染出對應的 html 內容。

用到的 URL 相關的 Web API 有:window.location.hash、hashchange event

//inde.html
<body>
    <a href="#1">檢視第1個頁面</a>
    <a href="#2">檢視第2個頁面</a>
    <a href="#3">檢視第3個頁面</a>
    <a href="#4">檢視第4個頁面</a>
    <div id="app"></div>
    <script src="./index.js"></script>
</body>
//index.js
const app = document.querySelector("#app")

const div1 = document.createElement('div')
div1.innerHTML = '我是第1個頁面的內容'

const div2 = document.createElement('div')
div2.innerHTML = '我是第2個頁面的內容'

const div3 = document.createElement('div')
div3.innerHTML = '我是第3個頁面的內容'

const div4 = document.createElement('div')
div4.innerHTML = '我是第4個頁面的內容'

const div404 = document.createElement('div')
div404.innerHTML = '您輸入的頁面不存在'

const routeTable = {
    '1': div1,
    '2': div2,
    '3': div3,
    '4': div4
}

function route(){
    const hash = window.location.hash.substr(1) || '1'  //保底值為預設路由
    let div = routeTable[hash]

    if(!div){
        div = div404
    }//404頁面

    app.innerHTML = ''
    app.appendChild(div)
}

route()

window.addEventListener('hashchange',()=>{
    route()
})

4. 手動完善路由器之巢狀路由

上述手寫的路由器只是單層的路由,巢狀路由是指,在第一層路由下的一個頁面中還有向下層繼續跳轉的頁面。url 要記錄完整的路由資訊,因此巢狀路由為如下形式 #1/1

//index.js
const app = document.querySelector('#app')

const div1 = document.createElement('div')
div1.innerHTML = `
    <br/>
    我是第1個頁面的內容,我有兩個路由
    <a href="#1/1">檢視1.1頁面</a>
    <a href="#1/2">檢視1.2頁面</a>
`
const div11 = document.createElement('div')
div11.innerHTML = '我是1.1頁面的內容'
const div12 = document.createElement('div')
div12.innerHTML = '我是1.2頁面的內容'

const div2 = document.createElement('div')
div2.innerHTML = `
    <br/>
    我是第2個頁面的內容,我有兩個路由
    <a href="#2/1">檢視2.1頁面</a>
    <a href="#2/2">檢視2.2頁面</a>
    `
const div21 = document.createElement('div')
div21.innerHTML = '我是2.1頁面的內容'
const div22 = document.createElement('div')
div22.innerHTML = '我是2.2頁面的內容'

const div3 = document.createElement('div')
div3.innerHTML = '我是第3個頁面的內容'

const div4 = document.createElement('div')
div4.innerHTML = '我是第4個頁面的內容'

const div404 = document.createElement('div')
div404.innerHTML = '您輸入的頁面不存在'

const routeTable = {
    '1': div1,
    '2': div2,
    '3': div3,
    '4': div4
}

const routeTable2 = {
    '1/1': div11,
    '1/2': div12,
    '2/1': div21,
    '2/2': div22
}

const hashTable = {
    1: routeTable,
    2: routeTable2
}//不同的層數對應不同的 routeTable

function route(table){
    const hash = window.location.hash.substr(1) || '1'
    let div = table[hash]

    if(!div){
        div = div404
    }

    app.innerHTML = ''
    app.appendChild(div)
}

route(routeTable)

window.addEventListener('hashchange',()=>{
    const hash = window.location.hash.substr(1) || '1'
    const hashArray = hash.split('/')
    const table = hashTable[hashArray.length]
    route(table)
})

5. 更改路由模式為 history

上述內容是用 hash 的模式實現了前端路由器。所謂的 hash 就是 window.location.hash 獲得的以 # 開頭的 url。hash 模式的路由器的實現依賴的原理就是 url# 後面的內容不會被瀏覽器傳送到伺服器,# 後面的內容變化不會引起瀏覽器的重新整理,可以隨時修改 # 後面的內容來模擬頁面跳轉。

hash 模式是所有情況下都可以使用的,但是有一個致命的缺點就是:SEO不友好。為什麼會這樣呢?那是因為搜尋引擎(SEO)的伺服器在收錄網頁資訊時是根據一個 url 對應一個頁面資訊的方式來收錄的。這樣才能在使用者搜尋到頁面中的關鍵字時準確地返回該頁面。hash 模式下,無論是 http://localhost:1234/#1還是 http://localhost:1234/#3/1,在向後臺傳送請求時的 url 都是 http://localhost:1234/,返回的都是同一個頁面,然後通過 JS檔案 修改頁面的顯示內容。這樣一來,在搜尋引擎的伺服器上儲存該 SPA 的永遠都是一個頁面,就無法準確地提供相關的搜尋。

在後來的發展中,Web API 中的 History 介面新增了兩個方法,就是 pushState()replaceState(),這兩個方法的作用簡單來說就是可以讓程式設計師手動隨意修改 url,而不是僅僅像 go() / back() / forward() 這樣有限制地修改 url。而且,這兩個方法最神奇的是不會引起瀏覽器的頁面重新整理,也就是說不會向後端發起請求。可看如下例項,注意 url 的變化。

因此有了這兩個神奇介面的出現,我們就可以修改 url 必須帶 # 的 SPA 前端路由的局面。具體思路就是,手動攔截 a 標籤的預設頁面重新整理事件,然後再手動改變 url

下面開始程式碼實現,其實只要在上述的 hash 模式上修改就行。獲取 url 的相關資訊就不能再使用 window.location.hash 屬性了,要用 window.location.pathname

用到的 API 為 window.location.pathname、window.history.pushState()

//index.html
<body>
    <a class="link" href="/1">檢視第1個頁面</a>
    <a class="link" href="/2">檢視第2個頁面</a>
    <a class="link" href="/3">檢視第3個頁面</a>
    <a class="link" href="/4">檢視第4個頁面</a>
    <div id="app"></div>
    <script src="./index.js"></script>
</body>
//index.js
const app = document.querySelector('#app')

const div1 = document.createElement('div')
div1.innerHTML = `
    <br/>
    我是第1個頁面的內容,我有兩個路由
    <a class="link" href="/1/1">檢視1.1頁面</a>
    <a class="link" href="/1/2">檢視1.2頁面</a>
`
const div11 = document.createElement('div')
div11.innerHTML = '我是1.1頁面的內容'
const div12 = document.createElement('div')
div12.innerHTML = '我是1.2頁面的內容'

const div2 = document.createElement('div')
div2.innerHTML = `
    <br/>
    我是第2個頁面的內容,我有兩個路由
    <a class="link" href="/2/1">檢視2.1頁面</a>
    <a class="link" href="/2/2">檢視2.2頁面</a>
    `
const div21 = document.createElement('div')
div21.innerHTML = '我是2.1頁面的內容'
const div22 = document.createElement('div')
div22.innerHTML = '我是2.2頁面的內容'

const div3 = document.createElement('div')
div3.innerHTML = '我是第3個頁面的內容'

const div4 = document.createElement('div')
div4.innerHTML = '我是第4個頁面的內容'

const div404 = document.createElement('div')
div404.innerHTML = '您輸入的頁面不存在'

const routeTable = {
    '1': div1,
    '2': div2,
    '3': div3,
    '4': div4
}

const routeTable2 = {
    '1/1': div11,
    '1/2': div12,
    '2/1': div21,
    '2/2': div22
}

const hashTable = {
    1: routeTable,
    2: routeTable2
}//不同的層數對應不同的 routeTable

function route(table){
    const pathname = window.location.pathname.substr(1) || '1'
    let div = table[pathname]

    if(!div){
        div = div404
    }

    app.innerHTML = ''
    app.appendChild(div)
}

document.body.addEventListener('click', (e)=>{
    e.preventDefault()
    const el = e.target
    if(el.tagName === 'A' && el.matches('.link')){
        const href = el.getAttribute('href')
        window.history.pushState(null, '', href)
        onStateChange()
    }
})

route(routeTable)

function onStateChange(){
    const pathname = window.location.pathname.substr(1) || '1'
    const pathArray = pathname.split('/')
    const table = hashTable[pathArray.length]
    route(table)
}

但是 history 模式下的前端路由,IE8 不支援,並且需要後端伺服器的支援。

因為重新整理後傳送的 url 請求為

後端伺服器面對來自前端瀏覽器發起的請求路徑原本就有自己的伺服器路由,因此會查詢自己的伺服器路由進行轉發,會被髮到 404 頁面。因此,若想使用 history 模式下的路由,需要讓後端伺服器配合,將所有前端路由都渲染到同一頁面,就是都返回同一個頁面。

history 模式的特點:
1. 需要後端伺服器的配合,將所有前端路由渲染至一個頁面
2. IE8 不支援

6. memory 模式

SPA 路由器的實現方式還有一種 memory 模式,memory 模式的實現不依賴於 url 的變化,就是說,頁面內容切換,url 始終保持不變,實現方法就是使用 window.localStorage 儲存當前路徑即可。在上述程式碼的基礎上修改兩句就是 memory 模式了,其他地方不變。

function route(table){

    //const pathname = window.location.pathname.substr(1) || '1'
    const pathname = window.localStorage.getItem('url') || '1'
    
    let div = table[pathname]
    if(!div){
        div = div404
    }
    app.innerHTML = ''
    app.appendChild(div)
}
document.body.addEventListener('click', (e)=>{
    e.preventDefault()
    const el = e.target
    if(el.tagName === 'A' && el.matches('.link')){
        const href = el.getAttribute('href')
        
        //window.history.pushState(null, '', href)
        window.localStorage.setItem('url', href.substr(1))
        
        onStateChange()
    }
})

但這種方式不適合網頁應用,url 資訊是儲存到本地的,無法準確分享頁面。因此只適合單機應用。在前端路由中一般不適用。

參考部落格

從頭開始學習vue-router
vue-router使用詳情