how-to-create-html-web-components-with-dart

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

How to create HTML Web Components with Dart

如何使用Dart创建HTML Web Components

I am a long time Web Components fan (since helping DevRel lit.dev and Material Web Components) and have also loved writing Dart in both Flutter applications and full stack apps.
Despite being used at so many companies, Web Components have faced a lot of pushback from JavaScript developers that use frameworks to target the web. ☹️
What you may not realize is that the web has a way to create new HTML tags that can be used in ANY JS framework or place that returns HTML and you can progressively enchance applications. 🤩
Since they are custom HTML tags, if you swap implementations, you do not need to update where it is used and you can ship components a separate files instead of one big bundle.
Dart used to support Web Components at one point and was even used by a precursor to Lit in a product call Polymer.
我是Web Components的长期爱好者(从协助lit.dev和Material Web Components的开发者关系工作开始),同时也喜欢在Flutter应用和全栈应用中编写Dart代码。
尽管Web Components已经被众多企业采用,但使用框架开发Web应用的JavaScript开发者却对其颇有抵触。☹️
你可能不知道的是,Web平台提供了创建新HTML标签的方式,这些标签可以在任意JS框架或任何能输出HTML的环境中使用,还能实现应用的渐进式增强。🤩
由于它们是自定义HTML标签,如果你更换实现方式,无需更新其使用位置,并且可以将组件作为独立文件发布,而非打包成一个大文件
Dart曾经支持Web Components(可查看历史仓库),甚至在Polymer(Lit的前身)产品中被使用过。

Creating a Web Component in Javascript

使用Javascript创建Web Component

To create a web component in Javascript you just need to extend HTML element and provide callbacks for when the component is mounted.
class HelloWorld extends HTMLElement {
  static observedAttributes = ["name"];

  constructor() {
    super();
  }

  update() {
    this.innerHTML = `Hello: ${this.getAttribute('name')}`;
  }

  connectedCallback() {
    console.log("Custom element added to page.");
    this. update();
  }

  disconnectedCallback() {
    console.log("Custom element removed from page.");
  }

  adoptedCallback() {
    console.log("Custom element moved to new page.");
  }

  attributeChangedCallback(name, oldValue, newValue) {
    console.log(`Attribute ${name} has changed.`);
    if (name === 'name') {
      this. update();
    }
  }
}

customElements.define("hello-world", HelloWorld);
We can then use it in HTML like the following:
<html>
  <body>
    <hello-world name="Rody"></hello-world>
    <script src="./index.js"></script>
  </body>
</html>
This works really well, and we don't even need a build step to create them!
要使用Javascript创建Web Component,你只需继承HTMLElement,并为组件挂载时提供回调函数。
class HelloWorld extends HTMLElement {
  static observedAttributes = ["name"];

  constructor() {
    super();
  }

  update() {
    this.innerHTML = `Hello: ${this.getAttribute('name')}`;
  }

  connectedCallback() {
    console.log("Custom element added to page.");
    this. update();
  }

  disconnectedCallback() {
    console.log("Custom element removed from page.");
  }

  adoptedCallback() {
    console.log("Custom element moved to new page.");
  }

  attributeChangedCallback(name, oldValue, newValue) {
    console.log(`Attribute ${name} has changed.`);
    if (name === 'name') {
      this. update();
    }
  }
}

customElements.define("hello-world", HelloWorld);
然后我们可以在HTML中这样使用它:
<html>
  <body>
    <hello-world name="Rody"></hello-world>
    <script src="./index.js"></script>
  </body>
</html>
这种方式运行效果很好,而且我们甚至不需要构建步骤就能创建它们!

Creating Web Components with Dart

使用Dart创建Web Component

To create them on the Dart side we need to use the js_interop package and the new web package.
We need to create a factory on the dart side that can create these JS classes without actually being able to create a class in the normal way (since JS and Dart classes are different).
There is a great API
Reflect.construct()
which allows us to take a normal function and invoke it class a class constructor. JavaScript did not always support native classes and was only added with ES6.
By using this built in API, we can create the classes with just pure Dart:
import 'dart:js_interop';
import 'dart:js_interop_unsafe';

import 'package:web/web.dart';

class WebComponent<T extends HTMLElement> {
  late T element;
  final String extendsType = 'HTMLElement';

  void connectedCallback() {}

  void disconnectedCallback() {}

  void adoptedCallback() {}

  void attributeChangedCallback(
    String name,
    String? oldValue,
    String? newValue,
  ) {}

