不用React Vue,只用原生JS,如何開發單頁面應用(SPA)?

語言: CN / TW / HK

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的能力,該怎麼開發單頁面應用呢?

開發單頁面應用,有哪些難題

在聊怎麼實現之前,我們要先想明白:開發單頁面應用,需要解決哪些難題?

  1. 多個頁面如何定義?
  2. 頁面切換時,不可以使用location.replace('新的網址')或document.href = '新的網址',因為它會使瀏覽器下載html文檔。我們需要用HTML5的History API,修改網址。
  3. \標籤導航時,不能使用原生的href屬性,因為它會使瀏覽器下載html文檔。我們需要監聽onclick事件,在裏面調用History API修改網址。
  4. 使用History API修改網址後,頁面不會有任何變化,只是瀏覽器URL變了。我們需要手動操控當前頁面DOM的銷燬、新頁面DOM的生成。
  5. 使用History API修改網址後,當用户點擊瀏覽器的「返回」、「前進」時,頁面不會刷新,只是瀏覽器URL變了。我們需要監聽事件onpopstate,即監聽用户點擊瀏覽器的「返回」、「前進」,然後操控當前頁面DOM的銷燬、新頁面DOM的生成。

以上是一些最基本的難題,如果你要追求極致用户體驗,還需要解決下面的難題:

  1. \標籤導航,需要藉助href屬性,給予用户在新窗口打開鏈接的權利。
  2. 當用户切換路由時,如果發生了臨界事件,要能夠做好兼容。例如,用户點擊了鏈接,準備渲染新頁面,此時立馬點擊了舊頁面某個按鈕,要執行舊頁面某個按鈕的回調函數。這可能有超出預期的結果。我們需要在切換路由後,就禁止舊頁面的一切事件回調。

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個專欄裏分享:《教你做小遊戲》《極致用户體驗》

「其他文章」