Bitwarden Clients Angular 代码现代化改造指南:CLI Schematics 迁移与 Bitwarden 架构模式
2026/9/15 7:22:07 网站建设 项目流程

Bitwarden Clients Angular 代码现代化改造指南:CLI Schematics 迁移与 Bitwarden 架构模式

【免费下载链接】clientsBitwarden client apps (web, browser extension, desktop, and cli).项目地址: https://gitcode.com/GitHub_Trending/cl/clients

本指南基于 Bitwarden clients 仓库(web、browser、desktop 三个 Angular 客户端的 monorepo)中的 Angular 现代化改造技能文档,系统讲解如何将遗留 Angular 组件与指令改造为符合现代架构的最佳实践。文章采用「自动 CLI 迁移 + Bitwarden 专属模式」的两步法,覆盖 standalone 组件、新控制流语法、Signal 输入输出/查询、inject()函数、OnPush 变更检测以及 Signals 与 Observables 的职责边界等核心议题。读完本文,你将掌握一套可在仓库内直接落地、可验证、可复用的 Angular 现代化迁移流水线,并理解其背后的架构决策记录(ADR)依据。

现代化改造的双层策略:为什么是「两步法」

Bitwarden clients 仓库对 Angular 现代化改造给出的核心策略是两层递进

  1. 自动迁移(Automated migrations)——使用 Angular CLI schematics 完成 standalone、控制流语法、signals 等机械化改造;
  2. Bitwarden 模式(Bitwarden patterns)——在自动迁移之后,依据 ADR 合规、OnPush 变更检测、正确的成员可见性(visibility)、薄组件(thin components)等仓库约定进行人工打磨。

该策略的核心理由是:Angular 提供的自动化 schematics 能够处理大量边界情况、同步更新测试并保证正确性,凡是有 CLI schematic 覆盖的改造点绝不手工迁移。手工迁移只应留给 CLI 工具覆盖不到的模式。这一点在技能文档中被明确标记为 CRITICAL 级约束,并在仓库的.claude/rules/angular-components.md中被重申:现代化遗留组件(NgModule → standalone、*ngIf@if@Input()input()等)时应使用angular-modernization技能,因为它能对迁移进行安全排序(例如 signals 必须先于 OnPush 落地)。

技能本身的元信息定义在 SKILL.md 的 frontmatter 中:其允许的工具集为 Read、Write、Glob 以及 Bash(仅限npx ng generate:*类命令),说明整个迁移过程以 CLI schematics 为主体、以文件读写为辅助。

三条硬性执行约定

在执行迁移命令前,必须遵守以下约定(来自 SKILL.md):

  • 一律使用npx ng运行命令,确保调用的是当前工作区解析到的 Angular CLI 版本;
  • 所有命令都必须作用于目录(directories)而非单个文件,使用--path选项指定目标目录;
  • 严格按顺序执行迁移,因为部分迁移之间存在依赖关系。

Step 1:按序执行 8 个 Angular CLI Schematics 迁移

技能文档按依赖顺序给出了完整的 CLI 迁移序列。以下逐一展开,包含命令、作用与改造前后对照。

1. Standalone 组件迁移

npx ng generate @angular/core:standalone --path=<directory> --mode=convert-to-standalone

将基于 NgModule 的组件架构转换为 standalone 架构。仓库规则(见 .claude/rules/angular-components.md)进一步规定:组件必须是 standalone 的,NgModule 仍可用于对组件进行分组,但内部组件必须 standalone,imports 声明在组件装饰器上,而非注册在NgModule.declarations中。迁移后的组件省略standalone: true(Angular 新版本中 standalone 已是默认值),任何显式声明standalone: false的组件都必须被迁移(见 migration-patterns.md)。

2. 控制流语法迁移

npx ng generate @angular/core:control-flow

将模板中的*ngIf*ngFor*ngSwitch迁移为@if@for@switch内置控制流。仓库规则明确要求使用内置@if/@for/@switch,而非结构型指令(.claude/rules/angular-components.md)。

3. Signal 输入迁移

