看到这儿,我猜你肯定已经看过一些博客、技术大会录像了,现在应该已经准备好踏上angular2这条不归路了吧!那么上路后,哪些东西是我们需要知道的?

下面就是一些新手常见错误汇总,当你要开始自己的angular2旅程时,尽量避免吧。

注:本文中,我假设诸位已经对angular2的基础知识有所了解。如果你是绝对新手,之前只听说过,完全没概念什么是angular2的,先去读读下面这些资料:

错误 #1:原生hidden属性绑定数据

AngularJS 1中,如果想切换DOM元素的显示状态,估计你会用AngularJS 1内置的指令如:ng-show 或者 ng-hide:

AngularJS 1示例:

<div ng-show="showGreeting">
Hello, there!
</div>

angular2里,新的模版语法允许你将表达式绑定到DOM元素的任何原生属性上。 这个绝对牛逼的功能带来了无限的可能。其中一项就是绑定表达式到原生的hidden属性上,和ng-show有点像,也是为元素设置display: none

angular2[hidden]示例(不推荐):

<div [hidden]="!showGreeting">
Hello, there!
</div>

第一眼看上面的例子,似乎就是AngularJS 1里的ng-show。其实不然,她们有着!important的不同。

ng-showng-hide都是通过一个叫ng-hide的CSS class来控制DOM元素的显示状态,ng-hide class就是简单的把元素设置成display: none。这里的关键在于,AngularJS 1ng-hide class里增加了!important,用来调整该class的优先级,使得它能够覆盖来自其他样式对该元素display属性的赋值。

再来说回本例,原生hidden属性上的display: none样式是由浏览器实现的。大多数浏览器是不会用!important来调整其优先级的。因此,通过[hidden]="expression"来控制元素显示状态就很容易意外的被其他样式覆盖掉。举个例子:如果我在其他地方对这个元素写了这样一个样式display: flex,这就比原生hidden属性的优先级高(看这里)。

基于这个原因,我们通常使用*ngIf切换元素存在状态来完成相同目标:

angular2*ngIf示例(推荐):

<div *ngIf="showGreeting">
Hello, there!
</div>

和原生hidden属性不同,angular2中的*ngIf不受样式约束。无论你写了什么样的CSS,她都是安全的。但还是有必要提一下,*ngIf并不是控制元素的显示状态,而是直接通过从模版中增加/删除元素该元素来达成显示与否这一效果的。

当然你也可以通过全局的样式给元素的hidden属性增加隐藏的优先级,譬如:display: none !important,来达到这个效果。你或许会问,既然angular小组都知道这些问题,那干嘛不在框架里直接给hidden加一个全局最高优先级的隐藏样式呢?答案是我们没法保证加全局样式对所有应用来说都是最佳选择。因为这种方式其实破坏了那些依赖原生hidden能力的功能,所以我们把选择权交给工程师。

错误 #2:直接调用DOM APIs

只有极少的情况需要直接操作DOMangular2提供了一系列牛X的高阶APIs来完成你期望的DOM操作,例如:queries。利用angular2提供的这些APIs有如下优势:

  • 单元测试里不直接操作DOM可以降低测试复杂度,使你的测试用例跑的更快

  • 把你的代码从浏览器中解藕,允许你在任何渲染环境里跑你的程序,譬如:web worker,或者完全离开浏览器(比如:运行在服务器端,亦或是Electron里)

当你手动操作DOM时,就失去了上述优势,而且代码越写越不易读。

AngularJS 1(或者压根没写过Angular的人)转型的朋友,我能猜到大概哪些场景是你们想直接操作DOM的。那我们来一起看下这些状况,我来演示下如何用queries重构她们。

场景 一:当需要获取当前组件模版里的某一个元素时

假设你的组件模版里有一个input标签,并且你希望在组件加载后立即让这个input自动获取焦点

