诞生于2009年,由Misko Hevery 等人创建的第一个版本AngularJS已经成为过去,这里我们将不再翻阅历史细数一二。本文所说Angular皆是指v10以后版本,截止目前写本文时间Angular最新版本v15已发布。
这里我们将与你一起深入Angular应用,了解在开发中常见的一些技巧。
1.核心知识
1.1 组件
1.1.1实现自定义组件双向绑定的两种方法
1.1.1.1.基于属性实现自定义双向绑定
自定义组件时的属性实现
private _value: number = 0;
@Input()
public get value(): number {
return this._value;
}
public set value(value: number) {
this._value = value;
this.valueChange.emit(this._value);
}
@Output() readonly valueChange = new EventEmitter<number>();
调用时绑定value:
<banana [(value)]="value" />
1.1.1.2.实现自定义组件的ngModel指令
如果希望自定义组件能够具有与表单元素相同的 ngModel 效果,可以通过在组件内实现 ControlValueAccessor 接口达到目的。
- ControlValueAccessor约束
interface ControlValueAccessor {
writeValue(obj: any): void;
registerOnChange(fn: any): void;
registerOnTouched(fn: any): void;
setDisabledState?(isDisabled: boolean): void;
}
最简实现示例参考如下
import { Component, EventEmitter, Input, Output, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'banana',
templateUrl: `./basic.template.html`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => BananaComponent),
multi: true
}
]
})
export class BananaComponent implements ControlValueAccessor {
private _innerValue: any = '';
get innerValue(): any {
return this._innerValue;
}
set innerValue(v: any) {
if (v !== this._innerValue) {
this._innerValue = v;
this.onChangeCallback(v);
}
}
private onChangeCallback: (_: any) => void = () => { };
writeValue(value: any) {
if (value !== this.innerValue) {
this.innerValue = value;
}
}
registerOnChange(fn: any) {
this.onChangeCallback = fn;
}
registerOnTouched(fn: any) {
}
}
- basic.template.html
<div class="row">
<div class="col-md-12">
innerValue:<input [(ngModel)]="innerValue" />
</div>
</div>
- 组件引用
<banana [(ngModel)]="innerValue" />
1.2 模板
实现一个自定义的树结构展示:ngTemplateOutlet与ngTemplateOutletContext
**ngTemplateOutlet **指令插入对应的 TemplateRef 内嵌视图 ngTemplateOutletContext设置 EmbeddedViewRef 的上下文对象,可通过 let语法来声明绑定上下文对象属性名,用来传递模板间变量。
// 模板
public list = [
{
id: 1,
name: '商品',
goods: [
{
id: 11,
name: '冰箱',
goods: [
{
id: 111,
name: '海尔'
},
{
id: 112,
name: '创维'
},
{
id: 113,
name: '美的'
}
]
},
{
id: 12,
name: '服饰',
goods: [
{
id: 121,
name: '安踏'
}
]
}]
},
{
id: 2,
name: '电影',
goods: [
{
id: 21,
name: '谢谢',
goods: [
{
id: 211,
name: '人海',
goods: [
{
id: 2111,
name: '葡萄'
}
]
}
]
}
]
}
];
重点:组件模板实现
<!-- 循环列表数据 -->
<ng-container *ngFor="let item of list">
<ng-container [ngTemplateOutlet]="goodItemTpl" [ngTemplateOutletContext]="{item:item}"></ng-container>
<ng-container [ngTemplateOutlet]="goodsTpl" [ngTemplateOutletContext]="{goods:item.goods}"></ng-container>
</ng-container>
<!-- 商品展示明细 -->
<ng-template let-gooditem="item" #goodItemTpl>
<div>{{ gooditem.id }} - {{ gooditem.name }}</div>
</ng-template>
<!-- 递归模板 -->
<ng-template let-goods="goods" #goodsTpl>
<ng-container *ngFor="let item of goods">
<ng-container [ngTemplateOutlet]="goodItemTpl" [ngTemplateOutletContext]="{item:item}"></ng-container>
<ng-container *ngIf="item.goods?.length" [ngTemplateOutlet]="goodsTpl" [ngTemplateOutletContext]="{goods:item.goods}"></ng-container>
</ng-container>
</ng-template>
这样就将多层json数据以树结构展示出来了
1.3 指令
Angular指令作用在于影响Dom布局或者修改Dom属性,分为结构型指令与属性型指令。 这里我们讲下ngTemplateOutlet指令(结构型指令),使用ngTemplateOutlet指令需要引入CommonModule模块。在Angular框架里NgTemplateOutlet类定义了两个输入属性:ngTemplateOutlet、ngTemplateOutletContext。ngTemplateOutlet是TemplateRef类型的模板片段,ngTemplateOutletContext负责给 EmbeddedViewRef附加上下文对象.通过内置指令ngTemplateOutlet可以实现组件定制化功能,在开发通用组件的时候可提高组件的规范化与定制化能力。 例:实现一个组件的初始加载状态
<div>
<div class="my-component" *ngIf="!loading else loadingTemp">
content
</div>
<ng-template #loadingTemp>
<ng-container [ngTemplateOutlet]="defaultTpl"></ng-container>
</ng-template>
<ng-template #defaultTpl>
<p>spinner</p>
</ng-template>
</div>
1.4 依赖注入
2.配置项
如何实现@angular/cli项目的自定义webpack配置?
2.1.安装相关依赖
npm i @angular-builders/custom-webpack -D
npm i @angular-builders/dev-server -D
2.2.创建webpack.config.js文件
module.exports = (angularWebpackConfig, options) => {
return angularWebpackConfig;
};
2.3.更改angular.json的配置
"build": {
"builder": "@angular-builders/custom-webpack:browser",
......
"customWebpackConfig": {
"path": "./extra-webpack.config.js",
"libraryName": "ng-library",
"libraryTarget": "umd"
}
}
"serve": {
"builder": "@angular-builders/custom-webpack:dev-server",
......
},
3.AOT
在浏览器下载和运行代码之前的编译阶段,Angular 预先(AOT)编译器会先把 Angular HTML 和 TypeScript 代码转换成高效的 JavaScript 代码。 AOT 编译分为三个阶段:代码分析、代码生成、模板类型检查
Angular编译有两种:Ahead-of-time (AOT) 和 just-in-time (JIT)。但是实际上使用的是同一个编译器,AOT和JIT的区别只是编译的时机和编译所使用的工具库不同。.metadata.json文件是Angular编译器产生的,它用json的形式记录了.ts中decorator信息、依赖注入信息,这样Angular在二次编译时不再需要从.ts中提取metadata。NgFactories是浏览器可执行的代码,无论是AOT还是JIT,angular-complier都输出NgFactories,只不过AOT产生的输出到*.ngfactory.ts文件中,JIT产生的输出到客户端内存中。AOT模式下浏览器只做两件事:下载bundle、执行代码(创建组件实例)
4.服务懒加载
Angular6之后提供了provideIn勇于将服务注册到Angular依赖注入机制中,providedIn可选值包括‘root’、SomeModule,root代表APPModule即将该服务注册到全局,这里我们讨论如何实现providedIn:LazyLoadedModule方式,这种方式可以防止在所需模块之外调用当前服务。在开发大型应用程序时,能够确保良好的依赖关系,避免混乱的服务注入。
这里需要注意一个问题,如果直接在服务定义式使用providedIn:LazyModule会出现循环依赖的问题,所以我们需要借助一个LazyServiceModule来避免。
import { NgModule } from '@angular/core';
@NgModule()
export class LazyServiceModule {}
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { SLLComponent } from './index.component';
import { SLLRoutingModule } from './sll.routing';
import { LazyServiceModule } from './lazyservice.module';
@NgModule({
declarations: [
SLLComponent
],
imports: [
LazyServiceModule,
CommonModule,
SLLRoutingModule
]
})
export class LazyModule { }
通过借助LazyServiceModule
可以完美避开循环依赖的问题,然后,LazyModule将以标准方式使用 Angular Router 为某些路由进行懒加载,LazyService也只负责服务于当前LazyModule模块。
5.自定义表单项
创建自定义表单项仍然需要借助ControlValueAccessor实现,该接口充当 Angular 表单 API 和 DOM 中的原生元素之间的桥梁
<form-address id="phone" type="text" formControlName="address" />
import { Component, OnInit, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
const AUTHTREE_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FormAddressComponent),
multi: true
}
@Component({
selector: 'form-address',
template: `
<textarea [(ngModel)]="addressValue"></textarea>
`,
providers: [AUTHTREE_VALUE_ACCESSOR]
})
export class FormAddressComponent implements ControlValueAccessor {
// ControlValueAccessor处理
private _addressValue: string = ``;
public get addressValue(): string {
return this._addressValue;
}
public set addressValue(value: string) {
this.onChange(this._addressValue);
this._addressValue = value;
}
private onChange = (_: any) => { };
writeValue(value: any): void {
if (value !== this._addressValue) {
this._addressValue = value || [];
}
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void { }
}
6.RxJs使用
RxJS全称Reactive Extensions for JavaScript,是使用 Observables 的响应式编程的库,它使编写异步或基于回调的代码更加容易。RxJS的运行就是Observable和Observer之间的互动游戏。
基于RxJs实现Angular组件间通信
import { Injectable } from "@angular/core";
import { Subject } from "rxjs";
@Injectable({
providedIn: 'root'
})
export class MessageService {
private messageSource = new Subject<string>();
public message$ = this.messageSource.asObservable();
messageAction(name: string) {
this.messageSource.next(name);
}
}
发送消息
constructor(private msgService: MessageService) { }
public sendMessage() {
this.msgService.messageAction('message content');
}
接收消息
this.msgService.message$.subscribe(msg=>{
console.log(msg);// message content
})
7.Angular库开发
Angular 库是一个 Angular 项目,它与应用的不同之处在于它本身是不能运行的。必须在某个应用中导入库并使用它。在实际开发中我们可能会将一些表单验证、文件预览功能模块等抽离出来封装成通用库。 使用 Angular CLI中可以通过命令快速生成一个新库的骨架:
ng new workspace --no-create-application
cd workspace
ng generate library demo-lib
使用demo-lib库
import { DemoLibModule } from 'demo-lib';
@NgModule({
declarations: [
],
imports: [
......
DemoLibModule
]
})
export class MyModule { }
8.最佳实践
项目源码下载(以后补充)