CHARLIE SAYS

查理如是说
DATE 2026-08-24
THEME
SERIES / ANGULAR / P-181 · Angular 高级教程

Angular 22+ 教程 16:Angular 组件 vs Web Components

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 ElementscustomElements.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 输出)任意框架、无框架页面
SSRAngular 增量 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 只应加在真正使用自定义元素的组件上,全局放开会掩盖拼写错误。

系列导航

← 算法 016:树:基础和Overview 目录 设计模式 016:责任链(Chain Of Responsibility) →
← 返回文章列表