Angular 开发者技能指南:现代 Angular(v20+/v22+)最佳实践与 Agent 落地规范
Angular 开发者技能指南现代 Angularv20/v22最佳实践与 Agent 落地规范【免费下载链接】angularDeliver web apps with confidence 项目地址: https://gitcode.com/GitHub_Trending/an/angular本篇技术指南以本仓库中面向 AI 编码代理Agent的 Angular Developer Skill 技能定义为骨架围绕现代 Angular 开发体系展开从版本决策、ng new执行规则到信号驱动响应式、Signal Forms、组件与输入输出、依赖注入、路由、可访问性、测试与 CLI 工具链。读完本文你将掌握一套可复制的 Angular v20 编码规范与验证流程既能直接指导人工编码也能作为让 Agent 稳定产出合规、可构建代码的操作手册。文中给出的源码证据均来自当前仓库版本见 package.json 的22.2.0-next.4。一、技能定义概述Agent 与开发者共享的编码总纲SKILL.md 的开头以 YAML frontmatter 声明了该技能的能力边界name: angular-developer description: Generates Angular code and provides architectural guidance. Trigger when creating projects, components, services, or HTTP communication, or for best practices on reactivity (signals, linkedSignal, resource, httpResource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling, testing, naming conventions, or CLI tooling. license: MIT metadata: author: Copyright 2026 Google LLC version: 1.0触发场景覆盖新建项目、组件/服务/HTTP 通信、响应式signals、linkedSignal、resource、httpResource、表单、依赖注入、路由、SSR、ARIA、动画、样式、测试、命名规范与 CLI 工具。三条全局铁律是所有后续章节的前提先分析版本再给建议Angular 各版本最佳实践与可用特性差异很大。用 Angular CLI 新建项目时除非用户指定版本否则不要指定版本号。生成代码遵守官方风格指南与最佳实践并使用 CLI 脚手架组件、服务、指令、管道与路由保证一致性。生成代码后必须运行ng build验证无编译错误若报错需分析并修复后再继续绝不能跳过此步——这是保证产出可用的关键闭环。二、新建项目ng new的严格三步决策流程当需要创建新 Angular 项目而用户未提供额外指引时采用下列默认规则除非用户另有说明使用最新稳定版 Angular。新项目表单统一使用Signal FormsAngular v22 起稳定详细用法见 signal-forms.md。SKILL.md 为执行ng new规定了精确的分支决策流程核心是显式版本 本地安装 npx 拉取最新Step 1检查用户是否显式指定版本。若用户要求特定版本如 Angular 15绕过本地安装、强制使用npxnpx angular/clirequested_version new project-nameStep 2检查是否已有本地 Angular 安装。若未指定版本先在终端运行ng version探测命令成功并返回已安装版本 → 直接使用本地/全局安装ng new project-nameStep 3回退到最新版。若未指定版本且ng version失败说明系统未安装 Angular→ 使用npx拉取最新版npx angular/clilatest new project-name这套流程的价值在于避免 Agent 无脑npm install -g污染环境、避免擅自固定旧版本也避免在有全局 CLI 时重复下载。它把探测环境 → 选择执行路径显式化是 Agent 编码稳定性的第一道闸门。三、组件开发规范与模板控制流组件是应用的基本构件。每个组件由承载行为的 TypeScript 类、HTML 模板与 CSS 选择器三部分组成详见 components.md。3.1 组件定义与元数据选项Component({ selector: app-profile, template: img srcprofile.jpg altProfile photo / button (click)save()Save/button , styles: img { border-radius: 50%; }, }) export class Profile { save() {} }元数据关键项selector模板中标识组件的 CSS 选择器、template/templateUrl内联或外链 HTML、styles/styleUrl/styleUrls内联或外链样式、imports列出模板所用组件/指令/管道。使用子组件需先将其加入消费方组件的imports。3.2 自闭合标签与内置控制流自闭合标签规则组件无投影内容或子节点时必须使用自闭合形式app-profile / app-user-card [user]currentUser() / router-outlet /if条件渲染支持else if/else并可用as别名复用表达式结果if (user.settings(); as settings) { pTheme: {{ settings.theme }}/p }for循环的track表达式为必需项性能与 DOM 复用隐式变量包括$index、$count、$first、$last、$even、$oddul for (item of items(); track item.id; let i $index, total $count) { li{{ i 1 }}/{{ total }}: {{ item.name }}/li } empty { liNo items to display./li } /ulswitch使用严格相等且无 fallthrough可用default never;实现联合类型的穷尽性检查新增未处理分支时直接编译报错switch (state) { case (on) { ... } case (off) { ... } default never; // 若新增 standby 等分支未处理则报错 }3.3 组件输入与输出现代 Angular 推荐基于信号的input()API见 inputs.mdComponent({ selector: app-user, template: p{{ label() }} ({{ age() }})/p, }) export class User { readonly name input(Guest); // 可选输入带默认值 readonly age input.requirednumber(); // 必填输入编译期校验 protected readonly label computed(() Name: ${this.name()}); }input()支持alias模板属性别名与transform入值预处理如内置booleanAttribute。model()创建支持双向绑定的输入custom-counter [(value)]mySignal /readonly value model(0); increment() { this.value.update((v) v 1); }旧式Input()仍受支持但新代码不推荐input.required()能把缺参错误提前到编译期输入转换函数必须纯净且可静态分析避免使用与 DOM 标准属性id、title冲突的输入名。信号化输出与自定义事件、宿主元素绑定的最佳实践分别见 outputs.md 与 host-elements.md。3.4 v20 命名规范Intent over Role命名规范 强调现代命名哲学Intent over Role但必须先尊重现有工程配置优先既有约定生成/重构前先检查相邻文件、angular.jsonschematics 配置与 ESLint 规则项目依赖.component.ts后缀时不要强行去后缀。去除角色后缀仅限现代项目v20 新工程中文件名去掉.component.ts/.service.ts/.directive.ts类名去掉Component/Service/Directive按意图命名如auth.ts、auth-data.ts、user-data-client.ts。模型例外接口/数据模型保留.model.ts后缀以声明类型契约user.model.ts中的interface User。文件/标识符一致product-list.ts对应class ProductList拆分的product-list.html/product-list.css与主文件同名测试文件为product-list.spec.ts。目录即上下文用core/、features/、shared/目录层级承担角色信息如features/profile/profile.ts类Profile、shared/components/button/button.ts类Button。避免命名冲突user.ts组件与user.model.ts模型都声明User时会冲突此时为组件取更具体的意图名class UserProfileinuser-profile.ts模型保留简单域名interface User。风格一致性不要在同一 feature 目录内混用新旧风格不确定时以传统角色后缀为最安全默认。四、响应式与数据管理Signals 体系signals-overview.md 定义了响应式基石signal是值的包装器变化时通知感兴趣的消费者。4.1 可写信号与只读暴露import {signal} from angular/core; const count signal(0); count.set(3); // 直接赋值 count.update((value) value 1); // 基于旧值更新服务向外暴露状态的最佳实践是防外部篡改private readonly _count signal(0); readonly count this._count.asReadonly(); // 消费方可读不可写4.2 computed 的三大特性computed()派生只读信号具备惰性求值读取时才运行派生函数、记忆化结果缓存、依赖变化才重算、动态依赖只追踪派生过程中实际读到的信号。4.3 响应式上下文与 untrackedAngular 在求值以下场景时自动进入响应式上下文computed信号、effect回调、linkedSignal计算、组件模板。若需在上下文内读取信号但不建立依赖避免随其重跑用untracked()effect(() { // 仅 currentUser 变化时触发counter 虽被读取但不触发 console.log(User: ${currentUser()}, Count: ${untracked(counter)}); });异步边界铁律响应式上下文仅对同步代码生效await之后的信号读取不会被追踪。因此必须在await之前完成信号读取// ❌ theme() 在 await 之后读取不被追踪 effect(async () { const data await fetchUserData(); console.log(theme()); }); // ✅ 在 await 前先读信号 effect(async () { const currentTheme theme(); const data await fetchUserData(); console.log(currentTheme); });4.4 依赖状态、异步资源与副作用linkedSignal创建与源信号绑定的可写状态见 linked-signal.md。resource把异步数据直接拉入信号状态见 resource.md。effect用于日志、第三方 DOM 操作afterRenderEffect等同时明确何时不应使用 effect见 effects.md。五、HTTP 通信与后端通信时使用 Angular HTTP API详见 http-client.mdprovideHttpClient负责以 provider 形式注册 HTTP 能力HttpClient承担请求执行拦截器interceptors横切请求/响应httpResource则把 HTTP 请求封装为响应式资源与信号体系无缝衔接。六、表单策略优先 Signal Forms这是本技能强调最重、约束最细的领域。新版应用优先 Signal Formsv22 稳定angular/forms/signals老应用或既有表单则沿用其当前策略。传统响应式表单、模板驱动表单分别见 reactive-forms.md 与 template-driven-forms.md。Signal Forms 详情见 signal-forms.md。6.1 核心理念与导入Signal Forms 是响应式、类型安全、模型驱动的表单方案表单结构直接由 signal 模型推导不再有FormControl/FormGroup/FormArray/FormBuilder也没有 builder且FormState类型不存在用FieldState。模型字段严禁使用null字符串用、数字用0、数组用[]。import {Component, signal} from angular/core; import { form, FormField, submit, disabled, hidden, readonly, debounce, applyWhen, applyEach, schema, validate, validateHttp, validateStandardSchema, metadata, } from angular/forms/signals;6.2 创建表单与校验规则Component({ imports: [FormField], }) export class Example { protected readonly userModel signal({ name: , email: , age: 0, address: {street: , city: }, hobbies: [] as string[], }); protected readonly userForm form(this.userModel, (schemaPath) { required(schemaPath.name, {message: Name is required}); // 仅 required 支持条件式 when 选项 required(schemaPath.name, { when({valueOf}) { return valueOf(schemaPath.age) 10; }, }); email(schemaPath.email, {message: Invalid email}); min(schemaPath.age, 18); max(schemaPath.age, 100); minLength(schemaPath.password, 8); maxLength(schemaPath.description, 500); pattern(schemaPath.zipCode, /^\d{5}$/); // when 只对 required 生效 }); }仓库佐证该 API 的模块边界可从 packages/forms/signals/index.ts 与 packages/forms/signals/public_api.ts 确认实现源码位于 packages/forms/signals/src配套测试在 packages/forms/signals/test。若需构建级验证可在任意版本 ≥ v22 的工程中通过ng build或ng test复现本文示例。6.3 FormField 与 FieldState必须先调用字段FormField是结构FieldState是数据/信号。必须把字段当函数调用才能取得其状态信号valid、touched、dirty、hidden等const f form(signal({cat: {name: pirojok-the-cat, age: 5}})); f.cat.name; // FormField拿不到任何状态旗标 f.cat.name.touched(); // ERRORFormField 上没有 touched() f.cat.name(); // FieldState调用后才能访问信号 f.cat.name().touched(); // VALID f.cat().name.touched(); // ERRORf.cat() 是状态没有子级模板同理bookingForm.hotelDetails().hidden()正确bookingForm.hotelDetails.hidden()报类型错。6.4 disabled / readonly / hidden 规则与绑定禁项字段可用性由 schema 规则控制disabled(schemaPath.password, {when: ({valueOf}) !valueOf(schemaPath.createAccount)}); hidden(schemaPath.shippingAddress, {when: ({valueOf}) valueOf(schemaPath.sameAsBilling}}); // 不删模型数据仅标记 hidden readonly(schemaPath.username);[formField]指令的禁止属性CRITICAL使用[formField]时不得在模板中设置静态或绑定min/max/[attr.min]/[attr.max]用 schema 校验替代、文本/数字/日期输入上的value/[value]/[attr.value]、[disabled]/[readonly]均由[formField]处理、不要绑定name。例外input typeradio与input typecheckbox上的静态value允许且必需——它标识该输入代表哪个选项而非字段值。input typeradio valueeconomy [formField]bookingForm.package.tier / !-- 对 -- input [value]someVar [formField]form.name / !-- 错 -- select [formField]userForm.countryoption valueusUS/option/select !-- 对 --注意checkbox 仅能绑定boolean字段数组字段如string[]在模板中应使用select multiple或把模型拆成多个 boolean 字段。6.5 访问状态、提交与错误处理const value this.userForm().value(); // 整个表单的值表单根也要调用 const isValid this.userForm().valid(); const errors this.userForm().errors(); // ValidationError[]{kind, message?} const isDirty this.userForm().dirty(); const isDisabled this.userForm().disabled();状态访问速记form().invalid()、form.field().dirty()、form.field.subfield().touched()、form.a.b.c.d().value()。唯一例外是 lengthform.children.length、form.length无括号、form.client.addresses.length模板for (income of form.addresses; track $index)。提交必须使用submit()它会在执行动作前自动把所有字段标记为 touched且回调必须是 async 并返回 Promiseimport {submit} from angular/forms/signals; onSubmit() { submit(this.userForm, async () { // async 关键字必须 await this.apiService.save(this.userModel()); // 仅在表单有效时执行 }); }错误对象为ValidationError { readonly kind: string; readonly message?: string }。自定义校验器不得返回null无错误时返回undefined。6.6 规则上下文与路径不是信号传给validate()、disabled()、applyWhen的函数接收上下文对象validate(schemaPath.username, ({ value, // SignalT本字段当前值 fieldTree, // FieldTreeT子字段group/array 时 state, // FieldStateTstate.valid()/state.dirty() 等 valueOf, // (path) T读取他字段的值会追踪依赖 stateOf, // (path) FieldState读取他字段的 valid/dirty pathKeys, // Signalstring[]从根到本字段的路径 }) { if (state.touched()) { ... } // 正确上下文里没有裸 touched() if (value() admin) return {kind: reserved, message: Username admin is reserved}; });schemaPath 及其子路径不是信号、不可调用跨字段读取必须走valueOf/stateOf// WRONG: applyWhen(p.ssn, () p.ssn().touched(), ...) ← p.ssn() 会抛错 applyWhen(p.ssn, ({stateOf}) stateOf(p.ssn).touched(), (ssnField) { ... }); // WRONG: applyWhen(isJoint, () {...}) ← 需要 3 个参数 applyWhen(s.spouse, ({valueOf}) valueOf(s.status) joint, (spousePath) { ... });6.7 数组与嵌套循环applyEach逐项应用规则回调只接收一个参数条目路径绝无 indexapplyEach(s.items, (item) { // 单个参数 required(item.name); });移除数组项 从数据模型中移除对应元素。嵌套for没有$parent外循环用let outerIndex $index存下标。提交按钮禁用态button [disabled]bookingForm().invalid()Submit/button。6.8 异步校验 validateAsync 与 debounce异步校验必须使用validateAsync()而非validate()三条硬性约束validateAsync(s.username, { params: ({value}) value(), // 1. params 必须是函数 factory: (username) resource({ params: username, // 2. resource() 用 params 传输入信号不是 request loader: async ({params: value}) { await new Promise((r) setTimeout(r, 1000)); return value taken; }, }), onSuccess: (isTaken) (isTaken ? {kind: taken, message: Username is already taken} : undefined), onError: () ({kind: error, message: Validation failed}), // 3. onError 必填 });debounce(s.username, 300)可将 UI 与模型间的同步延迟 300ms。6.9 高频错误对照表与排错错误场景WRONGRIGHT访问状态form.field.valid()form.field().valid()表单根旗标form.invalid()form().invalid()修改值form.field.set(x)更新模型信号this.model.update(...)规则上下文({touched}) touched()({state}) state.touched()调用 schemaPathapplyWhen(p.foo, () p.foo()x)applyWhen(p.foo, ({valueOf}) valueOf(p.foo)x)applyWhen 参数applyWhen(condition, (){})applyWhen(path, condition, schemaFn)三参数组长度form.items().lengthform.items.length结构性min/max 属性input min1 max10schema 中用min()/max()规则when 选项pattern(p.x, /.../, {when})when仅required()可用其余用applyWhen提交回调submit(form, () {...})submit(form, async () {...})异步 paramsparams: s.fieldparams: ({value}) value()validateAsync省略onErroronError必填resource() APIrequest: signalparams: signalapplyEachapplyEach(s.items, (item, index){})applyEach(s.items, (item){})单参数嵌套 for$parent.$indexlet outerIndex $index模型含 nullsignal({name: null})/0/[]典型编译错误修复详见 signal-forms.md 底部Property value does not exist on type FieldTree字段先调用再取.value()。Property set does not existSignal Forms 模型驱动改为更新模型 signal。Type string[] is not assignable to type string数组字段配单选select错误改用select multiple。NG8022: Setting the readonly/min/max/value attribute is not allowed与[formField]冲突改用 schema 规则。TS2322 ... booleancheckbox 绑定到数组字段改为 boolean 字段或select multiple。No pipe found with name number/json/date组件内用computed(() this.totalPrice().toFixed(2))格式化。6.10 完整实战星际订票表单signal-forms.md 给出了一个跨章节的大例子核心逻辑src/app/app.tsComponent({ selector: app-root, imports: [FormField], templateUrl: ./app.html, }) export class App { protected readonly model signal({ personalInfo: {firstName: , lastName: , email: , age: 0}, tripDetails: {destination: Mars, launchDate: }, package: {tier: economy, extras: [] as string[]}, companions: [] as Array{name: string; relation: string}, }); protected readonly bookingForm form(this.model, (s) { required(s.personalInfo.firstName, {message: First name is required}); email(s.personalInfo.email, {message: Invalid email address}); min(s.personalInfo.age, 18, {message: Must be at least 18}); validate(s.tripDetails.launchDate, ({value}) { const date new Date(value()); if (isNaN(date.getTime())) return undefined; return date new Date() ? {kind: pastData, message: Launch date must be in the future} : undefined; }); hidden(s.package.extras, {when: ({valueOf}) valueOf(s.package.tier) economy}); applyEach(s.companions, (companion) { required(companion.name, {message: Companion name required}); }); }); addCompanion() { this.model.update((m) ({...m, companions: [...m.companions, {name: , relation: }]})); } removeCompanion(index: number) { this.model.update((m) ({...m, companions: m.companions.filter((_, i) i ! index)})); } onSubmit() { submit(this.bookingForm, async () console.log(Booking Confirmed:, this.model())); } }模板侧src/app/app.html要点错误提示统一走bookingForm.xxx().touched() bookingForm.xxx().errors().length后取errors()[0].messageradio 组用静态value标注选项extras 区域按!bookingForm.package.extras().hidden()条件渲染、数组用select multiple动态同伴列表for (companion of bookingForm.companions; track $index)配[formField]companion.name。七、依赖注入DI 部分分为五个层面di-fundamentals.md、creating-services.md、defining-providers.md、injection-context.md、hierarchical-injectors.md基础DI 全貌、service 与inject()函数。创建与使用服务providedIn: root、注入组件或其他服务。Provider 定义自动 vs 手动提供、InjectionToken、useClass/useValue/useFactory与作用域。注入上下文inject()允许的位置、runInInjectionContext、assertInInjectionContext。层级注入器EnvironmentInjectorvsElementInjector、解析规则、optional/skipSelf修饰符、providersvsviewProviders。八、管道管道规范见 pipes.md内置管道需显式导入模板中优先使用管道模板之外不要为了调transform()而注入管道类应复用独立的格式化函数或抽取的纯函数同时区分纯管道与不纯管道的实现。九、可访问的 ARIA 组件构建无障碍自定义组件时angular-aria.md支持 Accordion、Listbox、Combobox、Menu、Tabs、Toolbar、Tree、Grid 等无头headless、可访问模式并对 ARIA 属性进行样式化处理把可访问性内建到组件结构与交互语义中。十、路由路由八类场景均配有专门参考定义路由URL 路径、静态 vs 动态段、通配符与重定向define-routes.md。加载策略急切 vs 惰性加载与上下文感知加载loading-strategies.md。出口router-outlet、嵌套与命名出口show-routes-with-outlets.md。导航声明式RouterLinkvs 编程式Routernavigate-to-routes.md。守卫CanActivate、CanMatch等访问控制route-guards.md。数据预取ResolveFndata-resolvers.md。生命周期与事件导航事件时序与调试router-lifecycle.md。渲染策略CSR、SSG预渲染、带水合的 SSRrendering-strategies.md。路由过渡动画基于 View Transitions APIroute-animations.md。十一、样式与动画Tailwind CSS 集成见 tailwind-css.md。动画取舍推荐原生 CSS 动画仅动态效果复杂场景再考虑旧式 DSLangular-animations.md。组件样式样式与视图封装View Encapsulation最佳实践component-styling.md。十二、测试测试四类规范单测基础以 Vitest 为主推荐的单测最佳实践、异步模式与TestBedtesting-fundamentals.md。组件 Harness通过标准 harness 模式做稳健的组件交互component-harnesses.md。路由测试用RouterTestingHarness获得可靠的导航测试router-testing.md。E2E搭建与执行端到端测试e2e-testing.md。十三、工具链13.1 Angular CLIcli.md依赖管理Angular 库一律用ng add而非npm install因为前者会执行初始化 schematics如改写angular.json、更新根 providersng add angular/material ng add tailwindcss ng add angular/fire ng update angular/corelatest angular/clilatest # 自动跑代码迁移代码生成速查表目标命令说明组件ng g c path/to/name-s内联样式、-t内联模板服务ng g s path/to/name生成服务指令ng g d path/to/name生成指令管道ng g p path/to/name生成管道守卫ng g g path/to/name函数式路由守卫环境ng g environments生成src/environments/并配置 file replacements注意没有生成单个路由定义的命令应先生成组件再手工把路由加入app.routes.ts的Routes数组。开发服务器与代理ng serve提供 HMR。后端代理新建src/proxy.conf.json{ /api/**: {target: http://localhost:3000, secure: false} }并在angular.json的 serve target 加入options: {proxyConfig: src/proxy.conf.json}。构建与部署ng build输出到dist/project-name/browser现代 Angular 使用 esbuild 系angular/build:applicationbuilder默认走生产配置AOT、压缩、tree-shaking。ng build --configurationstaging可切配置。测试用ng test、ng e2e未配置 E2E 框架时会提示安装 Cypress/Playwright/Puppeteer 等。部署先ng add angular/fire加 builder再ng deploy。13.2 现代化与 MCP代码现代化用官方迁移migrations把旧代码自动重构到现代标准migrations.md。Angular MCP Server可用的工具、配置与实验特性mcp.md。环境配置构建期与运行期配置策略environment-configuration.md。十四、从技能到仓库如何持续深入技能主入口 SKILL.md 是一个决策路由表其兄弟目录skills/dev-skills/angular-new-app面向新建应用场景。40 篇细则文档集中在 skills/dev-skills/angular-developer/references 目录覆盖本章节引用的全部主题是逐项深入的第一手材料。需要框架级证据时可对照真实实现源码例如 Signal Forms 的模块与公共 API 见 packages/forms/signals/index.ts 与 packages/forms/signals/public_api.ts实现与测试分别位于 packages/forms/signals/src 与 packages/forms/signals/test核心框架 APIsignal、computed、effect、input、model等的实现见 packages/core/src组件信号输入/输出等新特性的测试集中在 packages/core/test。版本口径以仓库根 package.json22.2.0-next.4与各包 packages/forms/package.json、packages/core/package.json 为准文中关于 v20/v21/v22 的稳定性表述请结合目标工程实际安装版本验证。结语一套可执行的现代 Angular 编码契约本技能定义的真正价值不在于罗列 API而在于把正确做事固化为决策流程先探测版本再选命令、能 scaffold 绝不手写、写完必须ng build验证、新表单一律 Signal Forms、响应式统一走信号体系、命名尊重既有工程。无论你是手工编码的工程师还是接入 Agent 的团队都可以把 SKILL.md 及其 references 作为团队级的编码契约配合本仓库中的 packages/core、packages/forms/signals 等实现与测试源码做到规范有出处、示例可运行、结论可验证。【免费下载链接】angularDeliver web apps with confidence 项目地址: https://gitcode.com/GitHub_Trending/an/angular创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考