隨處提供好記

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

過去兩年,Goodnotes 工程團隊一直在進行一項專案,將成功的 iPad 記事應用程式移植到其他平台。本案例研究將說明2022 年 iPad 年度應用程式如何透過 WebAssembly 和網頁技術,將應用程式移植到網頁、ChromeOS、Android 和 Windows 平台,並重複使用團隊十多年來持續開發的 Swift 程式碼。

Goodnotes 標誌。

Goodnotes 推出網頁版、Android 版和 Windows 版的原因

2021 年,Goodnotes 僅提供 iOS 和 iPad 版應用程式。Goodnotes 的工程團隊接受了巨大的技術挑戰:為其他作業系統和平台建立新版 Goodnotes。產品應與 iOS 應用程式完全相容,並顯示相同的筆記。在 PDF 上方或任何附加圖片上所做的任何筆記,都應與 iOS 應用程式顯示的筆記相同,並顯示相同的筆劃。新增的任何筆劃都應與 iOS 使用者可建立的筆劃相同,不受使用者使用的工具 (例如原子筆、螢光筆、鋼筆、形狀或橡皮擦) 影響。

Goodnotes 應用程式預覽畫面,顯示手寫筆記和素描。

根據需求和工程團隊的經驗,團隊很快就得出結論,認為重複使用 Swift 程式碼集是最佳做法,因為該程式碼集已在多年的時間內編寫並經過充分測試。不過,為什麼不將現有的 iOS/iPad 應用程式移植到其他平台或技術 (例如 Flutter 或 Compose Multiplatform)?如要轉移至新平台,就必須重寫 Goodnotes。這麼做可能會導致已實作的 iOS 應用程式與從零開始建構的新應用程式之間的開發競賽,或是在新的程式碼集趕上進度時,停止現有應用程式的新開發作業。如果 Goodnotes 可以重複使用 Swift 程式碼,團隊就能在跨平台團隊處理應用程式基本功能時,從 iOS 團隊導入的新功能中獲益,並達到功能一致性。

這項產品已解決 iOS 的許多有趣挑戰,以便新增以下功能:

  • 記事算繪。
  • 文件和筆記同步處理。
  • 使用無衝突的複製資料類型解決筆記衝突。
  • 用於 AI 模型評估的資料分析。
  • 內容搜尋和文件索引。
  • 自訂捲動體驗和動畫。
  • 所有 UI 層的檢視模型實作。

如果工程團隊能讓 iOS 程式碼集可用於 iOS 和 iPad 應用程式,並在 Goodnotes 可發布為 Windows、Android 或網頁應用程式的專案中執行,那麼所有這些功能都會變得更容易在其他平台上實作。

Goodnotes 的技術堆疊

幸好,我們有辦法在網路上重複使用現有的 Swift 程式碼,也就是 WebAssembly (Wasm)。Goodnotes 使用 Wasm 和開放原始碼、由社群維護的專案 SwiftWasm 建立原型。有了 SwiftWasm,Goodnotes 團隊就能使用已實作的所有 Swift 程式碼產生 Wasm 二進位檔。這個二進位檔可納入網頁,並以 Android、Windows、ChromeOS 和其他所有作業系統的漸進式網頁應用程式形式發布。

Goodnotes 的推出順序是先從 Chrome 開始,接著是 Windows,然後是 Android,最後是 Linux 等其他平台,所有平台都會以 PWA 為基礎。

目的是將 Goodnotes 做為 PWA 發布,並能在所有平台的應用程式商店中列出。除了 Swift (iOS 已使用的程式設計語言) 和 WebAssembly (用於在網路上執行 Swift 程式碼) 之外,這個專案還使用了以下技術:

  • TypeScript:最常用於網頁技術的程式設計語言。
  • React 和 webpack:最受歡迎的網頁框架和 bundler。
  • PWA 和服務工作站:這項專案的一大助力,因為團隊可以將應用程式做為離線應用程式推出,讓應用程式可像其他 iOS 應用程式一樣運作,而且使用者可以從商店或瀏覽器本身安裝應用程式。
  • PWABuilder:Goodnotes 用來將 PWA 包裝成原生 Windows 二進位檔的主要專案,以便團隊透過 Microsoft Store 發布應用程式。
  • 信任的網頁活動:公司用來將 PWA 發布為原生應用程式的最重要 Android 技術。

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

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

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

為什麼要使用 Wasm 和網頁?

雖然 Apple 並未正式支援 Wasm,但 Goodnotes 工程團隊認為,以下原因讓他們認為這是最佳做法:

  • 重複使用超過 10 萬行程式碼。
  • 能夠繼續開發核心產品,同時也能為跨平台應用程式做出貢獻。
  • 透過迭代式開發程序,盡快將應用程式推廣至所有平台。
  • 可控制如何轉譯相同的文件,而不會重複所有業務邏輯,並在實作中引入差異。
  • 同時享有所有平台的效能改善成果 (以及所有平台上實作的錯誤修正)。

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

疊代產品開發

團隊採用迭代式方法,盡可能快速地為使用者提供服務。Goodnotes 一開始推出的是產品的唯讀版本,使用者可以取得任何共用文件,並透過任何平台閱讀。只要透過連結,他們就能存取並閱讀自己在 iPad 上寫下的筆記。在編輯功能中新增下一個階段,讓跨平台版本與 iOS 版本相等。

兩張應用程式螢幕截圖,象徵從只讀產品轉換為完整功能產品。

