隨處提供好記

Goodnote 的行銷圖片顯示一名女性在 iPad 上使用產品。

過去兩年來,Goodnotes 工程團隊負責將成功的 iPad 筆記應用程式拓展至其他平台。此個案研究探討 2022 年的 iPad 應用程式如何在網站、ChromeOS、Android 和 Windows 上執行網頁技術,以及重複使用該團隊所開發相同 Swift 程式碼的 WebAssembly。

Goodnotes 標誌。

為何 Goodnotes 推出網頁版、Android 和 Windows 版

2021 年,Goodnotes 僅推出 iOS 和 iPad 專用的應用程式。Goodnotes 的工程團隊接受了一項巨大的技術挑戰:打造新版 Goodnotes,但對於額外的作業系統和平台。該產品應與 iOS 應用程式完全相容,並顯示與 iOS 應用程式相同的附註。凡是在 PDF 頂端拍攝的筆記,或是附加的任何圖片,皆應相等,且顯示與 iOS 應用程式相同的筆劃內容。新增的任何筆劃應等同於 iOS 使用者可以建立的筆劃,不受使用者使用的工具影響,例如畫筆、螢光筆、鋼筆、圖形或橡皮擦。

Goodnotes 應用程式預覽畫面含有手寫筆記和素描。

根據需求和工程團隊的經驗,團隊很快地認為,重複使用 Swift 程式碼集是最好的行動,因為已有多年的時間撰寫並經過充分測試。但何不直接將現有的 iOS/iPad 應用程式移植到其他平台或技術,例如 Flutter 或 Compose Multiplatform?如要改用新平台 必須重新編寫 Goodnotes這樣做可能會發生在已實作的 iOS 應用程式與從零新應用程式可以建構的開發競賽,或是在新程式碼集就緒時停止現有應用程式的新開發作業。如果 Goodnotes 可以重複使用 Swift 程式碼,團隊就能受益於 iOS 團隊導入的新功能,同時讓跨平台團隊努力開發應用程式基礎知識並維持觸及功能一致。

這項產品已經解決許多有趣的 iOS 挑戰,加入了以下功能:

  • 轉譯記事。
  • 文件和記事同步處理。
  • 使用無衝突複製資料類型的附註發生衝突。
  • AI 模型評估作業適用的資料分析功能。
  • 內容搜尋與文件索引。
  • 自訂捲動體驗和動畫。
  • 查看所有 UI 層的模型實作方式。

如果工程團隊可以取得 iOS 和 iPad 應用程式適用的 iOS 程式碼集,並在 Goodnotes 專案中以 Windows、Android 或網頁應用程式的形式提供,那麼這些做法都可以大幅簡化在其他平台上實作。

Goodnotes 的技術堆疊

幸運的是,有一種方法可以在網路上重複使用現有的 Swift 程式碼:WebAssembly (Wasm)。Goodnotes 使用開放原始碼和由社群維護的專案 SwiftWasm 建構了原型。透過 SwiftWasm,Goodnotes 團隊可以使用所有已實作的 Swift 程式碼產生 Wasm 二進位檔。此二進位檔可以包含在以漸進式網頁應用程式形式傳遞的網頁上,適用於 Android、Windows、ChromeOS 和其他作業系統。

Goodnote 以 PWA 為基礎,推出依序為 Chrome、Windows、Android 和其他平台 (例如 Linux) 的發布順序。

我們的目標是以 PWA 的形式發布 Goodnote,並將其發布到每個平台的商店中。除了 Swift、iOS 所使用的程式設計語言,以及用來在網路上執行 Swift 程式碼的 WebAssembly,該專案還使用下列技術:

  • TypeScript:最常用的網路技術程式設計語言。
  • React 和 webpack:最熱門的網路架構和組合器。
  • PWA 和 Service Worker:這項專案的重要功能,因為團隊推出的應用程式可像其他 iOS 應用程式一樣離線使用,並可從商店或瀏覽器本身進行安裝。
  • PWABuilder:主要專案 Goodnotes 會將 PWA 納入原生 Windows 二進位檔,方便團隊從 Microsoft Store 發布應用程式。
  • Trusted Web Activities:這是這家公司在內部將 PWA 發布為原生應用程式時最重要的 Android 技術。

