一文讀盡前端路由、後端路由、單頁面應用、多頁面應用

lzg9527發表於2019-10-22

前端路由

  • 定義:在單頁面應用,大部分頁面結構不變,只改變部分內容的使用
  • 優點:使用者體驗好,不需要每次都從伺服器全部獲取,快速展現給使用者
  • 缺點:使用瀏覽器的前進,後退鍵的時候會重新傳送請求,沒有合理地利用快取。單頁面無法記住之前滾動的位置,無法在前進,後退的時候記住滾動的位置

後端路由

通過使用者請求的url導航到具體的html頁面;每跳轉到不同的URL,都是重新訪問服務端,然後服務端返回頁面,頁面也可以是服務端獲取資料,然後和模板組合,返回HTML,也可以是直接返回模板HTML,然後由前端js再去請求資料,使用前端模板和資料進行組合,生成想要的HTML

前後端路由對比

  1. 從效能和使用者體驗的層面來比較的話,後端路由每次訪問一個新頁面的時候都要向伺服器傳送請求,然後伺服器再響應請求,這個過程肯定會有延遲。而前端路由在訪問一個新頁面的時候僅僅是變換了一下路徑而已,沒有了網路延遲,對於使用者體驗來說會有相當大的提升。     
  2. 在某些場合中,用ajax請求,可以讓頁面無重新整理,頁面變了但Url沒有變化,使用者就不能複製到想要的地址,用前端路由做單頁面網頁就很好的解決了這個問題。但是前端路由使用瀏覽器的前進,後退鍵的時候會重新傳送請求,沒有合理地利用快取。

單頁面的優勢

  • 不存在頁面切換問題,因為只在同一個頁面間切換,會更流暢,而且可以附加各種動畫和過度效果,使用者體驗更好。
  • 可以用到vue的路由和狀態保持,不用擔心切換造成的資料不同步。
  • 打包方便,有現成的腳手架可以用,也比較不容易出問題

單頁面的劣勢

  • 所有邏輯和業務都在一個頁面上,邏輯上不是很清楚,當業務變得複雜的時候改動起來就比較麻煩
  • 雞蛋都在一個籃子裡,只要一個地方出現錯誤,可能導致整個頁面出錯
  • 所有程式碼都在一個頁面,首次載入耗時較長,頁面體積較大

只有一張Web頁面的應用,是一種從Web伺服器載入的富客戶端,單頁面跳轉僅重新整理區域性資源 ,公共資源(js、css等)僅需載入一次 頁面跳轉:使用js中的append/remove或者show/hide的方式來進行頁面內容的更換; 資料傳遞:可通過全域性變數或者引數傳遞,進行相關資料互動

使用場景: 適用於高度追求高度支援搜尋引擎的應用

多頁面的優勢

  • 邏輯清楚,各個頁面按照功能和邏輯劃分,不用擔心業務複雜度

  • 單個頁面體積較小,載入速度比較有保證

  • 多頁面跳轉需要重新整理所有資源,每個公共資源(js、css等)需選擇性重新載入

  • 頁面跳轉:使用window.location.href = "./index.html"進行頁面間的跳轉;

  • 資料傳遞:可以使用path?account="123"&password=""路徑攜帶資料傳遞的方式,或者localstorage、cookie等儲存方式

使用場景: 高要求的體驗度,追求介面流暢的應用

多頁面的劣勢

  • 重複程式碼較多
  • 頁面經常需要切換,切換效果取決於瀏覽器和網路情況,對使用者體驗會有一定負面影響
  • 無法充分利用vue的路由和狀態保持,在多個頁面之間共享和同步資料狀態會成為一個難題

hash 模式

這裡的 hash 就是指 url 後的 # 號以及後面的字元。比如說 "www.baidu.com/#hashhash" ,其中 "#hashhash" 就是我們期望的 hash 值。

由於 hash 值的變化不會導致瀏覽器像伺服器傳送請求,而且 hash 的改變會觸發 hashchange 事件,瀏覽器的前進後退也能對其進行控制,所以在 H5 的 history 模式出現之前,基本都是使用 hash 模式來實現前端路由。

// 監聽hash變化,點選瀏覽器的前進後退會觸發
window.addEventListener('hashchange', function(event){ 
  let newURL = event.newURL; // hash 改變後的新 url
  let oldURL = event.oldURL; // hash 改變前的舊 url
},false)
複製程式碼

下面實現一個路由物件

class HashRouter{
    constructor(){
        //用於儲存不同hash值對應的回撥函式
        this.routers = {};
        window.addEventListener('hashchange',this.load.bind(this),false)
    }
    //用於註冊每個檢視
    register(hash,callback = function(){}){
        this.routers[hash] = callback;
    }
    //用於註冊首頁
    registerIndex(callback = function(){}){
        this.routers['index'] = callback;
    }
    //用於處理檢視未找到的情況
    registerNotFound(callback = function(){}){
        this.routers['404'] = callback;
    }
    //用於處理異常情況
    registerError(callback = function(){}){
        this.routers['error'] = callback;
    }
    //用於呼叫不同檢視的回撥函式
    load(){
        let hash = location.hash.slice(1),
            handler;
        //沒有hash 預設為首頁
        if(!hash){
            handler = this.routers.index;
        }
        //未找到對應hash值
        else if(!this.routers.hasOwnProperty(hash)){
            handler = this.routers['404'] || function(){};
        }
        else{
            handler = this.routers[hash]
        }
        //執行註冊的回撥函式
        try{
            handler.apply(this);
        }catch(e){
            console.error(e);
            (this.routers['error'] || function(){}).call(this,e);
        }
    }
}
複製程式碼

