资讯详情

Angular `<ng-content>` 完全指南:内容投影占位符、select 选择器与底层实现剖析

📅 2026/9/10 11:32:27 | 华诺云谱 👁 阅读
Angular `<ng-content>` 完全指南:内容投影占位符、select 选择器与底层实现剖析
Angularng-content完全指南内容投影占位符、select 选择器与底层实现剖析【免费下载链接】angularDeliver web apps with confidence 项目地址: https://gitcode.com/GitHub_Trending/an/angularng-content是 Angular 模板中的特殊占位符元素用于在组件模板内部标记外部传入的子内容应该被渲染投影到哪个位置。本文以 Angular 仓库中的官方 API 文档 ng-content.md 为骨架结合仓库内的组件内容投影指南content-projection.md与 Ivy 渲染器核心源码系统讲解ng-content的属性、select选择器规则、多插槽分发、兜底内容及其底层工作原理帮助你写出可复用的容器类组件卡片、弹层、布局壳等。ng-content是什么按照 ng-content.md 的定义Theng-contentelement specifies where to project content inside a component template.即ng-content指定了组件模板内部用于投影project内容的插槽位置。它的行为与 Web Components 原生slot类似但额外提供了一些 Angular 特有的能力。在渲染层面ng-content既不是组件也不是 DOM 元素它是一个特殊的占位符告诉 Angular 把外部传入的内容放在哪里。Angular 编译器会在构建期处理所有ng-content元素你不能在运行时插入、移除或修改ng-content你不能在ng-content上添加指令、样式或任意自定义属性。这一约束可以在源码中得到印证编译产物中ng-content会被翻译成 Ivy 指令instruction而非普通 DOM 创建指令见下文的底层实现剖析部分。属性一览ng-content的 API 面非常小只有一个核心属性属性描述selectCSS 选择器。只有匹配的元素才会被投影进这个ng-content插槽。select的语义是只从外部传入的投影内容中挑选匹配给定 CSS 选择器的元素其余元素不进入该插槽。Angular 支持的select选择器范围与原文档保持一致标签名tag name例如selectcard-title属性attribute例如select[header]或select[data-roletitle]CSS 类CSS class例如select.card-header:not伪类用于反向排除例如select:not([skip])上述任意组合例如selectdiv.card-title[data-id]。这些规则的匹配行为与原生 CSS 选择器保持一致。Angular 还允许通过select组合出element, element之类的并列形式将多种节点投进同一个插槽。基础用法把子内容投影进组件当你需要编写一个充当内容容器的组件时比如一个带阴影的自定义卡片组件直接在模板里写内容是不行的——模板是组件自身视图view的一部分。此时用ng-content作为插槽标记即可Component({ selector: custom-card, template: div classcard-shadow ng-content / /div , }) export class CustomCard { /* ... */ }当外部这样使用时组件宿主元素的所有子元素都会被渲染即投影到ng-content所在位置!-- 使用组件 -- custom-card pThis is the projected content/p /custom-card!-- 渲染后的 DOM -- custom-card div classcard-shadow pThis is the projected content/p /div /custom-card这里需要区分两个重要概念仓库指南 content-projection.md 明确指出content内容通过这种方式传给组件的、写在宿主标签内部的子节点view视图组件模板自身声明的元素。两者来源不同Angular 对它们的变更检测、作用域与依赖注入规则也不同详见下文Caveats。注意不应把ng-content与if、for、switch这类控制流块混用做条件渲染。因为Angular 只要看到ng-content插槽就会实例化并创建投影内容的 DOM 节点即使插槽本身被隐藏投影内容依然会被创建。若需要按条件渲染组件内容应使用模板片段方案参见同目录下的 ng-template.md。多插槽投影按select分发不同内容ng-content真正的威力在于支持一个组件内放置多个插槽用select分别捕获不同类型的内容。扩展上面的卡片示例我们为标题和正文各留一个插槽Component({ selector: card-title, template: ng-contentcard-title/ng-content, }) export class CardTitle {} Component({ selector: card-body, template: ng-contentcard-body/ng-content, }) export class CardBody {} Component({ selector: custom-card, template: div classcard-shadow ng-content selectcard-title / div classcard-divider/div ng-content selectcard-body / /div , }) export class CustomCard {}使用时Angular 会按宿主元素内部的书写顺序把每个子元素匹配到对应插槽Component({ selector: app-root, imports: [CustomCard, CardTitle, CardBody], template: custom-card card-titleHello/card-title card-bodyWelcome to the example/card-body /custom-card , }) export class App {}!-- 渲染后的 DOM -- custom-card div classcard-shadow card-titleHello/card-title div classcard-divider/div card-bodyWelcome to the example/card-body /div /custom-card兜底插槽与未匹配即不渲染多插槽布局有一个隐式规则ng-content上select指定的选择器与 Angular 组件自身的 selector 使用同一套语法与匹配逻辑在仓库指南中明确提到Theng-contentplaceholder supports the same CSS selectors as component selectors参见 content-projection.md。围绕select有三种典型编排方式1. 多个select插槽 一个不带select的插槽不带select的插槽相当于通配兜底会捕获所有没有被其他select匹配到的元素!-- 组件模板 -- div classcard-shadow ng-content selectcard-title / div classcard-divider/div !-- 捕获除 card-title 之外的所有内容 -- ng-content / /div!-- 使用组件 -- custom-card card-titleHello/card-title img src... / pWelcome to the example/p /custom-card!-- 渲染后的 DOM -- custom-card div classcard-shadow card-titleHello/card-title div classcard-divider/div img src... / pWelcome to the example/p /div /custom-card2. 组件没有任何不带select的兜底插槽此时凡是不能匹配任一插槽select的元素不会渲染进 DOM相当于被静默丢弃。这在源码层面由matchingProjectionSlotIndex的返回值体现当节点不匹配任何插槽且不存在通配插槽时返回null该节点便不会被分入任何投影桶见 projection.ts。3. 匹配顺序遵循先到先得一个子节点只会被第一个匹配到它的插槽捕获。运行时匹配器遍历投影槽位命中即返回槽位下标源码注释明确写着 first matching selector captures a given node见 projection.ts因此插槽声明顺序会影响分发结果。兜底内容Fallback Content如果调用方没有传入任何匹配的内容Angular 允许在ng-content自身内部书写兜底内容在插槽为空时展示!-- 组件模板 -- div classcard-shadow ng-content selectcard-titleDefault Title/ng-content div classcard-divider/div ng-content selectcard-bodyDefault Body/ng-content /div!-- 使用组件只传了 card-title未提供 card-body -- custom-card card-titleHello/card-title /custom-card!-- 渲染后的 DOM -- custom-card div classcard-shadow card-titleHello/card-title div classcard-divider/div Default Body /div /custom-card兜底内容并不是简单的文本替换而是一段内置的内嵌模板ɵɵprojection指令在运行时检查对应插槽是否为空isEmpty为空时才会调用insertFallbackContent去实例化兜底视图见 projection.ts 及其insertFallbackContent实现。值得注意的是兜底视图的声明declaration会先于投影节点进行这是为了保证与 hydration客户端水合机制的正确协作——源码注释专门说明了这一点fallback content 无论插槽是否为空都会被声明因为组件不同的实例可能有不同的占用情况projection.ts。用ngProjectAs为投影内容改名实际场景中调用方往往不想为了配合组件插槽而引入额外的语义化标签。此时可以用ngProjectAs特殊属性把任意元素伪装成某个 CSS 选择器去参与匹配!-- 组件模板 -- div classcard-shadow ng-content selectcard-title / div classcard-divider/div ng-content / /div!-- 使用组件用 h3 冒充 card-title -- custom-card h3 ngProjectAscard-titleHello/h3 pWelcome to the example/p /custom-card!-- 渲染后的 DOMh3 进入 selectcard-title 的插槽 -- custom-card div classcard-shadow h3Hello/h3 div classcard-divider/div pWelcome to the example/p /div /custom-card匹配时Angular 会比较ngProjectAs的值与各插槽的select选择器而不是元素自身身份。运行时逻辑可在matchingProjectionSlotIndex中看到当节点存在ngProjectAs属性时走isSelectorInSelectorList(ngProjectAsAttrVal, slotValue)分支否则才用isNodeMatchingSelectorList按节点真实特征匹配projection.ts。限制ngProjectAs只支持静态值不能绑定动态表达式。使用注意事项Caveats虽然内容被渲染在接收组件内部但它仍然属于声明它的那个父组件。Angular 会把它计入父组件的视图这会带来两个容易被忽视的影响变更检测投影内容是在父组件运行变更检测时被检查的。如果接收组件使用OnPush策略Angular 可以跳过检查该组件自己的模板但不会跳过投影内容——因为投影内容隶属于父组件。因此下面的写法中即使onpush-wrapper是OnPush内部投影的expensive-component仍会在父组件每个变更检测周期被检查!-- 父组件模板默认变更检测 -- onpush-wrapper !-- 每个父级周期仍会被检查OnPush 在这里帮不上忙 -- expensive-component / /onpush-wrapper依赖注入投影内容从父组件的注入器获取依赖而不是接收组件的viewProviders。如果想深入了解组件级 vs 视图级 provider 的差异可阅读仓库文档 hierarchical-dependency-injection源码路径为 adev/src/content/guide/di。另外要警惕某些库组件不支持投影子内容。菜单、标签页、列表等组件通常通过ContentChildren查询子元素来建立键盘导航、焦点管理或 ARIA 属性它们假设自己直接拥有子元素。如果人为包一层再投影进去往往会导致功能静默失效。因此在使用内容投影前务必查阅目标库组件的文档确认其是否支持投影子内容。底层实现剖析从编译到渲染仓库指南的内容来自用户视角要真正理解ng-content值得从编译器与运行时两个层面看一遍它如何落地。编译期提取ngContentSelectors编译器在对组件模板做语义分析t2时会把组件模板中所有ng-content的选择器收集起来。在 t2_api.ts 的DirectiveMeta中就有对应字段/** * If the directive is a component, includes the selectors of its ng-content elements. */ ngContentSelectors: string[] | null;组件定义编译时这些选择器会写入组件的定义映射ngContentSelectors由编译器调用ɵɵprojectionDef指令在运行时进行插槽分发表初始化相关代码位于 compiler.ts。运行时ɵɵprojectionDef与ɵɵprojection在 Ivy 运行时packages/core/src/render3/instructions/projection.tsng-content由两条指令协作完成ɵɵprojectionDef负责分发。它以组件宿主元素的所有直接子节点为输入遍历组件模板里收集到的全部插槽选择器把每个投影节点分配到对应的桶bucket中。未传参时默认退化为单个通配符插槽源码注释fall back to a single projection slot with the wildcard selectorprojection.ts分发结果挂在组件宿主 TNode 的projection字段上。整个过程只对componentNode.projection为空的首次渲染执行一次。ɵɵprojection负责落位。它在每个ng-content位置被调用读取该插槽序号对应的已分发节点通过applyProjection把它们插入当前 DOM 位置若插槽为空且有兜底模板则渲染兜底内容。指令签名也印证了select与插槽的对应关系export function ɵɵprojection( nodeIndex: number, selectorIndex: number 0, // 0 代表通配符 *默认否则是 projectionDef 中从 1 起的槽位下标 attrs?: TAttributes | null, fallbackTemplateFn?: ComponentTemplateunknown, fallbackDecls?: number, fallbackVars?: number, ): void其中matchingProjectionSlotIndexprojection.ts是实现select语义的核心遍历槽位时遇到*会先记录为兜底槽位继续向后找若最终没有任何显式选择器命中则返回通配槽位下标。结合前文可知这就是无select的插槽捕获一切未匹配内容、select插槽先到先得这一文档规则的源码级解释。与ng-container、ng-template的边界ng-content与同目录下的另外两个模板元素经常被一起提及但职责完全不同ng-container.md一个不产生额外 DOM 元素的分组容器用于在单个元素上只能挂一个结构型指令的前提下组合*ngIf/*ngFor或配合*ngTemplateOutlet实例化模板视图属于模板逻辑容器ng-template.md声明一个默认不渲染的模板片段由结构指令或ngTemplateOutlet按需实例化是按条件渲染组件内容的正确工具ng-content则用于接收并摆放由组件外部传入的现成内容是插槽。当需要把组件的一部分内容投影进来时使用ng-content当需要声明一段内部可复用、可按需渲染的模板时使用ng-template/ng-container。小结ng-content是 Angular 组合式组件设计的基石之一其全部能力可以归纳为一句话在模板中声明一个或多个位置把宿主元素内部传入的子内容按 CSS 选择器分发并渲染到对应位置。掌握以下要点即可在生产中熟练使用单插槽ng-content /投影全部子内容多插槽用select按标签/属性/类/:not组合分发未匹配内容要么落入无select的兜底插槽要么不渲染兜底内容写在ng-content内部插槽为空时展示ngProjectAs可让任意元素以指定的选择器身份参与匹配仅静态值牢记投影内容隶属父组件视图影响变更检测与依赖注入并谨慎对依赖ContentChildren的库组件使用投影。仓库内还有更多一手的模板示例与测试可供深入学习content-projection.md完整用户指南、programmatic-rendering.md程序化渲染对照、projection.ts运行时实现、t2_api.ts编译器接口。【免费下载链接】angularDeliver web apps with confidence 项目地址: https://gitcode.com/GitHub_Trending/an/angular创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。