0%

Angular Service

在Angular中,Service是一个可注入的类,用于封装可重用的功能。Service可以在任何Component中使用,也可以用于其他Service

Service可以完成以下工作,很多和UI无关的操作都可以用Service来完成,这样可以保持Component的简洁。

  • 调用API获取后台数据 - API Service
  • 验证用户输入 - Form Validation
  • 日志服务 - Logging Service
  • 数据库操作 - Database Service

创建Service

使用Angular CLI创建Service,下面的CLI命令生成一个OrderService

1
ng generate service order # or, ng g s order

该命令会在项目根目录下生成一个order.service.ts文件,我们在其中添加一个getOrder方法,用于获取订单信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// order.service.ts
import { Injectable } from '@angular/core';
import {Order} from "./Order";

@Injectable({
providedIn: 'root'
})
export class OrderService {
constructor() { }

getOrder():Promise<Order> {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
id: 1,
name: 'Order 1',
});
}, 1000);
});
}
}

Order类型定义如下

1
2
3
4
5
// order.ts
export interface Order {
id: number;
name: string;
}

可以看到,OrderService对应的Class是用@Injectable装饰器修饰的,这样Angular就可以将其注入到Component中。我们还给@Injectable传递了一个参数providedIn: 'root',这表示该Service是一个全局Service,可以在整个Application中使用。

使用Service

通过构造函数注入Service

ProductComponent中使用OrderService,我们需要在ProductComponent的构造函数中注入OrderService,然后调用OrderService的方法。(注意,由于OrderService是providedIn: 'root'的,所以使用者不需要在providers数组中声明它)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import {OrderService} from "../order.service";
import {Order} from "../Order";

export class ProductComponent implements OnInit {
order: Order | null;

// inject OrderService
constructor(private orderService: OrderService) {
this.order = null;
}

async ngOnInit() {
this.order = await this.orderService.getOrder();
console.log(this.order);
}
}

通过inject函数注入Service

Angular 14引入了一个新的函数inject, 可以用来注入Service,如下: 使用这种方式,我们不必再依赖构造函数的参数,可以在任何地方注入Service。

1
2
3
4
import {OrderService} from "../order.service";
export class ProductComponent implements OnInit {
orderService = inject(OrderService);
}

两种注入方式的区别

那么使用inject函数注入比使用constructor注入有何好处呢?主要体现在继承时,假设我有一个BaseComponent,其构造函数中注入了某个service, 另外一个组件ProductComponent继承自BaseComponent,则ProductComponent的构造函数也需要注入这个service才能调用super方法。

1
2
3
4
5
// base.component.ts
export class BaseComponent {
constructor(protected orderService: OrderService) {
}
}
1
2
3
4
5
6
// product.component.ts
export class ProductComponent extends BaseComponent {
constructor(override orderService: OrderService) {
super(orderService);
}
}

而使用inject函数注入则不需要。父组件的service会自动注入到子组件中。

1
2
3
4
// base.component.ts
export class BaseComponent {
orderService = inject(OrderService)
}
1
2
3
4
5
6
7
8
9
// product.component.ts
export class ProductComponent extends BaseComponent implements OnInit {
ngOnInit() {
// this.orderService is available here
this.orderService.getOrder().then(order => {
console.log(order);
})
}
}

providedIn vs providers

在Angular中,我们可以使用providedIn或者providers来指定Service的提供范围。providedIn是Angular 6中引入的新特性,用于替代providers

如果在定义Service时指定了providedIn: 'root',那么Angular会在应用启动时自动将该Service注入到根模块中,这样就可以在整个应用中使用该Service。在使用该Service的Component中,就不必再在providers中声明该Service。

如果定义Service时没有指定providedIn,那么就需要在使用该Service的Component中的providers中声明该Service。

1
2
3
4
5
6
7
8
@Component({
selector: 'app-product',
standalone: true,

templateUrl: './product.component.html',
styleUrl: './product.component.css',
providers: [OrderService] // <--- provide service here
})

当Angular找不到一个Service的提供者时,会抛出一个错误,相信大家都见过这个错误,从下面的错误可知,_OrderService没有提供者。

1
2
ERROR Error [NullInjectorError]: R3InjectorError(Environment Injector)[_OrderService -> _OrderService]: 
NullInjectorError: No provider for _OrderService!

更进一步,我们可以将Service的使用范围限定在某个Component中,这样其他Component就无法使用该Service。

1
2
3
4
5
6
7
// order.service.ts
@Injectable({
providedIn: ProductComponent, // --- only available in ProductComponent
})
export class OrderService {
// ...
}

注意,以上代码无法在应用启动时自动注入Service,使用者仍然需要在providers中声明该Service。

1
2
3
4
5
6
7
8
9
// product.component.ts
@Component({
selector: 'app-product',
standalone: true,

templateUrl: './product.component.html',
styleUrl: './product.component.css',
providers: [OrderService] // <--- provide service here
})

