Angular CDK Bidi 双向文本方向(LTR/RTL)支持全解析:Directionality 服务与 Dir 指令实战指南
2026/9/12 4:34:09 网站建设 项目流程

Angular CDK Bidi 双向文本方向(LTR/RTL)支持全解析:Directionality 服务与 Dir 指令实战指南

【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/components

本指南以@angular/cdk_bidi包的公开 API 报告(goldens/cdk/bidi/index.api.md)为核心骨架,结合该包在仓库中的文档、源码与单元测试,系统讲解 Angular CDK 中 LTR/RTL 布局方向体系的完整用法。你将掌握如何用Directionality服务读取全局方向、如何用Dir指令让组件感知最近的祖先方向上下文、CDK 如何解释auto值,以及DIR_DOCUMENT令牌在测试中的价值——这些能力是 overlay 定位、键盘导航等方向敏感组件正确工作的基础。

包定位:@angular/cdk/bidi是什么

bidi(Bidirectional Text,双向文本)是 Angular CDK 中的一个基础包,它为组件提供了一套"获取并响应应用 LTR/RTL 布局方向变化"的公共机制。正如该包文档(src/cdk/bidi/bidi.md)开篇所述:

Thebidipackage provides a common system for components to get and respond to change in the application's LTR/RTL layout direction.

在阿拉伯语、希伯来语、波斯语等从右向左书写的语言环境中,页面布局需要整体镜像翻转。CDK 的众多组件(如 overlay 浮层定位、菜单键盘导航)都必须知道当前元素处于 RTL 还是 LTR 环境才能正确工作。bidi包正是为此提供统一抽象的底层模块。

该包的公开 API 表面非常精简,由 API Extractor 生成的报告 goldens/cdk/bidi/index.api.md 明确列出了全部对外导出:

公开成员类型作用
BidiModuleNgModule导入后即可使用Dir指令,并注入Directionality服务
DirectionalityService应用(或子树)的方向上下文,暴露当前方向与变更流
DirDirective匹配带dir属性的元素,自身即作为Directionality提供
DIR_DOCUMENTInjectionToken<Document>注入 document 的令牌,便于在测试中伪造方向来源
DirectionType'ltr' \| 'rtl'字面量联合类型

所有符号均标注@public,即该包的稳定公共契约;_rawDirvalueSignal等以_开头或标注(undocumented)的成员属于内部实现细节,不在公共 API 承诺范围内。入口统一由 src/cdk/bidi/public-api.ts 导出,并经 src/cdk/bidi/index.ts 转发。

第一步:导入BidiModule

要使用该包的能力,首先需要在模块中导入BidiModule。其实现(src/cdk/bidi/bidi-module.ts)非常简洁:

import {NgModule} from '@angular/core'; import {Dir} from './dir'; @NgModule({ imports: [Dir], exports: [Dir], }) export class BidiModule {}

从源码结构看,BidiModule本身没有提供任何服务——DirectionalityDIR_DOCUMENT均声明为providedIn: 'root'(详见后文),因此只要导入该模块即可在任意组件中注入方向服务。

import {NgModule} from '@angular/core'; import {BidiModule} from '@angular/cdk/bidi'; @NgModule({ imports: [BidiModule], }) export class MyModule {}

注入Directionality服务读取当前方向

当应用引入了BidiModule后,组件即可注入Directionality来获取当前的文本方向(RTL 或 LTR)。API 报告中Directionality的公开成员为:

  • readonly change: EventEmitter<Direction>——方向变化时发出事件的流;
  • get value(): Direction——当前方向;
  • readonly valueSignal: WritableSignal<Direction>(undocumented)内部实现)。

官方文档示例:响应方向变化

src/cdk/bidi/bidi.md 给出了注入并使用Directionality的完整示例:

@Component({ ... }) export class MyWidget implements OnDestroy { /** Whether the widget is in RTL mode or not. */ private isRtl: boolean; /** Subscription to the Directionality change EventEmitter. */ private _dirChangeSubscription = Subscription.EMPTY; constructor(dir: Directionality) { this.isRtl = dir.value === 'rtl'; this._dirChangeSubscription = dir.change.subscribe(() => { this.flipDirection(); }); } ngOnDestroy() { this._dirChangeSubscription.unsubscribe(); } }

要点拆解:

  • 初始值:构造函数中通过dir.value === 'rtl'一次性读取当前方向,用于首次渲染;
  • 响应变化:订阅dir.change事件流,当方向变化时触发flipDirection()之类的布局翻转逻辑;
  • 资源释放:在ngOnDestroyunsubscribe防止内存泄漏。虽然Directionality.ngOnDestroy()complete事件流(源码见 src/cdk/bidi/directionality.ts),但规范做法仍是主动取消订阅。

