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

「其他文章」