角度陷阱旁路和节省时间

使用Angular,您可以做任何事情。 或几乎所有东西。 但是有时这种阴险的“几乎”导致这样一个事实,即开发人员在浪费时间,方法是创建变通办法,或者试图了解为什么发生了某些事情,或者为什么有些事情没有按预期进行。



我们今天翻译的这篇文章的作者说,他想分享一些技巧,这些技巧将帮助Angular开发人员节省一些时间。 他将谈论Angular的陷阱,他(不仅是他)有机会遇到。

1号 您应用的自定义指令不起作用


因此,您找到了一个不错的第三方Angular指令,并决定将其与Angular模板中的标准元素一起使用。 太好了! 让我们尝试这样做:

<span awesomeTooltip="'Tooltip text'"> </span> 

您启动应用程序……什么都没有发生。 您像任何普通的,有经验的程序员一样,会查看Chrome开发者工具控制台。 而且您在那里什么都看不到。 该指令不起作用,Angular是静默的。

然后,团队中的一些聪明人决定将指令放在方括号中。

 <span [awesomeTooltip]="'Tooltip text'"> </span> 

之后,由于浪费了一些时间,我们在控制台中看到以下内容。


事情就是这样:我们只是忘记了使用指令导入模块

现在问题的原因非常明显:我们只是忘记了将指令模块导入Angular应用程序模块。
这导致了一条重要规则:切勿使用不带方括号的指令。

您可以在此处尝试使用指令。

2号 ViewChild返回未定义


假设您创建了一个指向Angular模板中描述的文本输入元素的链接。

 <input type="text" name="fname" #inputTag> 

您将使用RxJS fromEvent函数创建一个流,该流将进入该流。 为此,您需要一个指向输入字段的链接,可以使用Angular ViewChild装饰器获得该ViewChild

 class SomeComponent implements AfterViewInit {    @ViewChild('inputTag') inputTag: ElementRef;    ngAfterViewInit(){        const input$ = fromEvent(this.inputTag.nativeElement, 'keyUp')    } ...   } 

在这里,使用RxJS fromEvent函数,我们创建了一个流,输入该字段的数据将落入该流中。
测试此代码。


失误

发生什么事了

实际上,以下规则适用于此:如果ViewChild返回undefined ,请在模板中查找*ngIf

 <div *ngIf="someCondition">    <input type="text" name="fname" #inputTag> </div> 

在这里,他是问题的元凶。

此外,请在问题元素上方检查模板是否包含其他结构性指令或ng-template
考虑此问题的可能解决方案。

▍解决问题的方式№1


如果不需要,您可以简单地隐藏模板元素。 在这种情况下,该元素将始终继续存在,并且ViewChild将能够在ngAfterViewInit挂钩中返回指向它的链接。

 <div [hidden]="!someCondition">    <input type="text" name="fname" #inputTag> </div> 

▍解决问题的方式№2


解决此问题的另一种方法是使用setter。

 class SomeComponent {   @ViewChild('inputTag') set inputTag(input: ElementRef|null) {    if(!input) return;       this.doSomething(input);  }  doSomething(input) {    const input$ = keysfromEvent(input.nativeElement, 'keyup');    ...  } } 

在这里,只要Angular为inputTag属性分配了特定的值,我们就会根据在输入字段中输入的数据创建一个流。

以下是与此问题相关的几个有用资源:

  • 在这里您可以看到Angular 8中ViewChild的结果可以是静态的也可以是动态的。
  • 如果您在使用RxJ时遇到困难,请观看视频课程。

3号 更新由* ngFor生成的列表时的代码执行(元素出现在DOM中之后)


假设您有一些有趣的自定义指令来组织可滚动列表。 您将把它应用于使用Angular *ngFor创建的列表。

 <div *ngFor="let item of itemsList; let i = index;"     [customScroll]     >  <p *ngFor="let item of items" class="list-item">{{item}}</p>   </div> 

通常,在这种情况下,更新列表时,需要考虑列表中发生的更改,调用类似scrollDirective.update类的scrollDirective.update来配置滚动行为。

似乎可以使用ngOnChanges挂钩完成此操作:

 class SomeComponent implements OnChanges {  @Input() itemsList = [];   @ViewChild(CustomScrollDirective) scroll: CustomScrollDirective;  ngOnChanges(changes) {    if (changes.itemsList) {      this.scroll.update();    }  } ... } 