  Iterable<String> get observedAttributes => [];

  bool get formAssociated => false;

  ElementInternals? get internals => element['_internals'] as ElementInternals?;
  set internals(ElementInternals? value) {
    element['_internals'] = value;
  }

  R getRoot<R extends JSObject>() {
    final hasShadow = element.shadowRoot != null;
    return (hasShadow ? element.shadowRoot! : element) as R;
  }

  static void define(String tag, WebComponent Function() create) {
    final obj = _factory(create);
    window.customElements.define(tag, obj);
  }
}

@JS('Reflect.construct')
external JSAny _reflectConstruct(
  JSObject target,
  JSAny args,
  JSFunction constructor,
);

final _instances = <HTMLElement, WebComponent>{};

JSFunction _factory(WebComponent Function() create) {
  final base = create();
  final elemProto = globalContext[base.extendsType] as JSObject;
  late JSAny obj;

  JSAny constructor() {
    final args = <String>[].jsify()!;
    final self = _reflectConstruct(elemProto, args, obj as JSFunction);
    final el = self as HTMLElement;
    _instances.putIfAbsent(el, () => create()..element = el);
    return self;
  }

  obj = constructor.toJS;
  obj = obj as JSObject;

  final observedAttributes = base.observedAttributes;
  final formAssociated = base.formAssociated;

  obj['prototype'] = elemProto['prototype'];
  obj['observedAttributes'] = observedAttributes.toList().jsify()!;
  obj['formAssociated'] = formAssociated.jsify()!;

  final prototype = obj['prototype'] as JSObject;
  prototype['connectedCallback'] = (HTMLElement instance) {
    _instances[instance]?.connectedCallback();
  }.toJSCaptureThis;
  prototype['disconnectedCallback'] = (HTMLElement instance) {
    _instances[instance]?.disconnectedCallback();
    _instances.remove(instance);
  }.toJSCaptureThis;
  prototype['adoptedCallback'] = (HTMLElement instance) {
    _instances[instance]?.adoptedCallback();
  }.toJSCaptureThis;
  prototype['attributeChangedCallback'] = (
    HTMLElement instance,
    String name,
    String? oldName,
    String? newName,
  ) {
    _instances[instance]?.attributeChangedCallback(name, oldName, newName);
  }.toJSCaptureThis;

  return obj as JSFunction;
}
This may seem like a lot to digest, but that is ok. It simply does some JS magic to upgrade functions to classes and provide the correct callbacks to create the web components.
If you want a package that does this for you, html_web_components is on pub.dev.
To create a Web Component like we did before, we can just extend the class and define the component.
import 'package:html_web_components/html_web_components.dart';

class HelloWorld extends WebComponent {
  @override
  List<String> observedAttributes = ['name'];

  void update() {
    element.innerText = "Hello: ${element.getAttribute('name')}!";
  }

  @override
  void connectedCallback() {
    super.connectedCallback();
    update();
  }

  @override
  void attributeChangedCallback(
    String name,
    String? oldValue,
    String? newValue,
  ) {
    super.attributeChangedCallback(name, oldValue, newValue);
    if (observedAttributes.contains(name)) {
      update();
    }
  }
}

void main() {
  WebComponent.define('hello-world', HelloWorld.new);
}
This should look very similar (that is the goal) and makes it so easy to publish the compoents or build a full web application with it.
要在Dart中创建Web Component,我们需要使用js_interop包和新的web包
我们需要在Dart端创建一个工厂,无需以常规方式创建类(因为JS和Dart的类是不同的)就能创建这些JS类。
有一个很棒的API
Reflect.construct()
,它允许我们将普通函数当作类构造函数调用。JavaScript并非一直支持原生类,它是在ES6中才新增的。
通过使用这个内置API,我们可以仅用纯Dart创建类:
import 'dart:js_interop';
import 'dart:js_interop_unsafe';

import 'package:web/web.dart';

class WebComponent<T extends HTMLElement> {
  late T element;
  final String extendsType = 'HTMLElement';

  void connectedCallback() {}

  void disconnectedCallback() {}

  void adoptedCallback() {}

  void attributeChangedCallback(
    String name,
    String? oldValue,
    String? newValue,
  ) {}

  Iterable<String> get observedAttributes => [];

  bool get formAssociated => false;

  ElementInternals? get internals => element['_internals'] as ElementInternals?;
  set internals(ElementInternals? value) {
    element['_internals'] = value;
  }

