Angular8 日常开发避坑指南(30个)

3,080 阅读8分钟

1.else使用

  • 语法糖else
   <div *ngIf="isLoggedIn; else loggedOut">
   Welcome back, friend.
   </div>

   <ng-template #loggedOut>
   Please friend, login.
   </ng-template>
  • code 等同于下方
<ng-template [ngIf]="isLoggedIn" [ngIfElse]="loggedOut">
 Welcome back, friend.
</ng-template>

<ng-template #loggedOut>
 Please friend, login.
</ng-template>
  • 在高级一点:
<ng-container *ngIf="isLoggedIn;then loggedIn;loggedOut">
<ng-container>

<ng-template #loggedIn>
 Welcome back, friend.
</ng-template>

<ng-template #loggedOut>
 Please friend, login.
</ng-template>

2.ng-show废弃

<div [hidden]="!isLoggedIn">
 Welcome back, friend.
</div>

3.angular中使用angularjs组件

  • angularjs组件
  export const heroDetail = {
  bindings: {
    hero: '<',
    deleted: '&'
  },
  template: `
    <h2>{{$ctrl.hero.name}} details!</h2>
    <div><label>id: </label>{{$ctrl.hero.id}}</div>
    <button ng-click="$ctrl.onDelete()">Delete</button>
  `,
  controller: function() {
    this.onDelete = () => {
      this.deleted(this.hero);
    };
  }
};
  • 定义angular指令
import { Directive, ElementRef, Injector, Input, Output, EventEmitter } from '@angular/core';
import { UpgradeComponent } from '@angular/upgrade/static';
import { Hero } from '../hero';

@Directive({
  selector: 'hero-detail'
})
export class HeroDetailDirective extends UpgradeComponent { 
 # 需要对应于angularjs组件定义的数据绑定
  @Input() hero: Hero;
  @Output() deleted: EventEmitter<Hero>;

  constructor(elementRef: ElementRef, injector: Injector) {
    super('heroDetail', elementRef, injector);
  }
} 
  • 使用指令,引用angularjs组件
import { Component } from '@angular/core';
import { Hero } from '../hero';

@Component({
  selector: 'my-container',
  template: `
    <h1>Tour of Heroes</h1>
    <hero-detail [hero]="hero"
                 (deleted)="heroDeleted($event)">
    </hero-detail>
  `
})
export class ContainerComponent {
  hero = new Hero(1, 'Windstorm');
  heroDeleted(hero: Hero) {
    hero.name = 'Ex-' + hero.name;
  }
}

4.常用模板语法

  • 结构指令:

    • 列表渲染
    <li *ngFor="let hero of heroes;">
        {{ hero }}
    </li>
    
    • 列表渲染并展示序号
      <li *ngFor="let hero of heroes;let i = index;">
         {{i+1}} {{ hero }}
     </li>
    

    推荐添加trackBy,提升性能

     <li *ngFor="let hero of heroes;trackBy:trackByFn">
        {{ hero }}
    </li>
    trackByFn(index, item) {
     return item.id;
    }
    
    • 条件渲染
    <li *ngIf="isHidden">
        {{ hero }}
    </li>
    
    • 条件选择
    <div [ngSwitch]="hero?.emotion">
      <app-happy-hero    *ngSwitchCase="'happy'"    [hero]="hero"></app-happy-hero>
      <app-sad-hero      *ngSwitchCase="'sad'"      [hero]="hero"></app-sad-hero>
      <app-confused-hero *ngSwitchCase="'confused'" [hero]="hero"></app-confused-hero>
      <app-unknown-hero  *ngSwitchDefault           [hero]="hero"></app-unknown-hero>
    </div>
    
  • 属性指令:

    • 数据单向输入
     # 动态绑定,更新会触发对应子组件
     <app-hero-detail [hero]="currentHero"></app-hero-detail>
     
     # 绑定字符串,非变量值
     <app-item-detail childItem="parentItem"></app-item-detail>
    
    • 事件反馈
     <w-button (click)="handlerClick" />
     
     # 双向数据绑定
     <input [(ngModel)]="currentItem.name">
    
     # 等效于
     <input [value]="currentItem.name"
        (input)="currentItem.name=$event.target.value" >
    
    • 属性添加
    <button [attr.aria-label]="help">help</button>
    
    <div [class.special]="isSpecial">Special</div>
    
    <button [style.color]="isSpecial ? 'red' : 'green'">
    
    <button [class]="{foo: true, bar: false}" />
    
    • 自定义双向数据绑定 - x和xChange
     import { Component, Input, Output, EventEmitter } from '@angular/core';
    
     @Component({
        selector: 'app-sizer',
        templateUrl: './sizer.component.html',
        styleUrls: ['./sizer.component.css']
     })
     export class SizerComponent {
    
    
     @Input()  size: number | string;
     @Output() sizeChange = new EventEmitter<number>();
    
     dec() { this.resize(-1); }
     inc() { this.resize(+1); }
    
     resize(delta: number) {
         this.size = Math.min(40, Math.max(8, +this.size + delta));
         this.sizeChange.emit(this.size);
     }
    
     }
    
     # html
     <app-sizer [(size)]="fontSizePx"></app-sizer> 等效于
     
     <app-sizer [size]="fontSizePx" (sizeChange)="fontSizePx=$event"></app-sizer>
    

