在先前的步骤中,我们看到了一个控制器和一个模板如何一起工作来将一个静态的HTML文件转化为动态页面(view)。一般说来,这在单页应用中一种非常常见的模式(在Angular应用中尤其是这样):
·客户端代码“掌管”并和视图层实现了动态交互,通过在数据模型和状态中即刻更新视图来反应改变,这经常是用户交互的结果(我们不久将在第5步中看到一个例子),这种做法取代了在服务端创建一个静态HTML页面的做法。
模板(视图层包含绑定和展示逻辑的部分)作为一个蓝图,以此来决定我们数据怎么组织和展示给用户。而控制器提供了执行绑定和在我们的模板中申请行为和逻辑的环境(context)。
应用中依然有几个可以做得更好的地方:
1.要是我们想在我们应用的不同部分复用相同的功能,怎么做呢?
我们可能得复制整个模板(包括控制器)。这样做容易出错并且会降低可维护性。
2.作用域,这被用于将我们的控制器和模板粘合到一起形成一个动态页面,不是从页面的其他部分中分离出来的。这意味着在我们的视图层中,页面不同部分的一个随机,不相关的改变可能导致不可预料且很难debug的副作用。
(好吧,可能对于我们这小小的范例不用关心这些,但当面临一个更大,真正的应用时这些考虑是合理的。)
这一步中最大的不同将会在下面列出。您也可以点击这里在GitHub上查看全部的不同。
组件救援!
既然这种组合(模板+控制器)是如此公共和常见的模式,Angular提供了一种简单且优雅的方式来将它们组合成可复用且独立的实体,这被称为组件。另外,Angular为我们组件中的每一个实体创建了一个所谓的isolate作用域,这意味着应用中不再有原型继承,并且我们的组件不会影响应用中的其他部分,反之亦然。
(由于这是一份介绍性的tutorial,我们不会深入介绍Angular 组件的所有特性。您可以通过阅读开发者手册的组件部分来进一步了解组件及其使用模式。
实际上,一种观点认为组件其孪生兄弟的武断(opinionated),精简的(stripped-down)版本,这个孪生兄弟更为复杂和冗余(但确实很有效)--指令,这是用Angular的方式来教会HTML的新把戏,你可以阅读开发者手册的指令部分。
注:指令是一个高级话题,所以您可能得延迟学习它们知道您精通了基础部分。)
我们使用Angular模块中的.component()方法来创建一个组件。我们必须提供组件的名字和组件定义对象(Component Definition Object,缩写为CDO)。
请牢记(这点组件和指令一样)组件的名字使用驼峰命名(camelCase),但在我们的HTML中引用它的时候将会使用串联命名法(kebab-case)。
用最简单的形式,CDO将会仅仅包含一个模板和一个控制器。(我们甚至可以省略控制器,Angular会为我们创建一个傀儡控制器,这对于简单的“展示用的”组件是可以的,那将不对模板附加任何行为。)
来看个例子吧:
angular. module(‘myApp‘). component(‘greetUser‘, { template: ‘Hello, {{$ctrl.user}}!‘, controller: function GreetUserController() { this.user = ‘world‘; } });
现在,每次我们在视图层引入<greet-user></greet-user>
,Angular将会使用提供的模板将其扩展成一棵DOM字数并且通过制定的控制器实体来管理它。
不过等等,这个$ctrl是哪来的?它又指向哪里?
由于一些已经提及的理由(还有一些已经超出本tutorial范围的理由),避免直接使用作用域(scope)被认为是一次最佳实践。我们可以(而且应该)使用我们的控制器实体;比如将我们的数据和方法分配到我们控制器中(这里“this”包含在控制器构造器中),而不是直接分配给控制器。
我们可以通过别名来从模板中引用控制器。这次,解析我们表达式的环境就更为明了了。默认地,组件使用$ctrl作为控制器的别名,但我们可以在需要的时候重载它。
还有更多可选的操作,所以在您的应用中使用.component()之前请确保您查看过API参考手册。
使用组件
既然我们已经知道如何创建组件了,让我们用刚刚学习的技能来重构HTML页面吧。
app/index.html:
<html ng-app="phonecatApp"> <head> ... <script src="bower_components/angular/angular.js"></script> <script src="app.js"></script> <script src="phone-list.component.js"></script> </head> <body> <!-- Use a custom component to render a list of phones --> <phone-list></phone-list> </body> </html>
app/app.js
:
// Define the `phonecatApp` module angular.module(‘phonecatApp‘, []);
app/phone-list.component.js
:
// Register `phoneList` component, along with its associated controller and template angular. module(‘phonecatApp‘). component(‘phoneList‘, { template: ‘<ul>‘ + ‘<li ng-repeat="phone in $ctrl.phones">‘ + ‘<span>{{phone.name}}</span>‘ + ‘<p>{{phone.snippet}}</p>‘ + ‘</li>‘ + ‘</ul>‘, controller: function PhoneListController() { this.phones = [ { name: ‘Nexus S‘, snippet: ‘Fast just got faster with Nexus S.‘ }, { name: ‘Motorola XOOM™ with Wi-Fi‘, snippet: ‘The Next, Next Generation tablet.‘ }, { name: ‘MOTOROLA XOOM™‘, snippet: ‘The Next, Next Generation tablet.‘ } ]; } });
没错!输出的结果看起来是一样的,但让我们来看看我们收获了什么:
·我们的电话列表是可复用的,将 <phone-list></phone-list>放到页面的任何地方都可以获取一个电话列表。
·我们的主视图层(index.html)更加干净而且更具声明性了,仅仅通过查看它,我们就知道里边有一个电话列表。我们不用费心去考虑实现细节。
·从“外联影响(external influence)”看来,我们的组件是独立且安全的。同样的,我们不必担心意外地破坏了应用中的其他部分。我们组件中发生的故事依然待在我们的组件内。
·独立测试我们的组件变得更容易了。
关于文件命名的一点注解:
通过文件名前缀来区分不同类型的实体是一个好的实践。在这个tutorial中,我们使用.component前缀来表示组件,所以定义某个组件的名字会被命名为some-component.component.js.