第一版的唯讀產品開發作業耗時六個月,接下來的九個月則是專注於第一批編輯功能和 UI 畫面,讓您可以查看自己建立或他人與您共用的所有文件。此外,SwiftWasm 工具鍊可讓 iOS 平台的新功能輕鬆移植至跨平台專案。舉例來說,我們建立了新類型的觸控筆,並透過重複使用數千行程式碼,輕鬆實作跨平台功能。

建構這個專案是一次難得的體驗,Goodnotes 也從中學到很多。因此,以下各節將著重於網頁開發的相關技術要點,以及 WebAssembly 和 Swift 等語言的使用方式。

初始障礙

從許多不同的角度來看,這項專案的執行難度都非常高。團隊發現的第一個障礙,與 SwiftWasm 工具鍊有關。工具鍊是團隊的一大助力,但並非所有 iOS 程式碼都與 Wasm 相容。舉例來說,與 I/O 或 UI 相關的程式碼 (例如檢視區塊、API 用戶端的實作方式,或資料庫存取權) 無法重複使用,因此團隊需要開始重構應用程式的特定部分,才能從跨平台解決方案中重複使用這些部分。團隊建立的大部分提交要求都已重構為抽象依附元件,以便團隊日後使用依附元件注入或其他類似策略取代這些依附元件。iOS 程式碼原本混合了可在 Wasm 中實作的原始商業邏輯,以及負責輸入/輸出和使用者介面的程式碼,但 Wasm 不支援這兩者,因此無法在 Wasm 中實作。因此,一旦 Swift 商業邏輯可在不同平台之間重複使用,就必須在 TypeScript 中重新實作 IO 和 UI 程式碼。

解決效能問題

Goodnotes 開始著手編輯器後,團隊發現編輯體驗中存在一些問題,且我們的路線圖中也出現了艱難的技術限制。第一個問題與效能有關。JavaScript 是單執行緒語言。也就是說,它有一個呼叫堆疊和一個記憶體堆積。它會依序執行程式碼,並且必須先執行完一小段程式碼,才能繼續執行下一個程式碼。這項作業是同步的,但有時可能會造成不良影響。舉例來說,如果函式需要一段時間才能執行,或必須等待某項作業,就會在等待期間將所有作業凍結。這正是工程師必須解決的問題。評估程式碼庫中與轉譯層或其他複雜演算法相關的特定路徑,對團隊來說是一大難題,因為這些演算法是同步的,執行時會阻斷主執行緒。Goodnotes 團隊重新編寫這些程式碼,以便加快速度,並重構部分程式碼,使其變成非同步。他們也導入了產生策略,讓應用程式可以停止執行演算法,並在稍後繼續執行,讓瀏覽器更新使用者介面,避免遺漏影格。這對 iOS 應用程式而言並非問題,因為在 iOS 主要執行緒更新使用者介面時,它可以使用執行緒並在背景評估這些演算法。

工程團隊必須解決的另一個問題,是將以 DOM 附加的 HTML 元素為基礎的 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 層或特定架構的軟體設計模式,可在 UI 邏輯和商業邏輯之間建立明確的區隔,這類模式將非常實用。您必須為所有與 I/O 相關的程式碼插入並提供依附元件,因此清晰架構或六邊形架構原則也相當重要。如果您遵循這些架構,將實作詳細資料定義為抽象,並大量使用依附元件反轉原則,這項作業就會變得更容易。
  • Wasm 不會提供 UI 程式碼。因此,請決定要用於網頁的 UI 架構。
  • JSKit 可協助您將 Swift 程式碼與 JavaScript 整合,但請注意,如果您有熱路徑,跨越 JS-Swift 橋接可能會耗費較多,因此您需要以匯出的函式取代。如要進一步瞭解 JSKit 的運作方式,請參閱官方文件和「Swift 中的動態成員查詢,隱藏的寶石!」一文。
  • 您是否可以重複使用架構,取決於應用程式所遵循的架構,以及您使用的非同步程式碼執行機制程式庫。MVVP 或可組合式架構等模式可協助您重複使用檢視畫面模型和部分 UI 邏輯,而不會將實作內容與您無法搭配 Wasm 使用的 UIKit 依附元件配對。RXSwift 和其他程式庫可能與 Wasm 不相容,請記住這一點,因為您必須在 Goodnotes 的 Swift 程式碼中使用 OpenCombine、異步/等待和串流。
  • 使用 gzip 或 brotli 壓縮 Wasm 二進位檔。請注意,對於傳統網頁應用程式而言,二進位檔的大小會相當大。
  • 即使您可以不使用 PWA 就使用 Wasm,也請務必至少納入服務工作者,即使您的網頁應用程式沒有資訊清單,或您不希望使用者安裝資訊清單,也一樣。服務工作站會免費儲存及提供 Wasm 二進位檔和所有應用程式資源,因此使用者不必每次開啟專案就下載這些資源。
  • 請注意,招募人才可能比預期困難。您可能需要聘請具備一定 Swift 經驗的網頁開發人員,或是具備一定網頁經驗的 Swift 開發人員。如果您能找到對這兩個平台都有些許瞭解的一般工程師,那就太棒了

結論

使用複雜的技術堆疊建構網站專案,同時處理充滿挑戰的產品,是令人難忘的體驗。這會是艱難的過程,但絕對值得。若未使用這種方法,Goodnotes 就無法在開發 iOS 應用程式的新功能時,同時發布 Windows、Android、ChromeOS 和網頁版。多虧這個技術堆疊和 Goodnotes 的工程團隊,Goodnotes 如今已無所不在,團隊也準備好繼續解決下一個挑戰!如要進一步瞭解這個專案,請觀看 Goodnotes 團隊在 NSSpain 2023 大會上發表的演講。歡迎試用 Goodnotes for web