npx ng generate @angular/core:signal-input-migration

@Input()迁移为 signal inputs(input()/input.required())。仓库中已有大量真实落地案例,例如组织成员邀请对话框的 by-link-tab.component.ts:

export class ByLinkTabComponent { readonly organizationId = input.required<OrganizationId, string>({ transform: (value: string) => value as OrganizationId, }); readonly showCoachMarks = input<boolean>(false); ... }

这段真实代码展示了 signal inputs 的两种形态:input.required<OrganizationId, string>({ transform })用于必填输入并借助transform完成类型转换;input<boolean>(false)则提供了默认值。

4. Signal 输出迁移

npx ng generate @angular/core:output-migration

@Output()迁移为 signal outputs(output())。signal output 取代了EventEmitter字段式的输出声明,使模板 I/O 全面信号化(见 .claude/rules/angular-components.md)。

5. Signal 查询迁移

npx ng generate @angular/core:signal-queries-migration

@ViewChild@ContentChild@ViewChildren@ContentChildren等查询装饰器迁移为viewChild()/viewChildren()等 signal queries。

6. inject() 函数迁移

npx ng generate @angular/core:inject-migration

将构造函数注入(constructor injection)迁移为inject()函数。仓库规则规定:使用inject()而非构造函数注入,构造函数注入仅保留在与非 Angular 客户端(如 CLI)共享的代码中(.claude/rules/angular.md)。迁移后的典型形态:

private userService = inject(UserService); private route = inject(ActivatedRoute);

7. 自闭合标签迁移

npx ng generate @angular/core:self-closing-tag

将模板中可自闭合的元素更新为自闭合语法,例如<my-component></my-component><my-component />

8. 未使用导入清理

npx ng generate @angular/core:unused-imports

移除迁移过程中产生的未使用导入,保持代码整洁。

Step 2:应用 Bitwarden 专属模式

CLI 迁移完成后,需要按 migration-patterns.md 中的详细示例应用 Bitwarden 仓库特有的七项模式:

  1. 添加 OnPush 变更检测——所有组件设置changeDetection: ChangeDetectionStrategy.OnPush。注意 OnPush 下原地修改数组/对象不会触发变更检测,必须创建新引用([...arr]{...obj})或改用 signals;
  2. 应用可见性修饰符——protected用于模板访问的成员,private用于内部实现;
  3. 将组件本地状态转换为 signals
  4. 保留服务层的 Observables(不转换为 signals);
  5. 将业务逻辑抽取到服务中(薄组件);
  6. 正确组织类成员顺序
  7. 为 standalone 架构更新测试

类成员组织规范

技能文档与配套模式文档给出了一套严格的类成员排序约定(migration-patterns.md),现代组件应按下述顺序组织:

@Component({...}) export class MyComponent { // 1. Inputs (public) @Input() data: string; // 2. Outputs (public) @Output() valueChange = new EventEmitter<string>(); // 3. ViewChild/ContentChild @ViewChild('template') template: TemplateRef<any>; // 4. Injected dependencies (private/protected) private userService = inject(UserService); protected dialogService = inject(DialogService); // 5. Public properties public formGroup: FormGroup; // 6. Protected properties (template-accessible) protected isLoading = signal(false); protected items$ = this.itemService.items$; // 7. Private properties private cache = new Map(); // 8. Lifecycle hooks ngOnInit() {} // 9. Public methods public save() {} // 10. Protected methods (template-accessible) protected handleClick() {} // 11. Private methods private processData() {} }

该顺序遵循「公共 → 注入依赖 → 模板可见 → 私有」的可读性优先级,与仓库中 .claude/rules/angular-components.md 的约定一致:protected用于仅被模板触达的成员,readonly用于组件级常量。

Step 3:验证

迁移完成后必须执行验证流程(SKILL.md):

npm run lint:fix # 修复 lint 与格式 npm run test # 运行测试

若出现任何错误,需逐一修复。这与仓库根 CLAUDE.md 中的验证要求一致:修改代码后运行npm run lint:fixnpm run prettiernpm run test:typesnpm test(可通过npm test -- <path-or-pattern>限定作用域)。

关键决策:Signals 与 Observables 的职责边界

现代化改造中最容易混淆的决策是「何时用 Signals、何时用 Observables」。技能文档依据两条 ADR 给出了明确边界:

  • Signals——仅用于组件本地状态(ADR-0027);
  • Observables——用于服务状态与跨组件通信(ADR-0003);
  • 使用toSignal()将 Observables 桥接进基于信号的组件。

.claude/rules/angular-components.md 对 signal API 家族做了更细的映射:

  • input()/input.required()—— 取代@Input()
  • output()—— 取代@Output()
  • viewChild()/viewChildren()—— 取代@ViewChild/@ViewChildren
  • computed()—— 用于派生状态,优于模板中直接调用的函数(只在依赖变化时重算)

computed() 优于 effect()

派生状态应使用computed()effect()只用于副作用(日志、分析、DOM 同步)。反例与正例对比:

❌ 反例:用 effect() 计算派生值

constructor() { effect(() => { const id = this.selectedId(); this.selectedItem.set(this.items().find(i => i.id === id) ?? null); }); }

✅ 正例:用 computed() 声明式派生

selectedItem = computed(() => this.items().find((i) => i.id === this.selectedId()) ?? null);

服务的 Observables 保持原样(ADR-0003)

服务层的 Observables 不要转换为 signals,组件中可直接暴露流并用async管道消费(migration-patterns.md):

// In component protected folders$ = this.folderService.folders$; // Template // <div *ngFor="let folder of folders$ | async"> // For explicit subscriptions constructor() { this.userService.user$ .pipe(takeUntilDestroyed()) .subscribe(user => this.handleUser(user)); }

仓库规则进一步补充:显式订阅必须经由takeUntilDestroyed()自动清理(在注入上下文之外的场景传入DestroyRef),且禁止嵌套.subscribe()调用,应使用switchMap(取消前序)、concatMap(顺序保持)或mergeMap(并行,谨慎使用)组合(.claude/rules/angular-components.md)。

用 toSignal() 桥接 Observables

将服务 Observables 转换为组件内 signals 时使用toSignal(),替代手写的Subject+takeUntil样板(migration-patterns.md):

Before:

private destroy$ = new Subject<void>(); users: User[] = []; ngOnInit() { this.userService.users$.pipe(takeUntil(this.destroy$)) .subscribe(users => this.users = users); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }

After:

protected users = toSignal(this.userService.users$, { initialValue: [] });

仓库真实组件中即同时使用了toSignaltoObservabletakeUntilDestroyed,例如 by-link-tab.component.ts 的导入:

import { takeUntilDestroyed, toObservable, toSignal } from "@angular/core/rxjs-interop";

组件架构:Standalone 与薄组件

现代化后的组件必须遵循以下架构约束(.claude/rules/angular-components.md):

  • Standalone:组件必须 standalone,imports 声明在组件装饰器;
  • OnPush:设置changeDetection: ChangeDetectionStrategy.OnPush
  • Host 绑定:使用装饰器中的host属性,而非@HostBinding/@HostListener
@Component({ selector: "app-example", changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule], host: { "[class.active]": "isActive()", "(click)": "onClick()", }, })
  • 薄组件:组件只包含视图逻辑,业务逻辑下沉到服务;
  • 组合优于继承:将大组件拆分为独立 standalone 片段,不同客户端在页面层定制并共享子组件,而非继承基类。

模板方面,仓库规则还要求输入框与按钮具备描述性 ID 以支持 QA 自动化(<component_name>_<html_element>_<readable_name>),且表单统一使用响应式表单(Reactive Forms)而非模板驱动表单。

模板语法:新控制流与绑定方式

新控制流语法

Before:

<div *ngIf="user$ | async as user; else loading"> <p *ngFor="let item of user.items">{{ item.name }}</p> </div> <ng-template #loading>Loading...</ng-template>

After:

@if (user$ | async; as user) { @for (item of user.items; track item.id) { <p>{{ item.name }}</p> } } @else { <p>Loading...</p> }

