Angular基础速通

single

single可以理解为是 angular 的 响应式状态管理,类比 react 的 hooks

特性 Angular Signal React Hooks
创建 signal(initialValue) useState(initialValue)
读取 signal() 调用 state 变量
更新 signal.set(newValue) setState(newValue)
更新(基于旧值) signal.update(old => old + 1) setState(old => old + 1)
衍生值 computed(() => ...) useMemo(() => ..., [deps])
副作用 effect(() => ...) useEffect(() => ..., [deps])

computed

类比 react 的 useMemo,自动缓存,single变化时重新计算

app.ts 复制代码
import { signal, computed } from '@angular/core';

@Component({...})
export class CartComponent {
  price = signal(100);
  quantity = signal(2);
  
  // 自动缓存,依赖变化时重新计算
  total = computed(() => this.price() * this.quantity());
}

上面代码中的 total 是计算Signal,是只读的,当price和quantity变化时重新计算

effect

类比 react 的 useEffect, single 变化时执行

ts 复制代码
import { effect } from '@angular/core';

@Component({...})
export class LoggerComponent {
  count = signal(0);
  
  constructor() {
    // 自动追踪依赖,变化时执行
    effect(() => {
      console.log('Count changed:', this.count());
    });
  }
}

但是很少会使用effect。避免副作用用来传播状态更改。

组件

组件结构

Angular组件分为

  • TypeScript 类
  • HTML 模板
  • CSS 样式
user-profile.ts 复制代码
@Component({
  // 可以理解为组件的名字
  selector: 'user-profile',
  // HTML模板
  template: `
    <h1>User {{ userName }}</h1>
    <p>This is the user profile page</p>
  `,
  // CSS
  styles: `h1 { font-size: 3em; } `,
})

// TypeScript类,放数据与逻辑的地方
export class UserProfile {
   userName = 'Jolyne'
}

当然,前端开发需要遵循组件化、模块化的开发,所以可以将 html / css 的部分抽离到单独的文件中去。html通过templateUrl 指定HTML模块,css通过styleUrl指定CSS模块。

user-pofile.ts 复制代码
@Component({
  selector: 'user-profile',
  templateUrl: 'user-profile.html',
  styleUrl: 'user-profile.css',
})
export class UserProfile {
  
}
user-pofile.html 复制代码
<h1>User profile</h1>
<p>This is the user profile page</p>
user-profile.css 复制代码
h1 { font-size: 3em;}

组合组件

实际项目中,一个组件可能由多个子组件构成。那么可以通过 templateimports 来组合组件

componentA.ts 复制代码
@Component({
  selector: 'componentA',
  template: `
    Username: {{ username }}
  `,
})
export class ComponentA {
  username = 'Jolyne';
}
componentB.ts 复制代码
@Component({
  selector: 'componentB',
  template: `
    age: {{ age }}
  `,
})
export class ComponentB {
  age = '26';
}
main.ts 复制代码
import {ComponentA} from 'ComponentA.ts';
import {ComponentB} from 'ComponentB.ts';

@Component({
  selector: 'main',
  imports: [ComponentA, ComponentB],
  template: `
    <div>
       <componentA />
       <componentB />
    </div>
  `,
})
export class Main {

}

注意:使用组件时,用的是 selector (名字),imports里面引入的是 Typescript类

组件输入

组件输入类似 React 或者 Vue 的 props。也就是父组件向子组件通信

input()

angular里面通过 input() 这个函数来定义的。input()返回一个InputSignal

app.ts 复制代码
import {Component} from '@angular/core';
import {User} from './user';

@Component({
  selector: 'app-root',
  template: `
    <app-user name="Simran" />
  `,
  imports: [User],
})
export class App {}
child.ts 复制代码
import {Component, input} from '@angular/core';

@Component({
  selector: 'app-user',
  template: `
    <p>The user's name is {{ name() }}</p>
  `,
})
export class User {
  readonly name = input<string>();
}

必要输入 input.required

比如props是必须的,通过 input.required 来声明

child.ts 复制代码
export class User {
   name = input.required<string>();
}

注意:如果是必要的输入,undefined 不会包含在 InputSignal 泛型参数中

配置输入 transform

有些时候可能需要根据 输入的值进行转换,则用到 transform

child.ts 复制代码
export class CustomSlider {
  label = input('', {transform: trimString});
}
function trimString(value: string | undefined): string {
  return value?.trim() ?? '';
}
app.ts 复制代码
<custom-slider [label]="systemVolume" />

每当 systemVolume 的值更改时,Angular 都会运行 trimString 并将 label 设置为结果。

需要注意的是:transform必须是纯函数

内置转换

