模板是编写 Angular 组件最重要的一环,你必须深入理解以下知识点才能玩转 Angular 模板:
对比各种 JS 模板引擎的设计思路
Mustache(八字胡)语法
模板内的局部变量
属性绑定、事件绑定、双向绑定
在模板里面使用结构型指令 *ngIf、*ngFor、ngSwitch
在模板里面使用属性型指令 NgClass、NgStyle、NgModel
在模板里面使用管道格式化数据
一些小 feature:安全导航、非空断言
“深入理解”的含义是:你需要很自如地运用这些 API,写代码的时候不翻阅 API 文档。
因为很多新手之所以编码效率不高,其中一个主要的原因就是在编码过程中不停翻文档、查资料。
对比各种 JS 模板引擎的设计思路
几乎每一款前端框架都会提供自己的模板语法:
在 jQuery 如日中天的时代,有 Handlebars 那种功能超强的模板
React 推崇 JSX 模板语法
当然还有 Angular 提供的那种与“指令”紧密结合的模板语法
综合来说,无论是哪一种前端模板,大家都比较推崇“轻逻辑”(logic-less)的设计思路。
何为“轻逻辑”?
简而言之,所谓“轻逻辑”就是说,你不能在模板里面编写非常复杂的 JavaScript 表达式。比如,Angular 的模板语法就有规定:
你不能在模板里面 new 对象
不能使用 =、+=、-= 这类的表达式
不能用 ++、-- 运算符
不能使用位运算符
为什么要“轻逻辑”?
最重要的原因是怕影响运行性能,因为模板可能会被执行很多次。
比如你编写了以下 Angular 模板:
-
{{race.name}}
很明显,浏览器不认识 *ngFor 和 {{…}} 这种语法,因此必须在浏览器里面进行“编译”,获得对应的模板函数,然后再把数据传递给模板函数,最终结合起来获得一堆 HTML 标签,然后才能把这一堆标签插入到 DOM 树里面去。
如果启用了 AOT,处理的步骤有一些变化,@angular/cli 会对模板进行“静态编译”,避免在浏览器里面动态编译的过程。
而 Handlebars 这种模板引擎完全是运行时编译模板字符串的,你可以编写以下代码:
//定义模板字符串
var source=`
- {{name}}
{{#each races}}
{{/each}}
`;
//在运行时把模板字符串编译成 JS 函数
var templateFn=Handlebars.compile(source);
//把数据传给模板函数,获得最终的 HTML
var html=templateFn([
{name:'人族'},
{name:'神族'},
{name:'虫族'}
]);
注意到 Handlebars.compile 这个调用了吧?这个地方的本质是在运行时把模板字符串“编译”成了一个 JS 函数。
鉴于 JS 解释执行的特性,你可能会担忧这里会有性能问题。这种担忧是合理的,但是 Handlebars 是一款非常优秀的模板引擎,它在内部做了各种优化和缓存处理。模板字符串一般只会在第一次被调用的时候编译一次,Handlebars 会把编译好的函数缓存起来,后面再次调用的时候会从缓存里面获取,而不会多次进行“编译”。
上面我们多次提到了“编译”这个词,因此很显然这里有一个东西是无法避免的,那就是我们必须提供一个 JS 版的“编译器”,让这个“编译器”运行在浏览器里面,这样才能在运行时把用户编写的模板字符串“编译”成模板函数。
有一些模板引擎会真的去用 JS 编写一款“编译器”出来,比如 Angular 和 Handlebars,它们都真的编写了一款 JS(TS)版的编译器。而有一些简单的模板引擎,例如 Underscore 里面的模板函数,只是用正则表达式做了字符串替换而已,显得特别简陋。这种简陋的模板引擎对模板的写法有非常多的限制,因为它不是真正的编译器,能支持的语法特性非常有限。
因此,评估一款模板引擎的强弱,最核心的东西就是评估它的“编译器”做得怎么样。但是不管怎么说,毕竟是 JS 版的“编译器”,我们不可能把它做得像 G++ 那么强大,也没有必要做得那么强大,因为这个 JS 版的编译器需要在浏览器里面运行,搞得太复杂浏览器拖不动!
以上就是为什么大多数模板引擎都要强调“轻逻辑”的最根本原因。
对于 Angular 来说,强调“轻逻辑”还有另一个原因:在组件的整个生命周期里面,模板函数会被执行很多次。你可以想象,Angular 每次要刷新组件外观的时候,都需要去调用一下模板函数,如果你在模板里面编写了非常复杂的代码,一定会增加渲染时间,用户一定会感到界面有“卡顿”。
人眼的视觉延迟大约是 100ms 到 400ms 之间,如果整个页面的渲染时间超过 400ms,界面基本上就卡得没法用了。有一些做游戏的开发者会追求 60fps 刷新率的细腻感觉,60 分之 1 秒约等于 16.7ms,如果 UI 整体的渲染时间超过了 16.7ms,就没法达到这个要求了。
轻逻辑(logic-less)带来了效率的提升,也带来了一些不方便,比如很多模板引擎都实现了 if 语句,但是没有实现 else,因此开发者们在编写复杂业务逻辑的时候模板代码会显得非常啰嗦。
目前来说,并没有完美的方案能同时兼顾运行效率和语法表现能力,这里只能取一个平衡。
Mustache 语法
Mustache 语法也就是你们说的双花括号语法 {{…}},老外觉得它像八字胡子,很奇怪啊,难道老外喜欢侧着头看东西?
好消息是,很多模板引擎都接受了 Mustache 语法,这样一来学习量又降低了不少,开心吧?
关于 Mustache 语法,你需要掌握 3 点:
它可以获取到组件里面定义的属性值
它可以自动计算简单的数学表达式,如加减乘除、取模
它可以获得方法的返回值
请依次看例子。
插值语法关键代码实例:
欢迎来到{{title}}!
public title = '假的星际争霸2';
简单的数学表达式求值:
1+1={{1+1}}
调用组件里面定义的方法:
可以调用方法{{getVal()}}
public getVal():any{
return 65535;
}
模板内的局部变量
{{heroInput.value}}
有一些朋友会追问,如果我在模板里面定义的局部变量和组件内部的属性重名会怎么样呢?
如果真的出现了重名,Angular 会按照以下优先级来进行处理:
模板局部变量 > 指令中的同名变量 > 组件中的同名属性。
复制
这种优先级规则和 JSP 里面的变量取值规则非常类似,对比一下很好理解对不对?你可以自己写代码测试一下。
值绑定
值绑定是用方括号来做的,写法:
public imgSrc:string="./assets/imgs/1.jpg";
很明显,这种绑定是单向的。
事件绑定
事件绑定是用圆括号来做的,写法:
对应 Component 内部的方法定义:
public btnClick(event):void{
alert("测试事件绑定!");
}
双向绑定
双向绑定是通过方括号里面套一个圆括号来做的,模板写法:
对应组件内部的属性定义:
public fontSizePx:number=14;
AngularJS 是第一个把“双向数据绑定”这个特性带到前端来的框架,这也是 AngularJS 当年最受开发者追捧的特性,之一。
根据 AngularJS 团队当年讲的故事,“双向数据绑定”这个特性可以大幅度压缩前端代码的规模。大家可以回想一下 jQuery 时代的做法,如果要实现类似的效果,是不是要自己去编写大量的代码?尤其是那种大规模的表单,一大堆的赋值和取值操作,都是非常丑陋的“面条”代码,而有了“双向数据绑定”特性之后,一个绑定表达式就搞定。
目前,主流的几款前端框架都已经接受了“双向数据绑定”这个特性。
当然,也有一些人不喜欢“双向数据绑定”,还有人专门写了文章来进行批判,也算是前端一景。
在模板里面使用结构型指令
Angular 有 3 个内置的结构型指令:*ngIf、*ngFor、ngSwitch。ngSwitch 的语法比较啰嗦,使用频率小一些。
*ngIf 代码实例:
显示还是不显示?
public isShow:boolean=true;
public toggleShow():void{
this.isShow=!this.isShow;
}
*ngFor 代码实例:
{{i+1}}-{{race.name}}
public races:Array=[
{name:"人族"},
{name:"虫族"},
{name:"神族"}
];
*ngSwitch 代码实例:
下载中...
正在读取...
系统繁忙...
public mapStatus:number=1;
特别注意:一个 HTML 标签上只能同时使用一个结构型的指令。
因为“结构型”指令会修改 DOM 结构,如果在一个标签上使用多个结构型指令,大家都一起去修改 DOM 结构,到时候到底谁说了算?
那么需要在同一个 HTML 上使用多个结构型指令应该怎么办呢?有两个办法:
加一层空的 div 标签
加一层
在模板里面使用属性型指令
使用频率比较高的 3 个内置指令是:NgClass、NgStyle、NgModel。
NgClass 使用案例代码:
public currentClasses: {};
public canSave: boolean = true;
public isUnchanged: boolean = true;
public isSpecial: boolean = true;
setCurrentClasses() {
this.currentClasses = {
'saveable': this.canSave,
'modified': this.isUnchanged,
'special': this.isSpecial
};
}
.saveable{
font-size: 18px;
}
.modified {
font-weight: bold;
}
.special{
background-color: #ff3300;
}
NgStyle 使用案例代码:
用NgStyle批量修改内联样式!
public currentStyles: {}
public canSave:boolean=false;
public isUnchanged:boolean=false;
public isSpecial:boolean=false;
setCurrentStyles() {
this.currentStyles = {
'font-style': this.canSave ? 'italic' : 'normal',
'font-weight': !this.isUnchanged ? 'bold' : 'normal',
'font-size': this.isSpecial ? '36px' : '12px'
};
}
ngStyle 这种方式相当于在代码里面写 CSS 样式,比较丑陋,违反了注意点分离的原则,而且将来不太好修改,非常不建议这样写。
NgModel 使用案例代码:
ngModel只能用在表单类的元素上面
{{currentRace.name}}
public currentRace:any={name:"随机种族"};
请注意,如果你需要使用 NgModel 来进行双向数据绑定,必须要在对应的模块里面 import FormsModule 。
管道
管道的一个典型作用是用来格式化数据,来一个最简单的例子:
{{currentTime | date:'yyyy-MM-dd HH:mm:ss'}}
public currentTime: Date = new Date();
Angular 里面一共内置了 17 个指令(有一些已经过时了):
在复杂的业务场景里面,17 个指令肯定不够用,如果需要自定义指令,请查看这里的例子: https://angular.io/guide/pipes 。