一.注解
1.什么是装饰器(注解)
- 装饰器(注解)就是一个函数,但它是一个返回函数的函数
- 如果注解需要传递参数,则在声明注解的时候获取参数并使用即可
- 它是TypeScript的特性,而不是Angular的特性
2.自定义无参数注解
-
定义用于添加表情符号的注解类@Emoji
export function Emoji() { // target表示目标对象的类,key表示对应的属性名 return (target:object, key:string) => { let val = target[key]; const getter = () =>{ return val; } const setter = (value:string) => { val = `?${value}?`; } // JavaScript的定义:将target类中的key属性重新定义 Object.defineProperty(target, key, { get: getter,//替换读属性值的方法 set: setter,//替换写属性值的方法 enumerable: true, configurable: true }); } }
-
使用装饰器
export class TestComponent { @Emoji() result: string = 'Test'; //此时target是TestComponent, key是result // 装饰之后的结果是 ?Test? }
3.定义有参数注解
-
定义用于在调用方法前弹出对话框的注解类@Confirmable
export function Confirmable(message: string) { // descriptor为属性描述符[PropertyDescripter] // 此描述符就相当于上述方法Object.defineProperty的第三个参数 return (target:object, key:string, descriptor: PropertyDescripter) => { const original = descriptor.value;// 通过value属性获得操作的函数 descriptor.value = (...args: any) => { // 获取到方法参数值 const allow = windows.confirm(message);//弹出提示框 if(allow) { const result = original.apply(this, args);//如果点确定了就通过apply方法传递参数并调用函数 return result; } return null; } return descriptor; } }
-
使用装饰器
export class TestComponent { @Confirmable('您确定要执行吗?') // 点击确认就会执行console.log,点击取消则不会执行方法内容 handleClick() { console.log('点击执行'); } }
二.指令
1.Angular中的指令类型
- 组件:其实就是带模板的指令
- 结构型指令:改变宿主文档结构
- 属性型指令:改变宿主行为
2.内建指令
- 结构型指令
- ngIf
- ngFor
- ngSwich
- 属性型指令
- ngClass
- ngStyle
- ngModel
3.自定义属性型指令
-
定义指令
@Directive({ selector: '[appGridItem]'//添加[]表示需要依附在宿主上使用 }) export class AppGridItemDirective { }
-
使用
<!-- 指令所在的元素称为宿主 --> <div appGridItem> <img src="" alt="" appGridItemImage/> <span appGridItemTitle></span> </div>
三.指令的样式和事件绑定
指令没有模板,指令要寄宿在一个元素之上-宿主(Host)
1.相关注解
@HostBinding
绑定宿主的属性或者样式@HostListener
绑定宿主的事件- 组件的样式中也可使用
:host
这样一个伪类选择器
2.实例代码
-
上述实例使用注解实现
@Directive({ selector: '[appGridItem]'//添加[]表示需要依附在宿主上使用 }) export class AppGridItemDirective implements OnInit{ // 通过@HostBinding注解可以绑定宿主的style中的display属性值 @HostBinding('style.display') display = 'grid';//此时宿主的display属性值为grid // @HostBinding配合@Input()使用,可以在定义组件时使用<div [appGridItem]="'4px'">直接对style.height赋值 // @HostBinding('style.height') @Input() height = '3px'; //赋值修改成4px // 通过入参的ElementRef可以获得当前DOM类型的ElementRef constructor(private elr: ElementRef, private renderer2: Renderer2) {} // 指令执行ngOnInit时,宿主已经加载完成了 ngOnInit() { // 对宿主的display属性设置值 // this.rd2.setStyle(this.elr.nativeElement, 'display', 'grid'); } // 使用@HostListener绑定点击事件 @HostListener('click', ['$event.target']) //参数1:事件名称;参数2:事件所携带的数据 handleClick(ev) { console.log(ev);//打印了宿主的元素 } }
-
如果在组件的scss文件中使用
:host
伪类, 该伪类作用的元素就是当前组件:host { background: #000; //当前组件的背景是#000 }
四.组件嵌套与投影组件
1.组件嵌套
- 组件嵌套是不可避免的
- 过渡嵌套会陷入复杂和冗余
- 组件本身和外界的交互
- 通过@Input和@Output
- 避免组件嵌套导致冗余数据和事件传递
- 内容投影
- 路由
- 指令
- 服务
2.投影组件
-
ng-content是什么
- 通过
<ng-content>
标签可以设置动态内容,即父组件调用使用了ng-content的子组件时,通过ng-content标签的select元素可以定义保留的内部内容是什么
- 通过
-
表现形式
<ng-content select="样式类/HTML标签/指令"></ng-content>
-
适合场景
- 动态内容
- 容器组件
-
实例代码1(select是标签)
-
父组件的html模板
<app-test> <span> <div> This is Div </div> </span> <div> <span>This is span</span> <img src="./test.png"> </div> </app-test>
-
子组件(AppTestComponent)的html模板
<ng-content select="span"></ng-content>
-
显示内容
- 上述含义表示子组件只保留父组件定义的span中的元素,即只会显示This is Div
-
-
实例代码2(select是样式类)
-
父组件的html模板
<app-test> <span> <div> This is Div </div> </span> <div class="special"> <span>This is span</span> <img src="./test.png"> </div> </app-test>
-
子组件的html模板
<ng-content select=".special"></ng-content>
-
显示内容
- 上述含义表示子组件只保留父组件定义的special类标记的元素,即只会显示This is span和test.png图片
-
-
实例代码3(select是指定指令)
-
父组件的html模板
<app-test> <span> <div> This is Div </div> </span> <div appDirective class="special"> <span>This is span</span> <img src="./test.png"> </div> </app-test>
-
子组件的html模板
<ng-content select="[appDirective]"></ng-content>
-
显示内容
- 上述含义表示子组件只保留父组件定义的appDirective指令标记的元素,即只会显示This is span和test.png图片
-
-
实例代码4(使用多个ng-content选择内容)
-
父组件的html模板
<app-test> <span> <div> This is Div </div> </span> <div appDirective class="special"> <span>This is span</span> <img src="./test.png"> </div> </app-test>
-
子组件的html模板
<ng-content select="span"></ng-content> <ng-content select="[appDirective]"></ng-content>
-
显示内容
- 由于使用多个ng-content分别选择不同内容显示,则都会显示
-
-
通过组件投影的方式可以把逻辑提到父组件中进行处理,此时可以减少中间一层的@Input和@Output
五.路由
1.路由初步
- 路由是什么
- 路由(导航)本质上是切换视图的一种机制
- 路由的导航URL是否真实存在
- Angular的路由借鉴了大家熟知的浏览器URL变化导致页面切换的机制
- Angular是单页程序,路由显示的路径不过是一种保存路由状态的机制,这个路径在web服务器上不存在
- 路由实现
<router-outlet></router-outlet>
定义插座,用于定义下方插入的路由组件- 路由的好处在于代码的隔离
- 本地启动build后的服务
- 使用
npm install -g http-server
安装HttpServer - 使用prod模式打包并进入到dist目录
ng build --prod && cd dist
- 在httpServer上启动
http-server .
会把打包后的项目启动在8080上 - 访问浏览器的8080端口可以访问到项目,通过路由的方式可以进入到指定组件,但如果在指定路由中刷新页面,就会发现项目404。【因为刷新的时候会认为路由是API,会发送Get请求到服务器,服务器找到这个API发现不存在,就会返回404。解决这个问题:可以将404重定向到index.html,这样刷新就不会有问题了。】
- 使用
2.路由定义
-
定义路由数组【更详细的放前面,更宽泛(如
**任意匹配
)的放下面】- 路径
- 组件
- 子路由
-
导入RouterModule
- forRoot 【对于根模块来说的是
RouterModule.forRoot()
】 - forChild【对于功能模块(子模块)来说是
RouterModule.forChild()
】
- forRoot 【对于根模块来说的是
-
实例代码
const routes: Routes = [ { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: 'home', component: HomeComponent, children: [ { path: '', redirectTo: 'hot', pathMatch: 'full' }, { path: ':tabLink', component: HomeDetailComponent } ] }, { path: 'recommend', loadChildren: './recommend/recommend.module#RecommendModule'//懒加载 } ]; @NgModule({ imports: [RouterModule.forRoot(routes, {enableTracing: true})], //enableTracing表示是否允许debug跟踪 exports: [RouterModule] }) export class AppRoutingModule{}
3.子路由
1.子路由的写法
- 在父组件中添加
<router-outlet></router-outlet>
定义子路由的插座 - 在路由表里定义Routes对象指明children子路由的path和component
2.路径参数
- 配置
{path:':tabLink', component:HomeDetailComponent}
- 激活方式 【其中tab.link传递给的值就是tabLink的值】
<a [routerLink]="['/home',tab.link]">...</a>"
this.router.navigate(['home',tab.link])
- URL
http://localhost:4200/home/sports
- 读取【route是ActivatedRoute类型】
this.route.paramsMap.subscribe(params=>{...})
3.路径对象参数
- 配置
{path: ':tabLink', component:HomeDetailComponent}
- 激活
<a [routerLink]="['/home',tab.link,{name:'val1'}]">...</a>
this.router.navigate(['home',tab.link,{name:'val1'}]);
- URL
http://localhost:4200/home/sports;name=val1
- 上述链接实际上传递params有两个:一个是tabLink值为sports;另一个是name值为val1
- 可以通过以下方式从params中获取tabLink或name对应的value值
- 读取【route是ActivatedRoute类型】
this.route.paramsMap.subscribe(params=>{...});
4.查询参数
- 配置
{path:'home', component: HomeContainerComponent}
- 激活
<a [routerLink]="['/home']" [queryParams]={name:'val1'}>...</a>
this.router.navigate(['home'],{queryParams:{name:'val1'}});
- URL
http://localhost:4200/home?name=val1
- 读取【route是ActivatedRoute类型】
this.route.queryParamsMap.subscribe(params=>{ this.name = params.get('key'); ...;});
四.管道
1.管道的概念
- 管道的作用就是在视图上提供便利的值变化的方法
- 如在页面上将Data对象变到两天前,将1234.23变成$1,234.23
2.Angular内嵌的常用管道
- AsyncPipe:用来处理异步的管道
- DecimalPipe:处理数字的管道
- I18nSelectPipe:国际化的管道
- LowerCasePipe:把字母变小写的管道
- TitleCasePipe:把每个单词首字母大写的管道
- CurrencyPipe:货币处理的管道
- JsonPipe:调试使用,可以将对象转成Json字符串的管道
- PercentPipe:格式化成百分数的管道
- UpperCasePipe:将字母变大写的管道
- DatePipe:日期处理的管道
- I18nPluralPipe:处理国际化中复数的管道
- KeyValuePipe:处理字典对象的管道
- SlicePipe:字符串、数组等取某几位的管道
3.实现使用管道
-
ts文件内容
export class TestComponent { obj = { productId: 2, productName: 'JackProduct', model: 's', type: 'smart' } date = new Date(); price = 123.32; data = [1,2,3,4,5]; }
-
在模板中使用管道处理
<p> {{ obj | json }} </p> <!-- 输出结果是标准型的json: {"productId": 2,"productName": 'JackProduct',"model": 's',"type": 'smart'} --> <p> {{ date | date:'MM-dd' }} </p> <!-- 通过:定义格式,上述输出结果是07-09,也可以加yy等等 --> <p> {{ price | currency }} </p> <!-- 上述输出结果是$123.32 --> <!-- 如果要是想显示¥的话,需要在app.module中使用provider定义如下 --> <!-- providers:[{ provider: LOCALE_ID, useValue: 'zh-Hans' }] 且需要在AppModule的constructor()构造函数中通过registerLocaleData(localZh, 'zh')的方式将本地导入中国 使用currency:'CNY'就可以显示¥123.32 也可以通过currency:'CNY':'symbol':'4.0-2'表示小数点左侧至少4位,右侧0-2位显示是¥0,123.32 --> <p> {{ data | slice:1:3 }} </p> <!-- 还可以通过slice实现分片,获得索引值1(包含)-3(不包含)的 --> <!-- 输出结果: 2,3 -->
4.自定义管道
-
定义Pipe:用于自定义输出时间的转换格式
@Pipe({name: 'appAgo'}) export class AgoPipe implements Pipe { transform(value: any):any { if(value) { // 通过+将Date类型对象转成时间戳 const seconds = Math.floor((+new Date() - +new Date(value))/1000);//转成秒 // 如果小于30秒,则输出 刚刚 if(seconds < 30) { return '刚刚'; } const intervals = { 年: 3600 * 24 * 365, 月: 3600 * 24 * 30, 周: 3600 * 24 * 7, 日: 3600 * 24, 小时: 3600, 分钟: 60, 秒: 1 } let counter = 0;//设置计数器 for (const unitName in intervals ) { if(intervals.hasOwnProperty(unitName)) { const unitValue= interval[unitName]; // 从最大时间往小时间做舍尾除法,获得在多长时间之前 counter = Math.floor(seconds/unitValue); if(counter > 0) { return `${counter} ${unitName} 前` } } } // 如果都不满足则直接返回值 return value; } } }
-
使用管道,会根据上述逻辑进行相应输出
<p> {{ value | appAgo }} </p>
五.依赖注入
1.依赖注入的过程及使用
- 提供服务
@Injectable()
标记在服务中可以注入别的依赖
- 模块中声明
providers
数组中声明- 或者import对应模块
- 在组建中使用
- 构造函数中直接声明,Angular框架帮你完成注入
2.Angular自定义注入的方式【通常不用自己定义】
//注意Angular提供的依赖注入都是单例的
//自己定义池子
const injector = Injector.create({
providers: [
{
provide: Product,
//使用useFactory方式注入
useFactory: ()=>{
return new Product('小米手机',11);
},
deps: []
},{
provide: PurchaseOrder,
useClass: PurchaseOrder,//useClass表示直接提供一个PurchaseOrder实例,
deps:[ Product ]//deps属性表示PurchaseOrder中依赖的服务
}
//还有useExsiting表示使用其他地方创建好的对象实例
//useValue表示直接使用一个指定值
//...
]
})
//通过injector.get(Product)或injector.get(PurchaseOrder)的方式来获取
//或通过构造参数constructor(@Inject(Product)private product: Product)注入即可
3.Angular6服务注册新特性
-
Angular6以前注入服务都是在AppModule中的providers中注入
-
Angular6以后可以在定义服务的时候使用providedIn:XXX的方式自动注入
@Injectable({ providedIn: 'root'//表示注入到根 //providedIn: HomeModule表示注入到Module })
六.脏值检测
1.脏值检测概要
- 什么是脏值检测
- 当数据改变时更新视图(DOM)
- 什么时候会触发脏值检测
- 浏览器事件(如click,mouseover,keyup等)
- setTimeout()和setInterval()
- Http请求
- 如何进行检测
- 检查两个状态:当前状态和新状态
2.组件的生命周期
-
每一个属性会经历两次脏值检测,第一次是已经将值赋给属性了,第二次是检测属性是否赋值成功,如果没变化放行,如果有变化则会成为死循环了
-
注意不能在AfterViewChecked和AfterViewInit函数中更新属性值(console会报错),因为Angular是通过脏值检测机制更新DOM的,如果在AfterViewChecked和AfterViewInit中更新属性值,想把属性值同步到页面中就需要再做一次脏值检测,两次属性值不同则脏治检测不通过
-
如果需要在AfterViewChecked和AfterViewInit中更新属性,需要使用NgZone对象(依赖注入),NgZone是浏览器的js运行时划分出n个区域,每个区域面向自己程序相互不干扰。可以借助NgZone对象使得属性的改变运行在Angular程序区域之外,此时脏值检测就检测不到此属性的改变(绕过去~)。
//constructor(private ngZone: NgZone){}方式注入 this.ngZone.runOutsideAngualr(()=>{ setInterval(()=>{//通过异步的方式避开第二次脏值检测,此时第一次第二次脏值检测都会通过 this._title = "HelloJack"; },1000) });
-
如果想做倒计时功能等实时更新属性值的功能,可以通过ViewChild获取到Dom元素,并通过innerHTML的更改实时更改内部显示的内容,就可以做到倒计时的效果
3.脏值检测的OnPush策略
- 非OnPush(Default)策略的检测:只要组件树中任意一个节点的数据发生变化,都会跑一边整个树,会导致性能消耗
- OnPush策略:执行此策略时只对组件中有@Input注解的属性进行检测,如果属性发生改变则触发脏值检测,而且只会检测又脏值发生改变的节点和子树
4.OnPush策略实际代码应用
-
组件默认都是Default策略,即任意一个节点数据的变化都会遍历整个树
-
通过在组件的@Component中添加changeDetection值为ChangeDetectionStrategy.OnPush设置为OnPush策略
@Component({ selector: xxx, templateUrl: xxx, styleUrls: [xxx], changeDetection:ChangeDetectionStrategy.OnPush })
-
设定了OnPush策略的组件就会只看@Input属性的变化,只有@Input属性修饰的属性变换才会触发脏值检测,且只会触发此分支的,否则则不理【即笨组件】
5.OnPush策略修饰带来的问题
- 路由参数发生组件改变,不会销毁组件,而是重用组件,所以ngOnInit只会走一遍
- 如果将路由参数改变的组件变成OnPush策略后,由于没有@Input属性,如果在ngOnInit中的代码逻辑即会被执行但不发生变更检测即不会反映到页面上(如果写了获取页面数据变化的逻辑即不会在页面显示)
- 解决上述问题:
- 需要通过依赖的方式导入ChangeDetectorRef对象
constructor(private cd: ChangeDetectorRef)
- 通过
this.cd.markForCheck();
方法通知框架进行变化检查,如果属性发生了变化则需要在页面上也进行显示
- 需要通过依赖的方式导入ChangeDetectorRef对象