全局方向是如何确定的:body/html 优先级与DIR_DOCUMENT

Directionality的构造函数(src/cdk/bidi/directionality.ts)揭示了初始方向的计算规则:

constructor() { const _document = inject(DIR_DOCUMENT, {optional: true}); if (_document) { const bodyDir = _document.body ? _document.body.dir : null; const htmlDir = _document.documentElement ? _document.documentElement.dir : null; this.valueSignal.set(_resolveDirectionality(bodyDir || htmlDir || 'ltr')); } }

解析顺序为:

  1. 优先读取<body>元素的dir属性;
  2. 若 body 未设置,则回退到<html>元素的dir
  3. 两者均未设置时,默认'ltr'

这一优先级在单元测试 src/cdk/bidi/directionality.spec.ts 中有明确验证:should read dir from the body even it is also specified on the html elementshould default to ltr if nothing is specified on either body or the html element

其中DIR_DOCUMENT令牌的实现(src/cdk/bidi/dir-document-token.ts)值得一提:它以providedIn: 'root'注册,工厂函数inject(DOCUMENT)复用 Angular 平台自带的DOCUMENT令牌。单独定义这一令牌而非直接注入DOCUMENT的原因,源码注释写得很清楚:

单元测试中不能直接使用真实的 document(在 Safari 中修改真实dir会导致基于几何测量的测试失败),而重新 provide platform-browser 的DOCUMENT又会与测试代码自身的querySelector冲突。

因此测试中可轻松伪造 document(见 src/cdk/bidi/directionality.spec.ts):

TestBed.configureTestingModule({ providers: [{provide: DIR_DOCUMENT, useFactory: () => fakeDocument}], });

这也是为什么Directionality的注入使用{optional: true}——在没有 Angular 平台(如纯 SSR 或特殊测试环境)时不会抛错。

Dir指令:为子树提供局部方向上下文

BidiModule还导出了一个匹配任何带dir属性的元素的指令Dir。API 报告显示其关键声明:

ɵdir: [Dir, "[dir]", ["dir"], { "dir": { "alias": "dir"; "required": false; } }, { "change": "dirChange" }, ..., true]

即:选择器为[dir],输入属性dir(可选),输出事件dirChange。源码(src/cdk/bidi/dir.ts)进一步揭示了两个重要细节:

@Directive({ selector: '[dir]', providers: [{provide: Directionality, useExisting: Dir}], host: {'[attr.dir]': '_rawDir'}, exportAs: 'dir', })
  • 自身即Directionality:通过providers: [{provide: Directionality, useExisting: Dir}]Dir把自己以Directionality的身份提供给后代。这样任何注入Directionality的组件,拿到的是最近的祖先方向上下文,而不是全局值;
  • 保留原始属性:host 绑定'[attr.dir]': '_rawDir',把消费者传入的原始字符串(如'auto')原样写回 DOM,而value则返回规范化后的'ltr' | 'rtl'。测试should preserve the consumer-provided dir attribute while normalizing the directive value(directionality.spec.ts)专门验证了这一点。

基本用法

<!-- 为整个子树声明 RTL 布局 --> <div dir="rtl"> <my-widget></my-widget> <!-- 此组件注入的 Directionality.value === 'rtl' --> </div> <!-- 绑定与监听变化 --> <div [dir]="currentDirection()" (dirChange)="onDirectionChange($event)"> ... </div>

Directionality相同的 API

Dir实现Directionality接口,公开成员与之一致:change: EventEmitter<Direction>(输出别名dirChange)、dir属性(getter/setter)、valuegetter、valueSignal。因此对Dir也可以使用与 bidi.md 中Directionality示例完全相同的订阅模式。

setter 内部的完整逻辑

Dirdirsetter(src/cdk/bidi/dir.ts)展示了规范化与事件触发的完整链路:

set dir(value: Direction | 'auto') { const previousValue = this.valueSignal(); this.valueSignal.set(_resolveDirectionality(value)); this._rawDir = value; if (previousValue !== this.valueSignal() && this._isInitialized) { this.change.emit(this.valueSignal()); } }
  • 将输入值经_resolveDirectionality规范化为'ltr' | 'rtl'存入 signal;
  • 同时把原始字符串存入_rawDir以便原样写回 DOM;
  • 仅当值确实发生变化、且组件已完成初始化(ngAfterContentInit中置_isInitialized = true,见 dir.ts)时才发出dirChange事件,避免初始化阶段的冗余触发;
  • ngOnDestroychange.complete()结束事件流(dir.ts)。