Angular 包括两个内置的转换函数 booleanAttribute, numberAttribute

app.ts 复制代码
import {Component, input, booleanAttribute, numberAttribute} from '@angular/core';
@Component({/*...*/})
export class CustomSlider {
  disabled = input(false, {transform: booleanAttribute}); 
  value = input(0, {transform: numberAttribute}); 
}

注意:booleanAttribute将 "false" 视为 false。numberAttribute如果解析失败是 NaN

输入别名

通过 alias 设置输入的别名

child.ts 复制代码
@Component({/*...*/})
export class CustomSlider {
  value = input(0, {alias: 'sliderValue'});
}
app.ts 复制代码
<custom-slider [sliderValue]="50" />

@Input装饰器声明输入

child.ts 复制代码
@Component({...})
export class CustomSlider {
  @Input() value = 0;
}
app.ts 复制代码
<custom-slider [value]="50" />

@Input 的必要输入

app.ts 复制代码
@Component({...})
export class CustomSlider {
  @Input({required: true}) value = 0;
}

@Input 的 transform

app.ts 复制代码
@Component({
  selector: 'custom-slider',
  ...
})
export class CustomSlider {
  @Input({transform: trimString}) label = '';
}
function trimString(value: string | undefined) { return value?.trim() ?? ''; }

@Input 的 alias

child.ts 复制代码
@Component({...})
export class CustomSlider {
  @Input({alias: 'sliderValue'}) value = 0;
}
app.ts 复制代码
<custom-slider [sliderValue]="50" />

组件输出

output

组件输出通过 outputEventEmitter 实现。相当于就是子组件向父组件通信

child.ts 复制代码
import {Component, output} from '@angular/core';

@Component({
  selector: 'app-child',
  styles: `.btn { padding: 5px; }`,
  template: `
    {{count}}
    <button class="btn" (click)="addItem()">Add Item</button>
  `,
})
export class Child {
  count = 0;
  readonly addItemEvent = output<number>();

  addItem() {
    this.count++;
    this.addItemEvent.emit(
      this.count
    );
  }
}
app.ts 复制代码
import {Component} from '@angular/core';
import {Child} from './child';

@Component({
  selector: 'app-root',
  template: `
    <app-child (addItemEvent)="addItem($event)" />
    <p>🐢 all the way down {{ items.length }}</p>
    @for(item of items; track item) {
      <p>{{ item }}</p>
      }
  `,
  imports: [Child],
})
export class App {
  items = new Array();

  addItem(item: number) {
    this.items.push(item);
  }
}

@output 装饰器

跟 @Input 类似。

@output装饰器别名
child.ts 复制代码
@Component({/*...*/})
export class CustomSlider {
  @Output('valueChanged') changed = new EventEmitter<number>();
}
app.ts 复制代码
<custom-slider (valueChanged)="saveVolume()" />

ng-content

ng-content可以类比 React 里面组件封装时的 children,或者 vue 中的 slot。它就是个占位符,不是组件也不是DOM元素

单插槽

custom-card.ts 复制代码
@Component({
  selector: 'custom-card',
  template: '<div class="card-shadow">
       <ng-content></ng-content>
  </div>',
})
export class CustomCard {/* ... */}
app.ts 复制代码
@Component({
  selector: 'app-selector',
  template: `
    <custom-card> 
        <p>This is the projected content</p>
    </custom-card>
  `,
})

上面的代码就是 p 标签挂载到 ng-conent 的位置。渲染后的DOM结构如下

dom 复制代码
<custom-card>
  <div class="card-shadow">
    <p>This is the projected content</p>
  </div>
</custom-card>

多插槽

ng-content可以通过 select 属性实现多插槽

ts 复制代码
@Component({
  selector: 'card-title',
  template: `<ng-content>card-title</ng-content>`,
})
export class CardTitle {}
@Component({
  selector: 'card-body',
  template: `<ng-content>card-body</ng-content>`,
})
export class CardBody {}

// custom-card
Component({
  selector: 'custom-card',
  template: `
  <div class="card-shadow">
    <ng-content select="card-title"></ng-content>
    <div class="card-divider"></div>
    <ng-content select="card-body"></ng-content>
  </div>
  `,
})
export class CustomCard {}
app.ts 复制代码
@Component({
  selector: 'app-root',
  imports: [CustomCard, CardTitle, CardBody],
  template: `
    <custom-card>
      <card-title>Hello</card-title>
      <card-body>Welcome to the example</card-body>
    </custom-card>
`,
})
export class App {}

最后渲染的DOM结构如下

ts 复制代码
<custom-card>
  <div class="card-shadow">
    <card-title>Hello</card-title>
    <div class="card-divider"></div>
    <card-body>Welcome to the example</card-body>
  </div>
