Привет, меня зовут Максим, я Flutter-разработчик в компании Surf.
Мы продолжаем рассказывать про Flutter Web. И это вторая статья.
Разработка собственных библиотек
Межъязыковое взаимодействие JS-Dart устроено достаточно просто. Но вписать большой функционал только на Dart не всегда удобно. Как минимум, из-за отсутствия подсказок IDE о доступных методах в JS-объектах, а как максимум — из-за многословного преобразования комплексных объектов.
Большие модули гораздо удобнее разрабатывать на нативном языке (JS) и оставлять несложный интерфейс для взаимодействия с Dart-частью.
Простой пример
Реализуем несколько простых функций. Для этого создадим файл simple.js
в директории web/js
.
/// web/js/simple.js /// Простая функция с входным параметром function customPrint(value) { console.log(value); } /// Асинхронная функция, /// возвращающая Promise, /// аналог Future в Dart async function future() { await delay(1000); return new Promise((resolve, reject) => { // Здесь ваш асинхронный код // Если всё прошло успешно, вызовите resolve(result) // Если что-то пошло не так, вызовите reject(error) resolve('success'); }); } /// Вспомогательный метод, эмулирующий работу function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
Подключаем новую зависимость в файл web/index.html
.
<...> <script src="./js/simple.js"></script> </head>
Проверяем с помощью консоли, что методы успешно импортированы
Теперь описываем интерфейсы уже знакомым нам способом:
// lib/main.dart @JS('customPrint') external void customPrint(String message); @JS('future') external JSPromise<JSString> jsAsyncMethod();
Тут стоит обратить внимание, что JS Interop не умеет не явно преобразовывать Promise
-> Future
. По этой причине в описании интерфейса мы должны указать тип JSPromise
c соответствующим дженерик типом, если это необходимо. И не забыть о приведении к системе типов Dart с помощью геттера toDart
.
// lib/main.dart final message = await jsAsyncMethod().toDart; print(message);
Этих знаний будет достаточно, чтобы реализовать большую часть задач для Flutter Web.
Но иногда нам нужно прописать сложную бизнес-логику или использовать сторонние библиотеки. Для закрытия этого пула задач есть огромное количество технологий и инструментов для нативной веб-разработки. Наиболее близкими и приятными нам показались Node.js, npm, TypeScript и webpack
Вы спросите: зачем нам столько всего? Объясним:
-
Node.js — это не просто фреймворк, это среда выполнения JS-кода. Благодаря тому, что она не привязана к рантайму браузера, она позволяет использовать JS практически везде. Что, в свою очередь, очень популяризировало эту технологию за пределами веб-сайтов. У Node.js есть огромное комьюнити, которое реализовало множество библиотек. Они помогают не только взаимодействовать с браузером, но и, например, конвертировать видео, работать с документами, генерировать изображения и делать многое другое;
-
npm — пакетный менеджер для Node.js, в репозиториях которого хранятся библиотеки;
-
TypeScript — надстройка над языком JavaScript, которая позволяет удобно писать строго типизированный код и использовать подход ООП, что для нас более привычно;
-
webpack — из-за особенностей веб-стандартов мы должны разрабатывать один модуль, в рамках одного файла. А это не всегда удобно. Webpack позволяет собирать модуль разбитый на части в один или несколько бандлов.
А теперь разработаем первый модуль, и на этот раз, он будет прикладным.
Модульная библиотека
Отличие веб-приложений, от, например, мобильных, заключается в том, что в рамках одного браузера может быть запущено несколько вкладок с нашим приложением. А это может стать причиной не самых приятных последствий. Например, множественных оповещений для одного события.
Предлагаем реализовать простой плагин для обнаружения новых инстансов приложения.
Создадим новую директорию для исходников плагина {root_project}/packages/instanse_detector
Перейдём в эту директорию в терминале и инициализируем новый npm-модуль.
npm init -y
Подробную информацию об установке и настройке Node.js и npm в вашем окружении вы найдёте тут
В текущую директорию добавился файл package.json
, который служит для конфигурации нашего проекта, управления зависимостями и сборки. Эдакий pubspec.yaml
, только из мира Node.js.
Внесём правки:
// packages/instanse_detector/package.json { "name": "instance_detector", "version": "0.0.1", "scripts": { "tsc": "tsc", /// добавим алиас для запуска TS "build": "npm run tsc" }, "devDependencies": { // указываем, что используем TypeScript // для разработки "typescript": "^5.1.6" } }
Теперь выполним команду npm install
для установки необходимых зависимостей.
Следующим этапом подготовим конфигурацию для компиляции TypeScript.
Добавим файл tsconfig.json
в корень нашего модуля.
packages/instanse_detector/tsconfig.json { "compilerOptions": { /// указываем стандарты /// ES5 - максимальный для взаимодействия с Flutter web "target": "ES5", "module": "system", "moduleResolution": "node", /// директория, куда будет сохранен скомпилированный JS "outDir": "../../web/js/", }, "exclude": [ "node_modules", "build" ] }
Более подробную информацию о
tsconfig
вы найдёте тут
Пора переходить к реализации нашего модуля. Для этого создадим поддиректорию src
и добавим туда файл с расширением ts
.
// packages/instanse_detector/src/instanse_detector.ts /// Вспомогательное перечисление, для определения инстанса приложения enum Message { First, Second, } /// Для удобства определим тип метода с булевым параметром type OnInstanceEvent = (e: boolean) => void; class InstanceDetector { onEvent?: OnInstanceEvent; channel: BroadcastChannel; constructor() { /// Вкладки браузера могут общаться между собой /// при помощи каналов this.channel = new BroadcastChannel("simpl-instanse-detector"); this.channel.onmessage = (event) => { /// Если в канал поступило сообщение с типом First /// значит запущен новый инстанс приложения if (event.data === Message.First) { /// сообщаем в ответ что есть другой инстанс this.channel.postMessage(Message.Second); /// оповещаем Dart this.onEvent(true); } /// Если в получили событие Second /// Это означает, что мы главный инстанс if (event.data === Message.Second) { /// оповещаем Dart this.onEvent(false); } }; /// При инициализации сообщаем всем, что /// мы главный инстанс this.channel.postMessage(Message.First); } /// Метод установки коллбека на изменения состояния init(onEvent: OnInstanceEvent) { this.onEvent = onEvent; onEvent(false); } }
Для регистрации коллбэка мы передаем его через метод init
, а не через конструктор, что кажется более удобным вариантом. Но по загадочным причинам передача колбэков через конструктор работает нестабильно и не всегда корректно.
Итоговая структура нашего проекта может выглядеть так:
📁root_project_folder/ └──📁packages/ ├──📁instance_detector/ │ └──📁src/ │ └──📄instance_detector.ts ├──📄package.json └──📄tsconfig.json
Итак, мы запустили команду npm run build
в папке /web/js
. У нас появился скомпилированный JS-файл, который нужно подключить и использовать.
Можно обойтись этим методом и всё заработает. Но бывает, что приложение собирается только под веб и игнорирует другие платформы.
В текущем варианте, если мы попытаемся запустить приложение под платформами, отличными от web, то получим ошибку. Поэтому всю работу с JS стоит оборачивать в плагины.
Рассмотрим этот подход подробнее.
Обеспечение кросскомпиляции для разных платформ
В директории packages
генерируем плагин командой:
flutter create --template=package {name}
Разделим реализацию плагина для каждой из платформ. Объявим общий интерфейс.
class INativeInstanceDetector {}
Здесь мы обойдёмся пустым контрактом — нам не потребуются ни методы, ни параметры.
Создадим структуру для плагина:
📁project_foler └──📁lib_folder ├──📁lib │ ├──📁src │ │ ├──📁impl │ │ │ ├──📁io │ │ │ │ └──📄io.dart │ │ │ └──📁web │ │ │ ├──📄bindings.dart │ │ │ └──📄web.dart │ │ └──📄i_instance_detector.dart │ └──📄export.dart └──📁packages └──📁js_source ├──📁src │ └──📄instanse_detector.ts ├──📄package.json └──📄tsconfig.json
На этом этапе работа с io нам не очень важна — просто реализуем интерфейс, чтобы избежать ошибок сборки.
// lib_folder/lib/src/i_instance_detector.dart class NativeInstanceDetector extends INativeInstanceDetector { NativeInstanceDetector(Function(bool event) onInstanceAdded); }
Теперь реализуем веб-часть.
Опишем интерфейсы для работы с JS в файле bindings.dart
.
// lib_folder/lib/src/impl/web/bindings.dart @JS() library instance_detect.js; import 'dart:js_interop'; typedef OnInstanceAdded = void Function(bool event); @JS('InstanceDetector') extension type InstanceDetector._(JSObject _) implements JSObject { external InstanceDetector(); external JSFunction? get onEvent; external void init(JSFunction onEvent); }
Обратим внимание на расширение типа JSObject
. До этого момента мы вызывали либо простые методы, либо статичные — в них достаточно просто добавить аннотации @JS
@staticInterop
.
Здесь нам необходимо создать расширение типа JSObject
. Такой подход применяется для описания JS-объекта на стороне Dart, когда необходимо создать инстанс этого объекта и обращаться к его полям или методам.
После — реализуем INativeInstanceDetector
для web в файле web.dart
.
// lib_folder/lib/src/impl/web/web.dart class NativeInstanceDetector extends INativeInstanceDetector { final InstanceDetector detector; /// В конструкторе примим коллбэк, /// который будет передан в JS-слой NativeInstanceDetector(OnInstanceAdded onInstanceAdded) : detector = InstanceDetector() { /// Заводим инстанс обертки над JS-объектом InstanceDetector final detector = InstanceDetector(); /// Производим инициализацию, передавая коллбек, /// который будет вызван при запуски нового инстанса /// нашего приложения detector.init(onInstanceAdded.toJS); } }
Завершающим этапом подготовки будет export.dart
файл, в котором мы экспортируем нужную реализацию в зависимости от окружения:
// lib_folder/lib/export.dart library detector; export 'src/impl/io/io.dart' // по умолчанию if (dart.library.js) 'src/impl/web/web.dart' // web if (dart.library.io) 'src/impl/io/io.dart'; // io
Обратим внимание, что для правильной работы такого подхода имя реализации должно быть одинаковым для обеих платформ.
Добавляем плагин в pubspeck.yaml
:
dependencies: flutter: sdk: flutter detector: path: './packages/detector'
Использование плагина ничем не отличается от того, что мы стягиваем с pub.dev.
// lib/main.dart /// Заводим инстанс детектора и передаем туда коллбэк, /// который сработает на изменение состояния вкладки NativeInstanceDetector((event) { setState(() { if (event) { _isSecondary = event; } }); });
Такое разделение реализаций обеспечивает беспроблемную сборку и для веб, и для мобильных, и для десктопных приложений.
Что в итоге
Мы рассмотрели основы взаимодействия Dart и JS. В целом, этого вполне достаточно для подготовки приложений к работе в браузере. С помощью наших статей вы сможете реализовать практически любой функционал веб-приложений и обеспечить взаимодействие с браузером.
Но это не всё. В следующих статьях мы расскажем о компиляции веб-приложения в wasm.
Этот метод стал доступен не так давно, общую информацию и наше мнение о нём вы найдёте в нашем материале.
Мы поговорим о разработке и использовании wasm-модулей, о публикации веб-приложений, настройке ci/cd, оптимизации загрузки и оценим прирост производительности. До встречи!
Больше полезного про Flutter — в Telegram-канале Surf Flutter Team.
Кейсы, лучшие практики, новости и вакансии в команду Flutter Surf в одном месте. Присоединяйтесь!
ссылка на оригинал статьи https://habr.com/ru/articles/856986/
Добавить комментарий