Eine Flutter-Web-App von dart:html auf package:web und dart:js_interop migrieren
Eine schrittweise Migration weg von den veralteten dart:html, dart:js_util und package:js hin zu package:web 1.1.1 und dart:js_interop: wie Sie jeden problematischen Import mit dem dart2wasm-Compiler finden, was dart fix umbenennt und was nicht, die Fallstricke von JSImmutableListWrapper und innerHTML, und wie Sie mit flutter build web --wasm verifizieren.
Eine Flutter-Web-Codebasis mit einer Handvoll dart:html-Aufrufe ist eine Migration von einem halben Tag. Eine Codebasis, in der dart:html in gemeinsame Pakete, Mocks oder ein selbst gepflegtes Plugin gesickert ist, dauert eine Woche, und der Engpass ist fast nie der eigene Code: es ist die transitive Abhängigkeit, die die alte Bibliothek noch importiert. Optional ist davon nichts mehr. dart:html, dart:js, dart:js_util und package:js wurden in Dart 3.7 (Februar 2025) als veraltet markiert, keines davon kompiliert unter dart2wasm, und das Ersatzpaar, package:web 1.1.1 zusammen mit dart:js_interop, ist seit Juli 2024 stabil. Dieser Leitfaden zielt auf den aktuellen Stable-Kanal, Flutter 3.47.2 mit Dart 3.13.2 (veröffentlicht am 2026-08-27), und auf package:web 1.1.1, das Dart ^3.4.0 voraussetzt. Jede Compiler-Ausgabe unten stammt aus einem echten Lauf mit der Stable-Toolchain Flutter 3.44.8 / Dart 3.12.2 und demselben package:web 1.1.1.
Warum sich das nicht länger aufschieben lässt
- WebAssembly hängt daran.
dart2wasmweigert sich, ein Programm zu kompilieren, das transitivdart:htmlerreicht. Wer den Nutzen will, der in Flutter-Web-Apps mitflutter build web --wasmbauen beschrieben ist, zahlt diese Migration als Eintrittspreis, nicht als Optimierung. - Die Veraltung wirkt bereits.
dart analyzemeldetdeprecated_member_usein der Import-Zeile selbst, also schlägt jeder CI-Job mit--fatal-infosbereits fehl oder ist eine Konfigurationsänderung davon entfernt. package:webwird unabhängig vom SDK versioniert. Neue Browser-APIs kommen als Paketversion, statt auf ein SDK-Release zu warten, undpackage:webwird direkt aus dem Web IDL generiert, sodass die Namen zu MDN passen statt zu einem Dart-Styleguide von 2013.- Wer ein Paket veröffentlicht, blockiert seine Nutzer bis zur Migration. Ein einziger
dart:html-Import in einem Blattpaket blockiert den gesamten Abhängigkeitsgraph darunter.
Was bricht
| Bereich | Änderung | Schweregrad |
|---|---|---|
| Typnamen | Dart-Stilnamen kehren zu IDL-Namen zurück: aus HtmlElement wird HTMLElement, aus InputElement wird HTMLInputElement, aus AnchorElement wird HTMLAnchorElement | hoch, aber weitgehend automatisierbar |
| Collections | querySelectorAll und children liefern NodeList / HTMLCollection, die kein List implementieren | hoch |
| Typprüfungen | is und as funktionieren auf Browsertypen nicht mehr, weil jeder package:web-Typ zu JSObject gelöscht wird | hoch |
| Mocking | Extension Types haben keinen virtuellen Dispatch, daher kann ein Mock, der eine dart:html-Klasse implements, keinen package:web-Typ implementieren | hoch |
| Typsignaturen | innerHTML ist JSAny, Event-Listener nehmen JSFunction, daher brauchen die Aufrufstellen .toJS | mittel |
| Zonen | Callbacks werden nicht mehr automatisch an die aktuelle Zone gebunden | mittel |
| Bedingte Imports | dart.library.html muss zu dart.library.js_interop werden | mittel |
| Platform Views | View-Factories müssen ein package:web-Element liefern und sich über dart:ui_web registrieren | mittel |
dart:js_util | getProperty / setProperty / callMethod wandern nach dart:js_interop_unsafe mit JSAny-Schlüsseln | gering, mechanisch |
Checkliste vor dem Start
- Flutter 3.47.2 oder neuer im Stable-Kanal. Alles ab Flutter 3.22 (Dart 3.4) funktioniert, aber die unten beschriebenen Analyzer-Fixes sind in neueren SDKs besser.
flutter pub add web, was zuweb: ^1.1.1auflöst.- Ein CI-Job, der
flutter build web --wasmausführt, auch wenn Sie den Wasm-Build noch nicht ausliefern. Er ist der einzige zuverlässige Detektor für alte Imports, die in Abhängigkeiten stecken. - Ein Branch, keine Reihe kleiner Commits auf
main. Der Umbenennungsdurchlauf berührt viele Dateien auf einmal und lässt sich in Scheiben schlecht reviewen. - Eine Inventarliste der Pakete, von denen Sie abhängen und die zuletzt vor Mitte 2024 veröffentlicht wurden. Das sind die wahrscheinlichen Blocker.
Migrationsschritte
-
Finden Sie jeden problematischen Import mit dem Compiler, nicht mit grep.
grep -r "dart:html" lib/findet Ihren Code und übersieht die Abhängigkeit drei Ebenen tiefer, die Sie tatsächlich blockiert.dart2wasmgibt stattdessen die vollständige Importkette aus. Führen Sieflutter build web --wasmaus und lesen Sie den ersten Fehler:Target dart2wasm failed: ProcessException: Process exited abnormally with exit code 254: lib/legacy_bit.dart:1:8: Error: Dart library 'dart:html' is not available on this platform. import 'dart:html' as html; ^ Context: The unavailable library 'dart:html' is imported through these packages: main.dart => package:fweb => dart:html Detailed import paths for (some of) the these imports: main.dart => package:fweb/main.dart => package:fweb/legacy_bit.dart => dart:htmlDer Block “Detailed import paths” ist der nützliche Teil. Endet die Kette in einem Pub-Paket statt in Ihrem eigenen
lib/, haben Sie eine Abhängigkeit gefunden, die aktualisiert, geforkt oder ersetzt werden muss, bevor Ihre App umziehen kann.Verifizierung: Jeder vom Compiler ausgegebene Pfad ist notiert und als “eigener Code”, “eigenes Paket” oder “Drittanbieter” eingeordnet. Nichts bleibt als “wird schon passen” stehen.
-
Tauschen Sie den Import und fügen Sie die Abhängigkeit hinzu. Pro Datei wird aus
import 'dart:html' as html;einimport 'package:web/web.dart' as web;. Behalten Sie das Präfix. Ein Import vonpackage:webohne Präfix bringt mehrere hundert Top-Level-Namen in den Geltungsbereich und kollidiert mit Flutters eigenenElement,ImageundText.flutter pub add webVerifizierung:
flutter pub deps | grep webzeigtweb 1.1.1, und die Fehler der Datei wechseln von “deprecated” zu einer Liste undefinierter Namen. Undefinierte Namen sind Fortschritt, sie sind die sichtbar gewordene Umbenennungsarbeit. -
Führen Sie
dart fixfür die Typumbenennungen aus und erledigen Sie den Rest von Hand.package:webliefert einelib/fix_data.yamlmit 141 Umbenennungstransformationen aus, sodass der Analyzer die meisten alten Typnamen umschreiben kann, sobald der neue Import steht:dart fix --dry-run dart fix --applyIn einer Datei mit
InputElement,HtmlElementundCheckboxInputElementschreibtdart fix --applydie ersten beiden um und lässt das dritte unangetastet:// After dart fix --apply, package:web 1.1.1 final HTMLInputElement input = HTMLInputElement(); final HTMLElement box = document.querySelector('#box') as HTMLElement; final CheckboxInputElement cb = CheckboxInputElement(); // still undefinedCheckboxInputElementist keine Umbenennung, sondern ein Komforttyp ausdart:htmlohne IDL-Gegenstück. Die manuelle Form lautetHTMLInputElement()..type = 'checkbox'. Fehlt für einen Namen die Transformation, schlagen Sie die@Native-Annotation an der altendart:html-Klasse nach: ihr Wert ist der Name inpackage:web.Verifizierung:
dart analyzemeldet nullundefined_class- undundefined_function-Diagnosen in den migrierten Dateien. -
Ersetzen Sie
dart:js_utilundpackage:jsdurchdart:js_interop. Die alten dynamischen Zugriffe wandern nachdart:js_interop_unsafeund nehmenJSAny-Schlüssel stattString. Deklarierte Interop-Typen wechseln von@JS()-Klassen zu Extension Types überJSObject. Vorher:// dart:html + dart:js_util, Dart 3.12.2 import 'dart:convert'; import 'dart:html'; import 'dart:js_util' as js_util; void downloadCsv(String csv) { final blob = Blob([csv], 'text/csv'); final url = Url.createObjectUrlFromBlob(blob); AnchorElement(href: url) ..download = 'report.csv' ..click(); Url.revokeObjectUrl(url); } Future<Map<String, dynamic>> loadJson(String path) async { final text = await HttpRequest.getString(path); return jsonDecode(text) as Map<String, dynamic>; } void unsafeAccess() { final maybe = js_util.getProperty(window, 'myLegacyGlobal'); if (maybe != null) { js_util.callMethod(maybe, 'init', ['flutter']); } }Nachher:
// package:web 1.1.1 + dart:js_interop, Dart 3.12.2 import 'dart:convert'; import 'dart:js_interop'; import 'dart:js_interop_unsafe'; import 'package:web/web.dart'; void downloadCsv(String csv) { final blob = Blob([csv.toJS].toJS, BlobPropertyBag(type: 'text/csv')); final url = URL.createObjectURL(blob); final anchor = document.createElement('a') as HTMLAnchorElement ..href = url ..download = 'report.csv'; anchor.click(); URL.revokeObjectURL(url); } Future<Map<String, dynamic>> loadJson(String path) async { final response = await window.fetch(path.toJS).toDart; final text = await response.text().toDart; return jsonDecode(text.toDart) as Map<String, dynamic>; } void unsafeAccess() { final maybe = globalContext.getProperty<JSObject?>('myLegacyGlobal'.toJS); if (maybe != null) { maybe.callMethod<JSAny?>('init'.toJS, 'flutter'.toJS); } }Drei Muster sollten sitzen: aus
allowInterop(fn)wirdfn.toJS, ausjs_util.promiseToFuture(p)wirdp.toDart, und ein mit.toDarterwartetesJSPromise<T>liefert einFuture<T>. FürHttpRequestgibt es keinen direkten Ersatz, der sich lohnt; die Antwort heißtwindow.fetchoderpackage:http.Verifizierung:
dart analyzeist sauber, und keine Datei im Repository importiert nochdart:js,dart:js_utiloderpackage:js. -
Verschieben Sie Platform-View-Factories nach
dart:ui_web. Jeder Code, der eine HTML-View registriert, muss jetzt einpackage:web-Element liefern. Die Registry liegt indart:ui_web, undregisterViewFactoryist deklariert alsregisterViewFactory(String viewType, Function viewFactory, {bool isVisible = true}):// Flutter 3.44.8, package:web 1.1.1 import 'dart:ui_web' as ui_web; import 'package:flutter/widgets.dart'; import 'package:web/web.dart' as web; const _viewType = 'startdebugging-iframe'; void registerIframeFactory() { ui_web.platformViewRegistry.registerViewFactory(_viewType, (int viewId) { final iframe = web.document.createElement('iframe') as web.HTMLIFrameElement ..src = 'https://startdebugging.net/' ..style.border = 'none' ..style.width = '100%' ..style.height = '100%'; return iframe; }); } class EmbeddedSite extends StatelessWidget { const EmbeddedSite({super.key}); @override Widget build(BuildContext context) => const HtmlElementView(viewType: _viewType); }Verifizierung: Die View rendert unter
flutter run -d chrome, undflutter build web --wasmkompiliert die Datei ohne Beanstandung. -
Schreiben Sie bedingte Imports auf
dart.library.js_interopum. Die alte Schreibweise wählt unterdart2wasmstillschweigend die Stub-Implementierung, weildart.library.htmldort falsch ist, was zur Laufzeit einenUnsupportedErrorstatt eines Compile-Fehlers erzeugt. Das ist der schlimmste Fehlermodus dieser gesamten Migration:// lib/platform_open.dart, Dart 3.12.2 export 'src/open_stub.dart' if (dart.library.io) 'src/open_io.dart' if (dart.library.js_interop) 'src/open_web.dart';// lib/src/open_web.dart import 'package:web/web.dart' as web; void openUrl(String url) => web.window.open(url, '_blank');Verifizierung: Greppen Sie das Repository nach
dart.library.htmlund bestätigen Sie null Treffer, dann starten Sie die App auf einem nativen Ziel und im Web, um zu belegen, dass jeder Zweig weiterhin auflöst. Dieselbe Technik gilt für das größere Thema plattformspezifischer Code ohne Plugin. -
Reparieren Sie die Tests zuletzt, denn Mocks brechen anders.
package:web-Typen sind Extension Types überJSObject, daher kompiliert ein Fake, derimplements HTMLElementschreibt, nicht. Ersetzen Sie klassenbasierte Fakes durch echte DOM-Knoten, die im Test erzeugt werden, oder durch ein JS-Objekt, das Sie bauen und dem getesteten Code übergeben. Alles, was zudynamicgegriffen hat, um ein DOM-Member aufzurufen, funktioniert ebenfalls nicht mehr, weil Extension-Type-Member nur statisch aufgelöst werden.Verifizierung:
flutter testläuft durch, und in der Suite steht keineimplements-Klausel mehr, die auf einenpackage:web-Typ zeigt.
Verifizierung
Führen Sie alle vier aus, in dieser Reihenfolge:
dart analyze --fatal-infos
flutter test
flutter build web
flutter build web --wasm
Der letzte Befehl ist das eigentliche Tor. In einer migrierten App endet er mit Built build/web und legt main.dart.wasm, main.dart.mjs und den dart2js-Fallback main.dart.js in build/web ab. Schlägt er weiterhin fehl, nennt der Fehler genau die verbliebene Importkette. Danach laden Sie die App und klicken alles durch, was das DOM berührt: Dateidownloads, Zwischenablage, Iframes, localStorage und jedes JS-SDK, mit dem Sie per Interop sprechen.
Rollback-Plan
Ein Rollback pro Datei ist einfach, ein Rollback des gesamten Repositorys lohnt die Planung nicht. package:web und dart:html können im selben Programm koexistieren, Sie können also eine Datei migrieren, ausliefern und genau diese Datei zurücknehmen, wenn etwas bricht. Nicht möglich ist ein Rollback, nachdem Sie die dart:html-Codepfade gelöscht und einen Wasm-Build ausgeliefert haben, denn der Wasm-Build hat sie nie unterstützt. Behalten Sie den dart2js-Build als Produktionsziel, bis der oben beschriebene Klickdurchlauf erledigt ist; flutter build web --wasm erzeugt beide, und der Loader fällt von selbst zurück.
Fallstricke, die Sie vorher kennen sollten
Das offizielle JSImmutableListWrapper-Beispiel kompiliert nicht. JSImmutableListWrapper<T, U> kann U nicht aus dem Konstruktorargument ableiten und fällt daher auf die Schranke JSObject zurück:
for (final a in JSImmutableListWrapper(document.querySelectorAll('a'))) {
a.classList.add('link'); // error: The getter 'classList' isn't defined for the type 'JSObject'
}
Geben Sie beide Typargumente explizit an:
// package:web 1.1.1
for (final a in JSImmutableListWrapper<NodeList, Element>(
document.querySelectorAll('a'),
)) {
a.classList.add('link');
}
innerHTML ist JSAny, in beide Richtungen. Schreiben braucht .toJS, Lesen braucht einen Cast: final String s = el.innerHTML; scheitert mit “A value of type ‘JSAny’ can’t be assigned to a variable of type ‘String’”. Lesen Sie es als (el.innerHTML as JSString).toDart. Dasselbe gilt für outerHTML und für insertAdjacentHTML, dessen zweiter Parameter JSAny ist.
element.text ist ein Setter ohne Getter. package:web behält einen veralteten text-Setter zur Migrationserleichterung, aber Lesen erfordert textContent, das String? statt String ist. Code, der if (el.text.isEmpty) schrieb, braucht jetzt eine Null-Prüfung.
Callbacks verlieren ihre Zone. dart:html band Event-Callbacks automatisch an die aktuelle Zone; package:web tut das nicht. Wer auf zonenlokale Werte oder auf einen zonenbasierten Fehlerhandler baut, der Vorgänge innerhalb eines Listeners abfängt, bindet vor der Konvertierung manuell:
element.addEventListener(
'click',
Zone.current.bindUnaryCallback((Event event) {
// zone-local values are preserved here
}).toJS,
);
Typprüfungen ändern still ihre Bedeutung. obj is Window kompilierte unter dart:html problemlos; unter package:web wird jeder Typ zu JSObject gelöscht, die Prüfung ist also bedeutungslos. Verwenden Sie element.isA<HTMLInputElement>() (ab Dart 3.4) oder obj.instanceOfString('Window').
Manche dart:html-Gewohnheiten überleben als veraltete Shims. window.localStorage['k'] = 'v' besteht die Analyse weiterhin, mit ”’[]=’ is deprecated and shouldn’t be used. Use Storage.setItem instead”, und ein Top-Level-querySelector existiert mit “Directly use document.querySelector instead”. Sie kompilieren heute, sie sind kein Ziel. Wandeln Sie sie im selben Durchlauf um, sonst machen Sie diese Arbeit zweimal.
Event-Streams gibt es weiterhin, und sie sind der ergonomische Weg. package:web bringt Stream-Helper mit, input.onClick.listen(...) funktioniert also unverändert und liefert ElementStream<MouseEvent>. Bevorzugen Sie sie gegenüber rohem addEventListener plus .toJS für alles, was Sie abbrechen müssen. Beachten Sie, dass die Helper-Streams manche Events asynchron zustellen, wo dart:html synchron war; zeitkritischer Code braucht daher einen zweiten Blick.
Verwandte Beiträge
- Der Nutzen dieser Arbeit ist vollständig beschrieben in Flutter-Web-Apps mit WebAssembly bauen, inklusive der Frage, warum Firefox und Safari weiterhin den JavaScript-Build bekommen.
- Strukturell ist das derselbe breite, mechanische Durchlauf wie eine Flutter-2-App auf Flutter 3.x migrieren: ein Plan mit zwei Sprüngen und ein Compiler, der meldet, wann Sie fertig sind.
- Der Mechanismus der bedingten Imports aus Schritt 6 steckt auch hinter plattformspezifischem Code ohne Plugin.
- Wer gleichzeitig Flutter aktualisiert, liest was Flutter 3.47 am Desktop-Rendering geändert hat, bevor er eine visuelle Regression dieser Migration anlastet.
- Das Web ist außerdem der Ort, an dem sich Dart-Isolates anders verhalten als auf jeder anderen Plattform, was man wissen sollte, bevor man im selben Durchlauf CPU-lastige Arbeit verschiebt.
Quellen
- Migrate to package:web, dart.dev
- Past JS interop, dart.dev
- JS types and conversions, dart.dev
- Breaking changes and deprecations, dart.dev
- package:web auf pub.dev, Version 1.1.1
- API-Referenz zu EventStreamProviders, package:web
- dart:ui_web PlatformViewRegistry, Flutter-API-Dokumentation
- Announcing Dart 3.13, der Dart-Blog
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.