不用React Vue,只用原生JS,如何開發單頁面應用(SPA)?
theme: condensed-night-purple
我報名參加金石計劃1期挑戰——瓜分10萬獎池,這是我的第3篇文章,點選檢視活動詳情。
背景
之前我釋出了一篇文章,介紹自己做的小遊戲《Dice Crush》。
本文分享一項技術方案,正是我開發上述遊戲時用到的:不用React Vue,只用原生JS,如何開發單頁面應用?
什麼是單頁面應用
單頁面應用(Single-Page Application)是個相對古老的傳統的多頁面應用(Multi-Page Application)的名詞。
以前我們訪問網頁,每個頁面是一個html檔案。點選某個超連結,就跳轉到新的html頁面。每次瀏覽器訪問html時,需要重新下載整個html文件、JS和CSS依賴,才能展現出整個頁面。這個效率很低。
隨著非同步請求AJAX等技術的興起、HTML5規範的出現,開發者有了更優秀的頁面載入方案:一個網站的所有頁面,都是同一份html文件,用JS判斷路由,並動態展示內容。通過預載入等方式,把整個網站的頁面都下載到記憶體中。每當使用者點選超連結,準備切換頁面時,通過history API使瀏覽器更新URL而不必重新下載html文件,然後JS只要把現有的頁面解除安裝(隱藏),再把記憶體中的東西展示出來即可。這個過程完全避免了網路請求,極大提高了網站使用者體驗。
採用上述方案實現的Web應用就是單頁面應用。
React和Vue開發的基本都是單頁面應用
現代Web開發,大多數網站是用React或Vue開發的,它們基本都是單頁面應用。
開發者可以很方便的使用React、Vue開發單頁面應用,是因為React Router和Vue Router幫開發者實現了單頁面應用的核心邏輯。所以開發者不必關心細節,只要會用React Router和Vue Router即可。
這就導致一個問題:如果我們不用React或Vue(例如我的遊戲《Dice Crush》是用原生JS實現),沒有React Router和Vue Router的能力,該怎麼開發單頁面應用呢?
開發單頁面應用,有哪些難題
在聊怎麼實現之前,我們要先想明白:開發單頁面應用,需要解決哪些難題?
- 多個頁面如何定義?
- 頁面切換時,不可以使用location.replace('新的網址')或document.href = '新的網址',因為它會使瀏覽器下載html文件。我們需要用HTML5的History API,修改網址。
- \標籤導航時,不能使用原生的href屬性,因為它會使瀏覽器下載html文件。我們需要監聽onclick事件,在裡面呼叫History API修改網址。
- 使用History API修改網址後,頁面不會有任何變化,只是瀏覽器URL變了。我們需要手動操控當前頁面DOM的銷燬、新頁面DOM的生成。
- 使用History API修改網址後,當用戶點選瀏覽器的「返回」、「前進」時,頁面不會重新整理,只是瀏覽器URL變了。我們需要監聽事件onpopstate,即監聽使用者點選瀏覽器的「返回」、「前進」,然後操控當前頁面DOM的銷燬、新頁面DOM的生成。
以上是一些最基本的難題,如果你要追求極致使用者體驗,還需要解決下面的難題:
- \標籤導航,需要藉助href屬性,給予使用者在新視窗開啟連結的權利。
- 當用戶切換路由時,如果發生了臨界事件,要能夠做好相容。例如,使用者點選了連結,準備渲染新頁面,此時立馬點選了舊頁面某個按鈕,要執行舊頁面某個按鈕的回撥函式。這可能有超出預期的結果。我們需要在切換路由後,就禁止舊頁面的一切事件回撥。
1、定義多個頁面
每個頁面是由HTML+JS+CSS組成的。每個頁面需要對應一個路由。
我說一下我在遊戲《Dice Crush》中的做法。
它有3個頁面:主頁、選擇關卡頁面、遊戲頁面。如下圖:
我給每個頁面定義了一個template.js,用於存放html字串。比如:
``js
const template =
Dice Crush
之後渲染頁面時,只需要document.body.innerHtml = template,就可以把該頁面的模板渲染到html文件上了。當然,渲染頁面時,還需要給button繫結click事件。
因此,我們給每個頁面宣告一個template,再宣告一個用於渲染該頁面的函式(功能主要是給document.body.innerHtml賦值、給button新增click事件),就可以了。之後需要渲染哪個頁面,就呼叫哪個頁面的渲染方法。
2、頁面切換,使用History API切換URL
需要切換頁面時,我們需要使用history.pushState(null, '', '新的頁面URL')來修改瀏覽器URL,同時呼叫上述渲染頁面方法,把頁面渲染在瀏覽器中。
3、a標籤的問題
我們需要注意,如果給\標籤添加了href,最好給它繫結這樣的click事件:
js
linkElement.onclick = function (event) {
if (event.button !== 0) return;
if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
event.preventDefault();
window.history.pushState(null, '', 'new-page.html');
// 手動渲染新的頁面
};
event.button表示按下的是滑鼠哪個按鍵(0是主按鍵,通常指滑鼠左鍵或預設值)。如果使用者是滑鼠中鍵按下a標籤、或者使用者同時按下了Ctrl(Windos)、Command(Mac)、Shift,那麼他應該期望是在新視窗開啟,我們使用href原生行為即可。如果使用者同時按下了Option,那麼他應該期望是開啟選單欄,我們也執行原生行為。其它情況,都表明使用者要在本頁面點開那個網址,我們攔截原生的href,通過history.pushState實現,並手動渲染新的頁面。
4、手動載入新頁面、解除安裝舊頁面
由於我們頁面渲染是通過document.body.innerHtml實現的,所以會在載入新頁面時自動解除安裝舊頁面。
當然,如果你的舊頁面在window上添加了一些事件監聽器、計時器,也要記得手動解除安裝掉。做好清除工作,不然會出問題。
5、頁面初次載入與監聽事件onpopstate
頁面初次載入時,我們需要根據路由渲染一個頁面,示例程式碼如下:
js
const init = () => {
if (window.location.pathname.includes('play')) {
renderGame();
} else if (window.location.pathname.includes('select')) {
renderSelect();
} else {
renderHome();
}
};
init();
同樣,當頁面onpopstate時,即使用者點選了「返回」、「前進」,依然停留在本頁面時,我們也需要重新根據當前路由渲染一下頁面。需要執行如下邏輯:
js
window.onpopstate = init;
至此,我們手寫的一個單頁面應用就開發完成啦~這也是我在遊戲《Dice Crush》中使用的方案,你學會了嗎?
寫在最後
我是HullQin,獨立開發了《聯機桌遊合集》,是個網頁,可以很方便的跟朋友聯機玩鬥地主、五子棋等遊戲,不收費無廣告。還獨立開發了《合成大西瓜重製版》。還開發了《Dice Crush》參加Game Jam 2022。喜歡可以關注我噢~我有空了會分享做遊戲的相關技術,會在這2個專欄裡分享:《教你做小遊戲》、《極致使用者體驗》。
- 一定給Button加上這個CSS,否則在使用者設定的字型或字號下,文案就換行了!
- [Go WebSocket] 多房間的聊天室(七)刪除房間時,順便清除房間鎖
- 一行程式碼,讓網頁變為黑白配色
- 聯機象棋釋出!開啟URL就能聯機對戰!觀戰!單機演練!分享殘局!
- 開發個「英雄殺記牌器」
- [教你做小遊戲] 太捲了!開發象棋,為了減少40%儲存空間,我學了下Huffman Coding
- [教你做小遊戲] 極致壓縮:用2至5位二進位制表示17種可能性
- [教你做小遊戲] 車炮能移動17個位置,針對90種出發點,如何建立0-16和目標點的對映?
- 不用React Vue,只用原生JS,如何開發單頁面應用(SPA)?
- 火爆全網的 Evil.js 原始碼解讀
- 在網際網路,摸爬滾打了幾年,我悟了。面對如今經濟形勢,普通打工人如何應對?
- [Go WebSocket] 你的第二個Go WebSocket服務: 聊天室
- 我們用48h,合作創造了一款Web遊戲:Dice Crush,參加國際賽事
- [JS真好玩] 掘金前端你好:作者榜出bug啦,我們一起看怎麼修。順便分享創意技巧:改EventListener
- [教你做小遊戲] 展示鬥地主撲克牌,支援按出牌規則排序!支援按大小排序!
- [JS入門到進階] 手寫裁剪圖片的工具,並部署。一鍵裁剪掘金文章封面
- [JS入門到進階] 手寫解析URL引數的工具,並部署。用起來又快又爽!
- 你必須要會uvloop!讓Python asyncio非同步程式設計效能直逼Go協程效能
- [JS入門到進階] 7條關於 async await 的使用口訣,新學 async await?背10遍,以後要考!快收藏
- [JS真好玩] 大招!用JS找到:哪 個 小 壞 蛋 給 我 連 點 2 次 贊 ?