Saltar al contenido principal

Migrar a package:web

Cómo migrar el código de interoperabilidad web de dart:html a package:web.

El paquete package:web de Dart expone el acceso a las APIs del navegador, permitiendo la interoperabilidad entre las aplicaciones Dart y la web. Usa package:web para interactuar con el navegador y manipular objetos y elementos en el DOM.

dart
import 'package:web/web.dart';

void main() {
  final div = document.querySelector('div')!;
  div.text = 'Text set at ${DateTime.now()}';
}

package:web vs dart:html

#

El objetivo de package:web es renovar cómo expone Dart las APIs web abordando varias preocupaciones con las librerías web de Dart existentes:

  1. Wasm compatibility

    Los paquetes solo pueden ser compatibles con Wasm si usan dart:js_interop y dart:js_interop_unsafe. package:web se basa en dart:js_interop, por lo que, de forma predeterminada, es compatible con dart2wasm.

    Las librerías web principales de Dart, como dart:html y dart:svg, están depreciadas y no están soportadas cuando se compila a Wasm.

  2. Staying modern

    package:web utiliza el Web IDL para generar automáticamente miembros de interoperabilidad y tipos de interoperabilidad para cada declaración en el IDL. Generar referencias directamente, a diferencia de los miembros y abstracciones adicionales en dart:html, permite que package:web sea más conciso, fácil de entender, más consistente y más capaz de mantenerse al día con el futuro de los desarrollos web.

  3. Versioning

    Debido a que es un paquete, package:web puede tener un control de versiones más sencillo que una librería como dart:html y evitar romper el código del usuario a medida que evoluciona. También hace que el código sea menos exclusivo y más abierto a las contribuciones. Los desarrolladores pueden crear sus propias declaraciones de interoperabilidad alternativas y usarlas junto con package:web sin conflictos.


Estas mejoras resultan naturalmente en algunas diferencias de implementación entre package:web y dart:html. Los cambios que más afectan a los paquetes existentes, como los renombramientos en IDL y las pruebas de tipo, se abordan en las secciones de migración siguientes. Aunque solo nos referimos a dart:html por brevedad, los mismos patrones de migración se aplican a cualquier otra librería web principal de Dart como dart:svg.

Migración desde dart:html

#

Elimina la importación de dart:html y reemplázala con package:web/web.dart:

dart
import 'dart:html' as html; // Remove
import 'package:web/web.dart' as web; // Add

Agrega web a las dependencies (dependencias) en tu pubspec:

dart pub add web

Las siguientes secciones cubren algunos de los problemas comunes de migración de dart:html a package:web.

Para cualquier otro problema de migración, consulta el repositorio dart-lang/web y reporta un problema (file an issue).

Renombramientos

#

Muchos de los símbolos en dart:html fueron renombrados de su declaración original en IDL para alinearse más con el estilo de Dart. Por ejemplo, appendChild se convirtió en append, HTMLElement se convirtió en HtmlElement, etc.

Por el contrario, para reducir la confusión, package:web utiliza los nombres originales de las definiciones IDL. Hay un dart fix disponible para convertir tipos que han sido renombrados entre dart:html y package:web.

Después de cambiar la importación, cualquier objeto renombrado generará nuevos errores de "no definido" (undefined). Puedes solucionar esto ya sea:

  • Desde la CLI, ejecutando dart fix --dry-run.
  • En tu IDE, seleccionando el dart fix: Rename to 'package:web name'.

La herramienta dart fix cubre muchos de los renombramientos de tipos comunes. Si te encuentras con un tipo de dart:html que no tiene una corrección de dart fix para renombrarlo, primero háznoslo saber reportando un problema (issue).

Luego, puedes intentar descubrir manualmente el nombre del tipo en package:web de un miembro existente en dart:html buscando su definición. El valor de la anotación @Native en la definición de un miembro de dart:html le indica al compilador que trate cualquier objeto JS de ese tipo como la clase de Dart que está anotando. Por ejemplo, la anotación @Native nos dice que el nombre JS nativo del miembro HtmlElement de dart:html es HTMLElement, por lo que el nombre en package:web también será HTMLElement:

dart
@Native("HTMLElement")
class HtmlElement extends Element implements NoncedElement { }

