Последние два года команда инженеров Goodnotes работала над проектом по переносу успешного приложения для заметок iPad на другие платформы. В этом исследовании рассматривается, как приложение iPad 2022 года попало в веб, ChromeOS, Android и Windows на базе веб-технологий и WebAssembly, повторно используя тот же код Swift, над которым команда работала более десяти лет.
Почему Goodnotes появился в веб-версии, на Android и Windows
В 2021 году Goodnotes был доступен только как приложение для iOS и iPad. Команда инженеров Goodnotes приняла на себя сложную техническую задачу: создать новую версию Goodnotes, но для дополнительных операционных систем и платформ. Продукт должен быть полностью совместим с приложением iOS и отображать те же заметки, что и приложение iOS. Любая заметка, сделанная поверх PDF-файла или любого прикрепленного изображения, должна быть эквивалентна и показывать те же штрихи, что и приложение iOS. Любой добавленный штрих должен быть эквивалентен тому, который могут создать пользователи iOS, независимо от инструмента, который использовал пользователь, например, ручки, маркера, перьевой ручки, фигур или ластика.
На основе требований и опыта команды инженеров команда быстро пришла к выводу, что повторное использование кодовой базы Swift будет наилучшим вариантом действий, учитывая, что она уже была написана и хорошо протестирована в течение многих лет. Но почему бы просто не перенести уже существующее приложение iOS/iPad на другую платформу или технологию, например Flutter или Compose Multiplatform? Для перехода на новую платформу потребуется переписать Goodnotes. Это может начать гонку разработки между уже реализованным приложением iOS и новым приложением, которое нужно построить с нуля, или повлечь за собой остановку новой разработки существующего приложения, пока новая кодовая база догоняет. Если Goodnotes сможет повторно использовать код Swift, команда сможет извлечь выгоду из новых функций, реализованных командой iOS, пока кроссплатформенная команда работает над основами приложения, и достичь паритета функций.
Продукт уже решил ряд интересных задач для iOS, добавив такие функции, как:
- Отображение заметок.
- Синхронизация документов и заметок.
- Разрешение конфликтов для заметок с использованием бесконфликтных реплицированных типов данных .
- Анализ данных для оценки модели ИИ.
- Поиск контента и индексация документов.
- Индивидуальная прокрутка и анимация.
- Просмотреть реализацию модели для всех слоев пользовательского интерфейса.
Все это было бы намного проще реализовать на других платформах, если бы команда инженеров могла получить кодовую базу iOS, уже работающую для приложений iOS и iPad, и реализовать ее как часть проекта, который Goodnotes мог бы поставлять в виде приложений для Windows, Android или веб-приложений.
Технологический стек Goodnotes
К счастью, был способ повторно использовать существующий код Swift в Интернете — WebAssembly (Wasm). Goodnotes построил прототип с использованием Wasm с открытым исходным кодом и поддерживаемым сообществом проектом SwiftWasm . С помощью SwiftWasm команда Goodnotes могла сгенерировать двоичный файл Wasm, используя весь уже реализованный код Swift. Этот двоичный файл можно было включить в веб-страницу, поставляемую как Progressive Web Application для Android, Windows, ChromeOS и любой другой операционной системы.
Целью было выпустить Goodnotes как PWA и иметь возможность разместить его в магазине каждой платформы. Помимо Swift, языка программирования, который уже используется для iOS, и WebAssembly, используемого для выполнения кода Swift в Интернете, проект использовал следующие технологии:
- TypeScript: наиболее часто используемый язык программирования для веб-технологий.
- React и Webpack: самые популярные фреймворки и упаковщики для Интернета.
- PWA и service workers: Огромные возможности для этого проекта, поскольку команда может поставлять наше приложение как автономное приложение, которое работает как любое другое приложение iOS, и вы можете установить его из магазина или из самого браузера.
- PWABuilder: основной проект, который Goodnotes использует для упаковки PWA в собственный двоичный файл Windows, чтобы команда могла распространять наше приложение из Microsoft Store.
- Trusted Web Activities: важнейшая технология Android, которую компания использует для распространения своего PWA в качестве нативного приложения.
На следующем рисунке показано, что реализовано с использованием классических TypeScript и React, а что реализовано с использованием SwiftWasm и ванильного JavaScript, Swift и WebAssembly. Эта часть проекта использует JSKit , библиотеку взаимодействия JavaScript для Swift и WebAssembly, которую команда использует для обработки DOM на экране нашего редактора из нашего кода Swift, когда это необходимо, или даже для использования некоторых API, специфичных для браузера.
Зачем использовать Wasm и Интернет?
Несмотря на то, что Wasm официально не поддерживается Apple, команда инженеров Goodnotes посчитала такой подход лучшим решением по следующим причинам:
- Повторное использование более 100 тысяч строк кода.
- Возможность продолжать разработку основного продукта, одновременно внося вклад в кроссплатформенные приложения.
- Возможность скорейшего выхода на каждую платформу с использованием итеративного процесса разработки.
- Возможность контролировать отображение одного и того же документа без дублирования всей бизнес-логики и внесения различий в наши реализации.
- Получение выгоды от всех улучшений производительности, реализованных на каждой платформе одновременно (и всех исправлений ошибок, реализованных на каждой платформе).
Повторное использование более 100 тысяч строк кода и бизнес-логики, реализующей наш конвейер рендеринга, было фундаментальным. В то же время, обеспечение совместимости кода Swift с другими инструментальными цепочками позволяет им повторно использовать этот код на разных платформах в будущем, если это необходимо.
Итеративная разработка продукта
Команда использовала итеративный подход, чтобы как можно быстрее донести что-то до пользователей. Goodnotes начинался с версии продукта только для чтения, где пользователи могли получить любой общий документ и прочитать его с любой платформы. Просто по ссылке они могли получить доступ и прочитать те же заметки, которые они написали на своем iPad. На следующем этапе были добавлены функции редактирования, чтобы сделать кроссплатформенные версии эквивалентными версии iOS.
Первая версия продукта только для чтения заняла шесть месяцев разработки, следующие девять месяцев были посвящены первой группе функций редактирования и экрану пользовательского интерфейса, где вы можете проверить все документы, которые вы создали или которыми кто-то поделился с вами. Кроме того, новые функции платформы iOS было легко перенести в кроссплатформенный проект благодаря SwiftWasm Toolchain. Например, был создан новый тип ручки, который легко реализовали кроссплатформенно, повторно используя тысячи строк кода.
Создание этого проекта было невероятным опытом, и Goodnotes многому научился из него. Вот почему в следующих разделах мы сосредоточимся на интересных технических моментах веб-разработки и использовании WebAssembly и таких языков, как Swift.
Первоначальные препятствия
Работа над этим проектом была очень сложной с разных точек зрения. Первое препятствие, с которым столкнулась команда, было связано с цепочкой инструментов SwiftWasm. Цепочка инструментов была огромным подспорьем для команды, но не весь код iOS был совместим с Wasm. Например, код, связанный с вводом-выводом или пользовательским интерфейсом, такой как реализация представлений, клиентов API или доступ к базе данных, не поддавался повторному использованию, поэтому команде нужно было начать рефакторинг определенных частей приложения, чтобы иметь возможность повторно использовать их из кросс-платформенного решения. Большинство созданных командой PR были рефакторингами для абстрактных зависимостей, чтобы команда могла позже заменить их с помощью внедрения зависимостей или других подобных стратегий. Код iOS изначально смешивал сырую бизнес-логику, которая могла быть реализована в Wasm, с кодом, отвечающим за ввод/вывод и пользовательский интерфейс, который не мог быть реализован в Wasm, поскольку Wasm не поддерживает ни то, ни другое. Поэтому код ввода-вывода и пользовательского интерфейса нужно было повторно реализовать в TypeScript, как только бизнес-логика Swift была готова к повторному использованию между платформами.
Решены проблемы с производительностью
Когда Goodnotes начал работать над редактором, команда выявила некоторые проблемы с опытом редактирования, и в нашу дорожную карту попали сложные технологические ограничения. Первая проблема была связана с производительностью. JavaScript — однопоточный язык. Это означает, что у него один стек вызовов и одна куча памяти. Он выполняет код по порядку и должен завершить выполнение фрагмента кода, прежде чем перейти к следующему. Он синхронный, но иногда это может быть вредно. Например, если функция выполняется долго или должна чего-то ждать, она замораживает все на это время. И это именно то, что должны были решить инженеры. Оценка некоторых конкретных путей в нашей кодовой базе, связанных со слоем рендеринга или другими сложными алгоритмами, была проблемой для команды, потому что эти алгоритмы были синхронными, и их выполнение блокировало основной поток. Команда Goodnotes переписала их, чтобы сделать их быстрее, и рефакторила некоторые из них, чтобы сделать их асинхронными. Они также ввели стратегию yield, чтобы приложение могло остановить выполнение алгоритма и продолжить его позже, позволяя браузеру обновить пользовательский интерфейс и избежать потери кадров. Это не было проблемой для приложения iOS, поскольку оно может использовать потоки и оценивать эти алгоритмы в фоновом режиме, пока основной поток iOS обновляет пользовательский интерфейс.
Другим решением, которое должна была найти инженерная группа, была миграция пользовательского интерфейса на основе HTML-элементов, прикрепленных к DOM, в пользовательский интерфейс документа на основе полноэкранного холста. Проект начал показывать все заметки и контент, относящиеся к документу, как часть структуры DOM, используя HTML-элементы, как это делала бы любая другая веб-страница, но в какой-то момент перешел на полноэкранный холст, чтобы улучшить производительность на бюджетных устройствах за счет сокращения времени работы браузера над обновлениями DOM.
Инженерная группа определила следующие изменения как меры, которые могли бы уменьшить некоторые из возникших проблем, если бы они были реализованы в начале проекта.
- Разгружайте основной поток чаще, используя веб-воркеры для сложных алгоритмов.
- Используйте экспортированные и импортированные функции вместо библиотеки взаимодействия JS-Swift с самого начала, чтобы они могли снизить влияние выхода из контекста Wasm на производительность. Эта библиотека взаимодействия JavaScript полезна для получения доступа к DOM или браузеру, но она медленнее, чем собственные экспортированные функции Wasm.
- Убедитесь, что код позволяет использовать
OffscreenCanvas
изнутри, чтобы приложение могло разгрузить основной поток и перенести все использование API Canvas в веб-процесс, максимизируя производительность приложений при написании заметок. - Перенесите все выполнение, связанное с Wasm, в веб-воркер или даже в пул веб-воркеров, чтобы приложение могло снизить нагрузку на основной поток.
Текстовый редактор
Другая интересная проблема была связана с одним конкретным инструментом — текстовым редактором. Реализация этого инструмента для iOS основана на NSAttributedString
, небольшом наборе инструментов, использующем RTF под капотом. Однако эта реализация несовместима со SwiftWasm, поэтому кроссплатформенная команда была вынуждена сначала создать собственный парсер на основе грамматики RTF , а затем реализовать редактирование, преобразуя RTF в HTML и наоборот. Тем временем команда iOS начала работать над новой реализацией этого инструмента, заменив использование RTF на собственную модель, чтобы приложение могло представлять стилизованный текст в удобном виде для всех платформ, использующих один и тот же код Swift.
Эта задача была одним из самых интересных пунктов в дорожной карте проекта, поскольку она решалась итеративно на основе потребностей пользователя. Это была инженерная проблема, решенная с использованием подхода, ориентированного на пользователя, когда команде нужно было переписать часть кода, чтобы иметь возможность отображать текст, чтобы они включили редактирование текста во втором выпуске.
Итеративные релизы
Развитие проекта за последние два года было невероятным. Команда начала работать над версией проекта только для чтения и через несколько месяцев выпустила совершенно новую версию с множеством возможностей редактирования. Чтобы часто выпускать изменения кода в производство, команда решила широко использовать флаги функций. Для каждого выпуска команда могла включать новые функции, а также выпускать изменения кода, реализующие новые функции, которые пользователь мог бы увидеть через несколько недель. Однако есть кое-что, что, по мнению команды, они могли бы улучшить! Они считают, что введение динамической системы флагов функций помогло бы ускорить процесс, поскольку это устранило бы необходимость повторного развертывания для изменения значений флагов. Это дало бы Goodnotes большую гибкость, а также ускорило бы развертывание новой функции, поскольку Goodnotes не нужно было бы связывать развертывание проекта с выпуском продукта.
Работа офлайн
Одной из основных функций, над которой работала команда, была поддержка офлайн. Возможность редактировать и изменять документы — это одна из функций, которую можно ожидать от любого приложения, подобного этому. Однако это не простая функция, поскольку Goodnotes поддерживает совместную работу. Это означает, что все изменения, внесенные разными пользователями на разных устройствах, должны попадать на каждое устройство, не требуя от пользователей разрешения каких-либо конфликтов. Goodnotes давно решил эту проблему, используя CRDT под капотом. Благодаря этим бесконфликтным реплицированным типам данных Goodnotes может объединять все изменения, внесенные в любой документ любым пользователем, и объединять изменения без каких-либо конфликтов слияния. Использование IndexedDB и хранилища, доступного для веб-браузеров, стало огромным фактором, способствующим совместной работе в автономном режиме в Интернете.
Вдобавок ко всему, открытие веб-приложения Goodnotes приводит к первоначальной стоимости загрузки около 40 МБ из-за размера двоичного файла Wasm. Изначально команда Goodnotes полагалась исключительно на обычный кэш браузера для самого пакета приложений и большинства конечных точек API, которые они использовали, но оглядываясь назад, можно было бы извлечь выгоду из более надежного API Cache и сервисных рабочих ранее. Изначально команда уклонялась от этой задачи из-за ее предполагаемой сложности, но в конце концов поняла, что Workbox сделал ее намного менее пугающей.
Рекомендации по использованию Swift в Интернете
Если у вас есть приложение iOS с большим количеством кода, который вы хотите повторно использовать, будьте готовы, потому что вы собираетесь начать невероятное путешествие. Вот несколько советов, которые могут показаться вам интересными, прежде чем вы начнете.
- Проверьте, какой код вы хотите повторно использовать. Если бизнес-логика вашего приложения реализована на стороне сервера, скорее всего, вы хотели бы повторно использовать ваш код пользовательского интерфейса, и Wasm вам здесь не поможет. Команда кратко рассмотрела Tokamak , совместимую со SwiftUI структуру для создания браузерных приложений с WebAssembly, но она оказалась недостаточно зрелой для потребностей приложения. Однако, если ваше приложение имеет сильную бизнес-логику или алгоритмы, реализованные как часть клиентского кода, Wasm станет вашим лучшим другом.
- Убедитесь, что ваша кодовая база Swift готова. Шаблоны проектирования программного обеспечения для слоя пользовательского интерфейса или определенных архитектур, создающие сильное разделение между вашей логикой пользовательского интерфейса и вашей бизнес-логикой, будут очень полезны, поскольку вы не сможете повторно использовать реализацию слоя пользовательского интерфейса. Принципы чистой архитектуры или гексагональной архитектуры также будут основополагающими, поскольку вам придется внедрять и предоставлять зависимости для всего кода, связанного с вводом-выводом, и это будет намного проще сделать, если вы будете следовать этим архитектурам, где детали реализации определяются как абстракции, а принцип инверсии зависимостей широко используется.
- Wasm не предоставляет код пользовательского интерфейса. Поэтому определитесь с фреймворком пользовательского интерфейса, который вы хотите использовать для веба.
- JSKit поможет вам интегрировать ваш код Swift с JavaScript, но имейте в виду, если у вас есть hotpath, пересечение моста JS–Swift может быть дорогим, и вам придется заменить его экспортированными функциями. Вы можете узнать больше о том, как JSKit работает под капотом, в официальной документации и в посте Dynamic Member Lookup in Swift, a hidden jewel!.
- Возможность повторного использования архитектуры будет зависеть от архитектуры, которой следует ваше приложение, и используемой вами библиотеки механизма выполнения асинхронного кода. Такие шаблоны, как MVVP или компонуемая архитектура, помогут вам повторно использовать ваши модели представлений и часть логики пользовательского интерфейса без связывания реализации с зависимостями UIKit , которые вы не можете использовать с Wasm. RXSwift и другие библиотеки могут быть несовместимы с Wasm, поэтому имейте это в виду, поскольку вам придется использовать OpenCombine , async/await и потоки в коде Swift в Goodnotes.
- Сожмите бинарный файл Wasm с помощью gzip или brotli. Помните, что размер бинарного файла будет довольно большим для классических веб-приложений.
- Даже если вы можете использовать Wasm без PWA, убедитесь, что вы хотя бы включили service worker, даже если у вашего веб-приложения нет манифеста или вы не хотите, чтобы пользователь его устанавливал. Service worker сохранит и бесплатно предоставит двоичный файл Wasm и все ресурсы приложения, так что пользователю не придется загружать их каждый раз, когда он открывает ваш проект.
- Имейте в виду, что найм может оказаться сложнее, чем ожидалось. Возможно, вам придется нанять сильных веб-разработчиков с некоторым опытом работы на Swift или сильных разработчиков Swift с некоторым опытом работы в сети. Если вы сможете найти инженеров-универсалов с некоторыми знаниями обеих платформ, это будет здорово
Выводы
Создание веб-проекта с использованием сложного технологического стека при работе над продуктом, полным трудностей, — невероятный опыт. Это будет сложно, но оно того стоит. Goodnotes никогда бы не выпустил версию для Windows, Android, ChromeOS и веб-версию при работе над новыми функциями для приложения iOS без использования этого подхода. Благодаря этому технологическому стеку и команде инженеров Goodnotes, Goodnotes теперь везде, и команда готова продолжать работать над следующими задачами! Если вы хотите узнать больше об этом проекте, вы можете посмотреть выступление команды Goodnotes на NSSpain 2023. Обязательно попробуйте Goodnotes для веб-сайтов !