auto值的特殊解释:基于浏览器语言而非文本内容

HTML 原生规范允许dir="auto",浏览器会根据元素文本内容的首个强方向字符来推断方向。CDK 对此有不同的解释,bidi.md 明确说明了原因与差异:

对于性能考量,CDK 通过查看浏览器语言(navigator.language)并与一组已知的 RTL 区域设置匹配来解析auto值。这与浏览器基于元素文本内容的处理方式不同。

之所以采用"基于语言的简化匹配"而非"基于内容",是因为按文本内容推断计算成本高,而 overlay、键盘导航等场景只需知道元素大体处于 RTL 还是 LTR 布局即可正确工作。

底层实现:RTL 区域正则

_resolveDirectionalityRTL_LOCALE_PATTERN定义在 src/cdk/bidi/directionality.ts:

/** Regex that matches locales with an RTL script. Taken from `goog.i18n.bidi.isRtlLanguage`. */ const RTL_LOCALE_PATTERN = /^(ar|ckb|dv|he|iw|fa|nqo|ps|sd|ug|ur|yi|.*-_)(?!.*-_($|-|_))($|-|_)/i; export function _resolveDirectionality(rawValue: string): Direction { const value = rawValue?.toLowerCase() || ''; if (value === 'auto' && typeof navigator !== 'undefined' && navigator?.language) { return RTL_LOCALE_PATTERN.test(navigator.language) ? 'rtl' : 'ltr'; } return value === 'rtl' ? 'rtl' : 'ltr'; }

该正则源自goog.i18n.bidi.isRtlLanguage,可识别的 RTL 语言涵盖:阿拉伯语(ar)、库尔德语(ckb)、迪维希语(dv)、希伯来语(he/iw)、波斯语(fa)、曼丁哥语(nqo)、普什图语(ps)、信德语(sd)、维吾尔语(ug)、乌尔都语(ur)、意第绪语(yi),以及带 RTL 文字标签(如ArabHebrThaa等)的区域设置,同时排除明确标注Latn/Cyrl文字的情况(例如fa-Latn仍视为 LTR)。

_resolveDirectionality的完整规则可归纳为:

输入值处理结果
'rtl'(大小写不敏感)'rtl'
'ltr'(大小写不敏感)'ltr'
'auto'navigator.language匹配 RTL 区域正则'rtl'
'auto'且语言不匹配或不可用'ltr'
其他任何非法值(如'not-valid''ltr'(安全回退)

大小写不敏感与非法值回退均有测试佐证(directionality.spec.ts、L119-L128、L144-L149)。

关键行为验证:来自测试的实证

src/cdk/bidi/directionality.spec.ts 对该包的核心契约做了完整验证,可作为使用时的行为参考:

  • Service 层面:html 元素方向可被读取;body 优先级高于 html;默认'ltr';非法值回退'ltr'change流在 destroy 时 complete;
  • Dir 指令层面:指令自身以Directionality身份被注入(should provide itself as Directionality);值变化时发出dirChange事件(changeCount从 0 变为 1);销毁时 complete 事件流;保留原始dir="auto"属性同时规范化内部值;大小写不敏感(RTLrtl)。

实践要点与最佳实践总结

  1. 全局方向:在index.html<html><body>上设置dir属性,Directionality服务会自动读取(body 优先);
  2. 局部覆盖:在子树上使用<div dir="rtl">,该子树内的组件通过Dir提供的局部Directionality感知最近上下文,实现全局 RTL 页面中的局部 LTR 区块(反之亦然);
  3. 响应变化:订阅dir.change或模板中的(dirChange)事件,在方向切换时执行布局翻转逻辑,并记得在销毁时取消订阅;
  4. auto语义差异:CDK 按浏览器语言解析auto,与浏览器按文本内容推断的语义不同,方向敏感的场景(overlay、键盘导航)请按 CDK 语义理解;
  5. 测试友好:通过DIR_DOCUMENT令牌注入伪造 document,可在不触碰真实 DOM 的情况下测试各种方向组合。

延伸阅读

  • API 报告原文:goldens/cdk/bidi/index.api.md
  • 包内使用文档:src/cdk/bidi/bidi.md
  • 核心实现:src/cdk/bidi/directionality.ts、src/cdk/bidi/dir.ts
  • 令牌定义:src/cdk/bidi/dir-document-token.ts
  • 模块声明:src/cdk/bidi/bidi-module.ts
  • 行为测试:src/cdk/bidi/directionality.spec.ts
  • 同仓库中依赖该方向能力的示例:overlay 定位、菜单/列表键盘导航等模块均以Directionality作为方向上下文来源

【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/components

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询