Para encontrar la definición en dart:html de un miembro no definido en package:web, prueba cualquiera de los siguientes métodos:

  • Presiona Ctrl o Command y haz clic en el nombre no definido en el IDE y elige Ir a la definición (Go to Definition).
  • Busca el nombre en la documentación de la API de dart:html y revisa su página bajo Annotations.

Del mismo modo, es posible que encuentres una API no definida en package:web cuya definición del miembro correspondiente en dart:html utilice la palabra clave native. Verifica si la definición utiliza la anotación @JSName para un renombramiento; el valor de la anotación te dirá el nombre que el miembro utiliza en package:web:

dart
@JSName('appendChild')
Node append(Node node) native;

native es una palabra clave interna que significa lo mismo que external en este contexto.

Element y HTMLElement

#

Element ahora es HTMLElement, pero el extension type Element también existe como un tipo base para HTMLElement, SVGElement, etc. Consulta la documentación de MDN de Element. querySelector devuelve un Element, ya que puede devolver otros subtipos de Element además de HTMLElement como SVGElement. Se necesita una conversión hacia abajo (downcast) a HTMLElement si necesitas acceder a sus métodos.

dart
element.querySelector('#selectme')!.className = 'test'; // valid in both

element.querySelector('#selectme')!.style.color = 'red'; // Remove
(element.querySelector('#selectme') as HTMLElement).style.color = 'red'; // Add

Operaciones de lista

#

A diferencia de dart:html, los métodos de package:web como Element.querySelectorAll y Element.children devuelven valores que no implementan List. Si se requiere una List, deberás envolverla con una clase.

Para operaciones inmutables, puedes usar JSImmutableListWrapper:

dart
final anchors = document.querySelectorAll('a');
for (final anchor in anchors) {} // Remove
for (final anchor in JSImmutableListWrapper(anchors)) {} //Add

for (final child in parent.children) {} // Remove
for (final child in JSImmutableListWrapper(parent.children)) {} // Add

Debes agregar la implementación para operaciones mutables simples como removeWhere:

dart
parent.children.removeWhere(test); // Remove

for (var i = parent.children.length - 1; i >= 0; --i) {
  if (test(parent.children.item(i)!)) {
    parent.children.item(i)!.remove();
  }
}  // Add

Ejemplos comunes de manipulación del DOM

#

Aquí hay algunos ejemplos comunes de cambios triviales que deben realizarse al migrar de dart:html a package:web:

dart
element.querySelector('#selector')?.innerHtml = 'something'; // Remove
element.querySelector('#selector')?.innerHTML = 'something'.toJS; // Add

element.parent.classes.add('class'); // Remove
element.parentElement.classList.add('class'); // Add

element.appendHtml(html); // Remove
element.insertAdjacentHTML('beforeend', html.toJS); // Add

var checkbox = CheckboxInputElement(); // Remove
var checkbox = HTMLInputElement()..type='checkbox'; // Add

element.querySelectorAll('a').classes.add('link'); // Remove
for (final a in JSImmutableListWrapper(element.querySelectorAll('a'))) {
  a.classList.add('a');
} // Add

Pruebas de tipo

#

Es común que el código que utiliza dart:html emplee comprobaciones en tiempo de ejecución como is. Cuando se usa con un objeto de dart:html, is y as verifican que el objeto sea el tipo de JS dentro de la anotación @Native. En cambio, todos los tipos de package:web se materializan (reify) como JSObject. Esto significa que una prueba de tipo en tiempo de ejecución dará como resultado un comportamiento diferente entre los tipos de dart:html y package:web.

Para poder realizar pruebas de tipo, migra cualquier código de dart:html que use pruebas de tipo is para usar métodos de interoperabilidad como instanceOfString o el ayudante isA más conveniente y tipado (disponible a partir de Dart 3.4). La sección Compatibilidad, comprobaciones de tipo y conversiones de tipo (casts) de la página de tipos de JS cubre las alternativas en detalle.

dart
obj is Window; // Remove
obj.instanceOfString('Window'); // Add

element is InputElement; // Remove
element.isA<HTMLInputElement>(); // Add

Firmas de tipo

#