如果在其他Component中尝试使用该Service,会抛出一个错误,如下:

1
ERROR TypeError: Cannot read properties of undefined (reading 'provide')

Service的Scope

providedIn: ‘root’

providedIn: 'root' - 表示Service是一个全局Service,可以在整个应用中使用。Angular会在应用启动时自动将该Service注入到根模块中。

providedIn: ‘platform’

providedIn: 'platform' - 表示Service是一个平台Service,这种服务可以跨越多个Angular应用实例共享同一个实例,只要这些应用实例运行在同一页面上。这个主要在微前端项目中使用,单体Angular应用用不到。

providedIn: ‘any’

providedIn: 'any' - 这种方式下,每个lazy load的module都会有一个独立的Service实例。而所有的eager load的module共享一个Service实例。

lazy loading与Service实例

References

  1. https://angular.dev/api/core/Injectable#providedIn

What is a pipe

Angular中的pipe就是一个简单的函数,该函数接收一个输入值,然后返回一个转换后的值。pipe只需在一处定义,然后任意模板都可使用。

Generate pipe

1
ng generate pipe full-name

This will create a new file full-name.pipe.ts in the src/app folder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { Pipe, PipeTransform } from '@angular/core';

interface Person {
firstName: string;
lastName: string;
}

@Pipe({
name: 'fullName',
standalone: true
})
export class FullNamePipe implements PipeTransform {
transform(value: Person, ...args: unknown[]): unknown {
return value.lastName + ', ' + value.firstName;
}
}

用@Pipe装饰器来定义pipe,name属性是pipe的名字,standalone属性是可选的,如果设置为true,则表示该pipe是一个独立的pipe,不依赖于任何Module。pure参数是可选的,默认为true。pure pipe只有当输入变化时才重新计算。

每个Pipe都实现了PipeTransform接口,该接口只有一个方法transform,该方法接收一个输入值和一些可选参数,然后返回一个转换后的值。在这里,我们根据输入的Person变量,返回对应的全名。

How to use pipe

在component中引入pipe,然后在模板中使用。上面的FullNamePipe可以这样使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// component.ts
import {FullNamePipe} from "../full-name.pipe";

@Component({
// ...
imports: [
FullNamePipe,
],
// ...
})

person = {
firstName: 'Philip',
lastName: 'Zhang',
}
1
2
<!-- component.html -->
{{ person | fullName }}

也可以直接在代码中使用pipe:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { FullNamePipe } from "../full-name.pipe";

@Component({
// ...
providers: [FullNamePipe],
})
export class YourComponent {
person = {
firstName: 'Philip',
lastName: 'Zhang',
};

constructor(private fullNamePipe: FullNamePipe) {
const fullName = this.fullNamePipe.transform(this.person);
console.log(fullName); // logs "Zhang, Philip"
}
}

Naming convention

  1. pipe name should be in camelCase. - fullName
  2. pipe class should be in PascalCase and end with Pipe. - FullNamePipe

Pass parameter to pipe

pipe可以接收多个参数,参数之间使用:分隔。

1
{{ value | pipe:param1:param2... }}

Pipe chaining

多个pipe可以使用|符号连接在一起,前一个pipe的输出会作为后一个pipe的输入。

1
{{ value | pipe1 | pipe2 | pipe3 }}

Pipe precedence

pipe的执行顺序是从左到右,并且pipe的优先级高于 :?操作符.

Bad

1
{{ value > 0 ? expression1 : expression2 | pipe }}

is equal to

1
{{ value > 0 ? expression1 : (expression2 | pipe) }}

Good

1
{{ (value > 0 ? expression1 : expression2) | pipe }}

References

https://angular.io/guide/pipes-custom-data-trans

VSCode

  1. Ctrl + P - Search by file name
  2. Ctrl + Shift + P - Open command palette
  3. Alt + Left Arrow - Go back
  4. Ctrl + K, Ctrl + 0 - Fold all
  5. Ctrl + K, Ctrl + J - Unfold all

Angular

Create project with specific version

The following command create angular app with angular 15

1
npx @angular/cli@15 new my-app

Git

Checkout a remote branch

For single remote, use the following command.

  1. git fetch – fetch the remote branch
  2. git checkout -b local-branch-name origin/remote-branch-name – checkout the remote branch
  3. git switch -t remote-branch-name – switch to the remote branch, use sgit branch -v -a to make sure your branch is ready.
  4. checkout from tag: git checkout tags/v1.0.0 -b branch-name

Delete branch

  1. git branch -d branch-name – delete a local branch
  2. git branch -D branch-name – force delete a local branch
  3. git push origin --delete branch-name – delete a remote branch

Diff

Diff staged/indexed changes

  1. git diff --cached
  2. git status -v – show the changes in the index(staged changes)

2. checkout remote branch

Before checking out a remote branch, you need to fetch the remote branch first.