你或许已经知道通过@ViewChild/@ViewChildren这两个queries可以获取当前组件模版里的内嵌组件。但在这个例子里,你需要的是获取一个普通的HTML元素,而非一个组件。一开始估计你就直接注入ElementRef来操作了:

直接操作ElementRef(不推荐)

@Component({
selector: 'my-comp',
template: `
<input type="text" />
<div> Some other content </div>
`
})
export class MyComp {
constructor(el: ElementRef) {
el.nativeElement.querySelector('input').focus();
}
}

其实我想说的是,这种做法没必要。

解决方案:@ViewChild配合local template variable

程序员们没想到的是除了组件本身,其他原生元素也是可以通过local variable获取的。在写组件时,我们可以直接在组件模版里给这个input标签加标记(譬如:#myInput), 然后把标记传给@ViewChild用来获取该元素。当组件初始化后,你就可以通过renderer在这个input标签上执行focus方法了。

@ViewChild配合local variable(推荐)

@Component({
selector: 'my-comp',
template: `
<input #myInput type="text" />
<div> Some other content </div>
`
})
export class MyComp implements AfterViewInit {
@ViewChild('myInput') input: ElementRef; constructor(private renderer: Renderer) {} ngAfterViewInit() {
this.renderer.invokeElementMethod(this.input.nativeElement,
'focus');
}
}

场景 二:当需要获取用户映射到组件里的某个元素时

如果你想获取的元素不在你的组件模版定义里怎么办?举个例子,假设你有个列表组件,允许用户自定义各列表项,然后你想跟踪列表项的数量。

当然你可以用@ContentChildren来获取组件里的“内容”(那些用户自定义,然后映射到你组件里的内容),但因为这些内容可以是任意值,所以是没办法向刚才那样通过local variable来追踪她们的。

一种方法是,要求用户给他将要映射的列表项都加上预定义的local variable。这样的话,代码可以从上面例子改成这样:

@ContentChildrenlocal variable(不推荐)

// user code
<my-list>
<li *ngFor="#item of items" #list-item> {{item}} </li>
</my-list> // component code
@Component({
selector: 'my-list',
template: `
<ul>
<ng-content></ng-content>
</ul>
`
})
export class MyList implements AfterContentInit {
@ContentChildren('list-item') items: QueryList<ElementRef>; ngAfterContentInit() {
// do something with list items
}
}

可是,这需要用户写些额外的内容(#list-item),真心不怎么优雅!你可能希望用户就只写<li>标签,不要什么#list-item属性,那肿么办?

解决方案:@ContentChildren配合li选择器指令

介绍一个好方案,用@Directive装饰器,配合他的selector功能。定义一个能查找/选择<li>元素的指令,然后用@ContentChildren过滤用户映射进当前组件里的内容,只留下符合条件的li元素。

@ContentChildren配合@Directive(推荐)

// user code
<my-list>
<li *ngFor="#item of items"> {{item}} </li>
</my-list> @Directive({ selector: 'li' })
export class ListItem {} // component code
@Component({
selector: 'my-list'
})
export class MyList implements AfterContentInit {
@ContentChildren(ListItem) items: QueryList<ListItem>; ngAfterContentInit() {
// do something with list items
}
}

注:看起来只能选择<my-list>里的li元素(例如:my-list li),需要注意的是,目前angular2尚不支持"parent-child"模式的选择器。如果需要获取组件里的元素,用@ViewChildren、 @ContentChildren这类queries是最好的选择

错误 #3:在构造器里使用获取的元素

第一次使用queries时,很容易犯这样的错:

在构造器里打印query的结果(错误)

@Component({...})
export class MyComp {
@ViewChild(SomeDir) someDir: SomeDir; constructor() {
console.log(this.someDir);// undefined
}
}

当看到打印出来undefined后,你或许以为你的query压根不能用,或者是不是构造器哪里错了。事实上,你就是用数据用的太早了。必须要注意的是,query的结果集在组件构造时是不能用的。

幸运的是,angular2提供了一种新的生命周期管理钩子,可以非常轻松的帮你理清楚各类query什么时候是可用的。

  • 如果在用view query(譬如:@ViewChild@ViewChildren)时,结果集在视图初始化后可用。可以用ngAfterViewInit钩子

  • 如果在用content query(譬如:@ContentChild@ContentChildren)时,结果集在内容初始化后可用。可以用ngAfterContentInit钩子

来动手改一下上面的例子吧:

ngAfterViewInit里打印query结果集(推荐)

@Component({...})
export class MyComp implements AfterViewInit {
@ViewChild(SomeDir) someDir: SomeDir; ngAfterViewInit() {
console.log(this.someDir);// SomeDir {...}
}
}

错误 #4:用ngOnChanges侦测query结果集的变化

AngularJS 1里,如果想要监听一个数据的变化,需要设置一个$scope.$watch, 然后在每次digest cycle里手动判断数据变了没。在angular2里,ngOnChanges钩子把这个过程变得异常简单。只要你在组件里定义了ngOnChanges方法,在输入数据发生变化时该方法就会被自动调用。这超屌的!

不过需要注意的是,ngOnChanges当且仅当组件输入数据变化时被调用,“输入数据”指的是通过@Input装饰器显式指定的那些数据。如果是@ViewChildren, @ContentChildren的结果集增加/删除了数据,ngOnChanges是不会被调用的。

如果你希望在query结果集变化时收到通知,那不能用ngOnChanges。应该通过query结果集的changes属性订阅其内置的observable。只要你在正确的钩子里订阅成功了(不是构造器里),当结果集变化时,你就会收到通知。

举例,代码应该是这个样子的:

通过changes订阅observable,监听query结果集变化(推荐)

@Component({ selector: 'my-list' })
export class MyList implements AfterContentInit {
@ContentChildren(ListItem) items: QueryList<ListItem>; ngAfterContentInit() {
this.items.changes.subscribe(() => {
// will be called every time an item is added/removed
});
}
}

如果你对observables一窍不通,赶紧的,看这里

错误 #5:错误使用*ngFor

angular2里,我们介绍了一个新概念叫"structural directives",用来描述那些根据表达式在DOM上或增加、或删除元素的指令。和其他指令不同,"structural directive"要么作用在template tag上、 要么配合template attribute使用、要么前缀"*"作为简写语法糖。因为这个新语法特性,初学者常常犯错。

你能分辨出来以下错误么?

错误的ngFor用法

// a:
<div *ngFor="#item in items">
<p> {{ item }} </p>
</div> // b:
<template *ngFor #item [ngForOf]="items">
<p> {{ item }} </p>
</template> // c:
<div *ngFor="#item of items; trackBy=myTrackBy; #i=index">
<p>{{i}}: {{item}} </p>
</div>

来,一步步解决错误

5a:把"in"换成"of"

// incorrect
<div *ngFor="#item in items">
<p> {{ item }} </p>
</div>

如果有AngularJS 1经验,通常很容易犯这个错。在AngularJS 1里,相同的repeater写作ng-repeat="item in items"

angular2将"in"换成"of"是为了和ES6中的for-of循环保持一致。也需要记住的是,如果不用"*"语法糖,那么完整的repeater写法要写作ngForOf, 而非ngForIn

// correct
<div *ngFor="#item of items">
<p> {{ item }} </p>
</div>

5b:语法糖和完整语法混着写

// incorrect
<template *ngFor #item [ngForOf]="items">
<p> {{ item }} </p>
</template>

混着写是没必要的 - 而且事实上,这么写也不工作。当你用了语法糖(前缀"*")以后,angular2就会把她当成一个template attribute,而不是一般的指令。具体来说,解析器拿到了ngFor后面的字符串, 在字符串前面加上ngFor,然后当作template attribute来解析。如下代码:

<div *ngFor="#item of items">

会被当成这样:

<div template="ngFor #item of items">

当你混着写时,他其实变成了这样:

<template template="ngFor" #item [ngForOf]="items">

从template attribute角度分析,发现template attribute后面就只有一个ngFor,别的什么都没了。那必然解析不会正确,也不会正常运行了。

如果从从template tag角度分析,他又缺了一个ngFor指令,所以也会报错。没了ngFor指令,ngForOf都不知道该对谁负责了。

可以这样修正,要么去掉"*"写完整格式,要么就完全按照"*"语法糖简写方式书写

// correct
<template ngFor #item [ngForOf]="items">
<p> {{ item }} </p>
</template> // correct
<p *ngFor="#item of items">
{{ item }}
</p>

5c:在简写形式里用了错误的操作符

// incorrect
<div *ngFor="#item of items; trackBy=myTrackBy; #i=index">
<p>{{i}}: {{item}} </p>
</div>

为了解释这儿到底出了什么错,我们先不用简写形式把代码写对了看看什么样子:

// correct
<template ngFor #item [ngForOf]="items" [ngForTrackBy]="myTrackBy" #i="index">
<p> {{i}}: {{item}} </p>
</template>

在完整形式下,结构还是很好理解的,我们来试着分解一下:

  • 我们通过输入属性向ngFor里传入了两组数据:

    • 绑定在ngForOf上的原始数据集合items

    • 绑定在ngForTrackBy上的自定义track-by函数

  • #声明了两个local template variables,分别是:#i#itemngFor指令在遍历items时,给这两个变量赋值

    • i是从0开始的items每个元素的下标

    • item是对应每个下标的元素

当我们通过"*"语法糖简写代码时,必须遵守如下原则,以便解析器能够理解简写语法:

  • 所有配置都要写在*ngFor的属性值里

  • 通过=操作符设置local variable

  • 通过:操作符设置input properties

  • 去掉input properties里的ngFor前缀,譬如:ngForOf,就只写成of就可以了

  • 用分号做分隔

按照上述规范,代码修改如下:

// correct
<p *ngFor="#item; of:items; trackBy:myTrackBy; #i=index">
{{i}}: {{item}}
</p>

分号和冒号其实是可选的,解析器会忽略它们。写上仅仅是为了提高代码可读性。因此,也可以再省略一点点:

// correct
<p *ngFor="#item of items; trackBy:myTrackBy; #i=index">
{{i}}: {{item}}
</p>

结论

希望本章的解释对你有用。Happy coding!

原文地址:5 Rookie Mistakes to Avoid with Angular 2

来源:https://segmentfault.com/a/1190000004969541

Angular2新人常犯的5个错误的更多相关文章

  1. Web开发人员常犯的10个错误

    说到开发一个运行在现代网络中的网站:Web开发人员需要选择虚拟主机平台和底层数据存储,准备编写HTML.CSS和JavaScript用的工具,要有设计执行方式,以及一些可用的JavaScript库/框 ...

  2. Python开发者最常犯的10个错误

    Python是一门简单易学的编程语言,语法简洁而清晰,并且拥有丰富和强大的类库.与其它大多数程序设计语言使用大括号不一样 ,它使用缩进来定义语句块. 在平时的工作中,Python开发者很容易犯一些小错 ...

  3. Java程序员常犯的10个错误

      本文总结了Java程序员常犯的10个错误. #1. 把Array转化成ArrayList 把Array转化成ArrayList,程序员经常用以下方法: List<String> lis ...

  4. OI中常犯的傻逼错误总结

    OI中常犯的傻逼错误总结 问题 解决方案 文件名出错,包括文件夹,程序文件名,输入输出文件名  复制pdf的名字  没有去掉调试信息  调试时在后面加个显眼的标记  数组开小,超过定义大小,maxn/ ...

  5. Spring常犯的十大错误,你踩过吗?

    1.错误一:太过关注底层 我们正在解决这个常见错误,是因为 "非我所创" 综合症在软件开发领域很是常见.症状包括经常重写一些常见的代码,很多开发人员都有这种症状. 虽然理解特定库的 ...

  6. Java开发人员最常犯的10个错误

    这个列表总结了10个Java开发人员最常犯的错误. Array转ArrayList 当需要把Array转成ArrayList的时候,开发人员经常这样做: List<String> list ...

  7. C# 程序员最常犯的 10 个错误(转)

    关于C#关于本文常见错误 #1:把引用当做值来用,或者反过来常见错误 #2:误会未初始化变量的默认值常见错误 #3:使用不恰当或未指定的方法比较字符串常见错误 #4:使用迭代式 (而不是声明式)的语句 ...

  8. C# 程序员最常犯的 10 个错误http://www.oschina.net/translate/top-10-mistakes-that-c-sharp-programmers-make

    来源:http://www.oschina.net/translate/top-10-mistakes-that-c-sharp-programmers-make 关于C# C#是达成微软公共语言运行 ...

  9. python开发者常犯的10个错误(转)

    常见错误1:错误地将表达式作为函数的默认参数 在Python中,我们可以为函数的某个参数设置默认值,使该参数成为可选参数.虽然这是一个很好的语言特性,但是当默认值是可变类型时,也会导致一些令人困惑的情 ...

随机推荐

  1. docker之容器管理

    一.docker常用的创建命令 [root@node03 ~]# docker create --help [root@node03 ~]# docker run --help OPTIONS说明: ...

  2. Selenium+Python自动化测试环境搭建和搭建过程遇到的问题解决

    环境搭建: 第一步:安装Python  网址:https://www.python.org/ 按照如图提示安装,并且配置环境变量(安装时候选中pip会自动安装Python的包管理工具 pip,推荐选择 ...

  3. Unity导入模型出现 (Avatar Rig Configuration mis-match. Bone length in configuration does not match position in animation)?

    昨天遇到这两个模型导入的问题,查了一下资料,自己摸索了一下解决方法..总结一下~ 出现的原因:(问题1)Warning 当模型文件导入以后并且设置Animation Type是Generic的时候,动 ...

  4. 安装logstash及logstash的初步使用-处理DNS日志

    安装logstash 需要高版本的java 使用1.4版本的java会有报错 # Can't start up: not enough memory 查询java信息 rpm -qa | grep j ...

  5. PHP Laravel 连接并访问数据库

    第一次连接数据库 数据库配置位于config/database.php数据库用户名及密码等敏感信息位于.env文件创建一个测试表laravel_course <?php namespace Ap ...

  6. 11.5 Daily Scrum

    请把现在当成11月5日······   Today's tasks  Tomorrow's tasks 丁辛 餐厅列表数据结构设计 餐厅列表UI设计             李承晗           ...

  7. Fast Failure Detection and Recovery in SDN with Stateful Data Plane

    文章名称:Fast Failure Detection and Recovery in SDN with Stateful Data Plane 利用SDN的带状态数据平面进行快速故障检测和恢复 发表 ...

  8. 关于Win10系统下VC2013安装Unit test出现问题的解决办法

    话不多说,先上图~~~ 很多同学在Vs2013安装Unit test组件时会弹出这样的对话框,极其极其让人崩溃. 当我看到这个对话框时,首先中规中矩的去官网下载.NET(但是我怎么可能没有!游戏环境包 ...

  9. grunt入门讲解1:grunt的基本概念和使用

    Grunt和 Grunt 插件是通过 npm 安装并管理的,npm是 Node.js 的包管理器. Grunt 0.4.x 必须配合Node.js >= 0.8.0版本使用.老版本的 Node. ...

  10. Tomcat下bootstrap启动分析

    "C:\Program Files\Java\jdk1.7.0\bin\javaw.exe" -agentlib:jdwp=transport=dt_socket,suspend= ...