Angular 2模板语法全解析:从数据绑定到结构型指令
2026/9/15 4:55:07 网站建设 项目流程

Angular 2 正式发布那会儿,不少从 AngularJS(也就是 1.x)过来的老前端,第一反应是翻文档,第二反应是怀疑人生。原来用ng-click点一下就完事了,现在要写(click);原来一个ng-repeat就能循环渲染,现在又是*ngFor又是let item of items,多了一层东西到底图什么?

我当时也是这么过来的。等真正用 Angular 2 写了几个项目、接手过别人留下的代码之后,才慢慢意识到:Angular 2 的模板语法不是换了个写法,而是换了一套数据流的思考方式。方括号、圆括号、星号这些符号,本质上都是在给数据流向做标注。搞懂了这套符号背后的逻辑,再看模板的时候,脑子里会自动浮现出一条条"组件数据往 DOM 流、用户操作往组件流"的箭头,写起来顺手,排查起问题来也更直接。

所以这篇文章不打算只列语法规则,我想把"为什么这样设计"和"实际开发里到底怎么用"一起讲清楚。适合刚上手 Angular 2 想系统过一遍模板语法的读者,也适合已经写过一段时间但有些语法只知道能用、不知道什么时候该用、甚至踩过坑的开发者。

1. 入门之前:Angular 2 模板语法的设计逻辑比我预想的重要

1.1 从 AngularJS 到 Angular 2,模板语法不是换写法,是换思维

AngularJS 时代我们写模板,最常见的方式是往 HTML 里塞一堆ng-开头的指令:ng-repeat做循环,ng-click绑事件,ng-show/ng-hide控制显隐,ng-bind输出数据,ng-model做双向绑定。功能上没毛病,但有一个问题:指令各自为政,语法形态不统一,也没有表达出数据的流向。

Angular 2 把这个体系整个推倒重来了。新的模板语法只用三类符号,就覆盖了绝大多数场景:

语法含义数据流方向
[属性]="表达式"属性绑定组件 → DOM
(事件)="语句"事件绑定DOM → 组件
[(x)]="属性"双向绑定双向
*指令结构型指令控制 DOM 增减

这个设计的精妙之处在于:模板里的 HTML 不再只是一堆静态标签,而是变成了数据流的可视化表达。方括号像是一个"输入管道",圆括号像是一个"输出管道",组合起来的香蕉括号(banana in a box)就是双向管道。你不需要看到组件的 TypeScript 代码,光读模板就能大致推断出组件上有哪些属性、会触发哪些方法。

1.2 模板到底是什么:"带标记的 HTML"如何变成视图

Angular 2 的模板,形式上还是 HTML,但里面掺了 Angular 的标记语法。浏览器不认识这些方括号和圆括号,所以 Angular 编译器会先把模板编译成组件视图定义,再实例化成真实的 DOM。

这里的重点在于:模板中的绑定表达式会在每个变更检测周期被重新求值。Angular 通过 Zone.js 拦截了浏览器的事件、定时器、Promise 回调等异步操作,在它们执行完之后自动触发变更检测,从组件树的根部往下检查所有绑定表达式的值是否发生变化,变了就更新 DOM。

这也是为什么你在模板里写的每一个绑定,都相当于给框架递了一个"监听清单"。框架知道哪个属性被模板用了,值一变就会去更新对应的节点。这跟我们以前用 jQuery 手动$(el).text(xxx)更新 DOM 是完全不同的两个时代的方法论。

1.3 模板表达式的边界:哪些写法被禁止了

模板里可以写表达式,但不是所有 JavaScript 表达式都能用。很多初学者会踩这个坑,把模板里当 JS 环境来写,结果编译报错。

Angular 2 模板表达式的限制包括:

  • 不支持语句ifforwhile这些都不能用。
  • 不支持new运算符:不能在模板里new Date()
  • 不支持自增自减i++i--都不行。
  • 不支持位运算|有特殊含义(管道符),&>>这些也别想用。
  • 不支持赋值操作=+=-=都是非法语法(事件绑定里的赋值除外,比如(click)="count = count + 1"是允许的)。
  • 不能引用全局变量windowdocumentMathconsole这些全都不行。