1
2
git fetch
git checkout -b local-branch-name origin/remote-branch-name

3. Checkout a specific commit.

The following command will create a new branch from the specific commit, you can find the commit id by git log command or from the git history.

1
git checkout -b new_branch commit-hash

Undo

1. Undo last commit

  1. git reset --soft HEAD~1 // undo the last commit and keep the changes in working directory.
  2. git reset --hard HEAD~1 // undo the last commit and remove all changes.

2. Undo staged changes(not committed yet)

  1. git reset HEAD file-name // unstage a file
  2. git reset HEAD . // unstage all files

3. Undo unstaged changes(changes are not added or committed)

  1. git checkout -- . // undo all changes in the working directory, same as git checkout .?
  2. git checkout -- file-name // undo changes in a specific file
  3. git checkout -- '*.js' // undo changes in all js files

4. Undo untracked files

  1. git clean -f // remove untracked files
  2. git clean -f -d // remove untracked files and directories

git restoregit reset的区别是什么?

Stash

git stash – save the changes
git stash list – list all stashes
git stash apply "stash@{n}" – apply the nth stash
git stash apply n – apply the nth stash
git stash pop – apply the latest stash and remove it from the stash list

Chrome

  1. F12 - Open developer tools
  2. Ctrl + Shift + P - Open command palette(only works after you open developer tools)
  3. Ctrl + P - Open file(after you open developer tools), this is pretty useful when you want to find a file in the source tab.
  4. Ctrl + Mouse Left Click - Open a link in a new tab. (also can use Mouse Wheel click)

Yarn

  1. Input chrome://settings in the address bar to open the settings page.

Jest

  1. jest - Run all test
  2. jest --coverage - Run test with coverage report
  3. jest --watch - Run test in watch mode
  4. jest test.spec.ts - Run a specific test file
  5. jest test.spec.ts --coverage - Run a specific test file with coverage report

Windows

Win + Shift + S - Take a screenshot

Reference

Angular对路由的支持非常强大,可以实现多种路由模式,本文主要介绍辅助路由。

辅助路由

辅助路由(auxiliary route)是一种特殊的路由,它的主要应用场景是为当前页面添加弹窗,主路由和辅助路由对应的组件同时显示,当关闭辅助路由组件(弹窗)时,主路由仍然保持显示。

实现步骤

创建Project

首先参考这篇创建一个Angular项目。注意从Angular 17开始,默认就使用Standalone Component来创建组件了,不在使用NgModule了。

添加路由配置

打开app.routes.ts文件,添加路由配置:

1
2
3
4
export const routes: Routes = [
{path: 'book', component: BookComponent},
{path: 'book-detail', outlet: 'detail', component: BookDetailComponent},
];

添加router-outlet

app.component.html文件中添加router-outlet:从Angular17开始,可以使用自关闭组件了,也就是说<router-outlet></router-outlet>可以简化为<router-outlet />,注意第二个outlet添加了name属性,用来给outlet命名。

1
2
<router-outlet />
<router-outlet name="detail"/>

添加book组件

1
ng generate component book

book组件的模板文件中添加一个按钮,点击按钮时显示book-detail组件:

打开book.component.html,添加如下代码:

1
2
<p>book works!</p>
<button (click)="openBookDetail()">Book Detail</button>

打开book.component.ts,添加按钮点击事件处理函数,这里调用navigate方法来实现路由跳转。注意该方法的第一个参数是数组,如果要跳转到命名的outlet, 则格式为:

1
[{outlets: {outlet-name: 'path-name'}}]

以下代码中: detail为命名的outletbook-detailpath-name

1
2
3
4
5
6
7
8
export class BookComponent {
constructor(private router: Router) {
}

openBookDetail() {
this.router.navigate([{outlets: {detail: 'book-detail'}}]);
}
}

添加book-detail组件

1
ng generate component book-detail

打开book-detail.component.html,添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
<div class="book-detail">
<h1 class="book-detail-title">Book Detail</h1>
<div class="book-detail-body">
<p>Book name: XXX</p>
<p>Author: XXX</p>
<p>Price: XXX</p>
<p>Category: XXX</p>
<p>Published date: XXX</p>
<p>Quantity: XXX</p>
</div>
<button class="close-button" (click)="closeBookDetail()">X</button>
</div>

打开book-detail.component.ts,添加按钮点击事件处理函数:这里将path设置为null,表示关闭对应的组件。

1
2
3
4
5
6
7
8
export class BookDetailComponent {
constructor(private router: Router) {
}

closeBookDetail() {
this.router.navigate([{outlets: {detail: null}}]);
}
}

运行项目

1
ng serve

打开浏览器,访问http://localhost:4200/book

alt text

点击Book Detail按钮,弹出book-detail组件,此时路由变华为https://localhost:4200/book(detail:book-detail)

  • detail: 表示辅助路由的名称,定义在outlet属性中。
  • book-detail: 表示辅助路由的路径。定义在router文件中。

