CHARLIE SAYS

查理如是说
DATE 2026-08-24
THEME
SERIES / ANGULAR / P-171 · Angular 高级教程

Angular 22+ 教程 03:环境搭建与 Getting Started(TS 6 / Node 22+)

工欲善其事,必先利其器。本文从零开始:确认环境、创建第一个 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 默认就是生产级产物。

系列导航

← 算法 003:线性表:链表 目录 设计模式 003:简单工厂(Simple Factory) →
← 返回文章列表