指南 - 其他
基于 Angular 21.x 编写 —— 与生态集成、移动端、组件库、AngularJS 升级、Storybook
速查
- Monorepo:Nx(事实标准)/ Lerna / pnpm workspace
- 移动端:Ionic + Capacitor(推荐)/ NativeScript(已下滑)
- 桌面:Electron + Angular / Tauri + Angular
- UI 库:Angular Material(官方)/ PrimeNG / NG-ZORRO(Ant Design)/ Clarity(VMware)
- Tailwind:
ng add @ngneat/tailwind或手动配置 - UnoCSS:
ng add unocss(社区 schematic)/ 手动加 PostCSS - Storybook:
ng add @storybook/angular - 从 AngularJS(v1.x)升级:
@angular/upgrade双引擎共存 → 渐进式替换
Nx 集成(Monorepo)
Nx 是 Nrwl(Angular 早期核心团队成员创办)的 monorepo 工具,深度集成 Angular CLI。
创建 Nx 工作空间
pnpm dlx create-nx-workspace@latest my-org --preset=angular-standalone
# 或 multi-app preset
pnpm dlx create-nx-workspace@latest my-org --preset=angular-monorepomy-org/
├── apps/
│ ├── web/ # Angular 应用
│ └── admin/ # 另一个 Angular 应用
├── libs/
│ ├── ui/ # 共享组件库
│ ├── data-access/ # API services
│ └── feature-auth/ # 业务模块
├── nx.json # Nx 配置
└── package.jsonNx 关键命令
# 创建库
nx g @nx/angular:lib ui
# 创建组件到 lib
nx g @nx/angular:component button --project=ui --standalone
# 构建特定项目
nx build web
# 运行特定项目
nx serve web
# 影响图分析(只跑变更影响的项目)
nx affected:test
nx affected:lint
nx affected:build
# 可视化依赖图
nx graphTag-based Dependency Constraints
project.json 加 tag:
{ "tags": ["scope:web", "type:feature"] }eslint.config.js:
{
'@nx/enforce-module-boundaries': ['error', {
depConstraints: [
{ sourceTag: 'type:feature', onlyDependOnLibsWithTags: ['type:data-access', 'type:ui', 'type:util'] },
{ sourceTag: 'scope:web', onlyDependOnLibsWithTags: ['scope:web', 'scope:shared'] },
],
}],
}防止层级混乱(如 ui lib 误依赖 feature lib)。
Nx Cloud(增量缓存)
nx connect-to-nx-cloud构建 / 测试结果上传到 Nx Cloud,CI 命中缓存时秒级返回。免费 tier 够中小团队用。
Ionic(移动端 / 桌面)
Ionic 是基于 Web Components 的跨平台 UI 框架,原生支持 Angular。
创建 Ionic + Angular 项目
pnpm dlx @ionic/cli@latest start my-app blank --type=angular --capacitor
cd my-app
ionic serve # 在浏览器调试加 Capacitor 平台
Capacitor 是 Ionic 官方的原生桥(取代旧 Cordova):
ionic cap add ios
ionic cap add android
ionic cap sync # 同步 web 资源到原生项目
ionic cap run ios # 在 Xcode 跑
ionic cap run android使用 Ionic 组件
import { Component } from '@angular/core'
import { IonicModule } from '@ionic/angular/standalone'
import { addIcons } from 'ionicons'
import { home, settings } from 'ionicons/icons'
@Component({
selector: 'app-home',
imports: [IonicModule],
template: `
<ion-header>
<ion-toolbar>
<ion-title>Home</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
@for (item of items; track item) {
<ion-item>
<ion-icon name="home" slot="start" />
<ion-label>{{ item }}</ion-label>
</ion-item>
}
</ion-list>
<ion-fab vertical="bottom" horizontal="end">
<ion-fab-button (click)="add()">
<ion-icon name="add" />
</ion-fab-button>
</ion-fab>
</ion-content>
`,
})
export class Home {
items = ['Apple', 'Banana']
constructor() {
addIcons({ home, settings })
}
add() {
this.items.push(`Item ${this.items.length + 1}`)
}
}Ionic 7+ Standalone
Ionic 7 起组件是基于 Web Components 的 standalone 组件,不再依赖 NgModule。导入方式从 @ionic/angular 切到 @ionic/angular/standalone:
// app.config.ts
import { provideIonicAngular } from '@ionic/angular/standalone'
export const appConfig: ApplicationConfig = {
providers: [provideIonicAngular()],
}NativeScript + Angular(已下滑)
NativeScript 让 Angular 直接渲染原生 iOS / Android UI(非 WebView)。2020 年后被 React Native + Capacitor 蚕食,社区活跃度大幅下降,不推荐新项目用。
历史价值:早期想用 Angular 写真正原生 App 的唯一选择。
Tailwind CSS 集成
手动配置
pnpm add -D tailwindcss postcss autoprefixer
pnpm dlx tailwindcss init// tailwind.config.js
module.exports = {
content: ['./src/**/*.{html,ts}'],
theme: { extend: {} },
plugins: [],
}// src/styles.scss
@tailwind base;
@tailwind components;
@tailwind utilities;<button class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Click
</button>Tailwind 4(CSS-first 配置)
/* styles.css */
@import "tailwindcss";
@theme {
--color-primary: #dd0031;
}无需 tailwind.config.js。
Angular 17+ 默认 esbuild + Vite
旧 ng add 用的 schematic 是针对 webpack 的,新项目可以直接手装 Tailwind,esbuild 自动处理 PostCSS。
UnoCSS 集成
UnoCSS 是 Anthony Fu 的原子 CSS 引擎,兼容 Tailwind 语法但更快。
pnpm add -D unocss// uno.config.ts
import { defineConfig, presetWind, presetIcons } from 'unocss'
export default defineConfig({
presets: [
presetWind(),
presetIcons({ scale: 1.2 }),
],
content: {
pipeline: {
include: [/\.(html|ts)($|\?)/],
},
},
})// main.ts
import 'virtual:uno.css'需要让 Angular CLI 走 Vite(v17+ 默认)才能用 virtual:uno.css。esbuild builder 需要加 @unocss/postcss。
Storybook
ng add @storybook/angular会自动配置 .storybook/ 和 *.stories.ts 模板。
编写 story
// src/app/components/button.stories.ts
import type { Meta, StoryObj } from '@storybook/angular'
import { Button } from './button'
const meta: Meta<Button> = {
title: 'Components/Button',
component: Button,
argTypes: {
variant: { control: 'select', options: ['primary', 'secondary', 'danger'] },
disabled: { control: 'boolean' },
},
}
export default meta
type Story = StoryObj<Button>
export const Primary: Story = {
args: {
label: 'Primary',
variant: 'primary',
},
}
export const Secondary: Story = {
args: {
label: 'Secondary',
variant: 'secondary',
},
}
export const Disabled: Story = {
args: {
label: 'Disabled',
disabled: true,
},
}pnpm storybook # 启动 Storybook(默认 6006)
pnpm build-storybook # 构建静态站Angular Material
ng add @angular/material
# 交互式询问 Material 3 theme / typography / animations// app.config.ts(已自动加)
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'
export const appConfig: ApplicationConfig = {
providers: [provideAnimationsAsync()],
}使用组件(v17+ standalone)
import { Component } from '@angular/core'
import { MatButtonModule } from '@angular/material/button'
import { MatCardModule } from '@angular/material/card'
import { MatInputModule } from '@angular/material/input'
import { MatFormFieldModule } from '@angular/material/form-field'
@Component({
selector: 'app-login',
imports: [MatButtonModule, MatCardModule, MatInputModule, MatFormFieldModule],
template: `
<mat-card>
<mat-card-header>
<mat-card-title>Sign In</mat-card-title>
</mat-card-header>
<mat-card-content>
<mat-form-field appearance="outline">
<mat-label>Email</mat-label>
<input matInput type="email" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Password</mat-label>
<input matInput type="password" />
</mat-form-field>
</mat-card-content>
<mat-card-actions>
<button mat-flat-button color="primary">Login</button>
<button mat-button>Cancel</button>
</mat-card-actions>
</mat-card>
`,
})
export class Login {}Material 3(M3)支持
Angular Material v18+ 默认基于 Material Design 3,主题用 SCSS mixin:
@use '@angular/material' as mat;
html {
@include mat.theme((
color: (
primary: mat.$violet-palette,
tertiary: mat.$cyan-palette,
),
typography: Roboto,
density: 0,
));
}Angular CDK(无样式组件层)
Angular CDK 提供大量「无样式行为模块」:
@angular/cdk/overlay— Portal / Overlay@angular/cdk/drag-drop— 拖拽@angular/cdk/scrolling— 虚拟滚动@angular/cdk/a11y— 可访问性(focus trap / aria-live)@angular/cdk/clipboard— 剪贴板@angular/cdk/dialog— 对话框@angular/cdk/menu— 菜单@angular/cdk/testing— 测试 Harness
可以独立用(不依赖 Material 主题),是 React 生态 Radix UI / Headless UI 的对应物。
pnpm add @angular/cdk// 虚拟滚动示例
import { ScrollingModule } from '@angular/cdk/scrolling'
@Component({
imports: [ScrollingModule],
template: `
<cdk-virtual-scroll-viewport itemSize="50" style="height: 500px">
@for (item of items; track item.id) {
<div class="item">{{ item.name }}</div>
}
</cdk-virtual-scroll-viewport>
`,
})10000 条数据丝滑滚动。
PrimeNG
PrimeNG 是企业级 UI 库,组件数量最多(80+),覆盖 DataTable / TreeTable / 图表等高级场景。
pnpm add primeng// app.config.ts
import { providePrimeNG } from 'primeng/config'
import Aura from '@primeng/themes/aura'
export const appConfig: ApplicationConfig = {
providers: [
providePrimeNG({
theme: { preset: Aura, options: { darkModeSelector: '.dark' } },
}),
],
}// 使用
import { ButtonModule } from 'primeng/button'
import { TableModule } from 'primeng/table'
@Component({
imports: [ButtonModule, TableModule],
template: `
<p-table [value]="users" [paginator]="true" [rows]="10">
<ng-template pTemplate="header">
<tr>
<th>ID</th><th>Name</th><th>Email</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-user>
<tr>
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
</tr>
</ng-template>
</p-table>
<p-button label="Add" icon="pi pi-plus" />
`,
})NG-ZORRO(Ant Design for Angular)
NG-ZORRO 是阿里出品的 Angular UI 库,移植自 Ant Design。国内项目使用率最高之一。
ng add ng-zorro-antdimport { NzButtonModule } from 'ng-zorro-antd/button'
import { NzTableModule } from 'ng-zorro-antd/table'
@Component({
imports: [NzButtonModule, NzTableModule],
template: `
<button nz-button nzType="primary">Primary</button>
<nz-table #table [nzData]="users" nzPageSize="10">
<thead>
<tr><th>ID</th><th>Name</th></tr>
</thead>
<tbody>
@for (user of table.data; track user.id) {
<tr><td>{{ user.id }}</td><td>{{ user.name }}</td></tr>
}
</tbody>
</nz-table>
`,
})VMware Clarity
Clarity 是 VMware 的 Angular 优先 UI 库,针对企业级管理后台。社区相对小但设计严谨。
pnpm add @clr/angular @clr/ui @clr/icons从 AngularJS(v1.x)升级
如果你接手 AngularJS 1.x 老项目,直接重写到 Angular(v2+)是常见选择。但渐进式策略也可行:
@angular/upgrade 双引擎共存
pnpm add @angular/upgradeimport { downgradeComponent, UpgradeModule } from '@angular/upgrade/static'
// 1. 把 AngularJS module 包成 Angular 启动入口
@NgModule({
imports: [BrowserModule, UpgradeModule],
})
class AppModule {
ngDoBootstrap() {}
}
platformBrowserDynamic().bootstrapModule(AppModule).then(ref => {
const upgrade = ref.injector.get(UpgradeModule)
upgrade.bootstrap(document.body, ['legacyApp'])
})
// 2. 把 Angular 组件降级为 AngularJS directive
angular.module('legacyApp')
.directive('appNew', downgradeComponent({ component: NewCmp }))// 3. 把 AngularJS service 升级到 Angular DI
import { downgradeInjectable } from '@angular/upgrade/static'
angular.module('legacyApp')
.factory('NewService', downgradeInjectable(NewService))策略:
- 新功能用 Angular 写,通过
downgradeComponent嵌入老页面 - 老 service 逐个迁移到 Angular,
downgradeInjectable让 AngularJS 老代码继续可用 - 老页面逐个替换,最后移除 AngularJS
现实建议
AngularJS 1.x 已 EOL(2022.1),任何新功能都不再获得官方支持。如果项目规模 < 5 万行,建议直接重写:
- 工时通常比渐进迁移短(双引擎调试成本高)
- 类型安全 / 性能 / 工具链全面碾压旧版
- 一刀切重写时机会一并重构旧的架构债
服务器渲染(SSR)平台
Angular Universal(已合并到 @angular/ssr)
历史上 SSR 由独立的 @angular/universal 包提供。v17+ 起,SSR / SSG / Prerender 已经统一到 @angular/ssr,老的 @angular/universal 包标记为 deprecated。
ng add @angular/ssr 就是入口(详见 指南 - 高级 章节)。
Analog(社区元框架)
Analog 是 Brandon Roberts(Angular GDE)做的「Angular 的 Next.js」——基于 Vite,提供:
- 文件路由(
src/app/pages/自动生成 routes) - API 路由(
src/server/routes/) - Markdown SSG(适合写博客)
- 内置 Vitest
pnpm create analog@latest my-app适合纯 Angular 全栈站点 / 静态博客。生产采用率仍较低,但活跃维护。
桌面应用
Electron + Angular
ng new desktop-app
cd desktop-app
pnpm add -D electron electron-builder// electron/main.js
const { app, BrowserWindow } = require('electron')
const path = require('path')
function createWindow() {
const win = new BrowserWindow({ width: 1200, height: 800 })
win.loadFile(path.join(__dirname, '../dist/desktop-app/browser/index.html'))
}
app.whenReady().then(createWindow)Tauri + Angular
Tauri 用系统 WebView + Rust 后端,bundle 大小约为 Electron 的 1/10。
pnpm create tauri-app@latest my-app
# 选择 Angular template适合体积敏感的桌面工具。
浏览器扩展
直接用 Angular CLI 输出静态文件,配 manifest.json 即可作为 Chrome 扩展。
ng new ext --skip-tests --routing=false
ng build --configuration=production// manifest.json
{
"manifest_version": 3,
"name": "My Ext",
"version": "1.0",
"action": { "default_popup": "index.html" }
}复制 dist/ext/browser/ 内容到扩展目录加载即可。
Web Components 集成
详见 指南 - 高级 / Angular Elements。简言之:
import { createCustomElement } from '@angular/elements'
const el = createCustomElement(MyCmp, { injector })
customElements.define('my-cmp', el)任意 HTML / 别的框架(React / Vue / Svelte)都能用 <my-cmp /> 调用 Angular 组件。
表单库扩展
@ngneat/reactive-forms
更类型友好的 reactive form 替代品(v14 之前流行)。Angular 14 起官方 Typed Forms 已经能力足够,新项目不再需要。
@ngneat/forms-manager
跨组件 / 路由的表单状态持久化(草稿保存 / 跨页面恢复)。
formly
Formly 是 schema-driven 表单库——用 JSON 配置生成完整表单:
fields: FormlyFieldConfig[] = [
{
key: 'name',
type: 'input',
props: { label: 'Name', required: true },
},
{
key: 'email',
type: 'input',
props: { label: 'Email', type: 'email', required: true },
},
]适合后台管理系统(大量类似的 CRUD 表单)。
图表
NGX-Charts
ngx-charts 基于 D3.js 的 Angular 包装。响应式 / 主题化 / 服务端友好。
ECharts + ngx-echarts
pnpm add echarts ngx-echartsimport { NgxEchartsModule } from 'ngx-echarts'
@Component({
imports: [NgxEchartsModule],
template: `
<div echarts [options]="chartOptions" style="height: 400px"></div>
`,
})
export class Chart {
chartOptions = {
xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed'] },
yAxis: { type: 'value' },
series: [{ type: 'bar', data: [10, 22, 33] }],
}
}Apache ECharts 在国内非常流行,组件库生态最丰富。
Plotly / Chart.js / D3 直接用
pnpm add chart.jsimport { afterNextRender } from '@angular/core'
import Chart from 'chart.js/auto'
@Component({ /* ... */ })
export class ChartCmp {
constructor() {
afterNextRender(() => {
new Chart(this.canvas.nativeElement, { /* ... */ })
})
}
}afterNextRender 保证 SSR 安全。
动画系统
@angular/animations(经典)
pnpm add @angular/animationsimport { trigger, state, style, transition, animate } from '@angular/animations'
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'
// app.config.ts
providers: [provideAnimationsAsync()]@Component({
animations: [
trigger('openClose', [
state('open', style({ height: '200px', opacity: 1 })),
state('closed', style({ height: 0, opacity: 0 })),
transition('open <=> closed', animate('300ms ease-in-out')),
]),
],
template: `
<button (click)="open = !open">Toggle</button>
<div [@openClose]="open ? 'open' : 'closed'">Content</div>
`,
})Angular 动画系统在缩水
官方 v18+ 已经在轻量化动画系统(provideAnimationsAsync() 替代旧 provideAnimations()),逐步推荐用:
- CSS 动画 /
@starting-style(现代浏览器原生) - View Transitions API(路由切换:
withViewTransitions()) - Web Animations API(细粒度控制)
E2E 测试框架
Cypress
ng add @cypress/schematic// cypress/e2e/home.cy.ts
describe('Home', () => {
it('shows counter', () => {
cy.visit('/')
cy.contains('Count: 0')
cy.get('button').contains('+1').click()
cy.contains('Count: 1')
})
})Playwright
pnpm add -D @playwright/test
pnpm dlx playwright install// e2e/home.spec.ts
import { test, expect } from '@playwright/test'
test('shows counter', async ({ page }) => {
await page.goto('http://localhost:4200/')
await expect(page.getByText('Count: 0')).toBeVisible()
await page.getByRole('button', { name: '+1' }).click()
await expect(page.getByText('Count: 1')).toBeVisible()
})Playwright 速度 / 跨浏览器 / Debug 体验都比 Cypress 好,社区采用率快速上升。
Lint / 格式化
ESLint
ng add @angular-eslint/schematicsng lint # 跑 lint
ng lint --fix # 自动修@angular-eslint 提供 Angular 特定规则:
@angular-eslint/no-empty-lifecycle-method@angular-eslint/use-pipe-transform-interface@angular-eslint/template/no-distracting-elements
Prettier
pnpm add -D prettier eslint-config-prettier// .prettierrc
{
"semi": false,
"singleQuote": true,
"trailingComma": "all"
}pnpm dlx prettier --write src/CI / CD
GitHub Actions
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with: { version: 10 }
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm ng lint
- run: pnpm ng test --watch=false --browsers=ChromeHeadless
- run: pnpm ng buildNx Affected(monorepo)
- run: pnpm nx affected:test --base=origin/main --head=HEAD
- run: pnpm nx affected:build --base=origin/main --head=HEAD只跑变更影响的项目,CI 时间显著降低。
自定义 Schematics
pnpm dlx @angular-devkit/schematics-cli blank --name=my-schematic
cd my-schematic// src/my-feature/index.ts
import { Rule, Tree } from '@angular-devkit/schematics'
export function myFeature(options: { name: string }): Rule {
return (tree: Tree) => {
tree.create(`${options.name}.ts`, `export const ${options.name} = 1\n`)
return tree
}
}// src/collection.json
{
"schematics": {
"my-feature": {
"factory": "./my-feature/index#myFeature",
"schema": "./my-feature/schema.json"
}
}
}ng add ./my-schematic
ng generate my-schematic:my-feature --name=foo公司内可以做出标准化的「新增页面」/「新增服务」schematic,结合 lint 强制团队风格统一。
TypeScript 配置(高级)
tsconfig.json 推荐设置:
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"strict": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noPropertyAccessFromIndexSignature": true,
"useDefineForClassFields": false, // Angular 必须 false(与装饰器冲突)
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"isolatedModules": true,
"skipLibCheck": true
},
"angularCompilerOptions": {
"strictTemplates": true,
"strictInputAccessModifiers": true,
"strictDomEventTypes": true,
"strictNullInputTypes": true
}
}一份 Nx + Angular Material + Storybook 完整模板
推荐起手:
pnpm dlx create-nx-workspace@latest acme \
--preset=angular-monorepo \
--appName=web \
--style=scss \
--bundler=esbuild \
--ssr=false \
--pm=pnpm
cd acme
nx g @nx/angular:lib ui --standalone
nx g @nx/angular:lib data-access --standalone
ng add @angular/material
ng add @storybook/angular --project=ui
nx serve web这样会得到:
apps/web— 主应用libs/ui— 共享组件库 + Storybooklibs/data-access— API 服务nx.json— 构建任务编排- Material 3 + esbuild + Vitest 全套
下一步
- API 速查见 参考