alt text

点击弹窗上的关闭按钮,关闭book-detail组件,路由恢复为http://localhost:4200/book

同时显示多个辅助路由。

添加一个book-list组件,点击Book List按钮时显示book-list弹窗。

1
ng generate component book-list

打开book-list.component.html文件,添加如下代码:

1
2
3
4
5
6
7
8
9
10
<div class="book-list">
<h1 class="book-list-title">Book List</h1>
<div class="book-list-body">
<p>Book 1</p>
<p>Book 2</p>
<p>Book 3</p>
<p>Book 4</p>
</div>
<button class="close-button" (click)="closeBookList()">X</button>
</div>

打开book-list.component.ts文件,添加如下代码:

1
2
3
4
5
6
7
8
export class BookListComponent {
constructor(private router: Router) {
}

closeBookList() {
this.router.navigate([{outlets: {list: null}}]);
}
}

打开book-list.component.css 添加样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
.book-list {
position: absolute;
top: 30%;
left: 30%;
transform: translate(-30%, -30%);
z-index: 9;
display: flex;
flex-direction: column;
align-items: center;

width: 400px;
height: 400px;

background-color: white;
border-radius: 10px;
border: 1px solid gray;
box-shadow: 0 0 20px rgba(0,0,0,0.6);
}

.close-button {
position: absolute;
top: 16px;
right: 16px;
width: 40px;
}

打开book.component.html文件,添加Book List按钮:

1
2
3
<p>book works!</p>
<button (click)="openBookList()">Book List</button>
<button (click)="openBookDetail()">Book Detail</button>

打开book.component.ts文件,添加按钮点击事件处理函数:

1
2
3
openBookList() {
this.router.navigate([{outlets: {list: 'book-list'}}]);
}

app.routes.ts文件中添加路由配置:

1
2
3
4
5
export const routes: Routes = [
{path: 'book', component: BookComponent},
{path: 'book-list', outlet: 'list', component: BookListComponent},
{path: 'book-detail', outlet: 'detail', component: BookDetailComponent},
];

打开app.component.html文件,添加Book List对应的outlet

1
2
3
<router-outlet />
<router-outlet name="detail"/>
<router-outlet name="list" />

在次运行项目,依次点击Book ListBook Detail按钮,可以同时显示两个弹窗。观察此时路由的变化,注意有多个辅助路由时,按照路由outlet名字的字符串顺序显示,因为detail排在list前面,所以先显示detail,再显示list。无论先点击哪个按钮,路由顺序皆如此。

alt text

http://localhost:4200/book(detail:book-detail//list:book-list)

  • detail: 表示辅助路由的名称,定义在outlet属性中。
  • book-detail: 表示辅助路由的路径。定义在router文件中。
  • list: 表示辅助路由的名称,定义在outlet属性中。
  • book-list: 表示辅助路由的路径。定义在router文件中。
  • //: 用来分隔多个辅助路由。

不更改URL显示辅助路由

默认情况下,点击按钮后,路由会发生变化,URL会显示辅助路由的路径。如果不想更改URL,可以使用skipLocationChange选项。
book.component.ts文件中,添加按钮点击事件处理函数:

1
2
3
4
5
6
openBookList() {
this.router.navigate([{outlets: {list: 'book-list'}}], {skipLocationChange: true});
}
openBookDetail() {
this.router.navigate([{outlets: {detail: 'book-detail'}}], {skipLocationChange: true});
}

注意,如果有多个辅助路由,也要在关闭按钮点击事件处理函数中添加skipLocationChange选项,否则关闭一个弹窗时,另一个弹窗的URL会显示在地址栏中。

1
2
3
4
5
6
closeBookList() {
this.router.navigate([{outlets: {list: null}}], {skipLocationChange: true});
}
closeBookDetail() {
this.router.navigate([{outlets: {detail: null}}], {skipLocationChange: true});
}

再次点击Book ListBook Detail按钮,可以看到URL没有发生变化。依次关闭两个弹窗,URL仍然保持不变。

alt text

使用routerLink显示辅助路由

上面的例子中,我们通过点击按钮,并且在按钮事件处理函数中调用navigate方法来显示辅助路由。也可以使用routerLink来显示辅助路由。
app.component.html文件中,使用routerLink显示辅助路由:

1
2
<a [routerLink]="[{outlets: {list: 'book-list'}}]">Book List</a>
<a [routerLink]="[{outlets: {detail: 'book-detail'}}]">Book Detail</a>

主路由和辅助路由各自独立

