你不能只用單頁應用程式來建構互動式網路應用程式... 以及其他迷思

Tony Alaribe

#瀏覽器進步頌。

我經常在 Reddit 和 YCombinator 上看到新開發人員尋求技術堆疊建議的討論。不可避免地,會有人聲稱,如果不使用 React 或 AngularJS 等單頁應用程式 (SPA) 框架,就不可能建構高品質的應用程式。這讓我覺得很奇怪,因為即使在 SPA 革命之前,許多流行的多頁網路應用程式也提供了卓越的使用者體驗。

兩年前,我開始建構一個可觀測性平台,並選擇使用 HTMX 實驗多頁應用程式 (MPA) 方法。我想知道:考慮到大多數可觀測性平台都建立在 ReactJS 之上,伺服器渲染的 MPA 是否不足以應對資料密集的應用程式?

我的發現是,如果您注意某些細節,就可以建立出色的伺服器渲染應用程式。

以下是一些常見的 MPA 迷思以及我從中學到的知識。

#迷思 1:MPA 頁面轉換速度慢,因為每次頁面導覽都會下載 JavaScript 和 CSS

MPA 頁面轉換速度慢的觀念很普遍,而且並非完全沒有根據,因為這是瀏覽器的預設行為。然而,瀏覽器在過去十年中做出了重大改進來緩解這個問題。

為了說明,在下面的影片中,禁用快取的完整頁面重新載入需要 2.90 秒,直到觸發 DOMContentLoaded 事件。我是在一家 Wi-Fi 訊號不佳的咖啡廳錄製的,但讓我們以此作為參考點。請記住這個數字。

在 MPA 中使用 PJAX、Turbolinks 甚至 HTMX Boost 等函式庫來減少載入時間是很常見的。這些函式庫使用 Javascript 劫持頁面重新載入,並且僅在轉換之間交換 HTML body 元素。這樣,頁面頭部區段的大部分資源就不需要重新載入或重新下載。

但是,還有一種鮮為人知的方法可以減少在頁面轉換期間重新下載或評估的資源量。

#透過 Service worker 進行客戶端快取

使用 SPA 框架建構漸進式網路應用程式 (PWA) 的前端開發人員可能知道 service worker。

對於我們這些不是前端或 PWA 開發人員的人來說,service worker 是瀏覽器的內建功能。它們可讓您撰寫 Javascript 程式碼,該程式碼位於您的使用者和網路之間,攔截請求並決定瀏覽器如何處理它們。

service-worker-chart.png

由於它與 PWA 趨勢相關聯,service worker 僅在 SPA 開發人員中很普通,開發人員需要意識到這項技術也可用於常規的多頁應用程式。

在影片示範中,我們啟用了一個 service worker 來快取並重新整理目前頁面。您會注意到,當點擊連結重新載入頁面時,不會出現閃爍,從而帶來更流暢的使用者體驗。

此外,瀏覽器現在只提取 84 KB 的 HTML 內容(實際頁面資料),而不是像以前那樣傳輸超過 2 MB 的靜態資源。這種最佳化將 DOMContentLoaded 事件時間從 2.9 秒減少到 500 毫秒以下。令人印象深刻的是,此改進是在**不**使用 HTMX Boost、PJAX 或 Turbolinks 的情況下實現的。

#如何在多頁應用程式中實作 Service worker

您可能想知道如何在自己的 MPA 中複製這些效能提升。以下是一個簡單的指南

  1. 建立 sw.js 檔案:這是您的 service worker 腳本,將管理快取和網路請求。
  2. 列出要快取的檔案:在 service worker 中,指定應快取的所有資源(HTML、CSS、JavaScript、圖像)。
  3. 定義快取策略:指出應如何快取每種資源類型,例如,它們應永久快取還是定期重新整理。

透過實作 service worker,您可以有效地告訴瀏覽器如何處理網路請求和快取,從而縮短載入時間並提供更流暢的使用者體驗。

#使用 Workbox 產生 Service worker

雖然可以手動撰寫 service worker(並且有像這篇 MDN 文章這樣的優秀資源可以幫助您),但我更喜歡使用 Google 的 Workbox 函式庫來自動化這個過程。