5.ng-template

    <div *ngIf="isLoggedIn">
    Welcome back, friend.
    </div>
    # 等效于
    <ng-template [ngIf]="isLoggedIn">
        Please friend, login.
    </ng-template>

6.ng-content

# html
	
  <fa-input icon="envelope">
     <i class="fa fa-envelope"></i>
     <input inputRef type="email" placeholder="Email">
  </fa-input>

# js component 
@Component({
  selector: 'fa-input',
  template: `
    <ng-content ></ng-content>  # 匹配fa-input所有其他的没有指定匹配的
    <ng-content select="input"></ng-content> # 匹配fa-input中特定的input标签
  `})
export class FaInputComponent {
   ...
}

7.inject 单例模式

  • 单例模式
import { Injectable } from '@angular/core';

# inject root 利用webpack tree-shaking,优化打包
@Injectable({
    providedIn: 'root',
})
export class UserService {
}

  • privoder配置
@NgModule({
...
providers: [UserService],
...
})
  • 如果两者都提供了,那么请注意,从UserService中读取内存数据,可能读取不到!!!

8.angularjs ui-grid问题

  • 使用技术升级-ag-grid
  • 使用组件升级方案,参考总结3

directive包裹angularjs ui-grid,在angular中使用,但是需要注意一点:已经使用的ui-grid 文件配置,不可以随意修改,否则会影响现有线上的ui展示功能,

譬如:新版本不需要垂直滚动,手动修改默认angularjs ui-grid垂直滚动条配置,那么线上就会导致已有angularjs 表格垂直滚动条不展示,数据展示不全问题。

# 我们应该在自己的angular组件中定义配置项,而不应该在通用的配置修改
this.gridOptions = {
   enableFiltering: true,
   useExternalFiltering: true,
   columnDefs: [
     { name: 'name', enableFiltering: false },
     { name: 'gender' },
     { name: 'company', enableFiltering: false}
   ],
   enableHorizontalScrollbar: 1, # 0 关闭,1 开启
   enableVerticalScrollbar:0 # 0 关闭,1 开启
}

10.Render2 更新样式 ,ViewChild选取dom

#html 
<div #mydiv><input></div>

# js 
@ViewChild('mydiv') mydiv: ElementRef 
constructor( 
    private el:ElementRef,
    private renderer2: Renderer2){
}
ngOnInit(){
    this.renderer2.setStyle(this.el.nativeElement.querySelector('.btn1'),'background','green');
}

