Flutter Web. Часть 2

от автора

Привет, меня зовут Максим, я 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *