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

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

github.com/rodydavis开发6043 次安装2 周前更新
代码仓库

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

面向 Dart 开发者的教程,介绍如何借助 Dart 和 js_interop 构建 HTML Web Components。它讲解了可供不同 JavaScript 框架使用的自定义元素,也适合探索 Flutter web 替代方案的开发者。

主要功能

  1. 01用 Dart 创建自定义 HTML 元素
  2. 02使用 js_interop 和 web 包
  3. 03注册生命周期回调与属性
  4. 04在不同 JavaScript 框架间复用组件

适用场景

  1. 01用 Dart 构建可复用自定义元素
  2. 02为服务端渲染的 HTML 添加交互
  3. 03探索 Flutter web 的替代方案

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/rodydavis/skills --skill how-to-create-html-web-components-with-dart

安装后在 Claude Code 中输入/how-to-create-html-web-components-with-dart确认已加载。

详细介绍

如何使用 Dart 创建 HTML Web 组件

我一直是 Web Components 的爱好者(曾协助 DevRel lit.dev 和 Material Web Components),也一直喜欢在 Flutter 应用和全栈应用中编写 Dart。

尽管 许多公司都在使用 Web Components,但使用框架面向 Web 开发的 JavaScript 开发者仍对它们有很多抵触。☹️

你可能不知道,Web 提供了一种创建新 HTML 标签的方式,这些标签可用于任何 JS 框架,或任何返回 HTML 的场景,还能让应用逐步增强。🤩

因为它们是自定义 HTML 标签,所以即使更换实现,也无需更新使用位置,而且可以将组件作为独立文件发布,而不是打包成一个大文件。

Dart 曾经支持 Web Components,甚至曾被 Polymer 使用过。Polymer 是 Lit 的前身,曾用于一款名为 Polymer 的产品。

使用 Javascript 创建 Web Component

要在 Javascript 中创建 Web Component,只需扩展 HTML 元素,并提供组件挂载时调用的回调。

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>

这样效果很好,而且创建它们甚至不需要构建步骤!

使用 Dart 创建 Web Components

要在 Dart 端创建它们,我们需要使用 js_interop package 和新的 web package。

我们需要在 Dart 端创建一个工厂,用于创建这些 JS 类;由于 JS 和 Dart 的类不同,我们无法按常规方式创建类。

有一个很棒的 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 Components。

如果你想使用能代劳的 package,html_web_components 已发布在 pub.dev 上。

要像之前那样创建 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 应用变得非常简单。

总结

Web Components 让你能够升级客户端的交互体验,同时自由选择使用服务端渲染来生成模板文件,或仅在前端使用 SPA。你可以在任何 JS 框架中使用这些组件!🤯

我强烈建议你亲自试试,再决定是否放弃 Web Components。对于想寻找 Flutter Web 替代方案(甚至想与 Jaspr 一起使用)的 Flutter 开发者来说,这一点尤其重要。

你可以利用 Dart 丰富的包生态系统(pub.dev)以及编译为 WASM 和 JS 的能力。如果你使用 peanut 这样的构建工具,它甚至会为你生成脚本,尝试加载 WASM,并在必要时回退到 JS 🔥

如果你想查看代码,可以在 GitHub 上找到它。如果你有任何问题,或者想展示用它们构建的酷炫成果,欢迎联系我!

评论

支持 Markdown