Angular进阶知识回顾
一.注解
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()使用,可以在定义组件时使用直接对style.height赋值// @HostBinding('style.height') @Input() height = '3px'; //赋值修改成4px// 通过入参的ElementRef可以获得当前DOM类型的ElementRefconstructor(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标签的select元素可以定义保留的内部内容是什么
- 通过
-
表现形式
-
适合场景
- 动态内容
- 容器组件
-
实例代码1(select是标签)
-
父组件的html模板
<app-test><span><div>This is Divdiv>span><div><span>This is spanspan><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 Divdiv>span><div class="special"><span>This is spanspan><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 Divdiv>span><div appDirective class="special"><span>This is spanspan><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 Divdiv>span><div appDirective class="special"><span>This is spanspan><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服务器上不存在
- 路由实现
定义插座,用于定义下方插入的路由组件- 路由的好处在于代码的隔离
- 本地启动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.子路由的写法
- 在父组件中添加
定义子路由的插座 - 在路由表里定义Routes对象指明children子路由的path和component
2.路径参数
- 配置
{path:':tabLink', component:HomeDetailComponent}
- 激活方式 【其中tab.link传递给的值就是tabLink的值】
..."this.router.navigate(['home',tab.link])
- URL
http://localhost:4200/home/sports
- 读取【route是ActivatedRoute类型】
this.route.paramsMap.subscribe(params=>{...})
3.路径对象参数
- 配置
{path: ':tabLink', component:HomeDetailComponent}
- 激活
...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}
- 激活
...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> <p>{{ date | date:'MM-dd' }} p> <p>{{ price | currency }} p> <p>{{ data | slice:1:3 }} p>
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对象
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