</custom-card>

ng-template

定义了一个模板 ,但默认不会渲染任何内容,需要配合其他指令或机制才能将其渲染到 DOM 中

html 复制代码
<!-- 这段内容不会显示在页面上 -->
<ng-template>
  <p>这段文字不会显示</p>
</ng-template>

配合 *ngIf 使用。比如条件渲染不同的模板

html 复制代码
<!-- 直接用 *ngIf 的内联写法,实际上 Angular 会自动转换成 ng-template -->
<div *ngIf="isLoggedIn; else loggedOut">
  欢迎回来,用户!
</div>
<ng-template #loggedOut>
  <p>请先登录</p>
</ng-template>

ng-container

跟React里面的<></>类似。

特性 Angular ng-container React <> </> (Fragment)
不产生额外 DOM ✅ 不会创建真实 DOM 元素 ✅ 不会创建真实 DOM 元素
分组作用 ✅ 包裹多个元素但不影响布局 ✅ 包裹多个元素但不影响布局
避免破坏样式 ✅ 不会打乱 Flex/Grid 布局 ✅ 不会打乱 Flex/Grid 布局
ts 复制代码
<!-- Angular -->
<ng-container>
  <div>项目1</div>
  <div>项目2</div>
</ng-container>

<!-- React -->
<>
  <div>项目1</div>
  <div>项目2</div>
</>

只不过 ng-container 可以跟 *ngIf、*ngFor 一起使用,而 React Fragment 不支持

ts 复制代码
//angular:直接在容器上使用指令
<ng-container *ngIf="isLoggedIn; else guest">
  <h1>欢迎回来</h1>
  <p>您的订单:{{ orderCount }}</p>
</ng-container>

<ng-template #guest>
  <h1>请登录</h1>
  <a href="/login">去登录</a>
</ng-template>



// React:需要将条件逻辑写在外部
{isLoggedIn ? (
  <>
    <h1>欢迎回来</h1>
    <p>您的订单:{orderCount}</p>
  </>
) : (
  <>
    <h1>请登录</h1>
    <a href="/login">去登录</a>
  </>
)}

生命周期

方法 摘要
ngOnInit 在 Angular 初始化完组件的所有输入之后运行一次。
ngOnChanges 每次组件的输入发生更改时都会运行。
ngDoCheck 每次检查此组件是否有更改时都会运行。
ngAfterContentInit 在组件的内容初始化之后运行一次。
ngAfterContentChecked 每次检查此组件内容是否有更改时都会运行。
ngAfterViewInit 在组件的视图初始化之后运行一次。
ngAfterViewChecked 每次检查组件的视图是否有更改时都会运行。
afterNextRender 所有组件下次渲染到 DOM 后运行一次。
afterEveryRender 每次所有组件渲染到 DOM 后都会运行。
ngOnDestroy 在组件销毁之前运行一次。

ngOnInit

组件自身初始化完成前执行。只执行一次

ngOnChanges

在组件 输入(props)改变后运行

在初始化期间,第一次 ngOnChangesngOnInit 之前运行。

该生命周期接受一个 SimpleChanges 参数,包含 输入props 的先前值、当前值、标志(表示该输入props是否是第一次更改)

ts 复制代码
@Component({
  /* ... */
})
export class UserProfile {
  name = input('');
  ngOnChanges(changes: SimpleChanges) {
    for (const inputName in changes) {
      const inputValues = changes[inputName];
      console.log(`Previous ${inputName} == ${inputValues.previousValue}`);
      console.log(`Current ${inputName} == ${inputValues.currentValue}`);
      console.log(`Is first ${inputName} change == ${inputValues.firstChange}`);
    }
  }
}

注意:如果输入提供了 alias 别名,SimpleChanges 的 key 依旧是定义时的名字,而不是别名

ngOnDestory

组件销毁前执行一次。

ngDoCheck

组件更改前,ngDoCheck都会运行。

注意:该方法会非常频繁的运行,会存在严重的性能问题。尽量不用

ngAfterContentInit

组件内部的所有子组件都初始化之后运行一次

剩下的生命周期

直接看官方文档

生命周期接口

Angular 为每个生命周期方法提供了一个 TypeScript 接口。你可以选择导入并 implement 这些接口,以确保你的实现没有任何拼写错误。

每个接口的名称都与相应的方法相同,只是去掉了 ng 前缀。例如,ngOnInit 的接口是 OnInit

ts 复制代码
@Component({
  /* ... */
})
export class UserProfile implements OnInit {
  ngOnInit() {
    /* ... */
  }
}

生命周期执行顺序

看官方文档

ViewChild