  R getRoot<R extends JSObject>() {
    final hasShadow = element.shadowRoot != null;
    return (hasShadow ? element.shadowRoot! : element) as R;
  }

  static void define(String tag, WebComponent Function() create) {
    final obj = _factory(create);
    window.customElements.define(tag, obj);
  }
}

@JS('Reflect.construct')
external JSAny _reflectConstruct(
  JSObject target,
  JSAny args,
  JSFunction constructor,
);

final _instances = <HTMLElement, WebComponent>{};

JSFunction _factory(WebComponent Function() create) {
  final base = create();
  final elemProto = globalContext[base.extendsType] as JSObject;
  late JSAny obj;

  JSAny constructor() {
    final args = <String>[].jsify()!;
    final self = _reflectConstruct(elemProto, args, obj as JSFunction);
    final el = self as HTMLElement;
    _instances.putIfAbsent(el, () => create()..element = el);
    return self;
  }

  obj = constructor.toJS;
  obj = obj as JSObject;

  final observedAttributes = base.observedAttributes;
  final formAssociated = base.formAssociated;

  obj['prototype'] = elemProto['prototype'];
  obj['observedAttributes'] = observedAttributes.toList().jsify()!;
  obj['formAssociated'] = formAssociated.jsify()!;

  final prototype = obj['prototype'] as JSObject;
  prototype['connectedCallback'] = (HTMLElement instance) {
    _instances[instance]?.connectedCallback();
  }.toJSCaptureThis;
  prototype['disconnectedCallback'] = (HTMLElement instance) {
    _instances[instance]?.disconnectedCallback();
    _instances.remove(instance);
  }.toJSCaptureThis;
  prototype['adoptedCallback'] = (HTMLElement instance) {
    _instances[instance]?.adoptedCallback();
  }.toJSCaptureThis;
  prototype['attributeChangedCallback'] = (
    HTMLElement instance,
    String name,
    String? oldName,
    String? newName,
  ) {
    _instances[instance]?.attributeChangedCallback(name, oldName, newName);
  }.toJSCaptureThis;

  return obj as JSFunction;
}
这看起来可能有点复杂,但没关系。它只是通过一些JS技巧将函数升级为类,并提供正确的回调来创建Web Component。
如果你想要一个现成的包来完成这项工作,pub.dev上的html_web_components可以满足需求。
要创建我们之前那样的Web Component,只需继承该类并定义组件即可:
import 'package:html_web_components/html_web_components.dart';

class HelloWorld extends WebComponent {
  @override
  List<String> observedAttributes = ['name'];

  void update() {
    element.innerText = "Hello: ${element.getAttribute('name')}!";
  }

  @override
  void connectedCallback() {
    super.connectedCallback();
    update();
  }

  @override
  void attributeChangedCallback(
    String name,
    String? oldValue,
    String? newValue,
  ) {
    super.attributeChangedCallback(name, oldValue, newValue);
    if (observedAttributes.contains(name)) {
      update();
    }
  }
}

void main() {
  WebComponent.define('hello-world', HelloWorld.new);
}
这看起来应该非常相似(这正是我们的目标),让发布组件或使用它构建完整Web应用变得非常容易。

Conclusion

总结

Web Components allow you to upgrade your client side interactivity while having the freedom to use server rendering to create the template files or just use a SPA on the frontend. You can take these components and use them in ANY JS frameworks! 🤯
I would highly suggest that you try it out for yourself before you write off Web Components. This is especially true for Flutter developers wanting an alternative to Flutter web (and even use with Jaspr).
You can take advantage of Dart's great ecosystem of packages on pub.dev and the ability to compile to WASM and JS. If you use a builder like peanut it will even create the script that tries to load WASM and can fallback to JS for you 🔥
If you want to see the code, you can find it on GitHub. Reach out if you have any questions or want to show off something cool you built with them!
Web Component允许你提升客户端交互性,同时还能自由使用服务端渲染来创建模板文件,或者在前端使用单页应用。你可以将这些组件用于任意JS框架中!🤯
我强烈建议你亲自尝试一下,再对Web Component下结论。对于想要寻找Flutter Web替代方案的Flutter开发者来说尤其如此(甚至可以和Jaspr一起使用)。
你可以利用pub.dev上Dart丰富的包生态系统,以及编译为WASM和JS的能力。如果你使用peanut这样的构建工具,它甚至会自动创建尝试加载WASM并回退到JS的脚本🔥
如果你想查看代码,可以在GitHub上找到。如果有任何问题,或者想展示你用它们构建的酷炫项目,欢迎联系我!