前面提起过,主路由和辅助路由是平级关系,二者可自由变化,互补影响,比如我们可以在book组件下添加一个子路由book1,然后在book1下再添加子路由book2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// app.routers.ts
export const routes: Routes = [
{
path: 'book',
component: BookComponent,
children: [
{
path: 'book1',
component: Book1Component,
children: [
{
path: 'book2',
component: Book2Component,
}
]
},
]
},
];
1
2
3
<!-- book.component.html -->
<p>book works!</p>
<router-outlet />
1
2
3
<!-- book1.component.html -->
<p>book1 works!</p>
<router-outlet />
1
2
<!-- book2.component.html -->
<p>book2 works!</p>

此时点击book-detail按钮,观察路由变化,辅助路由自动append到主路由后面,无论主路由的层级有多深。

1
http://localhost:4200/book/book1/book2(detail:book-detail)

同时显示主路由和辅助路由

主路由的outlet nameprimary,我们只需在routerLink或者navigate函数中指定primary即可。

通过routerLink属性触发(浏览器url:http://localhost:4200/book(detail:book-detail))

1
<a [routerLink]="[{outlets: {primary: 'book', detail: 'book-detail'}}]">Book and detail</a>

通过router.navigate方法触发(浏览器url:http://localhost:4200/book(detail:book-detail))

1
this.router.navigate([{outlets: {primary: 'book', detail: 'book-detail'}}]);

如果主路由对应多级path,直接指定即可,如下:(浏览器url:http://localhost:4200/book/book1/book2(detail:book-detail))

1
this.router.navigate([{outlets: {primary: 'book/book1/book2', detail: 'book-detail'}}]);

一次触发多个辅助路由

上面的例子中我们是依次点击按钮来显示辅助路由的,Angular也支持一次触发多个辅助路由,

可以在routerLink中同时定义多个辅助路由,在app.component.html文件中,添加如下代码,当我们点击Book List and Details按钮时,将同时显示book-listbook-detail组件。Url也将变为http://localhost:4200/book(detail:book-detail//list:book-list)

1
2
<!-- app.component.html -->
<a [routerLink]="[{outlets: {list: 'book-list', detail: 'book-detail'}}]">Book List and Details</a>

使用navigate方法

1
2
3
openBookListAndDetail() {
this.router.navigate([{outlets: {list: 'book-list', detail: 'book-detail'}}]);
}

总结

  • 辅助路由的格式:(outletname: path),比如(list:book-list), list对应outlet name, book-list对应path
  • 主路由和辅助路由是平级关系,他们的outlet要定义在一个文件中。比如上面例子中book和book-list,book-detail三者都是平级关系,所以他们的outlet都定义在app.component.html文件中。
  • outlet属性中name用来表示辅助路由的名称,不能包含-,不能是字符串primary,否则无法显示。
  • html文件中如果使用了routerLink,那么同时也要定义outlet,否则无法显示。

Introduction

Component(组件)是Angular中的最小单元,通常来说,一个Component包含以下几个部分:

  1. 选择器
  2. 模板
  3. 样式

这三者中,哪个都不是必须的,但是至少要有模板。
我们可以使用Angular CLI来生成一个Component,比如:

1
ng generate component product

或者

1
ng g c product

生成后的组件如下

1
2
3
4
5
6
7
8
import { Component } from '@angular/core';

@Component({
selector: 'product', // 选择器
templateUrl: './product.component.html', // 模板
styleUrls: ['./product.component.less'] // 样式
})
export class ProductComponent {}

选择器

选择器是用来引用当前Component的,比如其他组件若想使用Product组件的话,那么可以在其他组件的模板中使用<product></product>来引用Product组件。选择器是给别人用的,组件本身并不使用自己的选择器。

1
2
<!-- other.component.html -->
<product></product>

Angular中的选择器是CSS选择器,可以是标签选择器、类选择器、属性选择器等等, 一般来说, 组件通常使用标签选择器,指令通常使用属性选择器, 其他类型的选择器使用较少, 但是也是可以的。比如使用类选择器来实现Product组件:

1
2
3
4
5
6
// product.component.ts
@Component({
selector: '.product',
templateUrl: './product.component.html',
styleUrls: ['./product.component.less']
})
1
2
<!-- other.component.html -->
<div class="product"></div>

运行效果是一样的, 选择器本质是Directive上的属性,而ComponentDirective的一个特例, 所以Component也可以使用选择器。关于选择器的详情,请看这里

注意: 选择器不是必须的,一个组件可以没有选择器,这样的组件只能通过路由来访问,或者动态加载。

模板

模板是Component的视图,可以是HTML文件模板(比如上面的Product组件),也可以是内联模板。内联模板一般使用模版字符串(``),因为模板字符串可以支持多行。

1
2
3
4
@Component({
selector: 'product',
template: `<div>Product works</div>`
})

这样的话,就不需要product.component.html文件了。通常来说,如果模板的内容较多,还是存放到文件中比较好,这样方便维护。

样式

样式是Component的样式,Angular支持多文件样式,单文件样式以及内联样式:

多文件样式

1
2
3
4
5
@Component({
selector: 'product',
templateUrl: './product.component.html',
styleUrls: ['./product.component.less', './product.item.css']
})

单文件样式

1
2
3
4
5
@Component({
selector: 'product',
templateUrl: './product.component.html',
styleUrl: './product.component.less'
})

内联样式

1
2
3
4
5
@Component({
selector: 'product',
templateUrl: './product.component.html',
styles: ['div { color: red; }']
})

这样的话,就不需要product.component.less文件了。通常来说,如果样式较多,还是存放到文件中比较好, 样式是依附于模板的,如果一个组件没有模板,那么也就没有样式。

对于一个Component来说:

  1. 只有component.ts文件是必须的。
  2. component.htmlcomponent.less是可选的, 可以使用内联模板和内联样式代替,也可以干脆没有。
  3. 可以没有selector(选择器),但是这样的Component只能通过路由来访问,或者动态加载。

一个极简组件:这个组件什么也不显示,没有样式,也没有选择器。只能在路由中使用。可以在ngOnInit中添加一些逻辑,比如用来弹出popup等。

1
2
3
@Component({
template: ''
})

component vs module

在Angular中,非Standalone组件必须隶属于一个NgModule,也就是必须在一个module的declarations中声明,否则无法使用该组件。
非Standalone组件至多能在一个module中声明,如果其他module也要使用这个组件,那么需要将这个组件声明在其module的exports中,然后在其他module中引入这个module。

Introduction

Use source map explorer to analyze the bundle size of a React app.

Steps

1. Install source-map-explorer

1
2
npm install -g source-map-explorer
yarn global add source-map-explorer

Turn on source map

In package.json, add the following line to the build script:

1
"build": "GENERATE_SOURCEMAP=true react-scripts build"

If this doesn’t work, open environment config file .env.production and add the following line:

1
GENERATE_SOURCEMAP=true

Don’t forget to set it to false after you finish analyzing the bundle size. source map should never be enabled in production.

2. Build the React app

1
2
npm run build
yarn build

3. Analyze the bundle size

1
source-map-explorer build/static/js/*.js

The output will show you the size of each file in the bundle, and the size of each dependency.

Introduction

Data Binding是Angular中最重要的概念之一。它是Angular中组件和模板间的通信机制。Angular中有四种数据绑定方式,注意数据绑定都发生在html模板中。

Interpolation

字符串插值是Angular中最简单的数据绑定形式。它使用双大括号{{}}将模板表达式包裹在其中。模板表达式是一段JavaScript代码,它会被Angular解析并插入到HTML中。

1
2
<!--app.component.html-->
<p>Hello, I'm {{name}}</p>
1
2
3
4
5
6
7
8
//app.component.ts
@Component({
//...
templateUrl: './app.component.html',
})
export class AppComponent {
name = 'Philip';
}

需要注意的是{{}}仅支持简单值的插值,比如数字,字符串,布尔值等。如果要插入对象,数组等复杂值,需要使用属性绑定。

Interpolation的数据流向是从组件传递到模板,不能反向传递。

Property Binding

Angular中属性绑定可以为HTML元素的属性赋值。属性绑定使用如下语法:

1
[property]="expression"

当Angular编译器遇到[property]时,它会对右侧的表达式expression进行求值,并将结果赋值给property属性。

比如我们要为一个图片的src属性赋值,可以使用如下代码,这里srcimg标签的一个属性,imageUrl又是组件的属性,属性绑定因此得名。

1
2
<!--app.component.html-->
<img [src]="imageUrl" alt="Angular Logo">
1
2
3
4
//app.component.ts
export class AppComponent {
imageUrl = 'https://www.angular.io/assets/images/angular.png';
}

属性绑定的数据流向是从组件传递到模板,不能反向传递。

Event Binding

事件绑定是Angular中将模板中的事件和组件中的方法关联起来的机制。事件绑定使用如下语法:

1
(event)="eventHandler()"

其中event是一个事件名,比如clickkeydownmousemove等等。eventHandler是组件中的某个方法,用来处理该事件。

下面的代码用于绑定buttonclick事件,当用户点击按钮时,Angular会调用组件中的updateName方法来更新name属性。

1
2
3
<!--app.component.html-->
<p>Hello, I'm {{name}}</p>
<button (click)="updateName('Philip Zhang')">Update name</button>
1
2
3
4
5
6
7
//app.component.ts
export class AppComponent {
name = 'Philip';
updateName(newName: string) {
this.name = newName;
}
}

在确定事件目标(event target)时,Angular首先判断指令是否有一个输出属性,如果有,Angular会调用这个输出属性,如果没有,Angular会调用DOM元素的事件。

比如下面这段代码,clickable对应一个自定义指令ClickDirective。Angular在处理myClick事件时,会首先检查ClickDirective是否有myClick输出属性,如果没有,会调用DOM元素button上的myClick事件。 — 注意myClick在这里只是一个名字,可以泛指任何事件。

1
2
<h4>myClick is an event on the custom ClickDirective:</h4>
<button type="button" (myClick)="clickHandler" clickable>click with myClick</button>

事件绑定的数据流向是从模板传递到组件,不能反向传递。

Two-way Binding

双向数据绑定是Angular中最强大的数据绑定方式,双向数据绑定使用如下语法:Angular团队给这个语法起了一个非常形象的名字,叫banana in a box。这个语法是将属性绑定和事件绑定结合在一起,实现了数据的双向绑定。

1
[(ngModel)]="property"

双向数据绑定的数据流向是双向的:

  1. 当组件内的数据变化时,页面上的内容跟着变化。
  2. 反之,当页面内容变化时,组件内的数据也会跟着变化。

下面是一个双向绑定的例子,当用户在input框中输入内容时,name属性会跟着变化,页面上显示的内容也会跟着变化。初始化的时候,name属性的值是Philip,页面上输入框内显示的内容也是Philip

1
2
3
<!--app.component.html-->
<p>Hello, I'm {{name}}</p>
<input [(ngModel)]="name" />

注意:使用双向绑定时,必须导入FormsModule模块。

1
2
3
4
5
6
7
8
9
10
11
//app.component.ts
@Component({
//...
templateUrl: './app.component.html',
imports: [
FormsModule // this is must be.
]
})
export class AppComponent {
name = 'Philip';
}

需要注意:使用双向绑定的ngModel指令时,必须在imports数组中导入FormsModule模块。

下图显示了Angular中四种数据绑定方式的数据流向。

angular-data-binding

  • [ ] - property binding
  • {{ }} - interpolation
  • ( ) - event binding
  • [( )] - two-way binding

Two-way binding between components

双向数据绑定不仅可以在组件内部使用,还可以在组件之间使用。比如下面的例子,我们定义了一个Counter组件,为了方便我们把所有代码都写到了一个文件里面。
Counter组件中的count是一个Input属性,由父组件传入。当用户点击增加或者减少按钮时,Counter组件会调用updateCount方法,更新count属性,并且通过changeCount事件将新的count值传递给父组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import {Component, EventEmitter, Input, Output} from '@angular/core';

@Component({
selector: 'app-counter',
standalone: true,
imports: [],
template: `
<button (click)="updateCount(-1)">-</button>
<span>{{ count }}</span>
<button (click)="updateCount(+1)">+</button>
`
})
export class CounterComponent {
@Input() count = 0;
@Output() countChange = new EventEmitter<number>();

updateCount(value: number) {
this.count += value;
this.countChange.emit(this.count);
}
}

需要注意的是:这里面的Output函数必须是xxxChange的形式,xxx是Input属性的名字,比如代码中的count。那么对应的Output属性就是countChange
这是Angular的约定。如果不是这个形式,Angular会认为这是一个普通的输出属性,而不是双向绑定的输出属性。(实测这种情况IDE会报错,但仍能运行)。

在父组件中调用Counter组件:

1
2
3
4
5
6
7
8
@Component({
// ...
template: `<app-counter [(count)]="initialCount"></app-counter>`,
imports: [CounterComponent]
})
export class AppComponent {
initialCount = 5;
}

@Input and @Output

Angular中的@Input和@output和属性绑定和事件绑定有些类似,但是它们是用来在父子组件之间传递数据的。@Input用来从父组件向子组件传递数据,@Output用来从子组件向父组件传递数据。
上面的例子中已经介绍了,词不赘述。

References

  1. https://angular.dev/guide/templates/binding

Introduction

<select> is a form control element that allows users to select an option from a list of options. It is used to create a drop-down list. <option> is a child element of <select>, which represents an option in a drop-down list.

Example

1
2
3
4
5
<select id="my_select">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>

Default value

You can set default value to a select by adding selected to the option element.

1
2
3
4
5
<select id="my_select">
<option value="1">1</option>
<option value="2" selected>2</option> <!-- default value -->
<option value="3">3</option>
</select>

Use ul/li to create a select list

Its 2024 now, I still can’t set styles to option like padding and margin. The only way to create a custom select list is to use ul and li elements.

1
2
3
4
5
<ul id="my_select">
<li value="1">1</li>
<li value="2">2</li>
<li value="3">3</li>
</ul>

Reference

https://stackoverflow.com/a/22681234/1487475

未完,待续。。。

Install Node.js

Download and install Node.js.

Install Angular CLI

1
npm install -g @angular/cli

Create Angular project

1
ng new my-app

If you want to project with specific angular version, use the following command:

1
npx @angular/cli@15 new angular15-app

Start project

1
2
cd my-app
ng serve

Open browser

Open browser and navigate to http://localhost:4200/.

Create angular app with specific version

1
npx @angular/cli@15 new angular15-app

References:

今天从源码的角度来看一下Angular是如何启动一个应用的。

创建Angular项目

首先,参照这篇文章创建一个Angular项目

源码分析

关于如何阅读Angular源码,请参考这里如何阅读Angular源码

使用VSCode或者WebStorm打开项目,找到src/main.ts文件,这是Angular应用的入口文件。可以看到如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

这里调用platformBrowserDynamic().bootstrapModule来启动Angular项目,我们到源码里面搜索bootstrapModule,找到如下代码:

packages/core/src/platform/platform_ref.ts

1
2
3
4
5
6
7
8
9
10
11
bootstrapModule<M>(
moduleType: Type<M>,
compilerOptions:
| (CompilerOptions & BootstrapOptions)
| Array<CompilerOptions & BootstrapOptions> = [],
): Promise<NgModuleRef<M>> {
const options = optionsReducer({}, compilerOptions);
return compileNgModuleFactory(this.injector, options, moduleType).then((moduleFactory) =>
this.bootstrapModuleFactory(moduleFactory, options), // <------- Here
);
}

bootstrapModule方法内部调用了 bootstrapModuleFactory 方法,我们继续搜索bootstrapModuleFactory,找到如下代码:

packages/core/src/platform/platform_ref.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bootstrapModuleFactory<M>(
moduleFactory: NgModuleFactory<M>,
options?: BootstrapOptions,
): Promise<NgModuleRef<M>> {
// Note: We need to create the NgZone _before_ we ins
// as instantiating the module creates some providers
// So we create a mini parent injector that just cont
// pass that as parent to the NgModuleFactory.
const ngZone = getNgZone(
options?.ngZone,
getNgZoneOptions({
eventCoalescing: options?.ngZoneEventCoalescing,
runCoalescing: options?.ngZoneRunCoalescing,
}),
);

// Note: Create ngZoneInjector within ngZone.run so that all of the instantiated services are
// created within the Angular zone
// Do not try to replace ngZone.run with ApplicationRef#run because ApplicationRef would then be
// created outside of the Angular zone.
return ngZone.run(() => {...}); // <------- Here
}

注意bootstrapModuleFactory这个方法比较长,我们简化一下,只截取其中的关键部分,首先这里创建了一个ngZone,用来做更新检测。然后调用了ngZone.run方法,这个方法是NgZone的方法,用来运行一个函数,并且确保这个函数在Angular的Zone中运行。在return ngZone.run()方法中的最后,又调用了_moduleDoBootstrap

packages/core/src/platform/platform_ref.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private _moduleDoBootstrap(moduleRef: InternalNgModuleRef<any>): void {
const appRef = moduleRef.injector.get(ApplicationRef);
if (moduleRef._bootstrapComponents.length > 0) {
moduleRef._bootstrapComponents.forEach((f) => appRef.bootstrap(f)); // <------- Here
} else if (moduleRef.instance.ngDoBootstrap) {
moduleRef.instance.ngDoBootstrap(appRef);
} else {
throw new RuntimeError(
RuntimeErrorCode.BOOTSTRAP_COMPONENTS_NOT_FOUND,
ngDevMode &&
`The module ${stringify(moduleRef.instance.constructor)} was bootstrapped, ` +
`but it does not declare "@NgModule.bootstrap" components nor a "ngDoBootstrap" method. ` +
`Please define one of these.`,
);
}
this._modules.push(moduleRef);
}

在该方法中,对于每个声明的bootstrap组件,都会调用appRef.bootstrap方法,这个方法是ApplicationRef的方法,用来启动一个组件。通常Angular程序只有一个bootstrap组件,就是AppComponent
再来看appRef.bootstrap方法:

packages/core/src/application/application_ref.ts

1
2
3
4
5
6
7
8
9
10
11
bootstrap<C>(
componentOrFactory: ComponentFactory<C> | Type<C>,
rootSelectorOrNode?: string | any,
): ComponentRef<C> {
// ...
const selectorOrNode = rootSelectorOrNode || componentFactory.selector;
const compRef = componentFactory.create(Injector.NULL, [], selectorOrNode, ngModule); // 1
// ...
this._loadComponent(compRef); // 2
// ...
}

这个方法同样很大,如前,这里省略一些细枝末节,直接展示最重要的代码。

  1. 首先根据bootstrap方法的第一个参数(通常是AppComponent)创建component。
  2. 然后调用内部方法_loadComponent来加载组件。

继续看_loadComponent方法:
packages/core/src/application/application_ref.ts

1
2
3
4
5
private _loadComponent(componentRef: ComponentRef<any>): void {
this.attachView(componentRef.hostView); // 1
this.tick(); // 2
// ...
}
  1. 首先调用attachView方法, 将组件的视图添加到视图树中。
  2. 然后调用tick方法,用来触发变更检测。

tick后续的方法调用如下:
tick -> _tick -> detectChangesInAttachedViews -> detectChangesInViewIfRequired -> detectChangesInternal

再看detectChangesInternal方法:
packages/core/src/render3/instructions/change_detection.ts

未完待续。。。