让父组件能获取子组件、子指令或DOM元素的引用。如果找不到目标值是 undefined

app.ts 复制代码
@Component({
  selector: 'custom-card-header',
})
export class CustomCardHeader {
  text: string;
}


@Component({
  selector: 'custom-card',
  template: '<custom-card-header>Visit sunny California!</custom-card-header>',
})
export class CustomCard {
  header = viewChild(CustomCardHeader);
  headerText = computed(() => this.header()?.text); //能访问到子组件的 text
}

获取DOM元素

ts 复制代码
@Component({
  selector: 'custom-card',
  template: '<input #myInput type="text" />',
})
export class CustomCard {
    @ViewChild('myInput') inputRef!: ElementRef<HTMLInputElement>;
    ngAfterViewInit() {
      this.inputRef.nativeElement.focus(); // 操作原生 DOM
    }
}

ContentChild

用于在子组件中获取通过 ng-content 投影进来的内容的引用

ts 复制代码
@Component({
  selector: 'app-button',
  template: `<button (click)="onClick()">{{ label }}</button>`
})
export class ButtonComponent {
  label = '点击我';
  
  onClick() {
    console.log('按钮被点击');
  }
  
  changeLabel(newLabel: string) {
    this.label = newLabel;
  }
}
app.ts 复制代码
import { Component, ContentChild, AfterContentInit } from '@angular/core';

@Component({
  selector: 'app-container',
  template: `<div class="container"><ng-content></ng-content></div>`
})
export class ContainerComponent implements AfterContentInit {
  @ContentChild(ButtonComponent) button!: ButtonComponent;

  ngAfterContentInit() {
    // 可以直接调用子组件的方法
    this.button.changeLabel('新按钮文字');
  }
}
使用.ts 复制代码
<app-container>
  <app-button></app-button>  <!-- 被投影进去 -->
</app-container>

ViewChild和ContentChild的区别

简单来说,@ViewChild 查自己家(模板内部的元素),@ContentChild 查客人带来的东西(投影进来的内容)。

控制流

就是条件渲染

@if 和 @else if 和 @else