为什么 Angular 要这么限制?原因很实际:模板表达式必须保持简单、可预测、无副作用。如果允许在模板里Math.random()或修改变量,每个变更检测周期执行结果都不一样,框架没办法做可靠的依赖追踪,调试会变成噩梦。

那如果确实需要DateMath这类能力怎么办?正确做法是在组件类里封装成属性或方法,再暴露给模板。这其实是 Angular 推荐的习惯:模板只做展示和简单计算,逻辑都往组件类里放。

2. 插值表达式与属性绑定:最容易混淆的一组语法

2.1 插值{{}}是属性绑定的语法糖

{{ }}插值表达式大概是所有人接触 Angular 2 模板语法时第一个见到的写法:

<p>Hello {{ name }}</p>

它的作用是把组件属性的值插入到文本节点中。但很多人不知道的是,插值表达式本质上就是属性绑定的一个语法糖。上述写法经过编译器处理,等效于:

<p [textContent]="name">Hello</p>

或者说,插值表达式最终会编译成一个针对textContent的属性绑定。所以概念上,插值{{ }}和属性绑定[property]最终走的是同一套变更检测机制,只是插值写起来更方便,适合内联在文本中。比如拼接字符串:

<p>用户 {{ user.name }} 在 {{ user.lastLogin }} 登录过</p>

这种场景用插值非常自然,而属性绑定[textContent]写起来反而很别扭。

2.2[disabled]="false"为什么必须用属性绑定

这是我在新项目里经常看到的问题。有人写:

<button disabled="{{ isDisabled }}">提交</button>

isDisabledfalse时,按钮依然处于禁用状态。原因在于:HTML 布尔属性的特征是"只要属性存在,值就是 true"。插值表达式永远输出的是字符串,所以当isDisabledfalse时,上面的写法实际上是disabled="false",而浏览器看到disabled属性存在,就认为是禁用。

改用属性绑定就正常了:

<button [disabled]="isDisabled">提交</button>

属性绑定传的是真正的布尔值false,Angular 会调用Renderer将属性的存在与否和值对应起来。

类似的布尔属性还有checkedreadonlyselectedmultiple等,只要涉及这些属性,一律用属性绑定,别用插值。

2.3 组件输入属性的类型问题

另一个常见坑发生在自定义组件上。假设你有个组件:

@Component({ selector: 'app-price', template: '<span>{{ price }}</span>' }) export class PriceComponent { @Input() price: number; }

如果你用插值传值:

<app-price price="{{ 199 }}"></app-price>

price拿到的会是字符串"199",如果里面做了price.toFixed(2),直接报错。正确做法是:

<app-price [price]="199"></app-price>

这样就传入了真正的数字类型。这个问题的本质是:插值输出的永远是字符串,属性绑定保留表达式的实际求值类型。所以凡是给组件传对象、数组、数字、布尔值,一律用属性绑定。

2.4 模板表达式里常见的编译错误

写模板时最容易出现的报错,第一是表达式里用了全局对象:

{{ Math.floor(price) }} <!-- 报错:Math is not defined -->

第二是在模板里尝试做赋值或自增:

{{ count++ }} <!-- 表达式不能包含自增操作 -->

第三是把管道符和位运算混了:

{{ value | 2 }} <!-- 管道符后面要接管道名 -->

遇到这些错误别慌,这是 Angular 在提醒你"这里不该放逻辑"。去组件类里写个方法或计算属性,模板就干净了。

3. 事件绑定与双向绑定:把数据流的方向搞清楚

3.1(click)背后发生了什么

事件绑定的语法是(事件名)="模板语句"

<button (click)="save()">保存</button> <input (input)="onInput($event)">

和属性绑定方向相反,事件绑定处理的是从 DOM 到组件的数据流。Angular 编译时会为这个按钮挂载原生的click监听器,事件触发后执行组件方法。

这里有个关键点:事件触发后,Angular 会自动执行变更检测。这是事件绑定和手动addEventListener最大的区别。我见过有人写了一个自定义指令,在内部用原生的element.addEventListener('click', ...)更新组件数据,结果界面怎么都不刷新,就是因为脱离了 Angular 的 Zone 监听范围,变更检测没有被触发。

所以能用(click)(input)(change)这类语法绑定的,绝对不要用原生监听。如果确实需要处理原生事件,也要在回调里手动触发变更检测(用ChangeDetectorRefNgZone.run),但这属于万不得已的兜底方案。