Muchas APIs en dart:html admiten varios tipos de Dart en sus firmas de tipo. Debido a que dart:js_interop restringe los tipos que se pueden escribir, algunos de los miembros en package:web ahora requerirán que conviertas el valor antes de llamar al miembro. Aprende cómo usar los métodos de conversión de interoperabilidad en la sección Conversiones de la página de tipos de JS.

dart
window.addEventListener('click', callback); // Remove
window.addEventListener('click', callback.toJS); // Add

Generalmente, puedes detectar qué métodos necesitan una conversión porque estarán marcados con alguna variación de la excepción:

A value of type '...' can't be assigned to a variable of type 'JSFunction?'

Importaciones condicionales

#

Es común que el código utilice una importación condicional basada en si dart:html está soportado para diferenciar entre nativo y web:

dart
export 'src/hw_none.dart'
    if (dart.library.io) 'src/hw_io.dart'
    if (dart.library.html) 'src/hw_html.dart';

Sin embargo, dado que dart:html está depreciado y no está soportado al compilar a Wasm, la alternativa correcta ahora es usar dart.library.js_interop para diferenciar entre nativo y web:

dart
export 'src/hw_none.dart' // Stub implementation
    if (dart.library.io) 'src/hw_io.dart' // dart:io implementation
    if (dart.library.js_interop) 'src/hw_web.dart'; // package:web implementation

Despacho virtual y simulación (mocking)

#

Las clases de dart:html admitían el despacho virtual, pero debido a que JS interop utiliza extension types, el despacho virtual no es posible. Del mismo modo, las llamadas dynamic con tipos de package:web no funcionarán como se espera (o podrían continuar funcionando solo por casualidad, pero dejarán de hacerlo cuando se elimine dart:html), ya que sus miembros solo están disponibles estáticamente. Migra todo el código que dependa del despacho virtual para evitar este problema.

Un caso de uso del despacho virtual es la simulación (mocking). Si tienes una clase de simulación que implementa una clase de dart:html, no se puede usar para implementar un tipo de package:web. En su lugar, prefiere simular el propio objeto de JS. Consulta el tutorial de simulación (mocking) para obtener más información.

APIs no nativas

#

Las clases de dart:html también podrían contener APIs que tienen una implementación no trivial. Estos miembros pueden o no existir en los ayudantes (helpers) de package:web. Si tu código depende de los detalles de esa implementación, es posible que puedas copiar el código necesario. Sin embargo, si crees que eso no es viable o si ese código también sería beneficioso para otros usuarios, considera reportar un problema (file an issue) o subir un pull request a package:web para dar soporte a ese miembro.

Zonas (Zones)

#

En dart:html, los callbacks se zonifican automáticamente. Este no es el caso en package:web. No hay una vinculación automática de callbacks en la zona actual.

Si esto es importante para tu aplicación, aún puedes usar zonas, pero tendrás que escribirlas tú mismo vinculando el callback. Consulta #54507 para obtener más detalles. Aún no hay una API de conversión o ayudante (helper) disponible para hacer esto de forma automática.

Ayudantes (Helpers)

#

El núcleo de package:web contiene miembros de interoperabilidad external, pero no proporciona otras funcionalidades que dart:html proporcionaba de forma predeterminada. Para mitigar estas diferencias, package:web contiene ayudantes (helpers) para brindar soporte adicional en el manejo de una serie de casos de uso que no están disponibles directamente a través de la interoperabilidad del núcleo. La librería de ayudantes contiene varios miembros para exponer algunas características heredadas (legacy) de las librerías web de Dart.

Por ejemplo, el núcleo de package:web solo tiene soporte para agregar y eliminar escuchas de eventos (event listeners). En su lugar, puedes usar los ayudantes de streams (stream helpers) que facilitan la suscripción a eventos con Streams de Dart sin tener que escribir ese código tú mismo.

dart
// Original dart:html version:
final htmlInput = InputElement();
await htmlInput.onBlur.first;

// Migrated package:web version:
final webInput = HTMLInputElement();
await webInput.onBlur.first;

Puedes encontrar todos los ayudantes y su documentación en el repositorio en package:web/helpers. Se actualizarán continuamente para ayudar a los usuarios en la migración y facilitar el uso de las APIs web.

Ejemplos

#

Aquí hay algunos ejemplos de paquetes que han sido migrados de dart:html a package:web: