以下内容基于Angular 文档中文版的学习
目录
HTML标签中{{}}插入值,[]绑定属性,()绑定事件,[(ngModel)]双向绑定
使用Angular CLI 工具创建项目
npm install -g @angular/cli@11.2.14
ng new my-app
cd my-app
ng serve --open
ng build 将Angular应用程序编译到输出目录。
ng serve 构建并提供应用程序,并在修改文件时重新构建。
ng generate 根据方案生成或修改文件。
ng generate module app-routing --flat --module=app
在当前目录生成app-routing模块并注册到AppModule中
ng generate module heroes/heroes --module app --flat --routing
在 heroes 目录下创建一个带路由的 HeroesModule,并把它注册到根模块 AppModule 中
ng generate component product-alerts
ng generate component customer-dashboard/CustomerDashboard
ng generate service cart
ng test 在特定项目上运行单体测试。
ng e2e 构建并提供Angular应用程序,并运行端到端测试。
HTML标签中{{}}插入值,[]绑定属性,()绑定事件,[(ngModel)]双向绑定
绑定属性
[property] = "variable
property = "{{variable}}"
[attr.property] = "xxx"
attribute 是由 HTML 定义的。property 是由 DOM (Document Object Model) 定义的。
少量 HTML attribute 和 property 之间有着 1:1 的映射,如id。
有些 HTML attribute 没有对应的 property,如rowspan。
有些 DOM property 没有对应的 attribute,如textContent。
property: [colSpan] , attribute: [attr.colspan]
angular不推荐在初始化后更改 attribute 的值,会报警告 。
类和样式绑定
绑定到单个 CSS class
[class.sale]="onSale"
当绑定表达式 onSale 为真值时,Angular 会添加类,当表达式为假值时,它会删除类 —— undefined 除外。
绑定到多个 CSS 类
[class]="classExpression"
表达式可以是以下之一:
用空格分隔的类名字符串 "my-class-1 my-class-2 my-class-3"
以类名作为键名并将真或假表达式作为值的对象 {foo: true, bar: false}
类名的数组 ['foo', 'bar']
对于任何类似对象的表达式(比如 object、Array、Map 或 Set,必须更改对象的引用,Angular 才能更新类列表。在不更改对象引用的情况下只更新其 Attribute 是不会生效的。
绑定到单一样式
<nav [style.background-color]="expression"></nav>
<nav [style.backgroundColor]="expression"></nav>
绑定到多个样式
[style]="styleExpression"
styleExpression 可以是如下格式之一:
样式的字符串列表,比如 "width: 100px; height: 100px; background-color: cornflowerblue;"
一个对象,其键名是样式名,其值是样式值,比如 {width: '100px', height: '100px', backgroundColor: 'cornflowerblue'}
注意,不支持把数组绑定给 [style]
当把 [style] 绑定到对象表达式时,该对象的引用必须改变,这样 Angular 才能更新这个类列表。在不改变对象引用的情况下更新其属性值是不会生效的。
事件绑定
<button (click)="onSave($event)">Save</button>
<button type="button" (myClick)="clickMessage=$event" clickable>click with myClick</button>
绑定到键盘事件
可以指定要绑定到键盘事件的键值或代码。它们的 key 和 code 字段是浏览器键盘事件对象的原生部分。默认情况下,事件绑定假定你要使用键盘事件上的 key 字段。你还可以用 code 字段。
键的组合可以用点(.)分隔。例如, keydown.enter 将允许你将事件绑定到 enter 键。你还可以用修饰键,例如 shift 、 alt 、 control 和 Mac 中的 command 键。
<input (keydown.shift.t)="onKeydown($event)" />
根据操作系统的不同,某些组合键可能会创建特殊字符,而不是你期望的组合键。
例如,当你同时使用 option 和 shift 键时,MacOS 会创建特殊字符。如果你绑定到 keydown.shift.alt.t ,在 macOS 上,该组合会生成 ˇ 而不是 t ,它与绑定不匹配,也不会触发你的事件处理程序。
要绑定到 macOS 上的 keydown.shift.alt.t ,请使用 code 键盘事件字段来获取正确的行为。
<input (keydown.code.shiftleft.altleft.keyt)="onKeydown($event)" />
双向绑定
Angular 的双向绑定语法是方括号和圆括号的组合 [()]。[] 进行属性绑定,() 进行事件绑定
为了使双向数据绑定有效,@Output() 属性的名字必须遵循 inputChange 模式,其中 input 是相应 @Input() 属性的名字。比如,如果 @Input() 属性为 size,则 @Output() 属性必须为 sizeChange。
子:
@Input() size!: number | string;
@Output() sizeChange = new EventEmitter<number>();
resize(delta: number) {
this.size = Math.min(40, Math.max(8, +this.size + delta));
this.sizeChange.emit(this.size);
}
父:
<app-sizer [(size)]="fontSizePx"></app-sizer>
等同于
<app-sizer [size]="fontSizePx" (sizeChange)="fontSizePx=$event"></app-sizer>
表单中的双向绑定
因为没有任何原生 HTML 元素遵循了 x 值和 xChange 事件的命名模式,所以与表单元素进行双向绑定需要使用 NgModel
循环
<div *ngFor="let product of products"></div>
IF
<p *ngIf="product.description"></p>
定义输入属性
子:
@Input() product!: Product;
父:
<app-product-alerts [product]="product"></app-product-alerts>
定义输出事件
子:
@Output() notify = new EventEmitter();
<button type="button" (click)="notify.emit()">Notify Me</button>
父:
<app-product-alerts [product]="product" (notify)="onNotify()"></app-product-alerts>
特殊符号
?. 用来检查问号前面的变量是否为null或者undefined时,程序不会出错;
{{currentHero?.name}}
!. 用来检查感叹号后面的变量为null或者undefined时,程序不会出错
{{hero!.name}}
?: 可以把某个属性声明为可选的。选参数与默认参数一定要放在必选参数之后声明。
interface Person {
name: string;
age?: number;
}
!: 断言属性为non-null,non-undefined,开启了strictNullChecks时不会报相应的警告
hero!: Hero
$: 声明属性为Observable流对象
heroes$!: Observable<Hero[]>
?? 空值合并运算符,当左侧操作数为 null 或 undefined 时,其返回右侧的操作数,否则返回左侧的操作数。
const v1 = expr1 ?? expr2
!! 强制转化为bool值
let flag1 = !!message
模板引用变量
Angular 根据你所声明的变量的位置给模板变量赋值:
如果在组件上声明变量,该变量就会引用该组件实例。
如果在标准的 HTML 标记上声明变量,该变量就会引用该元素。
如果你在 <ng-template> 元素上声明变量,该变量就会引用一个 TemplateRef 实例来代表此模板。
<input #phone placeholder="phone number" />
<button type="button" (click)="callPhone(phone.value)">Call</button>
指定名称的变量
如果该变量在右侧指定了一个名字,比如 #var="ngModel",那么该变量就会引用所在元素上具有这个 exportAs 名字的指令或组件。
<form #itemForm="ngForm" (ngSubmit)="onSubmit(itemForm)">
<label for="name">Name</label>
<input type="text" id="name" class="form-control" name="name" ngModel required />
<button type="submit">Submit</button>
</form>
<div [hidden]="!itemForm.form.valid">
<p>{{ submitMessage }}</p>
</div>
模板输入变量
<ul>
<ng-template ngFor let-hero let-i="index" [ngForOf]="heroes">
<li>Hero number {{i}}: {{hero.name}}
</ng-template>
</ul>
页面跳转(路由)
路由表定义
路由的顺序很重要,因为 Router 在匹配路由时使用“先到先得”策略,所以应该在不那么具体的路由前面放置更具体的路由。
首先列出静态路径的路由,然后是一个与默认路由匹配的空路径路由。通配符路由是最后一个,因为它匹配每一个 URL,只有当其它路由都没有匹配时,Router 才会选择它。
RouterModule.forRoot([
{ path: 'productList', component: ProductListComponent, title: 'Product List' },
{ path: 'products/:productId', component: ProductDetailsComponent },
{ path: 'cart', component: CartComponent },
{ path: 'first-component', component: FirstComponent,
children: [{ path: 'child-a', component: ChildAComponent },
{ path: 'child-b', component: ChildBComponent } ]},
{ path: '', redirectTo: '/productList', pathMatch: 'full' },
{ path: '**', component: PageNotFoundComponent },
])
路由模块占位
<router-outlet></router-outlet>
跳转
链接跳转
<a [routerLink]="['/products', product.id]">{{ product.name }}</a>
<a [routerLink]="['/heroes', { id: heroId, foo: 'foo' }]">Crisis Center</a>
参数对象作为可选参数拼接到URL中,变成/heroes;id=15;foo=foo
<a routerLink="/cart" routerLinkActive="activebutton" ariaCurrentWhenActive="page">Cart</a>
通过添加 routerLinkActive 指令,可以通知你的应用把一个特定的 CSS 类应用到当前的活动路由中。
代码跳转
this.router.navigate(['/heroes', { id: heroId, foo: 'foo' }]);
// 跳转添加信息
// ../list?page=1&name=ab#top
this.router.navigate(['../list'], {
relativeTo: this.route, // 指定要用于相对导航的根 URI
queryParams: { page: 1 }, // 将查询参数设置为 URL
fragment: 'top', // 设置 URL 的哈希片段
queryParamsHandling: "merge", // 如果处理查询参数:preserve :保留当前参数 / merge :将新参数与当前参数合并
preserveFragment: false, // 当为 true 时,为下一个导航保留 URL 片段
});
参数接收
constructor(private route: ActivatedRoute){ }
ngOnInit() {
// 第一种方式:如果不会发生自页面跳转,使用第一次的参数
const routeParams = this.route.snapshot.paramMap;
const productIdFromRoute = Number(routeParams.get('productId'));
// 第二种方式:会发生自页面跳转,组件会被复用,参数会发生变化,需要使用Observable监视参数变化
this.hero$ = this.route.paramMap.pipe(
switchMap((params: ParamMap) =>
this.service.getHero(params.get('id')!))
);
// 接收查询参数
this.sessionId = this.route.queryParamMap.pipe(map(params => params.get('session_id') || 'None'));
// 接收书签名
this.token = this.route.fragment.pipe(map(fragment => fragment || 'None'));
}
自定义标题策略
@Injectable({providedIn: 'root'})
export class TemplatePageTitleStrategy extends TitleStrategy {
constructor(private readonly title: Title) {
super();
}
override updateTitle(routerState: RouterStateSnapshot) {
const title = this.buildTitle(routerState);
if (title !== undefined) {
this.title.setTitle(`My Application | ${title}`);
}
}
}
@NgModule(...
providers: [
{provide: TitleStrategy, useClass: TemplatePageTitleStrategy},
]
})
路由守卫
使用路由守卫来防止用户未经授权就导航到应用的某些部分。守卫返回一个值,以控制路由器的行为:
true 导航过程会继续
false 导航过程就会终止,且用户留在原地。
UrlTree 取消当前导航,并开始导航到所返回的 UrlTree
Angular 中提供了以下路由守卫:
canActivate 需要身份验证
canActivateChild 保护子路由
canDeactivate 处理未保存的更改
canMatch 根据应用程序中的条件控制 Route 匹配
resolve 预先获取组件数据
canLoad 保护对特性模块的未授权加载
在分层路由的每个级别上,你都可以设置多个守卫。
路由器会先按照从最深的子路由由下往上检查的顺序来检查 canDeactivate() 守卫。
然后它会按照从上到下的顺序检查 canActivate() 守卫。
如果特性模块是异步加载的,在加载它之前还会检查 canLoad() 守卫。
除 canMatch 之外,如果任何一个守卫返回 false,其它尚未完成的守卫会被取消,这样整个导航就被取消了。如果 canMatch 守卫返回 false,那么 Router 就会继续处理这些 Routes 的其余部分,以查看是否有别的 Route 配置能匹配此 URL。
canActivate示例
export const yourGuard: CanActivateFn = (
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot) => {
// your logic goes here
}
{ path: '/your-path', component: YourComponent, canActivate: [yourGuard]}
这里是使用的函数,也可以是实现了CanActivate接口的类
LocationStrategy 和浏览器的网址样式
路由器通过两种 LocationStrategy 提供者来支持所有这些风格:
PathLocationStrategy 默认的 “HTML 5 pushState” 风格
HashLocationStrategy “hash URL”风格
RouterModule.forRoot() 函数把 LocationStrategy 设置成了 PathLocationStrategy,使其成为了默认策略。
你还可以在启动过程中改写(override)它,来切换到 HashLocationStrategy 风格。
RouterModule.forRoot(routes, { useHash: true }) // .../#/crisis-center/
自定义路由匹配器
// 添加匹配器
RouterModule.forRoot({
matcher: (url) => {
if (url.length === 1 && url[0].path.match(/^@[\w]+$/gm)) {
return {
consumed: url,
posParams: {
username: new UrlSegment(url[0].path.slice(1), {})
}
};
}
return null;
},
component: ProfileComponent
})
// ProfileComponent读取参数
username$ = this.route.paramMap
.pipe(
map((params: ParamMap) => params.get('username'))
);
// 路由链接
<a routerLink="/@Angular">my profile</a>
用命名出口(outlet)显示多重路由
路由表信息填写outlet名称
{ path: 'compose', component: ComposeMessageComponent, outlet: 'popup' },
跳转链接
<a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>
路由占位
<router-outlet name="popup"></router-outlet>
跳转后浏览器URL,括号内为第二路由
http://…/crisis-center(popup:compose)
清除第二路由的显示内容
this.router.navigate([{ outlets: { popup: null }}]);
管道处理
管道是在模板表达式中使用的简单函数,用于接受输入值并返回转换后的值。
管道用于数据转换,用法:{{xxxx | 管道1:管道参数1 | 管道2:管道参数2}}
例如:
{{ product.price | currency }}
{{ product.price | currency:'CAD':'symbol':'4.2-2':'fr' }}
{{ dateTime | date:'yyyy-MM-dd HH:mm:ss'}}
管道操作符要比三目运算符(?:)的优先级高,这意味着 a ? b : c | x 会被解析成 a ? b : (c | x)
官方管道:
String -> String
UpperCasePipe
LowerCasePipe
TitleCasePipe
Number -> String
DecimalPipe
PercentPipe
CurrencyPipe
Object -> String
JsonPipe
DatePipe
Tools
SlicePipe
AsyncPipe
Async pipe 负责订阅和变化检测,以及在组件被销毁时取消订阅。
定义:
shippingCosts!: Observable<{ type: string, price: number }[]>;
输出:
<div class="shipping-item" *ngFor="let shipping of shippingCosts | async">
I18nPluralPipe
I18nSelectPipe
HttpClient
使用例
app.module
import { HttpClientModule } from '@angular/common/http';
@NgModule({
imports: [
BrowserModule,
HttpClientModule,
服务类
import { HttpClient } from '@angular/common/http';
constructor(private http: HttpClient) {}
// 取得列表
this.http.get<{type: string, price: number}[]>('/assets/shipping.json');
httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
// 更新
return this.httpClient.put(this.heroesUrl, hero, this.httpOptions).pipe(
tap(_ => this.log(`updated hero id=${hero.id}`)),
catchError(this.handleError<any>('updateHero'))
);
// 添加
return this.httpClient.post<Hero>(this.heroesUrl, hero, this.httpOptions).pipe(
tap((newHero: Hero) => this.log(`added hero w/ id=${newHero.id}`)),
catchError(this.handleError<Hero>('addHero'))
);
// 删除
return this.httpClient.delete<Hero>(url, this.httpOptions).pipe(
tap(_ => this.log(`deleted hero id=${id}`)),
catchError(this.handleError<Hero>('deleteHero'))
);
// 搜索
return this.httpClient.get<Hero[]>(`${this.heroesUrl}/?name=${term}`).pipe(
tap(_ => this.log(`found heroes matching "${term}"`)),
catchError(this.handleError<Hero[]>('searchHeroes', []))
);
// 错误处理
private handleError<T>(operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
// TODO: リモート上のロギング基盤にエラーを送信する
console.error(error); // かわりにconsoleに出力
// TODO: ユーザーへの開示のためにエラーの変換処理を改善する
this.log(`${operation} failed: ${error.message}`);
// 空の結果を返して、アプリを持続可能にする
return of(result as T);
};
}
从服务器请求数据
使用 HttpClient.get() 方法从服务器获取数据。
该异步方法会发送一个 HTTP 请求,并返回一个 Observable,它会在收到响应时发出所请求到的数据。
返回的类型取决于你调用时传入的 observe 和 responseType 参数。
get() 方法有两个参数。要获取的端点 URL,以及一个可以用来配置请求的选项对象。
options: {
headers?: HttpHeaders | {[header: string]: string | string[]},
observe?: 'body' | 'events' | 'response', // 默认body
params?: HttpParams|{[param: string]: string | number | boolean | ReadonlyArray<string | number | boolean>},
reportProgress?: boolean,
responseType?: 'arraybuffer'|'blob'|'json'|'text', // 默认json
withCredentials?: boolean,
}
observe 和 response 选项的类型是字符串的联合类型,而不是普通的字符串。
正确:client.get('/foo', {responseType: 'text'})
错误:TypeScript 会把 options 的类型推断为 {responseType: string}。
const options = {
responseType: 'text',
};
client.get('/foo', options)
正确:使用 as const,可以让 TypeScript 知道确实想使用常量字符串类型
const options = {
responseType: 'text' as const,
};
client.get('/foo', options);
读取完整的响应体
getConfigResponse(): Observable<HttpResponse<Config>> {
return this.http.get<Config>(
this.configUrl, { observe: 'response' });
}
this.configService.getConfigResponse().subscribe(resp => {
// display its headers
const keys = resp.headers.keys();
this.headers = keys.map(key =>
`${key}: ${resp.headers.get(key)}`);
// access the body directly, which is typed as `Config`.
this.config = { ...resp.body! };
});
发起 JSONP 请求
在 Angular 中,通过在 NgModule 的 imports 中包含 HttpClientJsonpModule 来使用 JSONP。
searchHeroes(term: string): Observable {
term = term.trim();
const heroesURL = `${this.heroesURL}?${term}`;
// 'callback'是服务端接收的要包含回调函数名的参数名称
return this.http.jsonp(heroesUrl, 'callback').pipe(
catchError(this.handleError('searchHeroes', [])) // then handle the error
);
}
this.searchHeroes('王').subscribe(data => {
this.data = data;
})
处理请求错误
private handleError(error: HttpErrorResponse) {
if (error.status === 0) {
// 出现客户端或网络错误。相应地处理。
console.error('An error occurred:', error.error);
} else {
// 后端返回了一个不成功的响应代码。
// 响应主体可能包含出现问题的线索。
console.error(
`Backend returned code ${error.status}, body was: `, error.error);
}
// Return an observable with a user-facing error message.
return throwError(() => new Error('Something bad happened; please try again later.'));
}
getConfig() {
return this.http.get<Config>(this.configUrl)
.pipe(
retry(3), // 最多重试3次失败的请求
catchError(this.handleError) // 然后处理错误
);
}
拦截请求和响应
借助拦截机制,你可以声明一些拦截器,它们可以检查并转换从应用中发给服务器的 HTTP 请求。
这些拦截器还可以在返回应用的途中检查和转换来自服务器的响应。多个拦截器构成了请求/响应处理器的双向链表。
拦截器可以用一种常规的、标准的方式对每一次 HTTP 的请求/响应任务执行从认证到记日志等很多种隐式任务。
Angular 会按你提供拦截器的顺序应用它们。
请求时:HttpClient->拦截器1->拦截器2...->拦截器N->HttpBackend->服务器
响应时:HttpClient<-拦截器1<-拦截器2...<-拦截器N<-HttpBackend<-服务器
如果必须修改请求体,请执行以下步骤。
复制请求体并在副本中进行修改。
使用 clone() 方法克隆这个请求对象。
用修改过的副本替换被克隆的请求体。
示例:
// 设置默认请求头
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private auth: AuthService) {}
intercept(req: HttpRequest<any>, next: HttpHandler) {
const authToken = this.auth.getAuthorizationToken();
// Clone the request and replace the original headers with
// cloned headers, updated with the authorization.
const authReq = req.clone({
headers: req.headers.set('Authorization', authToken)
});
return next.handle(authReq);
}
}
// 请求响应日志
@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
constructor(private messenger: MessageService) {}
intercept(req: HttpRequest<any>, next: HttpHandler) {
const started = Date.now();
let ok: string;
// extend server response observable with logging
return next.handle(req)
.pipe(
tap({
// Succeeds when there is a response; ignore other events
next: (event) => (ok = event instanceof HttpResponse ? 'succeeded' : ''),
// Operation failed; error is an HttpErrorResponse
error: (error) => (ok = 'failed')
}),
// Log when response observable either completes or errors
finalize(() => {
const elapsed = Date.now() - started;
const msg = `${req.method} "${req.urlWithParams}" ${ok} in ${elapsed} ms.`;
this.messenger.add(msg);
})
);
}
}
// 可以在单独文件中引入所有拦截器
export const httpInterceptorProviders = [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true },
];
// 加到 AppModule 的 providers array 中
providers: [ httpInterceptorProviders ],
将元数据传递给拦截器
HttpClient 请求包含一个上下文,该上下文可以携带有关请求的元数据。
该上下文可供拦截器读取或修改,尽管发送请求时它并不会传输到后端服务器。
这允许应用程序或其他拦截器使用配置参数来标记这些请求,比如重试请求的次数。
创建上下文令牌
export const RETRY_COUNT = new HttpContextToken(() => 3);
HttpContextToken 创建期间传递的 lambda 函数 () => 3 有两个用途:
它允许 TypeScript 推断此令牌的类型:HttpContextToken<number>。这个请求上下文是类型安全的 —— 从请求上下文中读取令牌将返回适当类型的值。
它会设置令牌的默认值。如果尚未为此令牌设置其他值,那么这就是请求上下文返回的值。使用默认值可以避免检查是否已设置了特定值。
在发起请求时设置上下文值
this.httpClient.get('/data/feed', {
context: new HttpContext().set(RETRY_COUNT, 5),
}).subscribe(results => {/* ... */});
在拦截器中读取上下文值
export class RetryInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const retryCount = req.context.get(RETRY_COUNT);
return next.handle(req).pipe(
// Retry the request a configurable number of times.
retry(retryCount),
);
}
}
上下文是可变的(Mutable)
并且在请求的其他不可变转换过程中仍然存在。这允许拦截器通过此上下文协调来操作。
export const RETRY_COUNT = new HttpContextToken(() => 3);
export const ERROR_COUNT = new HttpContextToken(() => 0);
export class RetryInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const retryCount = req.context.get(RETRY_COUNT);
return next.handle(req).pipe(
tap({
// An error has occurred, so increment this request's ERROR_COUNT.
error: () => req.context.set(ERROR_COUNT, req.context.get(ERROR_COUNT) + 1)
}),
// Retry the request a configurable number of times.
retry(retryCount),
);
}
}
跟踪和显示请求进度
要想发出一个带有进度事件的请求,你可以创建一个 HttpRequest 实例,并把 reportProgress 选项设置为 true 来启用对进度事件的跟踪。
const req = new HttpRequest('POST', '/upload/file', file, {
reportProgress: true
});
每个进度事件都会触发变更检测,所以只有当需要在 UI 上报告进度时,你才应该开启它们。
当 HttpClient.request() 和 HTTP 方法一起使用时,可以用 observe: 'events' 来查看所有事件,包括传输的进度。
接下来,把这个请求对象传给 HttpClient.request() 方法,该方法返回一个 HttpEvents 的 Observable(与 拦截器 部分处理过的事件相同)。
return this.http.request(req).pipe(
map(event => this.getEventMessage(event, file)),
tap(message => this.showProgress(message)),
last(), // return last (completed) message to caller
catchError(this.handleError(file))
);
private getEventMessage(event: HttpEvent<any>, file: File) {
switch (event.type) {
case HttpEventType.Sent:
return `Uploading file "${file.name}" of size ${file.size}.`;
case HttpEventType.UploadProgress:
// Compute and show the % done:
const percentDone = event.total ? Math.round(100 * event.loaded / event.total) : 0;
return `File "${file.name}" is ${percentDone}% uploaded.`;
case HttpEventType.Response:
return `File "${file.name}" was completely uploaded!`;
default:
return `File "${file.name}" surprising upload event: ${event.type}.`;
}
}
安全:XSRF 防护
跨站请求伪造 (XSRF 或 CSRF)是一个攻击技术,它能让攻击者假冒一个已认证的用户在你的网站上执行未知的操作。
HttpClient 支持一种通用的机制来防范 XSRF 攻击。
当执行 HTTP 请求时,一个拦截器会从 cookie 中读取 XSRF 标记(默认名字为 XSRF-TOKEN),并且把它设置为一个 HTTP 头 X-XSRF-TOKEN,由于只有运行在你自己的域名下的代码才能读取这个 cookie,因此后端可以确认这个 HTTP 请求真的来自你的客户端应用,而不是攻击者。
默认情况下,拦截器会在所有的修改型请求中(比如 POST 等)把这个请求头发送给使用相对 URL 的请求。但不会在 GET/HEAD 请求中发送,也不会发送给使用绝对 URL 的请求。
你的服务器需要在页面加载或首个 GET 请求中把一个名叫 XSRF-TOKEN 的标记写入可被 JavaScript 读到的会话 cookie 中。
这个标记必须对每个用户都是唯一的,并且必须能被服务器验证,因此不能由客户端自己生成标记。把这个标记设置为你的站点认证信息并且加了盐(salt)的摘要,以提升安全性。
为了防止多个 Angular 应用共享同一个域名或子域时出现冲突,要给每个应用分配一个唯一的 cookie 名称。
配置自定义 cookie/header 名称
如果你的后端服务中对 XSRF 标记的 cookie 或头使用了不一样的名字,就要使用 HttpClientXsrfModule.withOptions() 来覆盖掉默认值。
imports: [
HttpClientModule,
HttpClientXsrfModule.withOptions({
cookieName: 'My-Xsrf-Cookie',
headerName: 'My-Xsrf-Header',
}),
],
异步取值
方法返回Observable,属性定义为正常:
heroes: Hero[] = [];
this.heroService.getHeroes().subscribe(heroes => this.heroes = heroes);
方法返回Observable,属性定义为Observable,输出时使用async管道:
shippingCosts!: Observable<{ type: string, price: number }[]>;
this.shippingCosts = this.cartService.getShippingCosts();
<div class="shipping-item" *ngFor="let shipping of shippingCosts | async">