hash模式和history模式 實現原理及區別

目前單頁應用(SPA)越來越成爲前端主流,單頁應用一大特點就是使用前端路由,由前端來直接控制路由跳轉邏輯,而不再由後端人員控制,這給了前端更多的自由。

目前前端路由主要有兩種實現方式:hash 模式和 history模式,下面分別詳細說明。

1. hash模式

比如在用超鏈接製作錨點跳轉的時候,就會發現,url後面跟了"#id",hash值就是url中從"#"號開始到結束的部分。

hash值變化瀏覽器不會重新發起請求,但是會觸發window.hashChange事件,假如我們在hashChange事件中獲取當前的hash值,並根據hash值來修改頁面內容,則達到了前端路由的目的。

<!-- html:菜單中href設置爲hash形式,id爲app中放置頁面內容 -->
<ul id="menu">
    <li>
        <a href="#index">首頁</a>
    </li>
    <li>
        <a href="#news">資訊</a>
    </li>
    <li>
        <a href="#user">個人中心</a>
    </li>
 
</ul>
 
<div id="app"></div>
//在window.onhashchange中獲取hash值,根據不同的值,修改app中不同的內容,起到了路由的效果
function hashChange(e){
    // console.log(location.hash)
    // console.log(location.href)
    // console.log(e.newURL)
    // console.log(e.oldURL)
 
    let app = document.getElementById('app')
    switch (location.hash) {
        case '#index':
            app.innerHTML = '<h1>這是首頁內容</h1>'
            break
        case '#news':
            app.innerHTML = '<h1>這是新聞內容</h1>'
            break
        case '#user':
            app.innerHTML = '<h1>這是個人中心內容</h1>'
            break
        default:
            app.innerHTML = '<h1>404</h1>'
    }
}
window.onhashchange = hashChange
hashChange()

上面這個實現方式比較簡陋,我們可以再封裝一下:

class Router {
    constructor(){
        this.routers = []  //存放我們的路由配置
    }
    add(route,callback){
        this.routers.push({
            path:route,
            render:callback
        })
    }
    listen(callback){
        window.onhashchange = this.hashChange(callback)
        this.hashChange(callback)()  //首次進入頁面的時候沒有觸發hashchange,必須要就單獨調用一下
    }
    hashChange(callback){
        let self = this
        return function () {
            let hash = location.hash
            console.log(hash)
            for(let i=0;i<self.routers.length;i++){
                let route = self.routers[i]
                if(hash===route.path){
                    callback(route.render())
                    return
                }
            }
        }
    }
}
 
let router = new Router()
router.add('#index',()=>{
    return '<h1>這是首頁內容</h1>'
}) 
router.add('#news',()=>{
    return  '<h1>這是新聞內容</h1>'
})
router.add('#user',()=>{
    return  '<h1>這是個人中心內容</h1>'
})
router.listen((renderHtml)=>{
    let app = document.getElementById('app')
    app.innerHTML = renderHtml
})

實現一個Router類,通過add方法添加路由配置,第一個參數爲路由路徑,第二個參數爲render函數,返回要插入頁面的html;通過listen方法,監聽hash變化,並將每個路由返回的html,插入到app中。這樣我們就實現了一個簡單的hash路由。

2. history模式

hash模式看起來是比較醜的,都帶個"#"號,我們也可以採取history模式,history就是我們平時看到的正常的連接形式。history模式基於window.history對象的方法。

HTML4中,已經支持window.history對象來控制頁面歷史記錄跳轉,常用的方法包括:

  • history.forward():在歷史記錄中前進一步
  • history.back():在歷史記錄中後退一步
  • history.go(n):在歷史記錄中跳轉n步驟,n=0爲刷新本頁,n=-1爲後退一頁。

HTML5中,window.history對象得到了擴展,新增的API包括:

  • history.pushState(data[,title][,url]):向歷史記錄中追加一條記錄
  • history.replaceState(data[,title][,url]):替換當前頁在歷史記錄中的信息。
  • history.state:是一個屬性,可以得到當前頁的state信息。
  • window.onpopstate:是一個事件,在點擊瀏覽器後退按鈕或js調用forward()back()go()時觸發。監聽函數中可傳入一個event對象,event.state即爲通過pushState()replaceState()方法傳入的data參數

