Irbisa · Шпаргалка

Flutter в вебе и на десктопе

Рендереры и wasm, отложенная загрузка, PWA, JS-интероп, окна десктопа, пробелы в плагинах

Senior-разработчик · 20 вопросов

  1. 01

    Как добавить поддержку веба или десктопа в существующий Flutter-проект и что при этом генерируется?

    Выполнить flutter create --platforms=web,macos,windows,linux . в корне проекта.

    # from the project root; lib/ is not touchedflutter create --platforms=web,macos,windows,linux . flutter doctor        # Xcode / Visual Studio / GTK toolchainsflutter devices       # chrome, macos, ...flutter run -d chrome…
  2. 02

    Какие ограничения у Flutter Web и когда он подходит?

    Flutter web рисует весь UI в canvas, поэтому он хорош для продуктов-приложений за логином (дашбордов, внутренних инструментов, редакторов, веб-версии мобильного приложения) и плохо подходит там, где страница должна открываться мгновенно, индексироваться или вести себя как документ.

  3. 03

    Какие рендереры использует Flutter web сегодня (CanvasKit или skwasm) и что стало с HTML-рендерером?

    Два, и оба представляют собой Skia, скомпилированную в WebAssembly: CanvasKit для сборки по умолчанию на JavaScript и skwasm для сборки с --wasm.

  4. 04

    Тимлид прочитал, что Flutter компилируется в WebAssembly, и спрашивает, почему прод всё ещё отдаёт JavaScript. Что вы ответите?

    Потому что --wasm включается явно, и даже тогда в сборке рядом с wasm лежит JavaScript-версия: flutter.js выбирает её по браузеру, и каждый, чей браузер не может запустить wasm-сборку, всё равно получает JS.

    # Emits main.dart.wasm AND main.dart.js; flutter.js picks per browserflutter build web --release --wasm # Multi-threaded skwasm needs cross-origin isolation on the HTML documentcurl -sI https://app.example.com/ | grep -i '^cross-origin'# Cross-Origin-Embedder-Policy: credentialless…
  5. 05

    Пользователи две секунды смотрят на белую страницу, прежде чем появится веб-приложение. Что выполняется до первого кадра Flutter и как это сократить?

    Загружается HTML, flutter_bootstrap.js выбирает сборку, скачиваются и компилируются main.dart.js и рендерер, инициализируется движок, и только потом runApp рисует.

    // index.html: <div id="boot">skeleton markup</div> and//   window.addEventListener('flutter-first-frame', () =>//       document.getElementById('boot')?.remove()); // before: every await here is white screen//   await Firebase.initializeApp(); await config.fetch(); runApp(...);…
  6. 06

    Админка импортирована через deferred as, но main.dart.js почти не похудел, а part-файл не появился. Почему?

    // main.dartimport 'admin/admin_page.dart' deferred as admin;import 'routes.dart'; Future<Widget> openAdmin() async {  await admin.loadLibrary();  return admin.AdminPage();} // routes.dart: imported eagerly by main.dartimport 'package:flutter/material.dart';import 'admin/admin_page.dart'; final routes = <String, WidgetBuilder>{  '/': (_) => const HomePage(),  '/admin': (_) => const AdminPage(),};

    dart2js выносит библиотеку в отдельный part-файл, только когда каждый путь к ней отложенный.

  7. 07

    На GitHub Pages под /my-app/ веб-сборка показывает пустую страницу, и все ассеты отдают 404. Что исправляет --base-href и что ещё меняется?

    index.html разрешает каждый относительный URL от своего <base href>, который по умолчанию равен /, поэтому приложение запрашивает /main.dart.js вместо /my-app/main.dart.js.

    # GitHub Pages project site: https://user.github.io/my-app/flutter build web --release --base-href /my-app/ grep '<base' build/web/index.html# <base href="/my-app/">  (web/index.html must keep <base href="$FLUTTER_BASE_HREF">)…
  8. 08

    Маркетинг говорит, что ни одна страница не появляется в Google, а любая расшаренная ссылка показывает один и тот же заголовок. Что с этим реально можно сделать?

    Изнутри Flutter-приложения ничего не сделать.

  9. 09

    Как сделать Flutter-веб-приложение устанавливаемым PWA и добиться, чтобы пользователи действительно получали новую сборку после деплоя?

    Сгенерированные manifest.json и иконки уже делают его устанавливаемым; офлайн-кэш и обновления остаются вашей заботой.

    # What browsers were told on the last deploycurl -sI https://app.example.com/main.dart.js | grep -i cache-control # nginx: revalidate the entrypoints, long caching only for files you version yourself#   location ~ ^/(index\.html|flutter_bootstrap\.js|main\.dart\.(js|mjs|wasm)|version\.json)$ {#     add_header Cache-Control "no-cache";…
  10. 10

    Каждый запрос из веб-сборки падает с «XMLHttpRequest error» или «Failed to fetch» без кода статуса, а на iOS всё работает. Что происходит?

    Браузер заблокировал ответ раньше, чем его увидел Dart; почти всегда это CORS, иногда смешанный контент.

  11. 11

    Хелпер импортирует dart:html, и wasm-сборка не компилируется. Чем это заменили и как сегодня вызывать JS-SDK из Dart?

    dart:html, dart:js, dart:js_util и package:js относятся к устаревшему interop, и для dart2wasm их нет.

    import 'dart:js_interop';import 'dart:js_interop_unsafe';import 'package:web/web.dart' as web; // before (not available to dart2wasm)://   html.document.getElementById('root')!.classes.add('ready');…
  12. 12

    Этот условный импорт работает в JavaScript-сборке, но под --wasm приложение молча использует заглушку. Почему?

    // storage.dartimport 'storage_stub.dart'    if (dart.library.io) 'storage_io.dart'    if (dart.library.html) 'storage_web.dart'; // storage_web.dartimport 'package:web/web.dart' as web; String? readToken() => web.window.localStorage.getItem('token'); // storage_stub.dartString? readToken() => null;

    Условие спрашивает, есть ли у цели компиляции dart:html, а у dart2wasm его нет.

  13. 13

    Найдите баг: редактор работает на Android, iOS и macOS, но в релизной веб-сборке вместо тулбара виден серый прямоугольник.

    import 'dart:io' show Platform;import 'package:flutter/material.dart'; class EditorToolbar extends StatelessWidget {  const EditorToolbar({super.key, required this.onSave});  final VoidCallback onSave;   @override  Widget build(BuildContext context) {    final desktop = Platform.isMacOS || Platform.isWindows || Platform.isLinux;    return Row(children: [      IconButton(icon: const Icon(Icons.save), onPressed: onSave),      if (desktop)        Text(Platform.isMacOS ? 'Cmd+S' : 'Ctrl+S'),    ]);  }}

    Platform берётся из dart:io, который веб-компиляторы принимают, но не реализуют: Platform.isMacOS бросает Unsupported operation: Platform._operatingSystem прямо в build, и релизная сборка рисует серый ErrorWidget вместо тулбара.

  14. 14

    Тот же экран плавно прокручивается на телефоне, но дёргается в браузере. Почему Flutter web медленнее и как найти и убрать эту стоимость?

    В браузере ваш Dart выполняется как JavaScript или wasm в главном потоке страницы, а с CanvasKit там же идёт и растеризация, так что build, layout, paint и raster делят один поток, который на мобильных разделён надвое.

  15. 15

    Маркетинговый сайт на React, и они хотят ваш Flutter-график на странице с обменом данными в обе стороны. Как его смонтировать и как общаться через границу?

    Flutter web монтируется в тот элемент, который вы передали загрузчику как hostElement, а стороны обмениваются данными только через явный JS interop, без общего состояния, роутера или шины событий.

    // web/flutter_bootstrap.js (your copy of the template)://   {{flutter_js}}//   {{flutter_build_config}}//   _flutter.loader.load({ config: { hostElement: document.querySelector('#chart') } });// React side: <div id="chart" style={{height: 360}} />//   el.addEventListener('chart-selection', (e) => setSelected(e.detail.id));…
  16. 16

    Найдите баг: после нескольких открытий и закрытий экрана оплаты каждый postMessage обрабатывается по несколько раз и появляется setState() called after dispose().

    class _PaymentFrameState extends State<PaymentFrame> {  String _status = 'waiting';  void _onMessage(web.MessageEvent e) {    if (e.origin == payOrigin) setState(() => _status = '${e.data.dartify()}');  }  @override  void initState() {    super.initState();    web.window.addEventListener('message', _onMessage.toJS);  }  @override  void dispose() {    web.window.removeEventListener('message', _onMessage.toJS);    super.dispose();  }}

    Каждый .toJS оборачивает tear-off в новую JS-функцию, и removeEventListener получает функцию, которую window не видел, поэтому ничего не снимается.

  17. 17

    Экспорт в CSV на macOS пишет файл, а в веб-сборке не делает ровно ничего. Где именно вас останавливает браузер?

    В вебе нет файловой системы.

    import 'dart:convert';import 'dart:js_interop';import 'package:file_selector/file_selector.dart';import 'package:web/web.dart' as web; // wrong on web: compiles, then fails; there is no documents directory…
  18. 18

    Через месяц приложение выходит в вебе, а в pubspec сорок зависимостей. Как найти те, у которых нет веб-реализации, раньше, чем это сделает QA?

    Проверяйте поддержку платформ как данные ещё до первой веб-сборки, потому что отсутствующая реализация проявляется тремя способами (упавшей сборкой, MissingPluginException на первом вызове или тихим бездействием), и громкий из них только первый.

  19. 19

    Десктопная сборка открывается в размере по умолчанию из runner, без нормального меню, а Cmd+Q выбрасывает несохранённую работу. Где здесь фреймворк, а где плагин?

    Окно почти не принадлежит Dart.

    import 'dart:ui' show AppExitResponse; // initial size lives in the native runner, not hereclass _EditorState extends State<Editor> {  late final AppLifecycleListener _lifecycle;…
  20. 20

    Десктопное приложение надо выпускать мимо сторов, и оно должно само обновляться. Что требует каждая платформа, прежде чем пользователь его запустит?

    Подписывайте и пакуйте под каждую ОС: подписанный MSIX или инсталлятор на Windows, подпись Developer ID плюс нотаризация на macOS, бандл, собранный на самой старой glibc и упакованный в .deb, Flatpak или AppImage на Linux.

    # --- Windows ---------------------------------------------------------------flutter build windows --release# ship all of build/windows/x64/runner/Release/ : exe + DLLs + data/dart run msix:create --release   # reads msix_config: from pubspec.yaml # --- macOS -----------------------------------------------------------------…