Goodnotes 技術堆疊包含 Swift、Wasm、React 和 PWA。

下圖顯示使用傳統 TypeScript 和 React 實作的內容,以及使用 SwiftWasm 和 vanilla JavaScript、Swift 和 WebAssembly 實作的內容。這部分的專案會使用 JSKit (Swift 和 WebAssembly 專用的 JavaScript 互通性程式庫),在需要時透過 Swift 程式碼處理編輯器畫面中的 DOM,甚至是使用某些瀏覽器專用的 API。

行動裝置和電腦上的應用程式螢幕截圖,顯示 Wasm 驅動的特定繪圖區域,以及 React 帶來的 UI 區域。

為什麼要使用 Wasm 和網頁版?

雖然 Apple 並未正式支援 Wasm,但 Goodnotes 工程團隊為何認為這個做法是最佳決定:

  • 重複使用超過 10 萬行程式碼。
  • 持續在核心產品上開發,同時提供跨平台應用程式。
  • 透過疊代開發程序,盡快取得各個平台的能力。
  • 擁有控制項可以在不複製所有商業邏輯的情況下轉譯同一份文件,並導入實作差異。
  • 享有同時在每個平台上完成的所有效能改善 (以及每個平台導入的所有錯誤修正)。

重複使用超過 10 萬行程式碼,以及實作轉譯管道的商業邏輯至關重要。同時,讓 Swift 程式碼與其他工具鍊相容,日後可視需要在不同平台上重複使用此程式碼。

疊代產品開發

為了盡快將內容提供給使用者,該團隊採取了疊代方法。Goodnotes 一開始提供這項產品的唯讀版本,可讓使用者從任何平台取得任何共用文件並讀取內容。他們只需提供連結,就可以從 iPad 存取及讀取撰寫的相同內容。下一個編輯功能階段,讓跨平台版本相當於 iOS 版本。

兩張應用程式螢幕截圖,象徵從唯讀改為功能完整的產品。

第一版唯讀產品需要六個月的開發時間,後續 9 個月則專門開發首次提供多項編輯功能,還有使用者介面畫面,可讓您查看自己建立的所有文件,或他人與您共用的文件。此外,由於 SwiftWasm 工具鍊,iOS 平台的新功能可讓您輕鬆轉移至跨平台專案。例如,我們重複使用數千行程式碼,建立了新型觸控筆,並輕鬆實作跨平台。

建立這項專案是一項令人驚豔的體驗,而 Goodnotes 從中獲得許多經驗。因此,以下各節將著重於探討網頁開發、 WebAssembly 和 Swift 等語言。

初始障礙

處理這項專案在很多不同的觀點是非常艱難的。該團隊發現的第一個障礙與 SwiftWasm 工具鍊有關。 工具鍊是該團隊的重要推手,但並非所有 iOS 程式碼都與 Wasm 相容。舉例來說,與 IO 或 UI 相關的程式碼 (例如檢視畫面、API 用戶端或資料庫存取權的實作) 無法重複使用,因此團隊需要開始重構應用程式的特定部分,才能從跨平台解決方案重複使用這些部分。團隊建立的大多數 PR 都會重構以提取依附元件,因此團隊之後可以使用依附元件插入或其他類似策略取代 PR。iOS 程式碼原本混合了可在 Wasm 中實作的原始商業邏輯,這些邏輯可在 Wasm 中實作,但程式碼負責輸入/輸出,以及因為 Wasm 不支援這類介面而無法在 Wasm 中實作的使用者介面。因此,一旦 Swift 商業邏輯準備好在平台之間重複使用,就必須在 TypeScript 中重新實作 IO 和 UI 程式碼。

已解決效能問題

當 Goodnotes 開始在編輯器上作業後,我們的團隊就發現編輯體驗有些問題,並在藍圖中解決棘手的技術限制。第一個問題與效能有關JavaScript 是一種單一執行緒語言。這表示它有一個呼叫堆疊和一個記憶體堆積。它會依序執行程式碼,而且必須先完成一段程式碼執行,才能進入下一個動作。這種資料採同步方式,但有時可能有害。舉例來說,如果函式需要一段時間才能執行,或必須等待某些內容,便會同時凍結所有內容。這就是工程師必須解決的問題在我們的程式碼集中,評估與算繪層或其他複雜演算法相關的某些特定路徑,對團隊來說是個問題,因為這些演算法是同步的,且執行中的演算法會封鎖主執行緒。Goodnotes 團隊進行了改良,加快速度,重構了一些內容來使其非同步此外,他們還導入了收益策略,讓應用程式可以停止演算法執行,並於稍後繼續,讓瀏覽器更新 UI 並避免捨棄影格。這並不是 iOS 應用程式的問題,因為可在主要 iOS 執行緒更新使用者介面時,使用執行緒並在背景評估這些演算法。

工程團隊必須解決的另一個解決方案,是將以附加至 DOM 的 HTML 元素為基礎的 UI,遷移至以全螢幕畫布為文件 UI 的文件 UI。專案已開始使用 HTML 元素,在 DOM 結構中顯示與文件相關的所有附註和內容,如同任何其他網頁一樣,但有時可能會藉由改用全螢幕畫布,減少瀏覽器在 DOM 更新作業的時間,改善低階裝置上的效能。

工程團隊確認了下列變更,因為這些變更可能減少部分遇到的問題,而且這些異動都是在專案開始時完成。

  • 針對大量演算法經常使用網路工作站,卸載主執行緒。
  • 開始啟動時,請使用匯出匯入的函式,而非 JS-Swift 互通性程式庫,以便減少從 Wasm 結構定義中取出的效能影響。這個 JavaScript 互通性程式庫有助於存取 DOM 或瀏覽器,但速度比原生的 Wasm 匯出函式慢。
  • 確保程式碼允許在背景中使用 OffscreenCanvas,以便應用程式卸載主執行緒,並將 Canvas API 的所有使用情形移至網路工作站,可在編寫附註時大幅提升應用程式效能。
  • 將所有 Wasm 相關執行作業移至網路工作站或甚至是網路工作站集區,讓應用程式可以減少主執行緒工作負載。

文字編輯器

另一個有趣的問題與文字編輯器有關。這項工具的 iOS 實作是以 NSAttributedString 為基礎,這是一個使用 RTF 的小型工具組。然而,這項實作方式與 SwiftWasm 不相容,因此系統被迫跨平台團隊先根據 RTF 文法建立自訂剖析器,然後再將 RTF 轉換為 HTML,藉此實作編輯體驗。此外,iOS 團隊也開始著手改善這項工具的新實作方式,將 RTF 的用法改為自訂模型,讓應用程式能在共用相同 Swift 程式碼的所有平台中,以友善的方式呈現樣式文字。

Goodnotes 文字編輯器。

這項挑戰是專案藍圖中最有趣的其中一個點,因為它會根據使用者的需求反覆解決。這項工程問題是以使用者為中心的方法解決,而團隊必須重新編寫部分程式碼,才能轉譯文字,讓第二個版本得以啟用文字編輯功能。

疊代版本

專案過去兩年的進化非常令人驚豔。該團隊開始著手研究專案的唯讀版本,但後數幾個月來,我們推出具有許多編輯功能的全新新版本。為了經常將程式碼變更發布至實際工作環境,我們的團隊決定大量使用功能旗標。每個版本都可以啟用新功能,以及針對使用者會在幾週後看到的新功能發布程式碼變更。但是,開發團隊認為自己可以改進!他們認為導入動態功能旗標系統有助於加快作業速度,因為這樣不需要重新部署變更旗標值。這將讓 Goodnotes 擁有更多彈性,也能加快新功能的部署速度,因為 Goodnotes 不需要將專案部署連結至產品發行版本。

離線作業

該團隊目前開發的其中一項主要功能是離線支援。對於這類應用程式的預期用途,您可以編輯文件及修改文件。但這並不是簡單的功能,因為 Goodnotes 也支援協同合作也就是說,不同使用者在不同裝置上進行的所有變更應該都會反映在每部裝置上,不必要求使用者解決任何衝突。Goodnotes 不久前就利用了 CRDT 解決了這個問題。透過這些免衝突的複製資料類型,Goodnotes 可以合併任何使用者對文件執行的所有變更,然後合併變更,而不會發生任何合併衝突。IndexedDB 和網路瀏覽器的可用儲存空間是使用離線協作功能的重要推手。