# 尽量减少应用层与渲染层之间强耦合关系, 推荐下方的render2 
# render2 api
abstract data: {...}
  destroyNode: ((node: any) => void) | null
  abstract destroy(): void
  abstract createElement(name: string, namespace?: string): any
  abstract createComment(value: string): any
  abstract createText(value: string): any
  abstract appendChild(parent: any, newChild: any): void
  abstract insertBefore(parent: any, newChild: any, refChild: any): void
  abstract removeChild(parent: any, oldChild: any, isHostElement?: boolean): void
  abstract selectRootElement(selectorOrNode: any, preserveContent?: boolean): any
  abstract parentNode(node: any): any
  abstract nextSibling(node: any): any
  abstract setAttribute(el: any, name: string, value: string, namespace?: string): void
  abstract removeAttribute(el: any, name: string, namespace?: string): void
  abstract addClass(el: any, name: string): void
  abstract removeClass(el: any, name: string): void
  abstract setStyle(el: any, style: string, value: any, flags?: RendererStyleFlags2): void
  abstract removeStyle(el: any, style: string, flags?: RendererStyleFlags2): void
  abstract setProperty(el: any, name: string, value: any): void
  abstract setValue(node: any, value: string): void
  abstract listen(target: any, eventName: string, callback: (event: any) => boolean | void): () => void

11.类计算属性

@Component({
  selector: 'fa-input',
  template: `
    <i class="fa" [ngClass]="classes"></i>
  `,
  styleUrls: ['./fa-input.component.css']
})
export class FaInputComponent {
  @Input() icon: string;
	
  get  classes() {
    const cssClasses = {
      fa: true
    };
    cssClasses['fa-' + this.icon] = true;
    return cssClasses;
  }
}

12.元素绑定

  • @HostBinding()可以为指令的宿主元素添加类、样式、属性等,
  • @HostListener()可以监听宿主元素上的事件。
import { Directive, HostBinding, HostListener } from '@angular/core';

@Directive({
  selector: '[highLight]' # 定义指令
})
export class HighLightDirective{
  colors = [
    'hotpink', 'lightskyblue', 'goldenrod', 'peachpuff'
  ]; 
  @HostBinding('style.color') color: string;
  @HostBinding('style.borderColor') borderColor: string;  # 定义样式
  @HostListener('keydown') onKeydown(){   # 定义监听
    const color = Math.floor(Math.random() * this.colors.length);
    this.color = this.borderColor = this.colors[colorPick];
  }
}

13.viewChild

  • @ViewChild 选择组件模板内的节点, 类型 ElementRef 或子组件
  • @ContentChild 选择当前组件引用的子组件 @ContentChild(组件名)
  • 区别在于ViewChild选择Shadow DOM, ContentChild 选择 Light DOM,一般情况下用ViewChild就ok了

14.元素宽度

  • element clientWidth

内联元素以及没有 CSS 样式的元素的 clientWidth 属性值为 0。Element.clientWidth 属性表示元素的内部宽度,以像素计。该属性包括内边距 padding,但不包括边框 border、外边距 margin 和垂直滚动条(如果有的话)。 当在根元素(元素)上使用clientWidth时(或者在上,如果文档是在quirks(怪异)模式下),将返回viewport的宽度(不包括任何滚动条).

  • jquery width()

始终指内容宽度,不包括border

width

15.scss 无效

# scss 文件
input {
 border: none;
 outline: none;
}
# 界面运行时css
input[_ngcontent-c0] {
 border: none;
 outline: none;
}
# 此时需要添加编译处理
:host ::ng-deep input {
 border: none;
 outline: none;
}

16. httpclient默认json格式接收

# 后端传递text,需要设置类型
this.http.get(this.configUrl,{responseType:'text'})
.subscribe((data: any) => this.config = { ...data });
# 源码
 get(url: string, options?: {
      headers?: HttpHeaders | {
          [header: string]: string | string[];
      };
      # 默认值有: response| body| event 
      observe?: 'body';# 默认读取的是response中的body 
      params?: HttpParams | {
          [param: string]: string | string[];
      };
      reportProgress?: boolean;
      # 默认值有: arraybuffer | json | blob |text
      responseType?: 'json';# 这里,ts参数类型可选,默认值json,
      withCredentials?: boolean;
  }): Observable<Object>;

17.内存泄漏风险

angular中推荐使用rxjs,进行响应式开发

import { from } from 'rxjs';

const data = fromEvent('click');
// Subscribe to begin listening for async result
this.$obser = data.subscribe({
  next(response) { console.log(response); },
  error(err) { console.error('Error: ' + err); },
  complete() { console.log('Completed'); }
});

上述的subscribe代码,在没有unsubscribe情况下,会出现订阅多次情况

我们可以通过多种方法优化

  • 1.在OnDestroy中unsubscribe下
