Trong hai năm qua, nhóm kỹ sư của Goodnotes đã nỗ lực triển khai một dự án để đưa ứng dụng ghi chú thành công trên iPad sang các nền tảng khác. Nghiên cứu điển hình này trình bày cách ứng dụng iPad của năm 2022 được phát triển cho web, ChromeOS, Android và Windows nhờ các công nghệ web và WebAssembly, đồng thời sử dụng lại cùng một mã Swift mà nhóm đã phát triển trong hơn 10 năm.
Lý do Goodnotes ra mắt trên web, Android và Windows
Vào năm 2021, Goodnotes chỉ có dưới dạng ứng dụng dành cho iOS và iPad. Nhóm kỹ sư tại Goodnotes đã chấp nhận một thách thức kỹ thuật lớn: tạo phiên bản mới của Goodnotes nhưng dành cho các hệ điều hành và nền tảng khác. Sản phẩm phải tương thích hoàn toàn và hiển thị cùng một ghi chú như ứng dụng iOS. Mọi ghi chú được ghi trên tệp PDF hoặc mọi hình ảnh đính kèm phải tương đương và hiển thị cùng một nét vẽ như ứng dụng iOS hiển thị. Bất kỳ nét vẽ nào được thêm vào phải tương đương với nét vẽ mà người dùng iOS có thể tạo, độc lập với công cụ mà người dùng đang sử dụng, ví dụ: bút, bút đánh dấu, bút máy, hình dạng hoặc cục tẩy.
Dựa trên các yêu cầu và kinh nghiệm của nhóm kỹ sư, nhóm nghiên cứu nhanh chóng kết luận rằng việc sử dụng lại cơ sở mã Swift sẽ là phương án tốt nhất, vì cơ sở mã này đã được viết và kiểm thử kỹ lưỡng trong nhiều năm. Nhưng tại sao không chỉ chuyển ứng dụng iOS/iPad hiện có sang một nền tảng hoặc công nghệ khác như Flutter hoặc Compose Multiplatform? Để chuyển sang một nền tảng mới, bạn sẽ phải viết lại Goodnotes. Việc này có thể bắt đầu một cuộc đua phát triển giữa ứng dụng iOS đã triển khai và ứng dụng mới sẽ được tạo từ đầu, hoặc liên quan đến việc dừng phát triển mới trên ứng dụng hiện có trong khi cơ sở mã mới đã bắt kịp. Nếu Goodnotes có thể sử dụng lại mã Swift, thì nhóm có thể hưởng lợi từ các tính năng mới do nhóm iOS triển khai trong khi nhóm đa nền tảng đang làm việc trên các nguyên tắc cơ bản của ứng dụng và đạt được tính năng tương đương.
Sản phẩm này đã giải quyết một số thách thức thú vị cho iOS để thêm các tính năng như:
- Kết xuất ghi chú.
- Đồng bộ hoá tài liệu và ghi chú.
- Giải quyết xung đột cho ghi chú bằng cách sử dụng Các loại dữ liệu sao chép không xung đột.
- Phân tích dữ liệu để đánh giá mô hình AI.
- Tìm kiếm nội dung và lập chỉ mục tài liệu.
- Trải nghiệm cuộn và ảnh động tuỳ chỉnh.
- Xem cách triển khai mô hình khung hiển thị cho tất cả các lớp giao diện người dùng.
Tất cả các tính năng này sẽ dễ dàng triển khai hơn nhiều cho các nền tảng khác nếu nhóm kỹ sư có thể sử dụng cơ sở mã iOS đang hoạt động cho các ứng dụng iOS và iPad và thực thi cơ sở mã đó trong một dự án mà Goodnotes có thể phân phối dưới dạng ứng dụng Windows, Android hoặc web.
Khối công nghệ của Goodnotes
May mắn thay, có một cách để sử dụng lại mã Swift hiện có trên web – WebAssembly (Wasm). Goodnotes đã tạo một nguyên mẫu bằng Wasm với dự án nguồn mở và do cộng đồng duy trì SwiftWasm. Với SwiftWasm, nhóm Goodnotes có thể tạo tệp nhị phân Wasm bằng tất cả mã Swift đã triển khai. Tệp nhị phân này có thể được đưa vào một trang web được phân phối dưới dạng Ứng dụng web tiến bộ cho Android, Windows, ChromeOS và mọi hệ điều hành khác.
Mục tiêu là phát hành Goodnotes dưới dạng PWA và có thể đăng ứng dụng này trên cửa hàng của mọi nền tảng. Ngoài Swift, ngôn ngữ lập trình đã được sử dụng cho iOS và WebAssembly dùng để thực thi mã Swift trên web, dự án này còn sử dụng các công nghệ sau:
- TypeScript: Ngôn ngữ lập trình được sử dụng thường xuyên nhất cho các công nghệ web.
- React và webpack: Khung và trình kết hợp phổ biến nhất cho web.
- Ứng dụng web tiến bộ (PWA) và trình chạy dịch vụ: Các yếu tố hỗ trợ rất lớn cho dự án này vì nhóm có thể phân phối ứng dụng của chúng tôi dưới dạng ứng dụng ngoại tuyến hoạt động giống như mọi ứng dụng iOS khác và bạn có thể cài đặt ứng dụng đó từ cửa hàng hoặc chính trình duyệt.
- PWABuilder: Dự án chính mà Goodnotes sử dụng để gói PWA vào một tệp nhị phân Windows gốc để nhóm có thể phân phối ứng dụng của chúng tôi qua Microsoft Store.
- Hoạt động web đáng tin cậy: Công nghệ Android quan trọng nhất mà công ty sử dụng để phân phối PWA dưới dạng ứng dụng gốc.
Hình sau đây cho thấy những gì được triển khai bằng TypeScript và React cổ điển, cũng như những gì được triển khai bằng SwiftWasm và JavaScript, Swift và WebAssembly. Phần này của dự án sử dụng JSKit, một thư viện khả năng tương tác JavaScript cho Swift và WebAssembly mà nhóm sử dụng để xử lý DOM trong màn hình trình chỉnh sửa từ mã Swift khi cần hoặc thậm chí sử dụng một số API dành riêng cho trình duyệt.
Tại sao nên sử dụng Wasm và web?
Mặc dù Apple không hỗ trợ chính thức Wasm, nhưng sau đây là lý do khiến nhóm kỹ sư Goodnotes cảm thấy đây là quyết định tốt nhất:
- Tái sử dụng hơn 100 nghìn dòng mã.
- Khả năng tiếp tục phát triển sản phẩm cốt lõi trong khi vẫn đóng góp cho các ứng dụng đa nền tảng.
- Sức mạnh của việc tiếp cận mọi nền tảng càng sớm càng tốt bằng cách sử dụng quy trình phát triển lặp lại.
- Có quyền kiểm soát để hiển thị cùng một tài liệu mà không cần sao chép tất cả logic nghiệp vụ và đưa ra sự khác biệt trong quá trình triển khai.
- Tận dụng tất cả các điểm cải thiện hiệu suất được thực hiện trên mọi nền tảng cùng một lúc (và tất cả các bản sửa lỗi được triển khai trên mọi nền tảng).
Việc sử dụng lại hơn 100 nghìn dòng mã và logic nghiệp vụ triển khai quy trình kết xuất là điều cơ bản. Đồng thời, việc làm cho mã Swift tương thích với các chuỗi công cụ khác cho phép họ sử dụng lại mã này trong các nền tảng khác trong tương lai nếu cần.
Phát triển sản phẩm lặp lại
Nhóm đã áp dụng phương pháp lặp lại để cung cấp nội dung cho người dùng nhanh nhất có thể. Goodnotes bắt đầu với phiên bản chỉ có thể đọc của sản phẩm, trong đó người dùng có thể lấy bất kỳ tài liệu nào được chia sẻ và đọc tài liệu đó trên bất kỳ nền tảng nào. Chỉ cần một đường liên kết, họ có thể truy cập và đọc chính những ghi chú mà họ đã viết trên iPad. Giai đoạn tiếp theo thêm các tính năng chỉnh sửa để làm cho phiên bản đa nền tảng tương đương với phiên bản iOS.
Phiên bản đầu tiên của sản phẩm chỉ có thể đọc mất 6 tháng để phát triển, sau đó 9 tháng tiếp theo được dành riêng cho nhóm tính năng chỉnh sửa đầu tiên và màn hình giao diện người dùng nơi bạn có thể kiểm tra tất cả tài liệu mà bạn đã tạo hoặc người khác đã chia sẻ với bạn. Ngoài ra, các tính năng mới của nền tảng iOS cũng dễ dàng được chuyển sang dự án đa nền tảng nhờ Chuỗi công cụ SwiftWasm. Ví dụ: một loại bút mới đã được tạo và dễ dàng triển khai trên nhiều nền tảng bằng cách sử dụng lại hàng nghìn dòng mã.
Việc xây dựng dự án này là một trải nghiệm tuyệt vời và Goodnotes đã học được rất nhiều từ dự án này. Đó là lý do các phần sau đây sẽ tập trung vào các điểm kỹ thuật thú vị về phát triển web và cách sử dụng WebAssembly cũng như các ngôn ngữ như Swift.
Những trở ngại ban đầu
Việc làm việc trên dự án này rất khó khăn từ nhiều góc độ. Rào cản đầu tiên mà nhóm phát hiện được liên quan đến chuỗi công cụ SwiftWasm. Chuỗi công cụ là một công cụ hỗ trợ rất lớn cho nhóm, nhưng không phải tất cả mã iOS đều tương thích với Wasm. Ví dụ: mã liên quan đến IO hoặc giao diện người dùng (chẳng hạn như việc triển khai thành phần hiển thị, ứng dụng API hoặc quyền truy cập vào cơ sở dữ liệu) không thể sử dụng lại. Vì vậy, nhóm cần bắt đầu tái cấu trúc các phần cụ thể của ứng dụng để có thể sử dụng lại các phần đó từ giải pháp đa nền tảng. Hầu hết các bản PR mà nhóm tạo ra đều được tái cấu trúc để trừu tượng hoá các phần phụ thuộc, nhờ đó, nhóm có thể thay thế các phần phụ thuộc đó bằng cách chèn phần phụ thuộc hoặc các chiến lược tương tự khác sau này. Mã iOS ban đầu kết hợp logic nghiệp vụ thô có thể được triển khai trong Wasm với mã chịu trách nhiệm về đầu vào/đầu ra và giao diện người dùng không thể được triển khai trong Wasm vì Wasm cũng không hỗ trợ. Vì vậy, bạn cần triển khai lại mã giao diện người dùng và IO trong TypeScript sau khi logic nghiệp vụ Swift đã sẵn sàng để sử dụng lại giữa các nền tảng.
Giải quyết các vấn đề về hiệu suất
Sau khi Goodnotes bắt đầu làm việc trên trình chỉnh sửa, nhóm đã xác định một số vấn đề về trải nghiệm chỉnh sửa và các hạn chế về công nghệ khó khăn đã xuất hiện trong lộ trình của chúng tôi. Vấn đề đầu tiên liên quan đến hiệu suất. JavaScript là một ngôn ngữ đơn luồng. Điều này có nghĩa là ứng dụng có một ngăn xếp lệnh gọi và một vùng nhớ khối xếp. Lệnh này thực thi mã theo thứ tự và phải hoàn tất việc thực thi một đoạn mã trước khi chuyển sang đoạn mã tiếp theo. Phương thức này là đồng bộ, nhưng đôi khi điều đó có thể gây hại. Ví dụ: nếu một hàm mất chút thời gian để thực thi hoặc phải chờ một điều gì đó, thì hàm đó sẽ đóng băng mọi thứ trong thời gian chờ đợi. Và đây chính là vấn đề mà các kỹ sư phải giải quyết. Việc đánh giá một số đường dẫn cụ thể trong cơ sở mã của chúng tôi liên quan đến lớp kết xuất hoặc các thuật toán phức tạp khác là một vấn đề đối với nhóm, vì các thuật toán này đồng bộ và việc thực thi các thuật toán này đang chặn luồng chính. Nhóm Goodnotes đã viết lại các hàm này để nhanh hơn và tái cấu trúc một số hàm để không đồng bộ. Họ cũng đã giới thiệu một chiến lược năng suất để ứng dụng có thể dừng quá trình thực thi thuật toán và tiếp tục sau đó, cho phép trình duyệt cập nhật giao diện người dùng và tránh bị rớt khung hình. Đây không phải là vấn đề đối với ứng dụng iOS vì ứng dụng này có thể sử dụng các luồng và đánh giá các thuật toán này ở chế độ nền trong khi luồng iOS chính cập nhật giao diện người dùng.
Một giải pháp khác mà nhóm kỹ sư phải giải quyết là di chuyển giao diện người dùng dựa trên các phần tử HTML đính kèm vào DOM sang giao diện người dùng tài liệu dựa trên canvas toàn màn hình. Dự án bắt đầu hiển thị tất cả ghi chú và nội dung liên quan đến một tài liệu trong cấu trúc DOM bằng cách sử dụng các phần tử HTML như mọi trang web khác, nhưng tại một thời điểm nào đó, dự án đã di chuyển sang canvas toàn màn hình để cải thiện hiệu suất trên các thiết bị cấp thấp bằng cách giảm thời gian trình duyệt xử lý các bản cập nhật DOM.
Nhóm kỹ sư đã xác định những thay đổi sau đây là những điều có thể giảm thiểu một số vấn đề gặp phải, nếu họ thực hiện những thay đổi này ngay từ đầu dự án.
- Giảm tải luồng chính hơn bằng cách thường xuyên sử dụng trình chạy web cho các thuật toán nặng.
- Sử dụng hàm đã xuất và hàm đã nhập thay vì thư viện tương tác JS-Swift ngay từ đầu để giảm tác động đến hiệu suất khi thoát khỏi ngữ cảnh Wasm. Thư viện tương tác JavaScript này rất hữu ích để truy cập vào DOM hoặc trình duyệt, nhưng chậm hơn các hàm xuất Wasm gốc.
- Đảm bảo mã cho phép sử dụng
OffscreenCanvas
trong nền để ứng dụng có thể giảm tải luồng chính và chuyển tất cả hoạt động sử dụng Canvas API sang một worker web nhằm tối đa hoá hiệu suất của ứng dụng khi ghi chú. - Di chuyển tất cả hoạt động thực thi liên quan đến Wasm sang một worker web hoặc thậm chí là một nhóm worker web để ứng dụng có thể giảm tải cho luồng chính.
Trình chỉnh sửa văn bản
Một vấn đề thú vị khác liên quan đến một công cụ cụ thể, đó là trình soạn thảo văn bản.
Việc triển khai iOS cho công cụ này dựa trên NSAttributedString
, một bộ công cụ nhỏ sử dụng RTF. Tuy nhiên, cách triển khai này không tương thích với SwiftWasm, vì vậy, trước tiên, nhóm đa nền tảng buộc phải tạo một trình phân tích cú pháp tuỳ chỉnh dựa trên cú pháp RTF, sau đó triển khai trải nghiệm chỉnh sửa bằng cách chuyển đổi RTF thành HTML và ngược lại. Trong khi đó, nhóm iOS đã bắt đầu triển khai công cụ mới cho công cụ này, thay thế việc sử dụng RTF bằng một mô hình tuỳ chỉnh để ứng dụng có thể hiển thị văn bản có kiểu theo cách thân thiện với tất cả các nền tảng dùng chung mã Swift.
Thử thách này là một trong những điểm thú vị nhất trong lộ trình dự án vì thử thách này được giải quyết lặp lại dựa trên nhu cầu của người dùng. Đây là một vấn đề kỹ thuật được giải quyết bằng cách sử dụng phương pháp tập trung vào người dùng, trong đó nhóm cần viết lại một phần mã để có thể hiển thị văn bản, vì vậy họ đã bật tính năng chỉnh sửa văn bản trong bản phát hành thứ hai.
Bản phát hành lặp lại
Dự án này đã phát triển đáng kinh ngạc trong 2 năm qua. Nhóm nghiên cứu bắt đầu làm việc trên một phiên bản chỉ có thể đọc của dự án và sau nhiều tháng, họ đã phát hành một phiên bản hoàn toàn mới với nhiều tính năng chỉnh sửa. Để thường xuyên phát hành các thay đổi về mã cho bản phát hành chính thức, nhóm đã quyết định sử dụng rộng rãi cờ tính năng. Đối với mỗi bản phát hành, nhóm có thể bật các tính năng mới và cũng phát hành các thay đổi về mã để triển khai các tính năng mới mà người dùng sẽ thấy sau vài tuần. Tuy nhiên, nhóm nghiên cứu cho rằng họ có thể cải thiện một số điểm! Họ cho rằng việc triển khai hệ thống cờ tính năng động sẽ giúp đẩy nhanh tiến trình, vì hệ thống này sẽ giúp bạn không cần phải triển khai lại để thay đổi giá trị cờ. Điều này sẽ giúp Goodnotes linh hoạt hơn và cũng đẩy nhanh quá trình triển khai tính năng mới vì Goodnotes không cần liên kết việc triển khai dự án với bản phát hành sản phẩm.
Làm việc ngoại tuyến
Một trong những tính năng chính mà nhóm đã làm việc là hỗ trợ ngoại tuyến. Khả năng chỉnh sửa và sửa đổi tài liệu là một tính năng mà bạn mong đợi ở mọi ứng dụng như thế này. Tuy nhiên, đây không phải là một tính năng đơn giản vì Goodnotes hỗ trợ cộng tác. Điều này có nghĩa là tất cả thay đổi do nhiều người dùng thực hiện trên nhiều thiết bị sẽ được áp dụng trên mọi thiết bị mà không cần người dùng phải giải quyết bất kỳ xung đột nào. Goodnotes đã giải quyết vấn đề này từ lâu bằng cách sử dụng CRDT. Nhờ các Loại dữ liệu sao chép không xung đột này, Goodnotes có thể kết hợp tất cả thay đổi mà người dùng thực hiện trên bất kỳ tài liệu nào và hợp nhất các thay đổi đó mà không có xung đột hợp nhất nào. Việc sử dụng IndexedDB và bộ nhớ có sẵn cho trình duyệt web là một yếu tố hỗ trợ rất lớn cho trải nghiệm cộng tác ngoại tuyến trên web.
Ngoài ra, việc mở ứng dụng web Goodnotes sẽ dẫn đến chi phí tải xuống ban đầu khoảng 40 MB do kích thước tệp nhị phân Wasm. Ban đầu, nhóm Goodnotes chỉ dựa vào bộ nhớ đệm trình duyệt thông thường cho gói ứng dụng và hầu hết các điểm cuối API mà họ sử dụng, nhưng nhìn lại thì có thể đã hưởng lợi từ Cache API và worker dịch vụ đáng tin cậy hơn trước đó. Ban đầu, nhóm này đã tránh nhiệm vụ này do giả định tính phức tạp của nhiệm vụ, nhưng cuối cùng, họ nhận ra rằng Workbox đã giúp nhiệm vụ này trở nên dễ dàng hơn rất nhiều.
Đề xuất khi sử dụng Swift trên web
Nếu bạn có một ứng dụng iOS có nhiều mã mà bạn muốn sử dụng lại, hãy sẵn sàng vì bạn sắp bắt đầu một hành trình tuyệt vời. Có một số mẹo mà bạn có thể thấy thú vị trước khi bắt đầu.
- Kiểm tra mã bạn muốn sử dụng lại. Nếu logic kinh doanh của ứng dụng được triển khai ở phía máy chủ, thì có thể bạn muốn sử dụng lại mã giao diện người dùng và Wasm sẽ không giúp bạn ở đây. Nhóm đã xem xét nhanh Tokamak, một khung tương thích với SwiftUI để tạo ứng dụng trình duyệt bằng WebAssembly, nhưng khung này chưa đủ hoàn thiện để đáp ứng nhu cầu của ứng dụng. Tuy nhiên, nếu ứng dụng của bạn có logic kinh doanh hoặc các thuật toán mạnh được triển khai như một phần của mã ứng dụng, thì Wasm sẽ là người bạn đồng hành tốt nhất.
- Đảm bảo cơ sở mã Swift của bạn đã sẵn sàng. Các mẫu thiết kế phần mềm cho lớp giao diện người dùng hoặc các cấu trúc cụ thể tạo ra sự phân tách rõ ràng giữa logic giao diện người dùng và logic nghiệp vụ sẽ rất hữu ích vì bạn sẽ không thể sử dụng lại cách triển khai lớp giao diện người dùng. Cấu trúc sạch hoặc nguyên tắc cấu trúc lục giác cũng sẽ là nền tảng cơ bản, vì bạn sẽ phải chèn và cung cấp các phần phụ thuộc cho tất cả mã liên quan đến IO. Việc này sẽ dễ dàng hơn nhiều nếu bạn tuân theo các cấu trúc này, trong đó thông tin triển khai chi tiết được xác định là các khái niệm trừu tượng và nguyên tắc đảo ngược phần phụ thuộc được sử dụng rộng rãi.
- Wasm không cung cấp mã giao diện người dùng. Do đó, hãy quyết định khung giao diện người dùng mà bạn muốn sử dụng cho web.
- JSKit sẽ giúp bạn tích hợp mã Swift với JavaScript, nhưng hãy lưu ý nếu bạn có một đường dẫn nhanh, việc chuyển qua cầu JS–Swift có thể tốn kém và bạn cần thay thế đường dẫn đó bằng các hàm đã xuất. Bạn có thể tìm hiểu thêm về cách hoạt động của JSKit trong tài liệu chính thức và bài đăng Dynamic Member Lookup in Swift, a hidden gem! (Truy vấn thành phần động trong Swift, một viên ngọc ẩn!).
- Việc bạn có thể sử dụng lại cấu trúc hay không sẽ phụ thuộc vào cấu trúc mà ứng dụng của bạn tuân theo và thư viện cơ chế thực thi mã không đồng bộ mà bạn sử dụng. Các mẫu như MVVP hoặc cấu trúc có thể kết hợp sẽ giúp bạn sử dụng lại các mô hình thành phần hiển thị và một phần logic giao diện người dùng mà không cần ghép nối quá trình triển khai với các phần phụ thuộc UIKit mà bạn không thể sử dụng với Wasm. RXSwift và các thư viện khác có thể không tương thích với Wasm, vì vậy, hãy lưu ý vì bạn sẽ phải sử dụng OpenCombine, chế độ không đồng bộ/chờ và luồng trong mã Swift của Goodnotes.
- Nén tệp nhị phân Wasm bằng gzip hoặc brotli. Xin lưu ý rằng kích thước của tệp nhị phân sẽ khá lớn đối với các ứng dụng web kiểu cũ.
- Ngay cả khi bạn có thể sử dụng Wasm mà không cần PWA, hãy đảm bảo rằng bạn ít nhất phải thêm một worker dịch vụ, ngay cả khi ứng dụng web của bạn không có tệp kê khai hoặc bạn không muốn người dùng cài đặt ứng dụng đó. Trình chạy dịch vụ sẽ lưu và phân phát tệp nhị phân Wasm miễn phí cũng như tất cả tài nguyên ứng dụng để người dùng không cần tải xuống mỗi khi mở dự án của bạn.
- Xin lưu ý rằng việc tuyển dụng có thể khó khăn hơn dự kiến. Bạn có thể cần thuê các nhà phát triển web giỏi có một số kinh nghiệm về Swift hoặc các nhà phát triển Swift giỏi có một số kinh nghiệm về web. Nếu bạn có thể tìm thấy các kỹ sư tổng quát có một số kiến thức về cả hai nền tảng, thì đó sẽ là điều tuyệt vời
Kết luận
Việc xây dựng một dự án web bằng một ngăn xếp công nghệ phức tạp trong khi làm việc trên một sản phẩm đầy thử thách là một trải nghiệm tuyệt vời. Việc này sẽ rất khó khăn, nhưng hoàn toàn xứng đáng. Goodnotes không thể phát hành phiên bản cho Windows, Android, ChromeOS và web trong khi vẫn phát triển các tính năng mới cho ứng dụng iOS nếu không sử dụng phương pháp này. Nhờ nhóm kỹ sư của Goodnotes và ngăn xếp công nghệ này, Goodnotes hiện đã có mặt ở mọi nơi và nhóm này sẵn sàng tiếp tục giải quyết những thách thức tiếp theo! Nếu muốn tìm hiểu thêm về dự án này, bạn có thể xem buổi nói chuyện mà nhóm Goodnotes đã tổ chức tại NSSpain 2023. Hãy nhớ dùng thử Goodnotes cho web!