history模式原理可以這樣理解,首先我們要改造我們的超鏈接,給每個超鏈接增加onclick方法,阻止默認的超鏈接跳轉,改用history.pushStatehistory.replaceState來更改瀏覽器中的url,並修改頁面內容。由於通過history的api調整,並不會向後端發起請求,所以也就達到了前端路由的目的。

如果用戶使用瀏覽器的前進後退按鈕,則會觸發window.onpopstate事件,監聽頁面根據路由地址修改頁面內容。

也不一定非要用超鏈接,任意元素作爲菜單都行,只要在點擊事件中通過 history 進行調整即可。

<!--html:-->
<ul id="menu">
    <li>
        <a href="/index">首頁</a>
    </li>
    <li>
        <a href="/news">資訊</a>
    </li>
    <li>
        <a href="/user">個人中心</a>
    </li>
 
</ul>
<div id="app"></div>
//js:
//改造超鏈接,阻止默認跳轉,默認的跳轉是會刷新頁面的
document.querySelector('#menu').addEventListener('click',function (e) {
    if(e.target.nodeName ==='A'){
        e.preventDefault()
        let path = e.target.getAttribute('href')  //獲取超鏈接的href,改爲pushState跳轉,不刷新頁面
        window.history.pushState({},'',path)  //修改瀏覽器中顯示的url地址
        render(path)  //根據path,更改頁面內容
    }
})
 
function render(path) {
    let app = document.getElementById('app')
    switch (path) {
        case '/index':
            app.innerHTML = '<h1>這是首頁內容</h1>'
            break
        case '/news':
            app.innerHTML = '<h1>這是新聞內容</h1>'
            break
        case '/user':
            app.innerHTML = '<h1>這是個人中心內容</h1>'
            break
        default:
            app.innerHTML = '<h1>404</h1>'
    }
}
//監聽瀏覽器前進後退事件,並根據當前路徑渲染頁面
window.onpopstate = function (e) {
    render(location.pathname)
}
//第一次進入頁面顯示首頁
render('/index')

上面這個寫法太low,我們可以用類封裝一下,通過 add 方法添加路由,通過 pushState 進行跳轉,初始化時更改所以超鏈接的跳轉方式:

class Router {
    constructor(){
        this.routers = []
        this.renderCallback = null
    }
    add(route,callback){
        this.routers.push({
            path:route,
            render:callback
        })
    }
    pushState(path,data={}){
        window.history.pushState(data,'',path)
        this.renderHtml(path)
    }
    listen(callback){
        this.renderCallback = callback
        this.changeA()
        window.onpopstate = ()=>this.renderHtml(this.getCurrentPath())
        this.renderHtml(this.getCurrentPath())
    }
    changeA(){
        document.addEventListener('click', (e)=> {
            if(e.target.nodeName==='A'){
                e.preventDefault()
                let path = e.target.getAttribute('href')
                this.pushState(path)
            }
        })
    }
    getCurrentPath(){
        return location.pathname
    }
    renderHtml(path){
        for(let i=0;i<this.routers.length;i++){
            let route = this.routers[i]
            if(path===route.path){
                this.renderCallback(route.render())
                return
            }
        }
    }
}
  
let router = new Router()
router.add('/index',()=>{
    return '<h1>這是首頁內容</h1>'
})
router.add('/news',()=>{
    return  '<h1>這是新聞內容</h1>'
})
router.add('/user',()=>{
    return  '<h1>這是個人中心內容</h1>'
})
router.listen((renderHtml)=>{
    let app = document.getElementById('app')
    app.innerHTML = renderHtml
})

當然,上面這個實現只是一個非常初級的 demo,並不能用於真正的開發場景,只是加深對前端路由的理解。

3. hash模式和history模式的區別

  • hash 模式較醜,history 模式較優雅
  • pushState 設置的新 URL 可以是與當前 URL 同源的任意 URL;而 hash 只可修改 # 後面的部分,故只可設置與當前同文檔的 URL
  • pushState 設置的新 URL 可以與當前 URL 一模一樣,這樣也會把記錄添加到棧中;而 hash 設置的新值必須與原來不一樣纔會觸發記錄添加到棧中
  • pushState 通過 stateObject 可以添加任意類型的數據到記錄中;而 hash 只可添加短字符串
  • pushState 可額外設置 title 屬性供後續使用
  • hash 兼容IE8以上,history 兼容 IE10 以上
  • history 模式需要後端配合將所有訪問都指向 index.html,否則用戶刷新頁面,會導致 404 錯誤
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章