app.ts 复制代码
import {Component} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    @if (isServerRunning) {
     <span>Yes, the server is running</span>
    } @else if (isLogin) {
      <span>No, the server is not running</span>
    } @else {
      <span>No, there is no anything true</span>
    }
  `,
})
export class App {
  isServerRunning = false
  isLogin = true
}

当然,在条件判断时,可以将条件存在一个变量里面复用

ts 复制代码
@if (user.profile.setting.name; as isUserNameExits) {
  {{ isUserNameExits }}
}

@for

就是循环

app.ts 复制代码
import {Component} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    @for(user of users; track user.id) {
    <p>{{ user.name }}</p>
    }
  `,
})
export class App {
  users = [
    {id: 0, name: 'Sarah'},
    {id: 1, name: 'Amy'},
    {id: 2, name: 'Rachel'},
    {id: 3, name: 'Jessica'},
    {id: 4, name: 'Poornima'},
  ]

注意:必须使用 track,你可以使用 id 或其他唯一标识符。可以理解为是 React/Vue 遍历时指定的key。 指定了 track 后,Angular在重渲染的时候提高性能。

@for内置变量

其实就跟 for 循环里面的 item/index 差不多

变量 含义
$count 迭代集合中的条目数
$index 当前行的索引
$first 当前行是否为第一行
$last 当前行是否为最后一行
$even 当前行索引是否为偶数
$odd 当前行索引是否为奇数
app.ts 复制代码
@for (item of items; track item.id; let idx = $index, e = $even, o = $odd) {
  <p>Item #{{ idx }}: {{ item.name }}。是否是偶数:{{e}}。是否是奇数:{{o}}</p>
}

@empty

@empty为@for兜底。当@for没有数据时,将显示 @empty 控制块的内容:

app.ts 复制代码
@for (item of items; track item.name) {
  <li> {{ item.name }}</li>
} @empty {
  <li aria-hidden="true"> There are no items. </li>
}

示例代码表示如果items有数据就展示name,没有就展示 there are no items

@switch

与js的swtich语法类似,只不过不需要指定 break/return

app.ts 复制代码
@switch (userPermissions) {
  @case ('admin') {
    <app-admin-dashboard />
  }
  @case ('reviewer') {
    <app-reviewer-dashboard />
  }
  @case ('editor') {
    <app-editor-dashboard />
  }
  @default {
    <app-viewer-dashboard />
  }
}

如果前面的 case 表达式均不与 switch 值匹配,则显示 @default 控制块的内容。如果没有 @case 与表达式匹配,并且没有 @default 控制块,则不显示任何内容。

属性绑定

属性绑定需要通过 [] 语法,可以绑定到 DOM、组件、指令实例

原生属性绑定

app.ts 复制代码
import {Component} from '@angular/core';

@Component({
  selector: 'app-root',
  styleUrls: ['app.css'],
  template: `
    <div [contentEditable]="isEditable"></div>
  `,
})
export class App {
  isEditable = true;
}

这样这个div就是可编辑的了。

组件属性绑定

当元素是 Angular 组件时,你可以使用属性(Property)绑定,使用相同的方括号语法来设置组件的输入属性(Property)

app.ts 复制代码
<my-listbox [value]="mySelection()" />

每次 mySelection 更改时,Angular 都会自动设置 MyListbox 实例的 value 属性

css类绑定

你也可以直接绑定到 class 属性(Property)。Angular 接受三种类型的值:

class 值的说明 TypeScript 类型
一个包含一个或多个 CSS 类(以空格分隔)的字符串 string
CSS 类字符串的数组 string[]
一个对象,其中每个属性(Property)名称都是一个 CSS 类名,每个对应的值根据真值与否决定是否将该类应用于元素。 Record<string, any>
app.ts 复制代码
@Component({
  template: `
    <ul [class]="listClasses"> ... </ul>
    <section [class]="sectionClasses()"> ... </section>
    <button [class]="buttonClasses()"> ... </button>
  `,
  ...
})
export class UserProfile {
  listClasses = 'full-width outlined';
  sectionClasses = signal(['expandable', 'elevated']);
  buttonClasses = ({
    highlighted: true,
    embiggened: false,
  });
}

DOM渲染后结果如下

html 复制代码
<ul class="full-width outlined"> ... </ul>
<section class="expandable elevated"> ... </section>
<button class="highlighted"> ... </button>

当然 angular也会智能的将所有类名组合渲染

app.ts 复制代码
@Component({
  template: `<ul class="list" [class]="listType()" [class.expanded]="isExpanded()"> ...`,
  ...
})
export class Listbox {
  listType = signal('box');
  isExpanded = signal(true);
}

DOM渲染后

html 复制代码
<ul class="list box expanded">

css 样式属性绑定

你可以直接绑定 css 的某个样式属性。比如 display: none

app.ts 复制代码
<section [style.display]="isExpanded() ? 'block' : 'none'">

当然也可以同时绑定多个 style 的值

style 值的描述 TypeScript 类型
一个包含零个或多个 CSS 声明的字符串,例如 "display: flex; margin: 8px" string
一个对象,其中每个属性(Property)名称都是一个 CSS 属性(Property)名称,每个对应的值都是该 CSS 属性(Property)的值。 Record<string, any>

比如

ts 复制代码
@Component({
  template: `
    <ul [style]="listStyles()"> ... </ul>
    <section [style]="sectionStyles()"> ... </section>
  `,
  ...
})
export class UserProfile {
  listStyles = signal('display: flex; padding: 8px');
  sectionStyles = signal({
    border: '1px solid black',
    'font-weight': 'bold',
  });
}

DOM渲染结果:

ts 复制代码
<ul style="display: flex; padding: 8px"> ... </ul>
<section style="border: 1px solid black; font-weight: bold"> ... </section>

事件处理

绑定事件

通过 () 绑定事件,回调格式是 callback()

app.ts 复制代码
import {Component} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <section (mouseover)="showSecretMessage()" (mouseleave)="hideMessage()">
      There's a secret message for you, hover to reveal 👀
      {{ message }}
    </section>
  `,
})
export class App {
  message = '';

  showSecretMessage() {
    this.message = 'Jolyne'
  }

  hideMessage() {
    this.message = "hide"
  }
}

注意:回调函数需要带上()

更多事件查看 元素上所有可用的事件

读取事件参数

通过 $event 变量访问事件对象

app.ts 复制代码
@Component({
  template: `
    <input type="text" (keyup)="updateField($event)" />
  `,
  ...
})
export class AppComponent {
  updateField(event: KeyboardEvent): void {
    console.log(`The user pressed: ${event.key}`);
  }
}

按键修饰符

如果想捕获特定的键盘事件,可以通过修饰符快速实现

app.ts 复制代码
@Component({
  template: `
    <input type="text" (keyup.enter)="updateField($event)" />
  `,
  ...
})
export class AppComponent{
  updateField(event: KeyboardEvent): void {
    console.log('The user pressed enter in the text field.');
  }
}

上面的代码就是当按下了 enter 键触发 updateField 回调

当然也可以触发多个按键

app.ts 复制代码
<input type="text" (keydown.code.alt.shiftleft)="updateField($event)" />

比如这个代码表示 shift + enter 同时按下时触发。

阻止默认事件

通过 preventDefault 阻止默认事件

app.ts 复制代码
@Component({
  template: `
    <a href="#overlay" (click)="showOverlay($event)">
  `,
})
export class AppComponent{
  showOverlay(event: PointerEvent): void {
    event.preventDefault();
    console.log('Show overlay without updating the URL!');
  }
}

可延迟视图

可以理解为懒加载,当满足某一特定条件时,才渲染。通过 @defer 实现。

@defer

默认情况下,当浏览器基于 requestIdleCallback 空闲后,再加载内容

app.ts 复制代码
<!-- @defer (on idle) -->
@defer { 
    <large-cmp />
}

@placeholder

在 @defer 渲染前显示的内容,可以指定 minimum 控制 placeholder 显示多久。(单位ms或s)

app.ts 复制代码
@defer {
  <large-component />
} @placeholder (minimum 500ms) {
  <p>Placeholder content</p>
}

上面的代码表示,@defer显示前,@placeholder显示至少 500ms

@loading

可以理解为就是 loading 态。当 loading 时,会替代 placeholder 显示。

app.ts 复制代码
@defer {
  <large-component />
} @loading {
  <img alt="loading..." src="loading.gif" />
} @placeholder {
  <p>Placeholder content</p>
}

同样,@loading 也可以指定 minimum(loading显示的最短时间)、after(@defer加载前需要等待的时间)

app.ts 复制代码
@defer {
  <large-component />
} @loading (after 100ms; minimum 1s) {
  <img alt="loading..." src="loading.gif" />
}

上面的代码表示,loading至少显示1秒。在@defer显示前需要等待 100ms时间。

这样的控制可以防止内容快速闪烁

@error

@error是 @defer 加载失败后显示的内容。与@placeholder和@loading类似

app.ts 复制代码
@defer {
  <large-component />
} @error {
  <p>Failed to load large component.</p>
}

指定 @defer 何时加载

通过 onwhen 指定

on

触发器 描述
idle 当浏览器空闲时触发。
viewport 当指定内容进入视口时触发
interaction 当用户与指定元素交互时触发
hover 当鼠标悬停在指定区域上方时触发
immediate 在非延迟内容完成渲染后立即触发
timer 在特定持续时间后触发
viewport

当指定内容使用 Intersection Observer API 进入视口时,viewport 触发器会加载延迟内容。

app.ts 复制代码
@defer (on viewport) {
  <large-cmp />
} @placeholder {
  <div>Large component placeholder</div>
}
interaction

当用户通过 clickkeydown 事件与指定元素交互时,interaction 触发器会加载延迟加载内容。

app.ts 复制代码
@defer (on interaction) {
  <large-cmp />
} @placeholder {
  <div>Large component placeholder</div>
}
hover

当鼠标通过 mouseoverfocusin 事件悬停在触发区域上方时,hover 触发器会加载延迟加载内容

app.ts 复制代码
@defer (on hover) {
  <large-cmp />
} @placeholder {
  <div>Large component placeholder</div>
}
immediate

immediate 触发器会立即 @defer。这意味着 @defer 会在所有其他非延迟加载内容完成渲染后立即加载。

app.ts 复制代码
@defer (on immediate) {
  <large-cmp />
} @placeholder {
  <div>Large component placeholder</div>
}
timer

timer 触发器会在指定的时长后加载延迟加载内容。单位 ms 或 s

app.ts 复制代码
@defer (on timer(500ms)) {
  <large-cmp />
} @placeholder {
  <div>Large component placeholder</div>
}

when

when 触发器接受自定义条件表达式,并在条件变为真值时加载延迟加载内容。

app.ts 复制代码
@defer (when condition) {
  <large-cmp />
} @placeholder {
  <div>Large component placeholder</div>
}

prefetch

预获取与 @defer 相关的 JS

app.ts 复制代码
@defer (on interaction; prefetch on idle) {
  <large-cmp />
} @placeholder {
  <div>Large component placeholder</div>
}

上面的代码表示:预取在浏览器空闲时开始,并且仅在用户与占位符交互后才渲染块的内容。

指令

属性指令

属性指令不会改变 DOM 结构,只是修改元素本身的属性、样式或行为。

比如自定义一个高亮文本的指令

highlight.directive.ts 复制代码
import { Directive, ElementRef, HostListener, Input } from '@angular/core';

@Directive({
  selector: '[appHighlight]'  // 使用方括号表示属性选择器
})
export class HighlightDirective {
  @Input() appHighlight = '';  // 接收传入的颜色值
  @Input() defaultColor = 'yellow';

  constructor(private el: ElementRef) {}

  @HostListener('mouseenter') onMouseEnter() {
    this.highlight(this.appHighlight || this.defaultColor);
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.highlight('');
  }

  private highlight(color: string) {
    this.el.nativeElement.style.backgroundColor = color;
  }
}

使用

html 复制代码
<!-- 基础用法 -->
<p appHighlight>悬停我高亮</p>

<!-- 传入颜色参数 -->
<p [appHighlight]="'red'">悬停变红</p>
<p [appHighlight]="color" [defaultColor]="'blue'">动态颜色</p>

常用的内置属性指令

指令 作用
ngClass 动态添加或移除 CSS 类
ngStyle 动态设置内联样式
ngModel 双向数据绑定(配合表单使用)

比如

html 复制代码
<!-- ngClass 示例 -->
<div [ngClass]="{'active': isActive, 'disabled': isDisabled}">状态</div>

<!-- ngStyle 示例 -->
<div [ngStyle]="{'color': textColor, 'font-size': fontSize + 'px'}">样式</div>

结构指令

结构指令改变 DOM 结构,通过添加、移除或替换 DOM 元素来控制渲染

常用的内置结构指令

指令 作用
*ngIf 条件性渲染元素
*ngFor 循环渲染列表
*ngSwitch / *ngSwitchCase / *ngSwitchDefault 多条件分支渲染
html 复制代码
<!-- *ngIf 示例 -->
<div *ngIf="isLoggedIn; else loginPrompt">欢迎回来</div>
<ng-template #loginPrompt>请先登录</ng-template>

<!-- *ngFor 示例 -->
<ul>
  <li *ngFor="let item of items; let i = index">
    {{ i + 1 }}. {{ item.name }}
  </li>
</ul>

<!-- *ngSwitch 示例 -->
<div [ngSwitch]="status">
  <p *ngSwitchCase="'loading'">加载中...</p>
  <p *ngSwitchCase="'success'">加载成功</p>
  <p *ngSwitchDefault>加载失败</p>
</div>

路由

路由感觉没什么写的,跟 react vue 大同小异。直接看文档即可Angular路由

表单

表单概览

主要是 ngModel 实现双向绑定(表单 --》 数据/数据 --》 表单)

app.ts 复制代码
import {Component} from '@angular/core';
import {FormsModule} from '@angular/forms';

@Component({
  selector: 'app-user',
  template: `
    <p>Username: {{ username }}</p>
    <p>{{ username }}'s favorite framework: {{ favoriteFramework }}</p>
    <label for="framework">
      Favorite Framework:
      <input id="framework" type="text" [(ngModel)]="favoriteFramework" />
    </label>
  `,
  imports: [FormsModule],
})
export class User {
  favoriteFramework = '';
  username = 'youngTech';
  
  showFramework() {
    alert(this.favoriteFramework);
  }
}

上面的代码就是 输入框通过 ngModel 双向绑定 favoriteFramework, 输入框输了值后,favoriteFramework更改,界面上也会重新渲染

响应式表单

响应式表单通过

    1. 导入 ReactiveForms 模块
    1. 使用 ormControl 创建 FormGroup 对象
    1. FormGroup 和 FormControl 链接到表单上
    1. 通过 ngSubmit 提交
app.ts 复制代码
import {Component} from '@angular/core';
import {FormGroup, FormControl} from '@angular/forms';
import {ReactiveFormsModule} from '@angular/forms';

@Component({
  selector: 'app-root',
  template: `
    <form [formGroup]="profileForm" (ngSubmit)="handleSubmit()">
      <input type="text" formControlName="name" />
      <input type="email" formControlName="email" />
      <button type="submit">Submit</button>
    </form>

