Historia del interop con JS en Dart
El soporte para Wasm acaba de llegar al beta actual de Flutter, gracias a un emocionante hito de interop con JavaScript alcanzado en Dart 3.3. Para…
El soporte para Wasm acaba de llegar al beta actual de Flutter, gracias a un emocionante hito de interoperabilidad con JavaScript alcanzado en Dart 3.3. Para celebrarlo, estamos echando un vistazo al viaje de una década de Dart y la interoperabilidad con JS.
#
La interoperabilidad ha sido un enfoque central desde el principio de Dart. Cuando Dart se lanzó por primera vez en 2011, fue diseñado para ser embeddable y multi-platform. Se ejecutaba en una máquina virtual independiente, se integraba en un navegador y se compilaba a JavaScript. Cuando Flutter llegó en 2015, estábamos listos para integrarlo allí también. Ahora, estamos emocionados de apuntar a runtimes de WasmGC también.
Al principio, trabajamos rápidamente para exponer las capacidades de cada plataforma donde Dart estaba integrado. Así surgieron nuestras bibliotecas específicas de plataforma del SDK:
dart:io exponía el sistema de archivos en la VM,
dart:html exponía las APIs del navegador en la web, y así sucesivamente. Estas bibliotecas se veían y se sentían como bibliotecas normales de Dart, pero detrás de escena ocultaban algunos primitivos nativos de bajo nivel sofisticados para hacerlas funcionar. Esta fue la primera forma de interop que inventamos. Era expresiva, pero restringida solo a bibliotecas del SDK.
En la web, los desarrolladores necesitaban acceso a más que solo las APIs del navegador. Así que empezamos a buscar formas de abrir la interoperabilidad para cubrir más objetivos. Como punto de partida, introdujimos
dart:js en 2013 para habilitar el acceso a bibliotecas de JavaScript.
// Short example JavaScript code to illustrate Dart/JS interop
window.myTopLevel = {
field1: 0,
method2() {
return this.field1;
}
}
// Access via `dart:js` (2013)
import 'dart:js' as js;
void main() {
// This line has a typo! oops :(
var object = js.context['myTopLevl'];
object['field1'] = 1;
// This call fails with a noSuchMethod because method2
// returns an int, oops
object.callMethod('method2', []).substr(1);
}
Ya sabíamos entonces que dart:js no era el modelo de programación que queríamos. Tenías que usar strings para acceder a nombres de JavaScript — ¡olvídate de encontrar problemas en tiempo de compilación, y ni pensar en autocompletado de código! La implementación también era costosa. Dependía en gran medida de boxes y deep copies para la mayoría de las operaciones. Así que continuamos
borrando ideas en 2014 y 2015 hasta que se lanzó la v0.6 de
package:js.
// Access via `package:js` (2015)
import 'package:js/js.dart';
// Magic annotations allow us to declare API signatures:
@JS()
class MyObject {
external int get field1;
external void set field1(int value);
external String method2();
}
@JS()
external MyObject get myTopLevel;
void main() {
// Access to code is less error prone: analyzer can check that
// these symbols match a declaration, and we get code-completion too!
var object = myTopLevel;
object.field1 = 1;
// But types are not checked, this unsoundly invokes substring on an int
object.method2().substring(1);
}
Con package:js finalmente tuvimos una API abierta que era eficiente y fácil de usar. Podías espolvorear algunas anotaciones en clases abstractas, y
voila, tenías acceso a las APIs de JavaScript. Todo funcionaba como magia, hasta que dejó de funcionar. Había mucho que no podías hacer con
package:js: acceder directamente a las APIs del navegador, renombrar miembros, conversiones, adjuntar lógica de Dart, y
más. Para compensar, también publicamos dart:js_util
— una API de bajo nivel ligera y eficiente similar a dart:js, como alternativa. Todas las limitaciones de
package:js realmente nos molestaban, pero teníamos las manos atadas. Necesitábamos más del lenguaje Dart para hacerlo mejor.
Por aquel entonces, ya estábamos trabajando en el cambio más grande que hemos hecho al lenguaje — estábamos haciendo Dart
sound. Irónicamente, cuando lanzamos el nuevo sistema de tipos con Dart 2.0 en 2018, ¡la interoperabilidad
empeoró! Más allá de aquellas limitaciones tempranas, esa magia que hacía especial a package:js tenía un lado oscuro — no podía verificar la validez de los tipos. Esto significaba que nuestra interoperabilidad era una fuente de unsoundness en nuestro lenguaje que de otra forma era sound.
Entonces, nuestro viaje cambió para enfocarse en mejorar tanto Dart como JS-interop como un esfuerzo concertado. Con principios claros (ser idiomático, expresivo, composicional, preciso, accesible, pragmático, no mágico y completo) nos dirigimos hacia un diseño anclado en el tipado y el despacho estático, y que desafió al lenguaje Dart. Lo que siguió fue una evolución en paralelo.
-
En 2019, Dart 2.7 añadió métodos de extensión estática. Podías adjuntar lógica personalizada de Dart a una clase de JS-interop y convertir valores, como un
Promisede JS en unFuturede Dart, sin usar wrappers. -
En 2021, publicamos
@staticInteropconpackage:jsv0.6.4. Por fin, JS-interop era suficientemente expresivo — podías exponer APIs del navegador que anteriormente eran gestionadas exclusivamente por bibliotecas del SDK comodart:html. -
En 2023, cuando eliminamos unsound null safety en Dart 3.0, pudimos finalmente ver el progreso que habíamos logrado, nuestros diseños y el trabajo con
@staticInteropdejaron claro que estábamos listos para abordar la brecha de soundness que teníamos desde hacía tanto tiempo.
Ese año, introdujimos la compilación a WasmGC y aprovechamos JS-interop para ejecutar frameworks ricos como Flutter web en él. Esto disparó el trabajo en JS Types para definir claramente el límite entre Dart y JS en el modelo de programación y encontrar una forma consistente de trabajar con JS tanto en los objetivos de compilación de Wasm como de JS. También iniciamos el experimento del lenguaje de extension types — una funcionalidad lanzada en Dart 3.3 que cierra la brecha entre el lenguaje Dart y JS-interop. Durante años, JS-interop tuvo comportamientos, como el type erasure, que no coincidían con nada más en Dart. Con las extension types, JS-interop pudo finalmente ser idiomático y obtener el soporte que merece en las herramientas de desarrollo de Dart.
A pesar de los muchos giros y vueltas a lo largo del camino, una cosa se mantuvo constante durante toda la década: la participación activa de nuestra comunidad de Dart. Los miembros de la comunidad dieron los primeros pasos probando y contribuyendo a
dart:js, y luego influyeron en el diseño de package:js. Escribieron herramientas para abordar brechas de funcionalidad (package:js_wrapping), y experimentaron con formas de mejorar la productividad autogenerando APIs de Dart (package:js_facade_gen,
package:js_bindings, package:typings). Cada contribución ayudó a mejorar el diseño de interop de Dart. A cada uno de ustedes, ¡gracias por hacer de esto una aventura tan emocionante!
Finalmente, aquí estamos en 2024. Publicamos dart:js_interop
en Dart 3.3 junto con package:web, las soluciones más nuevas para JS interop en Dart que hacen posible compilar
Flutter a Wasm.
// Access via `dart:js_interop` (2024)
import 'dart:js_interop';
// Declarations use extension types, which are very similar to package:js
// declarations. The main difference: they are statically dispatched.
extension type MyObject._(JSObject _) implements JSObject {
external int get field1;
external void set field1(int value);
external String method2();
}
@JS()
external MyObject get myTopLevel;
void main() {
var object = myTopLevel;
object.field1 = 1;
// At last, access is sound - this line fails with a type error
// when returning from method2.
object.method2().substring(1);
}
-
dart:js_interopes una forma de interop estática, sound, idiomática, expresiva y consistente basada en extension types que es capaz de exponer cualquier API de JavaScript o del navegador. -
package:webusadart:js_interoppara hacer lo quedart:htmlhacía hace 13 años, pero de una forma que está soportada tanto en JavaScript como en WasmGC.
Hoy, estamos emocionados de celebrar una nueva forma de interop entre Dart y JS y el futuro que habilita. Conociendo nuestro pasado, estamos seguros de que este no es el final del viaje, sino un punto emocionante en nuestra historia.
¡No podemos esperar a ver qué construirás con él!
Más de Dart
Dart DevTools: Analizando el rendimiento de la aplicación con el CPU Profiler
Ya seas un desarrollador de backend escribiendo una herramienta de línea de comandos con Dart, o un ingeniero de UX construyendo aplicaciones con Flutter, el rendimiento del programa es crítico para el éxito de tu proyecto. Las herramientas de línea de comandos deberían minimizar la latencia y las apps deberían ser responsivas y ágiles sin fotogramas perdidos. Como desarrolladores, hacemos todo lo posible para escribir código performante pero a veces no está claro por qué nuestro código no funciona tan bien como esperaríamos.
Mejor gestión de isolates con Isolate.run()
Dart 2.19 introduce una nueva función que hace que implementar concurrencia sea tan simple como una sola línea de código.