沒有請求的路由
在傳統開發中,瀏覽器點選一個超連結,就會像後端web伺服器傳送一個html文件請求,然後頁面重新整理。但開始單頁面開發後,就完全不同了。
單頁面?這個概念難以理解。我用一個js作為整個web應用,然後再這個js中操作dom變化,以此來實現頁面變化。這不叫單頁面嗎?這叫!但不完善,因為這種方法破壞了瀏覽器自帶的導航功能。比如前進,後退。所以單頁面前端應用要解決兩件事內容變化、導航變化。這是現代前端成立的基礎。
想必初次接觸vue-router
和nuxt
的人很多對前端路由困惑。明明瀏覽器位址列的連結變了,為什麼瀏覽器卻沒有傳送請求出去?至少我是很疑惑的。
這要歸功於一個瀏覽器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("這裡可以傳引數");
}
})
效果
可以看到,切換頁面時,位址列變了,但並沒有網路請求發出
完整程式碼
<!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")