注意@for必须提供track表达式以优化列表重渲染。

优先使用 Class/Style 绑定替代 ngClass/ngStyle

❌ 反例:

<div [ngClass]="{ 'active': isActive(), 'disabled': isDisabled() }"> <div [ngStyle]="{ 'width.px': width(), 'height.px': height() }"></div> </div>

✅ 正例:

<div [class.active]="isActive()" [class.disabled]="isDisabled()"> <div [style.width.px]="width()" [style.height.px]="height()"></div> </div>

当类或样式较多时,可用computed()组合派生(.claude/rules/angular-components.md)。

类型安全:无 TypeScript Enums

依据 ADR-0025,仓库禁止使用 TypeScript enum,改用 const 对象 + 类型别名(migration-patterns.md):

Before:

enum CipherType { Login = 1, SecureNote = 2, }

After:

export const CipherType = Object.freeze({ Login: 1, SecureNote: 2, } as const); export type CipherType = (typeof CipherType)[keyof typeof CipherType];

对于需要模板按名引用的 enum-like 输入,组件需将其暴露为protected readonly属性(.claude/rules/angular-components.md)。

响应式表单规范

protected formGroup = new FormGroup({ name: new FormControl('', { nonNullable: true }), email: new FormControl<string>('', { validators: [Validators.email] }), });

反模式清单:迁移时必须规避

技能文档明确列出的反模式(migration-patterns.md):

  • ❌ 在已有 CLI 迁移的情况下手工重构
  • ❌ 未使用takeUntilDestroyed()的手动订阅
  • ❌ 使用 TypeScript enums(应使用 const 对象,见 ADR-0025)
  • ❌ 构造函数注入与inject()混用
  • ❌ 在与非 Angular 代码共享的服务中使用 signals(ADR-0003)
  • ❌ 组件内承载业务逻辑
  • ❌ 使用代码区域(code regions)——应重构而非分区
  • ❌ 将服务 Observables 转换为 signals(ADR-0003)
  • ❌ 用effect()计算派生状态(应使用computed()
  • ❌ 使用ngClass/ngStyle(应使用[class.*]/[style.*]

迁移验收清单

完成迁移前逐项核对(SKILL.md):

  • 已添加 OnPush 变更检测
  • 已应用可见性修饰符(protected/private
  • 组件状态用 signals,服务状态用 observables
  • 类成员已按规范组织(见 migration-patterns.md 类成员组织)
  • 测试已更新并通过
  • 无新增 TypeScript enums
  • 无代码区域(code regions)

参考资源

技能文档与仓库规则文件共同构成了完整的知识体系:

  • angular-modernization 技能文档——迁移主流程与命令序列
  • Angular 迁移模式参考——组件架构、DI、响应式、模板与类型安全的完整示例
  • .claude/rules/angular.md——依赖注入与safeProvider()规则
  • .claude/rules/angular-components.md——组件级模式:signals、OnPush、控制流、订阅规范
  • .claude/rules/typescript.md——TypeScript 全局规则(enum-like、命名等)
  • .claude/CLAUDE.md——monorepo 架构边界与验证命令
  • 真实迁移样例:by-link-tab.component.ts——standalone + OnPush +input.required()+toSignal/toObservable/takeUntilDestroyed的综合落地

底层决策依据是仓库引用的三条 ADR:ADR-0003(Observable 数据服务)、ADR-0025(无 TypeScript Enums)与 ADR-0027(Angular Signals),可在 Bitwarden 的架构决策记录站点查阅;Angular 官方资源(Style Guide、Migrations、CLI Schematics)则提供了这些命令与模式的标准参考。需要注意的是,本文所述命令与规则以当前仓库锁定版本的 Angular 与工程约定为准,迁移前应先在目标目录小范围试运行并借助npm run lint:fixnpm run test闭环验证。

【免费下载链接】clientsBitwarden client apps (web, browser extension, desktop, and cli).项目地址: https://gitcode.com/GitHub_Trending/cl/clients

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

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

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

立即咨询