後端說,單頁面SPA和前端路由是怎麼回事

ggtc發表於2024-07-26

沒有請求的路由

在傳統開發中,瀏覽器點選一個超連結,就會像後端web伺服器傳送一個html文件請求,然後頁面重新整理。但開始單頁面開發後,就完全不同了。

單頁面?這個概念難以理解。我用一個js作為整個web應用,然後再這個js中操作dom變化,以此來實現頁面變化。這不叫單頁面嗎?這叫!但不完善,因為這種方法破壞了瀏覽器自帶的導航功能。比如前進,後退。所以單頁面前端應用要解決兩件事內容變化導航變化。這是現代前端成立的基礎。

想必初次接觸vue-routernuxt的人很多對前端路由困惑。明明瀏覽器位址列的連結變了,為什麼瀏覽器卻沒有傳送請求出去?至少我是很疑惑的。

這要歸功於一個瀏覽器API,History API。學過wpf的人可能對這玩意不陌生,因為wpf也可以藉助導航開發瀏覽器式應用程式。

導航

導航這種理念包括如下幾種功能

  • history.back()
    後退到上一個頁面

  • history.forward()
    前進到下一個頁面

  • history.go(-2)
    跳轉到前兩個頁面

但下面這三個方法才是實現單頁面的關鍵。因為呼叫這三個api設定location不會引起瀏覽器向伺服器傳送頁面請求

  • history.pushState(data, title, url)
  • history.popState()
  • history.replaceState(data, title, url)

history導航是一個棧,這是用來操作棧的方法,入棧、出棧、更新棧頂元素。只不過這個棧裡面存放的是頁面相關資訊。最重要的是pushState,其他的可以暫時不管,也不影響使用。

還有一個關鍵的,當點選導航瀏覽器前進和後退按鈕,會觸發事件window.onpopstate。在這裡,我們用js讀取導航棧中的資訊,並操作dom。這解決了和瀏覽器導航整合的問題。

這些 API 的主要目的是支援像單頁應用這樣的網站,它們使用 JavaScript API(如 fetch())來更新頁面的新內容,而不是載入整個新頁面。

其實到這裡,單頁面基實現的本原理已經清楚了。

實現一個簡單的單頁面應用

容器

單頁面應用需要一個容器,這裡我使用一個div作為頁面的容器。

<div id="app"></div>

實現頁面

頁面由模板js程式碼組成。為了方便書寫,各自放在一個script標籤中。在模板中宣告頁面結構,用type="text/html"屬性,使得我們可以獲得語法感知的提示。然後再定義指令碼,其實就是一個函式。在其中讀取模板,請求資料,然後渲染頁面到容器中

<!-- 模板 -->
	<script type="text/html" id="page1_html">
		<h1>首頁</h1>
		<h2>這是第一個頁面</h2>
		<div id="content"></div>
		<button style="background-color: lightcoral;" onclick="route.routeTo('/page2')">跳轉到關於頁面</button>
	</script>
<!-- 指令碼 -->
	<script id="page1_js">
		function loadPage1(){
			//把模板頁面替換進容器
			document.getElementById("app").innerHTML=document.getElementById("page1_html").innerHTML;
			//取資料然後生成內容,實際可能有ajax和fetch請求
			var data={
				text:"這是使用js生成的內容",
				id:1
			};
			document.getElementById("content").innerText=JSON.stringify(data);
		}
	</script>

前端路由排程

光有模板和指令碼不行,我們還需要一個排程演算法,用來呼叫頁面渲染函式、更新導航。就是所謂的前端路由功能了。為了便於觀察,我把宣告的頁面放在一個物件中,這就形成了路由表,方便搜尋。當然,不要這個也是可以的,可以手動呼叫指令碼渲染函式loadPage1

const route={
    page:[
        {
            url:"/page1",
            module:loadPage1
        },
        {
            url:"/page2",
            module:loadPage2
        }
    ]
}

有了路由表之後,就可以新增排程演算法,根據傳入的url,尋找到對應的物件,新增到導航棧中,然後呼叫指令碼渲染函式loadPageXXX

const route={
    routeTo:(url)=>{
        var page=route.page.find(r=>r.url==url);
        if(page==null){
            alert("檔案不存在");
        }
        else{
            if(window.location.pathname==url)return;
            history.pushState(page.url,"",page.url);
            page.module("這裡可以傳引數");
        }
    }
}
//預設跳轉到首頁
route.routeTo("/page1");