3.2 用$event和原生事件对象打交道

Angular 2 的事件绑定没有像 React 那样做合成事件包装,$event就是浏览器原生的Event对象。以输入框为例:

<input (input)="onInput($event)">
onInput(event: Event) { const value = (event.target as HTMLInputElement).value; this.searchText = value; }

需要阻止冒泡或取消默认行为时,直接在模板语句里调用:

<a href="/download" (click)="onDownload($event)">下载</a>
onDownload(event: Event) { event.preventDefault(); event.stopPropagation(); // 自定义下载逻辑 }

也可以把这两步直接在模板里写掉:

<a href="/download" (click)="$event.preventDefault(); download()">下载</a>

不过这种写法会让模板变得拥挤,我更建议逻辑多的场景放到组件方法里做。

3.3[(ngModel)]不是黑魔法,拆开看就两行

Angular 2 的双向绑定[(ngModel)]看起来像魔法,其实是一个语法糖,完全可以用属性绑定加事件绑定展开:

<!-- 等价写法一 --> <input [value]="name" (input)="name = $event.target.value"> <!-- 等价写法二:Angular 2 内部的实际做法 --> <input [ngModel]="name" (ngModelChange)="name = $event">

ngModel指令做的事情是:把组件属性映射到input的 value,同时监听input事件,把新值通过ngModelChange事件抛出去。所以[(ngModel)][ngModel](ngModelChange)的组合。

理解这个拆解后,你就能解释两件事:

  1. 为什么用[(ngModel)]需要FormsModule:因为ngModelFormsModule提供的指令,没在@NgModuleimports里引入它,模板根本识别不了。
  2. 为什么只写[ngModel]时,输入框的值不会随用户的输入变化:没有(ngModelChange)去更新数据,属性绑定只是单向的。

还有一种常见的变体需求:用户输入后想要实时过滤,同时给值加个 trim。可以拆开写:

<input [ngModel]="keyword" (ngModelChange)="onKeywordChange($event)">
onKeywordChange(value: string) { this.keyword = value.trim(); }

这样既保留了双向绑定的便捷,又能在中间插入处理逻辑。

3.4 自定义组件怎么支持[(value)]双向绑定

[(ngModel)]是 Angular 内置的,但如果你想让自己的组件也支持双向绑定,约定的语法是一样的,核心就是Input 和 Output 命名要成对。比如做一个自己封装的计数器组件:

@Component({ selector: 'app-counter', template: `<button (click)="decrement()">-</button>{{ count }}<button (click)="increment()">+</button>` }) export class CounterComponent { @Input() count: number; @Output() countChange = new EventEmitter<number>(); increment() { this.count++; this.countChange.emit(this.count); } decrement() { this.count--; this.countChange.emit(this.count); } }

父组件这样用:

<app-counter [(count)]="total"></app-counter>

规则很简单:@Input() xxx@Output() xxxChange成对出现,模板里就能用[(xxx)]双向绑定。这个约定被官方称为 banana-in-a-box,因为它长得像方括号包圆括号:[()]。如果你拆开来看:

<app-counter [count]="total" (countChange)="total = $event"></app-counter>

就一目了然了。

4. 结构型指令:DOM 的加减法比你想的讲究

4.1*号是怎么来的:ng-template 展开

结构型指令的特点是有星号*,比如*ngIf*ngFor*ngSwitchCase。加星号是为了让模板写起来更像 HTML,但本质上它是个语法糖。*ngIf其实展开成:

<ng-template [ngIf]="isLogged"> <p>欢迎回来</p> </ng-template>

ng-template是一个虚拟容器,它本身不会渲染任何 DOM,只负责包裹需要条件渲染的内容。手动写ng-template最大的价值在于,你可以更灵活地组织多个结构型指令,尤其是ngIfelse分支:

<div *ngIf="isLogged; else loginBox">欢迎回来</div> <ng-template #loginBox> <p>请先登录</p> </ng-template>

这里#loginBox是模板引用变量,指向一个ng-template的引用,else语句通过变量名找到它。

4.2 ngIf 与 hidden:销毁还是隐藏,这是个问题

很多人在做条件显隐时纠结用*ngIf还是[hidden]。它们的区别不只是语法,而是 DOM 处理方式完全不同:

对比项*ngIf[hidden]
DOM 是否保留条件为 false 时移除保留元素,仅添加display: none
组件生命周期每次创建/销毁都会触发ngOnInitngOnDestroy不触发生命周期
图片/接口请求条件为 false 时不创建,不请求元素存在,图片通常会加载
性能开销频繁切换时创建/销毁开销大无创建/销毁开销,但要一直维护 DOM
适用场景条件变化不频繁、初始化有成本的内容高频切换、轻量 UI 元素

我的实际建议是:绝大多数场景用*ngIf因为它能保证组件在不可见时不占用资源,而且状态可控。但如果有一段内容在用户操作下会非常高频地切换显隐(比如点击展开收起一个面板),而且面板内部没有图片、没有组件要初始化,用[hidden]会让交互流畅不少,因为每次切换不需要重新创建 DOM。

还有一点容易踩坑:[hidden]的样式优先级问题。如果元素上同时有类似.active { display: block }的自定义样式,display: none会被覆盖,导致hidden失效。遇到这种情况,要么用*ngIf,要么加上!important之类的补救措施。

4.3 ngFor 的真正威力:index/even/odd 和 trackBy 性能优化

*ngFor最基础的用法是循环渲染:

<li *ngFor="let item of items">{{ item.name }}</li>

但它提供的上下文变量非常实用:

<div *ngFor="let item of items; index as i; first as isFirst; last as isLast; even as isEven; odd as isOdd"> <span>{{ i }}: {{ item.name }}</span> <span *ngIf="isFirst">(最新)</span> </div>

这些在列表展示里很常用,比如给偶数行加背景色:[class.row-even]="isEven"

trackByngFor里最容易被忽略的性能优化项。默认情况下,Angular 不用 trackBy 时,遇到数组元素引用变化会销毁所有列表项 DOM 再重建。如果列表数据是通过接口轮询或刷新获得的,每次返回的都是新数组,即使内容完全一样,也会触发全量重建,结果就是输入框丢焦点、滚动位置丢失、图片重新请求。

加上 trackBy 可以解决:

trackById(index: number, item: any) { return item.id; }
<li *ngFor="let item of items; trackBy: trackById">{{ item.name }}</li>

这样 Angular 会根据item.id判断哪些项需要更新,相同 id 的 DOM 直接复用。我之前做过一个聊天列表,后端每 3 秒推一次消息,数据数组整体替换。刚开始没加 trackBy,用户正在输入框打字,消息到达后输入框直接失焦,加了trackBy: trackById后问题立刻消失。

4.4 手写一个结构型指令感受 ViewContainerRef 的力量

理解了*ngIf的展开之后,自己写一个结构型指令就有了基础。比如实现一个*appUnless(条件为 false 时显示,和*ngIf相反):

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; @Directive({ selector: '[appUnless]' }) export class UnlessDirective { private hasView = false; constructor( private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef ) {} @Input() set appUnless(condition: boolean) { if (!condition && !this.hasView) { this.viewContainer.createEmbeddedView(this.templateRef); this.hasView = true; } else if (condition && this.hasView) { this.viewContainer.clear(); this.hasView = false; } } }

用法:

<div *appUnless="isLoading">内容加载完成</div>

这里核心的两个依赖:TemplateRef指向模板内容,ViewContainerRef是视图容器,负责把模板实例化并插入视图,或者清空删除。理解这两个类,结构型指令的原理基本就通了。

5. 样式与属性绑定:改了类名,改样式,再进一步

5.1 ngClass 与 ngStyle 什么时候用、什么时候别用

动态控制样式,最常用的两个属性型指令是ngClassngStyle

ngClass支持三种写法:

<!-- 对象形式:key 是类名,value 是布尔条件 --> <div [ngClass]="{ active: isActive, disabled: isDisabled }">菜单项</div> <!-- 数组形式 --> <div [ngClass]="['active', 'highlighted']">内容</div> <!-- 字符串形式 --> <div [ngClass]="'active highlighted'">内容</div>

ngStyle的写法:

<div [ngStyle]="{ 'font-size': fontSize + 'px', backgroundColor: bgColor }">内容</div>

使用建议是:能用 class 解决的就别用 style。样式应该尽量收敛在 CSS 里,模板里出现一坨内联样式会让样式表和模板的关系变得混乱。但是遇到需要动态计算的值,比如拖拽的坐标、进度条的宽度百分比、随机颜色等,ngStyle是合理的工具:

<div class="progress-bar"> <div class="progress-bar-inner" [ngStyle]="{ width: progress + '%' }"></div> </div>

5.2 单类/单样式绑定的原生简洁写法

如果只是给一个元素动态加一个类名,用[ngClass]反而显得重。Angular 为单一类名和单一样式提供了更简洁的写法:

<!-- 单个类名 --> <div [class.active]="isActive">链接</div> <!-- 单个样式 --> <div [style.color]="isError ? 'red' : 'green'">状态</div> <!-- 带单位 --> <div [style.width.px]="progressWidth">进度</div>

[style.width.px]这种带单位的写法很实用——Angular 会自动给属性值拼上px单位,省去了手动拼接字符串。这种写法在 AngularJS 时代是没有的,用起来真的很顺手。

需要添加多个类名时,可以并列写多条:

<div [class.active]="isActive" [class.highlighted]="isHighlighted">内容</div>

这样可读性比一个[ngClass]对象更好,推荐在类名条件独立、逻辑清晰时使用。如果条件之间有关联(比如互斥状态),用[ngClass]的对象形式更合适。

5.3 写一个属性型指令:高亮、权限、水印都能做

属性型指令是模板语法的延伸,常见的应用是封装样式和行为的组合逻辑。做一个高亮指令:

import { Directive, HostBinding, HostListener, Input } from '@angular/core'; @Directive({ selector: '[appHighlight]' }) export class HighlightDirective { @Input() appHighlight = 'yellow'; @HostBinding('style.backgroundColor') backgroundColor: string; @HostListener('mouseenter') onMouseEnter() { this.backgroundColor = this.appHighlight; } @HostListener('mouseleave') onMouseLeave() { this.backgroundColor = null; } }

使用:

<p [appHighlight]="'lightblue'">鼠标移上来背景变色</p>

这里@HostBinding负责绑定宿主元素的属性(这里是style.backgroundColor),@HostListener负责监听宿主元素的事件。HostBindingHostListener这两个装饰器实现的效果,和直接在模板里写[style.xxx](mouseenter)是等价的,只是位置从模板挪到了指令类里。

实际项目里,权限指令也常用这个模式:按钮上标注需要的权限码,指令初始化时检查当前用户权限,没有则把按钮从 DOM 里移除或置灰。

6. 模板引用变量、安全导航与管道:让模板更健壮更省心

6.1#模板引用变量的作用域和典型用法

模板引用变量用#声明,可以在模板里拿到元素、组件实例或指令实例:

<input #nameInput> <button (click)="alert(nameInput.value)">获取值</button>

典型用法有三类:

  1. 操作原生 DOM#nameInput拿到的是HTMLInputElement,可以拿.value.focus()等。
  2. 拿到组件实例:给子组件打一个引用变量,可以在模板里直接访问子组件的公开属性和方法。
  3. 拿到指令的暴露实例:最典型的场景是模板驱动表单的ngForm
<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm)"> <input name="username" ngModel required> <button [disabled]="myForm.invalid">提交</button> </form>