没错,这里我们面临一个问题。 在浏览器显示更新的列表之前,将调用该挂钩。 结果,错误地执行了用于滚动列表的指令参数的重新计算。

*ngFor完成工作后,如何立即拨打电话?

您可以按照以下3个简单步骤进行操作:

▍第1步


将链接放置到应用了*ngFor#listItems )的元素上。

 <div [customScroll]>    <p *ngFor="let item of items" #listItems>{{item}}</p> </div> 

▍步骤2


使用Angular ViewChildren装饰器获取这些元素的列表。 它返回QueryList类型的实体。

▍第3步


QueryList类具有只读的changes属性,该属性在每次列表更改时都会QueryList事件。

 class SomeComponent implements AfterViewInit {  @Input() itemsList = [];   @ViewChild(CustomScrollDirective) scroll: CustomScrollDirective;  @ViewChildren('listItems') listItems: QueryList<any>;  private sub: Subscription;   ngAfterViewInit() {    this.sub = this.listItems.changes.subscribe(() => this.scroll.update())  } ... } 

现在问题已解决。 在这里,您可以尝试相应的示例。

4号 可以在没有参数的情况下运行查询时发生的ActivatedRoute.queryParam问题


以下代码将帮助我们理解此问题的本质。

 // app-routing.module.ts const routes: Routes = [    {path: '', redirectTo: '/home', pathMatch: 'full'},    {path: 'home', component: HomeComponent},  ];   @NgModule({    imports: [RouterModule.forRoot(routes)], //  #1    exports: [RouterModule]  })  export class AppRoutingModule { }    //app.module.ts  @NgModule({    ...    bootstrap: [AppComponent] //  #2  })  export class AppModule { }   // app.component.html  <router-outlet></router-outlet> //  #3    // app.component.ts  export class AppComponent implements OnInit {    title = 'QueryTest';     constructor(private route: ActivatedRoute) { }     ngOnInit() {      this.route.queryParams          .subscribe(params => {            console.log('saveToken', params); //  #4          });    }  } 

该代码的某些片段被注释为 #x 。 考虑他们:

  1. 在应用程序的主模块中,我们定义了路由,并在其中添加了RouterModule 。 配置了路由,以便如果URL中未提供路由,我们会将用户重定向到/home
  2. 作为下载组件,我们在主模块AppComponent指定。
  3. AppComponent使用<router-outlet>输出适当的路由组件。
  4. 现在最重要的是。 我们需要从URL获取queryParams作为路线的

假设我们获得了以下URL:

 https://localhost:4400/home?accessToken=someTokenSequence 

在这种情况下, queryParams将如下所示:

 {accessToken: 'someTokenSequence'} 

让我们在浏览器中查看所有这些工作。


测试实现路由系统的应用程序

在这里,您可能对问题的实质有疑问。 我们得到了参数,一切按预期进行...

查看上面的浏览器屏幕快照,以及控制台中显示的内容。 在这里,您可以看到queryParams对象已发出两次。 第一个对象为空;它在Angular路由器的初始化过程中发出。 只有在此之后,我们才能获得包含请求参数的对象(在我们的示例中为{accessToken: 'someTokenSequence'} )。

问题是,如果URL中没有请求参数,则路由器将不返回任何内容。 即-在发出第一个空对象之后,也可能不会指示可能没有参数的第二个对象也为空。


在执行不带参数的请求时,不会发出第二个对象

结果,事实证明,如果代码期望第二个对象可以从中接收请求数据,那么如果URL中没有请求参数,则它将不会启动。

如何解决这个问题? 在这里,RxJ可以帮助我们。 我们将基于ActivatedRoute.queryParams创建两个可观察对象。 像往常一样-考虑逐步解决问题的方法。

▍第1步