public ngOnDestory(){
    if( this.$obser){
         this.$obser.unsubscribe();
    }
}
  • 2.类似上面写法,只不过参考这段代码
# 使用Subscription,一处取消,取消所有的
import { interval } from 'rxjs';
 
const observable1 = interval(400);
const observable2 = interval(300);
 
 #也可以私用subscription = new Subscription(); //全局创建,然后依次add
const subscription = observable1.subscribe(x => console.log('first: ' + x));
const childSubscription = observable2.subscribe(x => console.log('second: ' + x));
 
subscription.add(childSubscription);
 
setTimeout(() => {
  // Unsubscribes BOTH subscription and childSubscription
  subscription.unsubscribe();
}, 1000);
  • 3.参考使用rxjs pipe,改用异步
# 具体细节还依赖rxjs operator熟练,后期整理一篇供大家参考
@Component({
  selector: 'async-observable-pipe',
  template: `<div><code>observable|async</code>:
       Time: {{ time$ | async }}</div>`
})
export class AsyncObservablePipeComponent {
  time$ = new Observable<string>(observer => {
    setInterval(() => observer.next(new Date().toString()), 1000);
  });
}

18.组件更新策略调整,优化组件树更新性能

@Component({
    selector: 'app-product',
    template: `...`,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductComponent { ... }

如下情况会触发OnPush

  • @Input值发生改变
  • 组件或者子组件,触发Dom事件
  • detectChanges 方法调用
  • async Pipe 发生改变

19. tooltip显示 getBoundingClientRect

# html
<div class="tips" #tooltip></div>

#ts
class ToolTipCompoment {
    @ViewChild('tips')
    tips: ElementRef;
    
    showTip(){
        const dom = this.tips.nativeElement;
        const pos = dom.getBoundingClientRect();
        ...
    }
}

20 Observable lazy push

#rxjs 
import { Observable } from 'rxjs';

const observable = new Observable(subscriber => {
  subscriber.next(1);
  subscriber.next(2);
  subscriber.next(3);
  setTimeout(() => {
    subscriber.next(4);
    subscriber.complete();
  }, 1000);
});

上述代码定义完毕,是否意味着我们就可以获取到数据呢?答案是否定的,请看官网,这样描述的:

To invoke the Observable and see these values, we need to subscribe to it:

code需要如下修改,添加subscribe:

import { Observable } from 'rxjs';

const observable = new Observable(subscriber => {
  subscriber.next(1);
  subscriber.next(2);
  subscriber.next(3);
  setTimeout(() => {
    subscriber.next(4);
    subscriber.complete();
  }, 1000);
});

console.log('just before subscribe');
observable.subscribe({
  next(x) { console.log('got value ' + x); },
  error(err) { console.error('something wrong occurred: ' + err); },
  complete() { console.log('done'); }
});
console.log('just after subscribe');

那么,同理,Rxjs的所有的operators,我们都需要Subscribe才能调用、获取到值

  • 一种方式: subscribe
  • 另一种方式,html界面,(count$ | async )

21. Angular httpclient 前端日志搜集失败

# 定义http api请求
getConfig() {
  return this.http.get(this.configUrl);
}

# 紧跟着我们会定义
showConfig() {
  this.configService.getConfig()
    .subscribe((data: Config) => this.config = {
        heroesUrl: data['heroesUrl'],
        textfile:  data['textfile']
    });
}

结合20,我们可以发现,如果不subscribe,http请求是发送不出去的。

尤其在前端记录日志,如果只是定义,没有subscribe,那么前端日志是不能记录成功的哦!!!

22. async 与 subscribe 效果等同

20 页面中导出数据,譬如 export json、csv文件时候,如果使用async 异步设置loading,那么会出现一次导出多份文件情况。

根本原因在于,asyn与subscribe效果等同,多次订阅,没有unsubscribe情况,触发多次保存

23. rxjs 创建数据流同异步之分

同步数据流

  • create - Observable 构造函数
  • of - 列举有限的
  • range - 特定范围
  • generate - 循环
  • repeat - 重复
  • empty - 空
  • never - 永不完结
  • throw - 抛出错误

异步数据流

  • interval - 特定间隔
  • timer - 定时
  • from
  • fromEvent
  • ajax
  • defer

编写代码时候,常常没有想到异步的存在,导致undefined异常发生。

24. angular iframe loading

我们期望的是,在iframe资源加载完毕,loading结束。

但是我们发现,chrome、safari load会触发两次,firefox、ie11 仅触发一次

# html
<div *ngIf="loading" class="v-loading"></div>
<iframe  [src]="url" (load)="load()">

# ts
class helperComponent extends onInit{
    loading = false;
    constructor(){
        console.log("this is contructor...");
        //this.loading = true; 通用的写法是,在构造的时候,初始化
    }
    ngOnInit(){
        console.log("this is init...");
        this.loading = true;
    }
    load(){
        console.log("this is load...");
        
    }
}
//output 
chrome|safari
this is contructor  // 如果这里初始化true,那么chrome,safari下,loading很快就结束了
this is load...  # 这里会多调用一次,
this is init...  # 所以,我们把loding= true,放置在这里 
this is load...

ff | ie11
this is contructor
this is init... 
this is load...

25. Array sort

# 请问,你想到结果是什么吗?是否会发生异常?
const arr = [1,undefined,2,undefined,4,undefined];
arr.sort((a,b) => {
   return a.name > b.name
});
#output 
[1, 2, 4, undefined, undefined, undefined]

原因如下

If compareFunction is supplied, all non-undefined array elements are sorted according to the return value of the compare function (all undefined elements are sorted to the end of the array, with no call to compareFunction)

26. ngrx undefined问题

ngrx 用于状态管理,类似于sessionStorage,有 get/set

# selectors
export const userLogin = createSelector(
  getCounterValue,
  (counter, props) => counter * props.multiply
);

ngOnInit() {
  this.counter = this.store.pipe(select(fromRoot.getCount, { multiply: 2 }))
}

我们在selector获取unfined,常见原因有两种:

  • 对应的action,没有触发,没有set对应的value
  • 对应的value设置了,但是get时候,写法不对,譬如
# 正确的写法
this.counter = this.store.pipe(select(fromRoot.getCount, { multiply: 2 }))

# 错误的写法
  this.counter = this.store.pipe(fromRoot.getCount, { multiply: 2 })

27. Cannot find module 'typescript'

TypeScript is required if you want to compile using ts-node.

npm install -D typescript
npm install -D ts-node

28. ngrx store pip sync or async ?

export const getList = () =>
  createSelector(
    (state, props) => state.counter[props.key]
);

console.log("before....");
this.store.pipe(
  select(fromRoot.getList(), { key: 'counter2'}),
  tap(() => {
    console.log("store....");
  })
);
console.log("after....");


//output
before....
store....
after....

29 rxjs EMPTY 与Of({})

EMPTY仅仅触发complete,不会触发next。在整合ngrx时,需要谨慎返回。

Of,next先触发,在执行complete

import { fromEvent, interval , of, EMPTY } from 'rxjs';
import { switchAll, map, tap } from 'rxjs/operators';
 
EMPTY.subscribe(
  () => {
    console.log('empty next');
  },
  () => {
    console.log('empty error');
  },
  () => {
    console.log('empty complete');
  }
)

of({}).subscribe(
  () => {
    console.log('of next');
  },
  () => {
    console.log('of error');
  },
  () => {
    console.log('of complete');
  }
)

//output
empty complete
of next
of complete

30 rxjs 编写angular export

即使关闭数据流监听

getData: () => {
    const data1:Observabl<any> = this.getDataone();
    const data2:Observable<any> = this.getDataTwo();
    return combineLatest([data1,data2]).pipe(map([data1,data2]) => {
        //数据整合,返回json str
        return jsonStr
    })
    //此处一定要关闭数据监听,不然会任何data1,data2改变,都会触发下载,即使没有点击下载按钮
    take(1),
}

this.exportService.getData().pipe(
 (res) => {
     //download js logic 
    
 }
).subscribe() 

更多推荐

Angular8 HttpClient 30分钟深入了解下

Angular Render2你了解吗?

20个你值得了解的Angular开源项目

Angular7/8 read local json的2种方法

Angular8 ui-grid替代方案ag-grid入门

深入了解指令

参考文献