这里#myForm="ngForm"不是拿 DOM 元素,而是把ngForm指令实例暴露给变量,这样模板里就能直接用myForm.invalidmyForm.value这些表单状态。

关于作用域要特别注意:模板引用变量只能在它声明所在的模板范围内使用,不能跨组件。你不能在一个组件的模板里访问另一个组件内部的引用变量。对于结构型指令内部的变量,作用域还会进一步限制在指令的模板范围内。

还有个小提醒:引用变量名尽量不要和组件类里的属性重名,否则在模板里变量的引用优先级更高,容易出现出乎意料的取值结果。

6.2 安全导航操作符?.不是偷懒工具而是防御策略

在模板里渲染嵌套对象时,最常见的报错就是Cannot read property 'xxx' of undefined。比如接口返回的数据结构是user.profile.nickname,但profile可能为null,直接渲染就会报错。

Angular 提供了安全导航操作符:

<p>{{ user?.profile?.nickname }}</p>

usernullundefined时,整个表达式返回null,不会继续往下访问,模板显示为空。

不过我想提醒一句:安全导航符是防御手段,不是逃避手段。如果一个字段的值正常来说必然存在,出现了undefined往往意味着上游数据结构出问题了。到处滥用?.会让错误被静默吞掉,排查时更难发现根因。