如果queryParams不为空,则第一个可观察对象paramsInUrl$paramsInUrl$数据:

 export class AppComponent implements OnInit {    constructor(private route: ActivatedRoute,                private locationService: Location) {    }    ngOnInit() {             //            //              const paramsInUrl$ = this.route.queryParams.pipe(            filter(params => Object.keys(params).length > 0)        );     ...    } } 

▍步骤2


仅当在URL中未找到请求参数时,第二个可观察对象noParamsInUrl$才会noParamsInUrl$空值:

 export class AppComponent implements OnInit {    title = 'QueryTest';    constructor(private route: ActivatedRoute,                private locationService: Location) {    }    ngOnInit() {                 ...        //   ,     ,   URL        //             const noParamsInUrl$ = this.route.queryParams.pipe(            filter(() => !this.locationService.path().includes('?')),            map(() => ({}))        );            ...    } } 

▍第3步


现在,使用RxJS 合并功能合并观察到的对象:

 export class AppComponent implements OnInit {    title = 'QueryTest';    constructor(private route: ActivatedRoute,                private locationService: Location) {    }    ngOnInit() {             //            //              const paramsInUrl$ = this.route.queryParams.pipe(            filter(params => Object.keys(params).length > 0)        );        //   ,     ,   URL        //             const noParamsInUrl$ = this.route.queryParams.pipe(            filter(() => !this.locationService.path().includes('?')),            map(() => ({}))        );        const params$ = merge(paramsInUrl$, noParamsInUrl$);        params$.subscribe(params => {            console.log('saveToken', params);        });    } } 

现在,观察到的param$对象仅param$值-不管queryParams中是否queryParamsqueryParams带有查询参数的对象)或否( queryParams空对象)。

您可以在此处尝试此代码。

5号 慢页


假设您有一个显示一些格式化数据的组件:

 // home.component.html <div class="wrapper" (mousemove)="mouseCoordinates = {x: $event.x, y: $event.y}">  <div *ngFor="let item of items">     <span>{{formatItem(item)}}</span>  </div> </div> {{mouseCoordinates | json}} // home.component.ts export class HomeComponent {    items = [1, 2, 3, 4, 5, 6];    mouseCoordinates = {};    formatItem(item) {              //           const t = Array.apply(null, Array(5)).map(() => 1);             console.log('formatItem');        return item + '%';    } } 

该组件解决了两个问题:

  1. 它显示一个元素数组(假定此操作执行一次)。 另外,它通过调用formatItem方法格式化显示的formatItem
  2. 它显示鼠标的坐标(此值显然会经常更新)。

您不希望该组件出现任何性能问题。 因此,请仅执行性能测试以遵守所有手续。 但是,此测试过程中会出现一些奇怪的现象。


很多formatItem调用和很多CPU负载

怎么了 但是事实是,当Angular重绘模板时,它将调用模板中的所有函数(在我们的示例中为formatItem函数)。 结果,如果在模板功能中执行了任何繁重的计算,则会给处理器带来压力,并影响用户对相应页面的感知方式。

如何解决? 预先执行在formatItem中执行的计算,并在页面上显示已经准备好的数据就足够了。

 // home.component.html <div class="wrapper" (mousemove)="mouseCoordinates = {x: $event.x, y: $event.y}">  <div *ngFor="let item of displayedItems">    <span>{{item}}</span>  </div> </div> {{mouseCoordinates | json}} // home.component.ts @Component({    selector: 'app-home',    templateUrl: './home.component.html',    styleUrls: ['./home.component.sass'] }) export class HomeComponent implements OnInit {    items = [1, 2, 3, 4, 5, 6];    displayedItems = [];    mouseCoordinates = {};    ngOnInit() {        this.displayedItems = this.items.map((item) => this.formatItem(item));    }    formatItem(item) {        console.log('formatItem');        const t = Array.apply(null, Array(5)).map(() => 1);        return item + '%';    } } 

现在,性能测试看起来更加不错。


仅6种格式项调用和较低的处理器负载

现在,该应用程序运行得更好。 但是,这里使用的解决方案具有一些并不总是令人愉快的功能:

  • 由于我们在模板中显示鼠标的坐标,因此mousemove事件仍会触发更改检查。 但是,由于我们需要鼠标的坐标,因此无法摆脱这一点。
  • 如果mousemove事件处理程序仅需要执行一些计算(不会影响页面上显示的内容),则可以使用以下方法来加快应用程序的速度:

    1. 您可以在事件处理函数内使用NgZone.runOutsideOfAngular 。 这样可以防止在mousemove事件mousemove开始进行更改检查(这只会影响此处理程序)。
    2. 您可以通过使用polyfills.ts中的以下代码来防止某些事件的zone.js补丁。 这将影响整个Angular应用程序。

 * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; //      

如果您对提高Angular应用程序性能的问题感兴趣- 在此处此处此处此处 -有关此的有用材料。

总结


既然您已经阅读了本文,那么Angular-developer工具库中应该会出现5个新工具,您可以使用它们解决一些常见问题。 希望您在这里找到的提示可以帮助您节省时间。

亲爱的读者们! 您是否知道什么可以帮助您节省开发Angular应用程序的时间?

Source: https://habr.com/ru/post/zh-CN459304/


All Articles