#使用 Workbox 的步驟

  1. 安裝 Workbox:透過 npm 或您偏好的套件管理員安裝 Workbox

    npm install workbox-cli --global
    
  2. 產生 Workbox 組態檔案:執行以下命令來建立組態檔案

    workbox wizard
    
  3. 設定資源處理:在產生的 workbox-config.js 檔案中,定義應如何快取不同的資源。使用 urlPattern 屬性(正規表示式)來比對特定的 HTTP 請求。對於每個相符的請求,指定快取策略,例如 CacheFirstNetworkFirst

    workbox-cfg.png

  4. 建置 Service worker:執行 Workbox 建置命令以根據您的組態產生 sw.js 檔案

    workbox generateSW workbox-config.js
    
  5. 在您的應用程式中註冊 Service worker:將以下腳本新增至您的 HTML 頁面以註冊 service worker

    <script>
      if ('serviceWorker' in navigator) {
        window.addEventListener('load', function() {
          navigator.serviceWorker.register('/sw.js').then(function(registration) {
            console.log('ServiceWorker registration successful with scope: ', registration.scope);
          }, function(err) {
            console.log('ServiceWorker registration failed: ', err);
          });
        });
      }
    </script>
    

透過遵循這些步驟,您可以指示瀏覽器在可能的情況下提供快取的資源,從而大幅縮短載入時間並提高多頁應用程式的整體效能。

Image showing the registered service worker from the chrome browser console.

顯示 Chrome 瀏覽器主控台中已註冊的 service worker 的圖像。

#推測規則 API:預先呈現頁面以實現即時頁面導覽。

如果您使用過 htmx-preloadinstantpage.js,您一定很熟悉預先呈現以及 「推測規則 API」旨在解決的問題。推測規則 API 旨在提高未來導覽的效能。它具有表達性的語法,用於指定應在目前頁面預先擷取或預先呈現哪些連結。

Speculation rules configuration example

推測規則組態範例

上面的腳本是推測規則如何設定的範例。它是一個 Javascript 物件,並且在不詳細說明的情況下,您可以看到它使用「where」、「and」、「not」等關鍵字來描述應該預先擷取或預先呈現哪些元素。

預先呈現的影響範例(Chrome 團隊)

#迷思 2:MPA 無法離線運作,也無法儲存更新以便在有網路時重試

從最後幾個部分,您知道 service worker 可以快取所有內容,並讓我們的應用程式完全離線運作。但是,如果我們想要儲存離線 POST 請求,並在有網路時重試它們,該怎麼辦?

workbox-offline-cfg.png

上面的組態 javascript 檔案顯示如何設定 Workbox 來支援兩種常見的離線情境。在這裡,您會看到背景同步,我們要求 service worker 快取因網路而失敗的任何請求,並重試長達 24 小時。

在下面,我們定義了一個離線捕獲處理程式,該處理程式在離線發出請求時觸發。我們可以傳回具有 HTML 或 JSON 回應的範本部分,或根據請求輸入動態建構回應。這裡的可能性是無限的。

#迷思 3:MPA 在頁面轉換期間總是閃爍白色

在 service worker 影片中,我們已經看到,如果我們設定快取和預先呈現,就不會發生這種情況。然而,直到 2019 年,這個迷思才普遍不成立。自 2019 年以來,大多數瀏覽器都會暫緩繪製下一個畫面,直到下一個頁面所需的所有資源都可用或達到逾時,因此在兩個頁面之間轉換時不會出現白色閃爍。這僅在同一個來源/網域內導覽時有效。

chrome.com 上的繪製保留文件.

#迷思 4:MPA 無法實現花俏的跨文件頁面轉換。

單頁應用程式框架的出現使得頁面之間的自訂轉換更加流行。不同導覽風格的吸引力來自於完全控制瀏覽器的頁面導覽。實際上,此類轉換主要在網路開發會議演講的示範中很受歡迎。

chrome.com 上的跨文件轉換文件.

這仍然是單頁應用程式的常見論點,尤其是在 Reddit 和 Hacker News 的評論區。然而,瀏覽器在過去幾年中一直致力於原生解決這個問題。Chrome 126 推出了跨文件檢視轉換。這表示我們可以建構 MPA,使其僅使用 CSS 或 CSS 和 Javascript,就包含這些花俏的動畫和頁面之間的轉換。

我最喜歡的部分是,我們可能能夠僅使用 CSS 來建立精美的跨文件轉換

cross-doc-transitions-css.png

您可以在 Google Chrome 公告頁面上快速了解更多資訊