更好的实践是:在数据源头(接口层或数据初始化处)就保证结构完整,比如用默认值初始化:

this.user = data.user || { profile: { nickname: '匿名用户' } };

这样模板可以放心使用user.profile.nickname,一旦真的拿不到也能看到兜底值,不容易整页崩溃。

Angular 2 还支持空值合并操作符??,和 TS 里的行为一致:

<p>{{ user?.nickname ?? '匿名用户' }}</p>

这个组合很适合做显示层兜底。

6.3 管道:内置管道、参数、链式调用与纯管道陷阱

管道(Pipe)是模板语法里处理显示格式的高效工具,语法是value | 管道名: 参数。常用的内置管道有这些:

管道作用示例
date日期格式化`{{ birthday
uppercase/lowercase大小写转换`{{ name
currency货币格式化`{{ price
percent百分比`{{ ratio
jsonJSON 输出`{{ obj
slice截取数组/字符串`{{ items
async异步数据自动解析`{{ data$

管道可以链式调用,前一个的输出是后一个的输入:

<p>{{ birthday | date: 'yyyy-MM-dd' | uppercase }}</p>

这里date管道输出的字符串再经过uppercase变成大写。

有个坑值得一说:默认情况下管道是纯管道(pure: true),只有当输入值的引用发生变化时才会重新执行。如果输入是数组或对象,你在组件里直接修改了它的某个属性(而不是替换整个引用),管道不会重新执行,显示结果不会更新。

一个真实的例子:我写过一个过滤管道,接收一个数组和一个关键字,结果点击"清空筛选"后列表纹丝不动,因为数组引用没变。解决方式有两个:要么在组件里每次赋值时创建新的数组(this.items = [...this.items]),要么把管道标记为非纯:

@Pipe({ name: 'myFilter', pure: false }) export class MyFilterPipe implements PipeTransform { // ... }

非纯管道会在每次变更检测时都执行,更新及时,但代价是性能开销变大,因为即使输入没变也会重新计算。所以我的建议是:优先保证输入引用更新;实在做不到再考虑纯管道和非纯管道的取舍,而且管道内逻辑要尽量轻量。

6.4 async 管道:处理异步数据最省事的办法

async管道在我个人看来是 Angular 模板语法里最能"省代码"的设计。它可以接收ObservablePromise,在模板中自动订阅、自动解包,并在组件销毁时自动退订。

组件类:

data$: Observable<string[]> = this.service.getData();

模板:

<ul> <li *ngFor="let item of data$ | async">{{ item }}</li> </ul>

同样,在*ngIf里使用 async 管道还能配合as语法,避免重复订阅:

<div *ngIf="user$ | async as user"> <p>{{ user.name }}</p> <p>{{ user.email }}</p> </div>

这里$event | async只订阅了一次,结果通过as user存成局部变量,后面的模板可以直接复用。这个写法极大简化了异步数据的处理逻辑——不用在组件里手动写subscribe,也不用担心忘了退订导致内存泄漏。

不过也要注意一点:async管道订阅的数据流如果不是冷 Observable,每次重新订阅可能会触发新的副作用。需要复用同一份数据的场景,更稳妥的做法是在组件里shareReplay(1)或用BehaviorSubject,再在模板里配合async使用。

收个尾:我实际踩过的模板语法相关的坑

写了这么多,最后分享一个我在真实项目中印象最深的教训:模板里[style.width.px]这种带单位的绑定,值必须是数字。我当时图省事直接绑定了一个字符串"50",结果样式一直没有生效,排查了很久才发现是类型问题。Angular 的模板其实有很多这样的小约定,平时不觉得,遇到问题会耗掉你不少时间。

另外提一句,做模板语法相关的工作时,尽量保持每个表达式的逻辑足够简单。模板写得太复杂,比如在模板里调用一个包含大量计算的方法,每次变更检测都会重新执行,页面卡顿往往悄无声息地吃在这些地方。正确做法是把计算结果缓存到组件属性里。

Angular 2 的模板语法学起来不难,但真正要熟练,需要在项目里反复碰壁才能体会各个语法的边界和取舍。这篇文章能帮你在入门时少走一些弯路,那就值了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询