    <h2>Profile Form</h2>
    <p>Name: {{ profileForm.value.name }}</p>
    <p>Email: {{ profileForm.value.email }}</p>
  `,
  imports: [ReactiveFormsModule],
})
export class App {
  profileForm = new FormGroup({
    name: new FormControl(''),
    email: new FormControl(''),
  });

  handleSubmit() {
    alert(this.profileForm.value.name + ' | ' + this.profileForm.value.email);
  }
}

验证表单

通过 Validators 验证

app.ts 复制代码
import {Component} from '@angular/core';
import {FormGroup, FormControl} from '@angular/forms';
import {ReactiveFormsModule, Validators} from '@angular/forms';

@Component({
  selector: 'app-root',
  template: `
    <form [formGroup]="profileForm">
      <input type="text" formControlName="name" name="name" />
      <input type="email" formControlName="email" name="email" />
      <button type="submit" [disabled]="!profileForm.valid">Submit</button>
    </form>
  `,
  imports: [ReactiveFormsModule],
})
export class App {
  profileForm = new FormGroup({
    name: new FormControl('', Validators.required),
    email: new FormControl('', [Validators.required, Validators.email]),
  });
}

注入服务

通过 @Injectable 创建可注入的服务,通过 inject 使用对应的服务

car.service.ts 复制代码
import {Injectable} from '@angular/core';

@Injectable({
  providedIn: 'root', 
})
export class CarService {
  cars = ['Sunflower GT', 'Flexus Sport', 'Sprout Mach One'];

  getCars(): string[] {
    return this.cars;
  }

  getCar(id: number) {
    return this.cars[id];
  }
}
app.ts 复制代码
import {Component, inject} from '@angular/core';
import {CarService} from './car.service';

@Component({
  selector: 'app-root',
  template: '<p> {{ carService.getCars() }} </p>',
})
export class App {
  carService = inject(CarService);
}

注意:@Injectable可以通过 providedIn定义程序的哪个部门可以使用这个服务。一般都是 root,即全局可用。

管道

基本使用

管道其实就是转换数据用到的纯函数。Angular内置了许多管道

app.ts 复制代码
import {Component} from '@angular/core';
import {LowerCasePipe} from '@angular/common';

@Component({
  selector: 'app-root',
  template: `
    {{ username | lowercase }}
  `,
  imports: [LowerCasePipe],
})
export class App {
  username = 'yOunGTECh';
}

首先引入 LowerCasePipe 管道,然后通过 {{ 传递给转换函数的值 | 管道的名称}} 使用

组合多个管道

多个管道组合时,angular从左到右运行管道。

app.ts 复制代码
<p>The event will occur on {{ scheduledOn | date | uppercase }}.</p>

上面的代码表示:先本地化日期,再以大写展示

向管道传递参数

在管道名字后加一个 :,然后跟数值即可。

app.ts 复制代码
<p>The event will occur at {{ scheduledOn | date:'hh:mm' }}.</p>

一个管道可能接受多个参数,也是用 : 隔开

app.ts 复制代码
<p>The event will occur at {{ scheduledOn | date:'hh:mm':'UTC' }}.</p>

管道优先级

管道优先级低于 二元运算符,包括 +-*/%&&||??

app.ts 复制代码
{{ firstName + lastName | uppercase }}

上面代码表示先进行 名字的拼接,把拼接后的值再传给 uppercase 管道

管道优先级 高于 三元运算符

app.ts 复制代码
{{ isAdmin ? 'Access granted' : 'Access denied' | uppercase }}

上面的代码相当于先执行 'Access denied' | uppercase ,再执行 三元运算符

如果要改变顺序,先执行三元再执行管道,要加小括号

app.ts 复制代码
{{( isAdmin ? 'Access granted' : 'Access denied') | uppercase }}

加了括号后,先执行三元,再执行管道

自定义 Pipe

通过 @Pipe 装饰器 和 implements PipeTransform 实现

pipe.ts 复制代码
@Pipe({
    name: 'toFixed2point'
})
export class ToFixed2pointPipePipe implements PipeTransform {
    transform(value: any, args?: any): any {
        return Number(value).toFixed(2);
    }
}

自定义的Pipe 内部需要定义 transform 方法,是个纯函数

注意:自定义 Pipe 的命名规则 大驼峰类目 + Pipe。比如 MyCustomerPipe

自定义管道检测数组或者对象

需要加上 pure: false,标记为非纯函数。

app.ts 复制代码
@Pipe({
    name: 'callback',
    pure: false
})
export class CallbackPipe implements PipeTransform {
    transform(items: any[], callback: (item: any) => boolean): any {
        if (!items || !callback) {
            return items;
        }
        return items.filter(item => callback(item));
    }
}

注意:除非绝对必要,否则应避免创建非纯管道

相关推荐
玄星啊1 小时前
AI 编程的第 30 天,我怀念古法 Coding 了
前端·ai编程
锋行天下2 小时前
半秒开!还有谁!!!
前端·vue.js·架构
代码搬运媛3 小时前
git 下中文文件名乱码问题解决
前端
CaffeinePro3 小时前
告别知识点零散!React零基础通关,从环境搭建到Ant Design页面实战
前端·react.js
cidy_983 小时前
水龙头领不到测试币?手把手用 Hardhat 本地环境零门槛学以太坊交易
前端
因_崔斯汀3 小时前
Three.js 3D 地图特效与材质实现指南
前端
angerdream3 小时前
手把手编写儿童手机远程监控App之vue3用 AI Agent生成菜单
前端
cidy_983 小时前
Git Pull 代码冲突后完整回退教程
前端
JING小白3 小时前
Day 1 重学Vue:响应式系统的“底层逻辑”变更,Vue2旧时代的终结与Vue3新时代的开启
前端·vue.js