不用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 次 贊 ?