再來一個例子

<body>
    <div id="nav">
        <a href="#/page1">page1</a>
        <a href="#/page2">page2</a>
        <a href="#/page3">page3</a>
        <a href="#/page4">page4</a>
        <a href="#/page5">page5</a>
    </div>
    <div id="container"></div>
</body>
複製程式碼
let router = new HashRouter();
let container = document.getElementById('container');

//註冊首頁回撥函式
router.registerIndex(()=> container.innerHTML = '我是首頁');

//註冊其他檢視回到函式
router.register('/page1',()=> container.innerHTML = '我是page1');
router.register('/page2',()=> container.innerHTML = '我是page2');
router.register('/page3',()=> container.innerHTML = '我是page3');
router.register('/page4',()=> {throw new Error('丟擲一個異常')});

//載入檢視
router.load();
//註冊未找到對應hash值時的回撥
router.registerNotFound(()=>container.innerHTML = '頁面未找到');
//註冊出現異常時的回撥
router.registerError((e)=>container.innerHTML = '頁面異常,錯誤訊息:<br>' + e.message);

複製程式碼

history 模式

在 HTML5 之前,瀏覽器就已經有了 history 物件。但在早期的 history 中只能用於多頁面的跳轉:

history.go(-1);       // 後退一頁
history.go(2);        // 前進兩頁
history.forward();     // 前進一頁
history.back();      // 後退一頁
複製程式碼

在 HTML5 的規範中,history 新增了以下幾個 API

history.pushState();         // 新增新的狀態到歷史狀態棧
history.replaceState();      // 用新的狀態代替當前狀態
history.state                // 返回當前狀態物件
複製程式碼

由於 history.pushState() 和 history.replaceState() 可以改變 url 同時,不會重新整理頁面,所以在 HTML5 中的 histroy 具備了實現前端路由的能力。

對於單頁應用的 history 模式而言,url 的改變只能由下面四種方式引起:

  • 點選瀏覽器的前進或後退按鈕
  • 點選 a 標籤
  • 在 JS 程式碼中觸發 history.pushState 函式
  • 在 JS 程式碼中觸發 history.replaceState 函式

下面實現一個路由物件

class HistoryRouter{
    constructor(){
        //用於儲存不同path值對應的回撥函式
        this.routers = {};
        this.listenPopState();
        this.listenLink();
    }
    //監聽popstate
    listenPopState(){
        window.addEventListener('popstate',(e)=>{
            let state = e.state || {},
                path = state.path || '';
            this.dealPathHandler(path)
        },false)
    }
    //全域性監聽A連結
    listenLink(){
        window.addEventListener('click',(e)=>{
            let dom = e.target;
            if(dom.tagName.toUpperCase() === 'A' && dom.getAttribute('href')){
                e.preventDefault()
                this.assign(dom.getAttribute('href'));
            }
        },false)
    }
    //用於首次進入頁面時呼叫
    load(){
        let path = location.pathname;
        this.dealPathHandler(path)
    }
    //用於註冊每個檢視
    register(path,callback = function(){}){
        this.routers[path] = callback;
    }
    //用於註冊首頁
    registerIndex(callback = function(){}){
        this.routers['/'] = callback;
    }
    //用於處理檢視未找到的情況
    registerNotFound(callback = function(){}){
        this.routers['404'] = callback;
    }
    //用於處理異常情況
    registerError(callback = function(){}){
        this.routers['error'] = callback;
    }
    //跳轉到path
    assign(path){
        history.pushState({path},null,path);
        this.dealPathHandler(path)
    }
    //替換為path
    replace(path){
        history.replaceState({path},null,path);
        this.dealPathHandler(path)
    }
    //通用處理 path 呼叫回撥函式
    dealPathHandler(path){
        let handler;
        //沒有對應path
        if(!this.routers.hasOwnProperty(path)){
            handler = this.routers['404'] || function(){};
        }
        //有對應path
        else{
            handler = this.routers[path];
        }
        try{
            handler.call(this)
        }catch(e){
            console.error(e);
            (this.routers['error'] || function(){}).call(this,e);
        }
    }
}
複製程式碼

再來一個例子

<body>
    <div id="nav">
        <a href="/page1">page1</a>
        <a href="/page2">page2</a>
        <a href="/page3">page3</a>
        <a href="/page4">page4</a>
        <a href="/page5">page5</a>
        <button id="btn">page2</button>
    </div>
    <div id="container">

    </div>
</body>
複製程式碼
let router = new HistoryRouter();
let container = document.getElementById('container');

//註冊首頁回撥函式
router.registerIndex(() => container.innerHTML = '我是首頁');

//註冊其他檢視回到函式
router.register('/page1', () => container.innerHTML = '我是page1');
router.register('/page2', () => container.innerHTML = '我是page2');
router.register('/page3', () => container.innerHTML = '我是page3');
router.register('/page4', () => {
    throw new Error('丟擲一個異常')
});

document.getElementById('btn').onclick = () => router.assign('/page2')


//註冊未找到對應path值時的回撥
router.registerNotFound(() => container.innerHTML = '頁面未找到');
//註冊出現異常時的回撥
router.registerError((e) => container.innerHTML = '頁面異常,錯誤訊息:<br>' + e.message);
//載入頁面
router.load();
複製程式碼

相關文章

相關文章