此連結託管了一個 多頁應用程式示範,您可以在其中使用跨文件檢視轉換 API 來模擬基於堆疊的動畫,來使用簡陋的伺服器渲染應用程式。

#迷思 5:使用 htmx 或 MPA,每個使用者動作都必須在伺服器上發生。

在討論 HTMX 時,我聽過很多次這種說法。因此,HTMX 的定位可能引起了一些混淆。但是您不必在伺服器端執行所有操作。許多 HTMX 和常規 MPA 使用者會繼續在適當的地方使用 Javascript、Alpine 或 Hyperscript。

在需要強大互動性的情況下,您可以利用元件島嶼架構,使用 WebComponents 或任何您選擇的 JavaScript 框架(例如 React、Angular 等)。這樣一來,您的整個應用程式就不會是單頁應用程式(SPA),而是可以針對應用程式中需要互動性的部分,特別使用這些框架。

上面的範例展示了 APItoolkit 中一個互動性很強的搜尋元件。它是一個使用 lit-element 實作的 Web Component,lit-element 是一個用於編寫 Web Component 的零編譯步驟函式庫。因此,整個 Web Component 的程式碼都放在一個 Javascript 檔案中。

#迷思 6:直接操作 DOM 很慢。因此,最好使用 React/Virtual DOM。

直接 DOM 操作的速度是建立 ReactJS 並普及虛擬 DOM 技術的主要動機。雖然虛擬 DOM 操作可能比直接 DOM 操作快,但這僅適用於執行許多複雜操作並在毫秒內刷新的應用程式,在這種情況下,效能的差異才會比較明顯。但我們大多數人並不是在構建這樣的軟體。

Svelte 團隊寫了一篇很棒的文章,標題是 「虛擬 DOM 純粹是多餘的開銷」。我建議您閱讀它,因為它更好地解釋了為什麼虛擬 DOM 對大多數應用程式來說並不重要。

#迷思 7:您仍然需要為每個小互動編寫 JavaScript。

隨著瀏覽器技術的進步,您可以避免一開始就編寫大量客戶端 Javascript。例如,網頁上的一個標準操作是根據按鈕點擊或切換來顯示和隱藏內容。現在,您只需使用 CSS 和 HTML 即可顯示和隱藏元素,例如,使用 HTML 輸入核取方塊來追蹤狀態。我們可以將 HTML 標籤設定為按鈕的樣式,並賦予它 for="checkboxID" 屬性,這樣點擊標籤就會切換核取方塊。

<input id="published" class="hidden peer" type="checkbox"/>
<label for="published" class="btn">toggle content</label>

<div class="hidden peer-checked:block">
    Content to be toggled when label/btn is clicked
</div>

我們可以將這樣的核取方塊與 HTMX 相結合,在點擊按鈕時從端點獲取內容。

<input id="published" class="peer" type="checkbox" name="status"/>
<div
        class="hidden peer-checked:block"
        hx-trigger="intersect once"
        hx-get="/log-item"
>Shell/Loading text etc
</div>

以上所有類別都是原生的 Tailwind CSS 類別,但您也可以手動編寫 CSS。以下是一個影片,展示了如何使用該程式碼在日誌瀏覽器中隱藏或顯示日誌項目。

#最終迷思:沒有一個「適當的」前端框架,您的客戶端 Javascript 將會是 義大利麵條般的難以維護

這可能是真的,也可能不是真的。

#誰在乎?我喜歡義大利麵。

我認為網路最有效率的日子是一些 PHP 和 JQuery 義大利麵條式的日子。當時開發了很多軟體,包括我們今天所知的許多流行網路品牌。它們大多數都是以所謂的義大利麵條程式碼建構的,這有助於它們提早發布產品並存活足夠長的時間進行重構,而不是一直都是義大利麵條。

#結論

這次談話的重點是要向您展示,在 2024 年,瀏覽器可以做到很多事情。在我們沒有注意到的情況下,瀏覽器已經縮小了差距,並借鑒了單頁應用程式革命的最佳想法。例如,WebComponents 的存在歸功於我們從單頁應用程式中學到的經驗。

所以現在,我們可以使用大部分瀏覽器工具(HTML、CSS,可能還有一些 Javascript)來構建互動性很強、甚至離線的 Web 應用程式,而且在使用者體驗方面也不會犧牲太多。

瀏覽器已經走了很長的路。給它一個機會吧!

</>