到了這一個,已經實現了單頁面。點選跳轉按鈕,位址列會變,頁面也會變。但有一個問題,點選瀏覽器導航按鈕不管用。這是因為我們還沒監聽popstate事件處理這個操作。

因為點選導航按鈕,位址列會變,但頁面渲染什麼內容?這需要我們去處理。所以在這個事件中,我們根據url,從路由表中找到頁面,然後渲染出來。

// 處理前進與後退
        window.addEventListener("popstate",(e)=>{
            var page=route.page.find(r=>r.url==e.state);
            if (page) {
                page.module("這裡可以傳引數");
            }
        })

效果

可以看到,切換頁面時,位址列變了,但並沒有網路請求發出

image

完整程式碼

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SPA頁面</title>
</head>
<body>
    <div id="app"></div>
    <!-- 首頁 -->
    <script type="text/html" id="page1_html">
        <h1>首頁</h1>
        <h2>這是第一個頁面</h2>
        <div id="content"></div>
        <button style="background-color: lightcoral;" onclick="route.routeTo('/page2')">跳轉到關於頁面</button>
    </script>
    <script id="page1_js">
        function loadPage1(){
            //把模板頁面替換進容器
            document.getElementById("app").innerHTML=document.getElementById("page1_html").innerHTML;
            //取資料然後生成內容,實際可能有ajax和fetch請求
            var data={
                text:"這是使用js生成的內容",
                id:1
            };
            document.getElementById("content").innerText=JSON.stringify(data);
        }
    </script>

    <!-- 關於頁 -->
    <script type="text/html" id="page2_html">
        <h1>關於</h1>
        <h2>這是第二個頁面</h2>
        <div id="content"></div>
        <button style="background-color: lightgreen;" onclick="route.routeTo('/page1')">跳轉到首頁</button>
    </script>
    <script id="page2_js">
        function loadPage2(){
            //把模板頁面替換進容器
            document.getElementById("app").innerHTML=document.getElementById("page2_html").innerHTML;
            //取資料然後生成內容,實際可能有ajax和fetch請求
            var data={
                text:"假設這裡是網站資訊",
                id:2
            };
            document.getElementById("content").innerText=JSON.stringify(data);
        }
    </script>
    
    <!-- 排程 -->
    <script>
        const route={
            page:[
                {
                    url:"/page1",
                    module:loadPage1
                },
                {
                    url:"/page2",
                    module:loadPage2
                }
            ],
            routeTo:(url)=>{
                var page=route.page.find(r=>r.url==url);
                if(page==null){
                    alert("檔案不存在");
                }
                else{
                    if(window.location.pathname==url)return;
                    history.pushState(page.url,"",page.url);
                    page.module("這裡可以傳引數");
                }
            }
        }
        // 處理前進與後退
        window.addEventListener("popstate",(e)=>{
            var page=route.page.find(r=>r.url==e.state);
            if (page) {
                page.module("這裡可以傳引數");
            }
        })
        //預設跳轉到首頁
        route.routeTo("/page1");
    </script>
</body>
</html>

結語

從上面的實現中,你們也能看到,單頁面應用就是把整個應用程式傳送到瀏覽器,在瀏覽器裡面去執行這個程式。所以相比與一個html檔案,一旦應用上規模,體積也相應的會很大。這就牽扯出chunk,把應用分塊的概念。第一次請求的時候只把排程部分、首頁以及相關幾個頁面傳入到瀏覽器,後續請求到了沒有傳輸的頁面時,再把後續檔案傳輸過來。

由於我們的頁面都是在一個網頁中,本質上是傳輸了一個web伺服器到瀏覽器中,在導航時,由js控制渲染。所以過渡效果、過濾器、請求管道、中介軟體、web伺服器具有的東西在網頁中實現也就有了可能。

但單頁面應用SPA,這個web伺服器一切都要歸功於瀏覽器提供的那個關鍵的功能,History API

當然,這裡還沒有處理複製一個url到新標籤頁開啟的功能。但這其實很簡單,就是首次跳轉根據location來呼叫route.routeTo("位址列的url")

相關文章