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 现代化改造给出的核心策略是两层递进:
- 自动迁移(Automated migrations)——使用 Angular CLI schematics 完成 standalone、控制流语法、signals 等机械化改造;
- 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 仓库特有的七项模式:
- 添加 OnPush 变更检测——所有组件设置
changeDetection: ChangeDetectionStrategy.OnPush。注意 OnPush 下原地修改数组/对象不会触发变更检测,必须创建新引用([...arr]、{...obj})或改用 signals; - 应用可见性修饰符——
protected用于模板访问的成员,private用于内部实现; - 将组件本地状态转换为 signals;
- 保留服务层的 Observables(不转换为 signals);
- 将业务逻辑抽取到服务中(薄组件);
- 正确组织类成员顺序;
- 为 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:fix、npm run prettier、npm run test:types与npm 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/@ViewChildrencomputed()—— 用于派生状态,优于模板中直接调用的函数(只在依赖变化时重算)
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: [] });仓库真实组件中即同时使用了toSignal、toObservable与takeUntilDestroyed,例如 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:fix与npm run test闭环验证。
【免费下载链接】clientsBitwarden client apps (web, browser extension, desktop, and cli).项目地址: https://gitcode.com/GitHub_Trending/cl/clients
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考