Web Components 是浏览器原生的组件标准(Custom Elements + Shadow DOM + Templates/Slots),Angular 组件则是框架私有抽象。两者经常被放在一起比较:“既然浏览器有了标准,为什么还要框架组件?“本文先各自讲清,再对比本质差异,最后给出选型建议。
Web Components 速览
用原生 API 写一个星级评分组件:
<script>
class XStarRating extends HTMLElement {
static get observedAttributes() {
return ['value'];
}
connectedCallback() {
this.render();
}
attributeChangedCallback() {
this.render();
}
render() {
const value = Number(this.getAttribute('value') ?? 0);
this.textContent = '\u2605'.repeat(value);
}
}
customElements.define('x-star-rating', XStarRating);
</script>
<x-star-rating value="4"></x-star-rating>
核心概念:
- Custom Elements:
customElements.define()注册标签,浏览器在解析到标签时实例化类。 - 生命周期回调:
connectedCallback/disconnectedCallback/attributeChangedCallback,粒度接近 Angular 的钩子,但需要自己接线。 - Shadow DOM:把组件 DOM/样式封装进 shadow root,实现真正的样式隔离(下一篇详述)。
- 无绑定系统:attribute 变化后要手动
render();事件用dispatchEvent抛出CustomEvent。
这套 API 是”汇编级”的原语——跨框架、稳定、永不废弃,但直接用它构建大型应用的效率远低于框架组件。
用 @angular/elements 打包 Angular 组件
@angular/elements 让你用 Angular 写组件、以自定义元素的形式发布:
ng add @angular/elements
import { Component, Injector, input, output } from '@angular/core';
import { createCustomElement } from '@angular/elements';
import { bootstrapApplication } from '@angular/platform-browser';
@Component({
selector: 'app-hello-button',
template: `<button type="button" (click)="greet.emit()">{{ label() }}</button>`,
})
export class HelloButtonComponent {
label = input('hello');
greet = output<void>();
}
bootstrapApplication(AppComponent, appConfig).then(app => {
const element = createCustomElement(HelloButtonComponent, {
injector: app.injector,
});
customElements.define('hello-button', element);
});
之后在任何页面——包括不跑 Angular 的老系统——都能使用:
<hello-button label="点我" id="btn"></hello-button>
<script>
document.querySelector('#btn').addEventListener('greet', () => {
console.log('clicked');
});
</script>
打包层的概念映射:
| Angular 概念 | 自定义元素表现 |
|---|---|
input() | 元素 attribute / property,自动同步 |
output() | CustomEvent,可用 addEventListener 监听 |
| 注入的服务 | 通过 createCustomElement 时传入的 injector 捕获 |
| 信号驱动的变更检测 | 包装层内部照常工作,外部无感 |
注意 createCustomElement 必须传 injector:自定义元素可能生存在 Angular 注入树之外,需要显式指定它从哪解析依赖。
本质差异对比
| 维度 | Angular 组件 | Web Components |
|---|---|---|
| 模板处理 | AOT 编译期生成渲染指令,类型受 strictTemplates 检查 | 运行时 innerHTML / 手写 DOM,无编译期校验 |
| 数据流 | 属性/事件绑定 + 变更检测自动同步 | 手动观察 attribute/property 变化并重渲染 |
| 依赖注入 | 层级注入器、InjectionToken、测试替身 | 无,依赖靠构造参数/闭包自行传递 |
| 状态响应 | signal / computed / effect 声明式 | attributeChangedCallback 手动 diff |
| 工具链 | CLI、Router、Forms、Testing、Material/CDK 全家桶 | 平台标准,配套靠社区 |
| 消费方 | 主要面向同框架应用(或经 elements 输出) | 任意框架、无框架页面 |
| SSR | Angular 增量 hydration 默认,生态成熟 | 声明式 Shadow DOM 等方案,场景有限 |
| 版本演进 | 随框架升级,API 有弃用周期 | 浏览器标准,极端稳定 |
一句话概括:Angular 组件把”编译期知识”用到了极致(模板静态分析、输入类型检查、tree-shaking),Web Components 把”运行时兼容”用到了极致。这是编译时 vs 运行时两种哲学,而非谁取代谁。
何时用哪个
| 场景 | 建议 |
|---|---|
| 单一 Angular 应用内部 | Angular 组件,没有悬念 |
| 公司级设计系统,前端栈不统一(Angular + React + Vue) | Web Components 输出(可以用 Angular + elements 实现) |
| 给第三方站点嵌入 widget(客服、支付、地图标注) | Web Components,或更保守的 iframe |
| 组件深度依赖 Angular 服务/路由/表单 | 留在 Angular 体系内;强行打包成元素会把 DI 也打包进去 |
| 一次构建、多年运行、极少迭代的后台插件位 | Web Components 的稳定性占优 |
选择 elements 打包时的隐性成本要提前评估:
- 体积:每个独立发布的元素包都要携带 Angular 运行时(或精心规划共享 bundle)。
- 调试:跨过包装层后,堆栈和变更检测行为都变得间接。
- DI 割裂:元素内部服务与应用共享状态,需要通过事件和属性桥接。
在 Angular 中消费自定义元素
v22 中模板遇到未注册的自定义元素会报编译错误,需在组件上声明 schema:
import { CUSTOM_ELEMENTS_SCHEMA, Component } from '@angular/core';
@Component({
selector: 'app-order-page',
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<x-star-rating [value]="rating()" (rate)="onRate($event)"></x-star-rating>
`,
})
export class OrderPageComponent {
rating = signal(4);
onRate(event: Event) {
this.rating.set((event as CustomEvent<number>).detail);
}
}
CUSTOM_ELEMENTS_SCHEMA 只应加在真正使用自定义元素的组件上,全局放开会掩盖拼写错误。