工欲善其事,必先利其器。本文从零开始:确认环境、创建第一个 v22 项目、读懂目录结构、跑起开发服务器,然后用一个计数器和一个待办清单把”组件 + 信号 + 新控制流 + @Service()“四件事串起来。完成本文,你就掌握了日常开发 80% 的动作。
前置条件:Node 22+ 与 TypeScript 6
Angular v22(2026 年 5 月发布)对运行环境的要求:
| 运行时 | 支持情况 |
|---|---|
| Node 22 | 推荐(LTS,最低要求) |
| Node 26 | 支持 |
| Node 20 | 已淘汰,CLI 会直接报错 |
先检查本机环境:
node -v # 应输出 v22.x.x 或更高
npm -v # 任意现代版本即可
TypeScript 6 不需要手动安装——ng new 生成的项目会把正确版本写进 package.json,首次 npm install 时自动就位。包管理器推荐 npm 或 pnpm,本文统一用 npm。
如果本机 Node 版本过低,用 nvm(或任意版本管理器)切到 22 即可。
创建项目
不需要全局安装任何东西,npx 直接用最新 v22 CLI:
npx @angular/cli@22 new tasks --defaults
cd tasks
不加 --defaults 时,CLI 会依次询问几个问题:
- 样式表格式:CSS / SCSS / Sass / LESS
- 是否启用 SSR(默认已包含增量 hydration)
- Zoneless 变更检测(v22 新项目默认开启)
--defaults 表示全部采用推荐值(CSS、按推荐决定 SSR 与 Zoneless)。常用定制项:
| 参数 | 作用 |
|---|---|
--style=scss | 使用 SCSS |
--ssr / --no-ssr | 明确开关服务端渲染 |
--skip-tests | 跳过生成 spec 文件 |
--skip-git | 不初始化 git 仓库 |
--package-manager=pnpm | 指定包管理器 |
如果长期做 Angular 开发,也可以全局安装:npm i -g @angular/cli,之后直接 ng new。但 npx 方式保证每次都用指定版本,团队协作更可控。
目录结构
生成的项目非常精简:
tasks/
├── angular.json # 工作区配置(第 5 篇详解)
├── package.json
├── tsconfig.json # TS 基础配置,严格模式
├── public/ # 静态资源,原样拷贝到产物
│ └── favicon.ico
└── src/
├── main.ts # 应用引导入口
├── index.html # 宿主页面
├── styles.css # 全局样式
└── app/
├── app.component.ts # 根组件
├── app.config.ts # 应用级配置(Provider)
└── app.routes.ts # 路由表
几个值得注意的点:
- 没有任何 NgModule——v19 起 standalone 是默认,v22 新项目里
app.module.ts不复存在。 - 没有环境文件目录,环境切换由构建配置承担(第 5 篇)。
public/取代了旧的src/assets/语义,构建时整体复制。
引导文件只有几行:
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig)
.catch(err => console.error(err));
app.config.ts 里集中放置应用级 Provider:
import { ApplicationConfig, provideZonelessChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideZonelessChangeDetection(),
provideRouter(routes),
],
};
两个细节:Zoneless 通过 provideZonelessChangeDetection() 显式提供(新项目模板默认包含);HttpClient 自 v21 起开箱即用,不再需要 provideHttpClient()。
启动开发服务器
npm start # 内部执行 ng serve
默认监听 http://localhost:4200。端口用环境变量覆盖最方便:
PORT=4300 ng serve
dev server 基于 Vite,冷启动秒级,改代码即时热更新。修改根组件 app.component.ts,浏览器会立刻反映——这就是日常开发的主循环。
计数器:组件 + 信号 + 新控制流
把根组件替换成一个计数器,感受 v22 的核心写法:
import { Component, computed, signal } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<section>
<h2>计数器</h2>
<p>当前值:{{ count() }}({{ parity() }})</p>
<div>
<button type="button" (click)="increment()">+1</button>
<button type="button" (click)="reset()">归零</button>
</div>
@if (count() === 0) {
<p>还没有开始计数</p>
} @else if (count() > 10) {
<p>已经超过 10 了</p>
} @else {
<p>继续</p>
}
</section>
`,
})
export class AppComponent {
count = signal(0);
parity = computed(() => (this.count() % 2 === 0 ? '偶数' : '奇数'));
increment() {
this.count.update(v => v + 1);
}
reset() {
this.count.set(0);
}
}
四个要点:
signal(0)创建可变响应式状态,读取必须调用:count();computed()声明派生状态,依赖变化自动重算。- 事件绑定
(click)="increment()"与插值、@if控制流都是模板原生能力,无需 import 任何指令。 - 不存在”手动刷新”概念:信号变了,视图就变——Zoneless 的日常体验就是如此朴素。
抽离状态:@Service()
组件不该囤积业务状态。v22 的服务写法是一行 @Service()(等价于过去的 @Injectable({ providedIn: 'root' })):
import { Service, computed, signal } from '@angular/core';
@Service()
export class CounterStore {
count = signal(0);
parity = computed(() => (this.count() % 2 === 0 ? '偶数' : '奇数'));
increment() { this.count.update(v => v + 1); }
decrement() { this.count.update(v => v - 1); }
}
组件用 inject() 拿到它,自己只剩”转接”职责:
import { Component, inject } from '@angular/core';
import { CounterStore } from './counter-store';
@Component({
selector: 'app-root',
template: `
<p>{{ store.count() }} 是 {{ store.parity() }}</p>
<button type="button" (click)="store.increment()">+</button>
`,
})
export class AppComponent {
protected readonly store = inject(CounterStore);
}
providedIn: 'root' 语义意味着全应用单例,任何组件注入的都是同一个实例。注意 @Service() 类内部注入依赖同样必须用 inject() 函数。
待办应用:@for / @empty / track
再进一步,把状态升级为列表,引入 @for。先写服务:
import { Service, computed, signal } from '@angular/core';
export interface Todo {
id: number;
title: string;
done: boolean;
}
@Service()
export class TodoStore {
private nextId = 1;
todos = signal<Todo[]>([]);
remaining = computed(() => this.todos().filter(t => !t.done).length);
add(title: string) {
const value = title.trim();
if (!value) return;
this.todos.update(list => [...list, { id: this.nextId++, title: value, done: false }]);
}
toggle(id: number) {
this.todos.update(list => list.map(t => (t.id === id ? { ...t, done: !t.done } : t)));
}
remove(id: number) {
this.todos.update(list => list.filter(t => t.id !== id));
}
}
模板用 @for 渲染,track 是必须的(替代旧 trackBy):
<section>
<h2>待办(还剩 {{ store.remaining() }} 项)</h2>
<input #box placeholder="要做什么?" (keyup.enter)="store.add(box.value); box.value = ''" />
<button type="button" (click)="store.add(box.value); box.value = ''">添加</button>
<ul>
@for (todo of store.todos(); track todo.id) {
<li>
<label>
<input type="checkbox" [checked]="todo.done" (change)="store.toggle(todo.id)" />
<span [class.done]="todo.done">{{ todo.title }}</span>
</label>
<button type="button" (click)="store.remove(todo.id)">删除</button>
</li>
} @empty {
<li class="empty">清单是空的,先加一条吧</li>
}
</ul>
</section>
值得留意的地方:
track todo.id让框架用稳定 id 做 diff,增删改时只挪动必要的 DOM 节点。@empty块在列表为空时渲染,天然替代了*ngIf="todos.length === 0"的惯用法。[class.done]单类开关、(keyup.enter)键盘伪事件,模板一章会系统展开;状态更新全部是”不可变替换”(update里返回新数组),与信号语义配合最佳。
生产构建
开发告一段落,看看产物:
ng build
- 默认即生产配置:压缩、AOT、输出哈希,无需额外参数。
- v22 构建器默认做精细的 chunk 拆分(initial + 按路由懒加载),不再需要手工调
splitChunks。 - 产物在
dist/tasks/browser/,是纯静态文件,扔到任何静态服务器或 CDN 即可。 - 超出体积预算(budgets)会直接报错或警告,配置细节在第 5 篇。
环境问题排查
新手最常撞见的几个坑:
Node.js version mismatch:Node 低于 22,CLI 拒绝执行,切版本即可。Port 4200 is already in use:换PORT=4300 ng serve。- 公司内网代理导致依赖安装失败:给 npm 配置 registry 与 proxy。
- 编辑器模板类型报红但构建通过:VS Code 的 Angular 语言服务扩展升到最新。
小结
- 环境:Node 22+(推荐 LTS)、TS 6 随项目自带、npx 免全局安装。
npx @angular/cli@22 new tasks --defaults一条命令拿到精简的 standalone 项目,没有 NgModule。ng serve+PORT环境变量,Vite dev server 秒级反馈。- 计数器 demo 串起
signal/computed/@if;待办 demo 串起@for track/@empty/ 事件绑定。 @Service()+inject()是 v22 抽离状态的标准姿势;ng build默认就是生产级产物。