Goodnotes 應用程式可離線運作。

此外,由於 Wasm 二進位檔大小,開啟 Goodnotes 網頁應用程式會導致初始下載成本約為 40 MB。起初,Goodnotes 團隊完全依賴應用程式套件本身和所使用的大部分 API 端點的一般瀏覽器快取,但若無意間,便可能因為較早的 Cache API 和服務工作站而獲益。該團隊原本因為假設的複雜性,因此放棄完成這項工作,但一開始就發現 Workbox 讓工作不容易受挫。

使用 Swift 網頁版時的相關建議

如果您想重複使用 iOS 應用程式,並想重複使用許多程式碼,請做好準備,因為您即將進入令人驚豔的旅程。開始之前,不妨先參考一些有趣的提示

  • 請檢查您要重複使用的程式碼。如果應用程式的商業邏輯是在伺服器端實作,您可能會想要重複使用 UI 程式碼,而 Wasm 並不可協助您。團隊簡單介紹了 Tokamak,這是與 SwiftUI 相容的架構,可運用 WebAssembly 建構瀏覽器應用程式,但不太能滿足應用程式需求。不過,如果您的應用程式的用戶端程式碼導入了高強度的商業邏輯或演算法,Wasm 就是最好的朋友。
  • 確認 Swift 程式碼集已準備就緒。使用者介面層或特定架構的軟體設計模式,可讓 UI 邏輯與商業邏輯緊密區隔,這非常實用,因為您將無法重複使用 UI 層實作。乾淨的架構或六角架構原則也是基本原則,因為您必須插入並提供所有 IO 相關程式碼的依附元件,而遵循這些架構,其中實作詳細資料定義為抽象,而依附元件反轉原則就是更簡單的做法。
  • Wasm 不提供 UI 程式碼。因此,請決定要用於網頁的 UI 架構。
  • JSKit 可協助您將 Swift 程式碼與 JavaScript 整合,但請注意,如果您有熱路徑,橫跨 JS 到 Swift 橋接器可能會耗費大量成本,而您需要以匯出的函式取代 Swift 程式碼。如要進一步瞭解 JSKit 的運作方式,請參閱官方說明文件Swift 中的動態成員查詢 (隱藏寶石) 文章。
  • 您是否能重複使用架構,取決於應用程式採用的架構,以及您使用的非同步程式碼執行機製程式庫。MVVP 或可組合架構等模式可協助您重複使用檢視模型和部分 UI 邏輯,而不必將實作與 Wasm 無法使用的 UIKit 依附元件結合。RXSwift 和其他程式庫可能與 Wasm 不相容,因此請特別留意,因為您必須使用 OpenCombine、async/await,以及透過 Goodnotes 的 Swift 程式碼建立串流。
  • 使用 gzip 或 brotli 壓縮 Wasm 二進位檔。請注意,針對傳統網頁應用程式,二進位檔的大小非常大。
  • 即使可以在沒有 PWA 的情況下使用 Wasm,即使網頁應用程式沒有資訊清單,或是您不希望使用者安裝,仍請確保您至少加入 Service Worker。Service Worker 將免費和所有應用程式資源儲存並提供 Wasm 二進位檔,因此使用者不必在每次開啟專案時都下載這些項目。
  • 請注意,招募人才可能比預期困難。您可能需要聘用優秀的網頁程式開發人員,並具備 Swift 或 Swift 開發人員的相關經驗。如果能在這兩種平台上找到 具備深厚知識的通才工程師

結論

使用複雜的技術堆疊建立網站專案,同時處理充滿挑戰的產品,是一項超棒的體驗。這肯定很困難,但總值得一試Goodnotes 提供 iOS 應用程式的新功能時,可能無法採用這種做法,甚至無法發布 Windows、Android、ChromeOS 和網頁版 適用的版本。多虧了技術堆疊和 Goodnotes 工程團隊,Goodnotes 現已無所不在,團隊已準備好繼續處理下一個挑戰!如要進一步瞭解這項專案,請觀看「談話時間 2023 年 NS 西班牙 的 Goodnotes 團隊」。請務必試試網頁版善意