关于 how-to-create-html-web-components-with-dart
面向 Dart 开发者的教程,介绍如何借助 Dart 和 js_interop 构建 HTML Web Components。它讲解了可供不同 JavaScript 框架使用的自定义元素,也适合探索 Flutter web 替代方案的开发者。
主要功能
- 01用 Dart 创建自定义 HTML 元素
- 02使用 js_interop 和 web 包
- 03注册生命周期回调与属性
- 04在不同 JavaScript 框架间复用组件
适用场景
- 01用 Dart 构建可复用自定义元素
- 02为服务端渲染的 HTML 添加交互
- 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 上找到它。如果你有任何问题,或者想展示用它们构建的酷炫成果,欢迎联系我!