关键要点
$onInit
和$onDestroy
等生命周期事件有效管理组件的设置和拆卸,确保资源得到适当的初始化和清理。$scope
进行交互,这符合Angular 2的实践,并提高了组件的模块化和可重用性。本文由Mark Brown和Jurgen Van de Moere共同评审。感谢所有SitePoint的同行评审员,使SitePoint的内容达到最佳状态!
2017年1月10日:文章更新,澄清了关于单向绑定的部分,并添加了关于单次绑定的信息。---
在Angular 1中,组件是创建自定义HTML元素的机制。过去可以使用Angular指令实现这一点,但组件建立在对Angular的各种改进之上,并强制执行构建和设计方面的最佳实践。
在本文中,我们将深入探讨组件的设计以及如何在应用程序中使用它们。如果您尚未开始在Angular 1中使用组件,您可以阅读我们最近的教程中关于它们的语法和设计的介绍。我的目标是概述一些最佳实践,这些实践将提高应用程序的质量。
还应该注意的是,通过新的组件API,Angular 2的许多最佳实践都被引入到Angular 1中,使您可以构建更容易以后重构的应用程序。Angular 2影响了我们思考和设计Angular 1组件的方式,但仍然存在许多明显的差异。Angular 1仍然是构建应用程序的非常强大的工具,因此我相信即使您不打算或尚未准备好迁移到Angular 2,投资使用组件改进您的应用程序也是值得的。
组件的设计应考虑许多关键特性,以使其成为应用程序的强大构建块。我们将更详细地探讨每一个特性,但以下是组件应遵循的主要概念。
现在让我们首先了解为什么以及如何将组件与应用程序的其余部分隔离和封装。
Angular 1功能的演变是为了启用隔离和封装的组件,这是有充分理由的。一些早期的应用程序与$scope
和嵌套控制器的使用高度耦合。最初Angular没有提供解决方案,但现在有了。
好的组件不会暴露其内部逻辑。由于它们的设计方式,这很容易实现。但是,除非绝对必要(例如发出/广播事件),否则应避免滥用组件使用$scope
。
组件应承担单一角色。这对于可测试性、可重用性和简单性非常重要。最好创建额外的组件,而不是超载单个组件。这并不意味着您不会拥有更大或更复杂的组件,它只是意味着每个组件都应专注于其主要工作。
我根据组件在应用程序中的作用将其分为四个主要组,以帮助您考虑如何设计组件。构建这些不同类型的组件没有不同的语法——只需要考虑组件所承担的特定角色即可。
这些类型基于我5年以上的使用Angular的经验。您可以选择略微不同的组织方式,但根本概念是确保您的组件具有明确的角色。
只有一个应用程序组件可以充当应用程序的根。您可以将其视为在Web应用程序的主体中只有一个组件,所有其他逻辑都通过它加载。
>
> >
>
这主要推荐用于Angular 2设计一致性,因此如果您希望迁移,将来会更容易。它还通过将应用程序的所有根内容移动到单个组件中(而不是将其中一些内容放在index.html文件中)来帮助测试。应用程序组件还为您提供了一个进行应用程序实例化的地方,因此您不必在应用程序运行方法中执行此操作,从而增强了可测试性并减少了对$rootScope
的依赖。
此组件应尽可能简单。如果可能,它可能只包含模板,而不包含任何绑定或控制器。但是,它不会替换ng-app
或引导应用程序的需要。
过去,我们在ui-router状态(或ngRoute路由)中链接控制器和模板。现在可以将路由直接链接到组件,因此组件仍然是控制器和模板配对的地方,但也有可路由的优点。
例如,使用ui-router,这就是我们链接模板和控制器的方式。
$stateProvider.state('mystate', {
url: '/',
templateUrl: 'views/mystate.html',
controller: MyStateController
});
现在,您可以将URL直接链接到组件。
$stateProvider.state('mystate', {
url: '/',
component: 'mystate'
});
这些组件可以从路由参数(例如项目ID)绑定数据,它们的作用是专注于设置路由以加载所需的其它组件。这种对路由定义的看似微小的更改实际上对于Angular 2迁移能力非常重要,但在Angular 1.5中也同样重要,因为它可以在组件级别更好地封装模板和控制器。
Angular 1实际上有两个路由模块,ngRoute和ngComponentRouter。只有ngComponentRouter支持组件,但它也已弃用。我认为最好的办法是使用ui-router。
您为应用程序构建的大多数唯一组件都是有状态的。在这里,您实际上将放置应用程序业务逻辑、发出HTTP请求、处理表单和其他有状态任务。这些组件可能对您的应用程序来说是独一无二的,它们专注于维护数据而不是视觉呈现。
假设您有一个控制器加载用户配置文件数据以显示,并且还有一个相应的模板(此处未显示)在指令中链接在一起。此代码段可能是完成这项工作的最基本的控制器。
.controller('ProfileCtrl', function ($scope, $http) {
$http.get('/api/profile').then(function (data) {
$scope.profile = data;
});
})
.directive('profile', function() {
return {
templateUrl: 'views/profile.html',
controller: 'ProfileCtrl'
}
})
使用组件,您可以比以前更好地设计它。理想情况下,您还应该使用服务而不是直接在控制器中使用$http
。
.component('profile', {
templateUrl: 'views/profile.html',
controller: function($http) {
var vm = this;
// 当组件准备好时调用,见下文
vm.$onInit = function() {
$http.get('/api/profile').then(function (data) {
vm.profile = data;
});
};
}
})
现在您有一个加载自身数据的组件,因此使其成为有状态的。这些类型的组件类似于路由组件,只是它们可能不用于链接到单个路由。
有状态组件将使用其他(无状态)组件来实际呈现UI。此外,您仍然希望使用服务,而不是将数据访问逻辑直接放在控制器中。
无状态组件专注于渲染而不管理业务逻辑,并且不必对任何特定应用程序都是唯一的。例如,大多数用于UI元素(例如表单控件、卡片等)的组件也不会处理加载数据或保存表单之类的逻辑。它们旨在高度模块化、可重用和隔离。
如果无状态组件只显示数据或控制模板中的所有内容,则可能不需要控制器。它们将接受来自有状态组件的输入。此示例从有状态组件(上面的profile示例)获取值并显示头像。
.component('avatar', {
template: '
',
bindings: {
username: ' },
controllerAs: 'vm'
})
要使用它,有状态组件将通过属性传递用户名,如下所示:
您使用的许多库都是无状态组件(和可能的服务)的集合。它们当然可以接受配置以修改其行为,但它们并非旨在负责其自身之外的逻辑。
这并不是组件的新功能,但在组件中使用它通常很明智。单向绑定的目的是避免将更多工作加载到摘要周期中,这是应用程序性能的主要因素。数据现在流入组件而无需查看其外部(这会导致当今存在的一些耦合问题),并且组件可以简单地根据该输入呈现自身。这种设计也适用于Angular 2,这有助于未来的迁移。
在此示例中,title属性仅根据提供的初始值绑定到组件一次。如果title由某个外部参与者更改,则它不会反映在组件中。表示绑定为单向的语法是使用
bindings: {
title: '}
当title属性更改时,组件仍将更新,我们将介绍如何侦听title属性的更改。建议您尽可能使用单向绑定。
Angular还能够单次绑定数据,因此您可以优化摘要周期。本质上,Angular将等待提供第一个非未定义的值到绑定中,绑定该值,然后(一旦所有绑定都已解析)从摘要周期中删除相关的观察者。这意味着特定绑定不会向未来的摘要循环添加任何处理时间。
这是通过在绑定表达式前面加上::
来完成的。如果您知道输入绑定在生命周期中不会更改,则只有这样做才有意义。在此示例中,如果title是单向绑定,它将继续在组件内部更新,但此处的绑定不会更新,因为我们将其指定为单次绑定。
>
{{::title}}>
您可能已经注意到$onInit
函数作为一项新功能。组件具有生命周期和相应的事件,您应该使用这些事件来帮助管理组件的某些方面。
$onInit()
组件生命周期的第一步是初始化。此事件在控制器和绑定初始化后运行。您几乎总是应该使用此方法进行组件设置或初始化。它将确保在运行之前所有值都可用于组件。如果您直接在控制器中访问绑定值,则不能保证这些值可用。
controller: function() {
var vm = this;
console.log(vm.title); // 可能尚未可用!
vm.$onInit = function() {
console.log(vm.title); // 保证可用!
}
}
$postLink()
下一步是链接模板中的任何子元素。当组件初始化时,不能保证它也已渲染在模板中使用的任何子元素。如果您需要以任何方式操作DOM,这一点很重要。一个重要的警告是,异步加载的模板在该事件触发时可能尚未加载。您可以始终使用模板缓存解决方案来确保模板始终可用。
controller: function() {
var vm = this;
vm.$postLink = function() {
// 通常可以安全地进行DOM操作
}
}
$onChanges()
当组件处于活动状态时,它可能需要对输入值的更改做出反应。单向绑定仍将更新您的组件,但我们有一个新的$onChanges
事件绑定来侦听输入何时更改。
对于此示例,假设向组件提供了产品标题和说明。您可以检测到如下所示的更改。您可以查看传递给函数的对象,该对象具有映射到可用绑定的对象,其中包含当前值和先前值。
bindings: {
title: '},
controller: function() {
var vm = this;
vm.$onChanges = function($event) {
console.log($event.title.currentValue); // 获取更新的值
console.log($event.title.previousValue); // 获取先前值
}
}
$onDestroy()
最后阶段是从页面中移除组件。此事件在控制器及其作用域销毁之前运行。重要的是清理组件可能创建的或持有内存的任何内容,例如事件侦听器、观察者或其他DOM元素。
controller: function() {
var vm = this;
vm.$onDestroy = function() {
// 重置或删除任何事件侦听器或观察者
}
}
要使用一组数据配置和初始化组件,组件应使用绑定来接受这些值。这有时被认为是组件API,这只是描述组件接受输入的方式的不同方法。
这里的挑战是为绑定提供简洁但清晰的名称。有时开发人员试图缩短名称以使其非常简洁,但这对于组件的使用来说是危险的。假设我们有一个接受股票代码作为输入的组件,以下哪一个更好?
bindings: {
smb: ' symbol: '}
希望您认为symbol
更好。有时开发人员也喜欢作为避免名称冲突的一种方式来为组件和绑定添加前缀。为组件添加前缀是明智的,例如md-toolbar
是Material工具栏,但为所有绑定添加前缀会变得冗长,应避免。
为了与其他组件通信,组件应发出自定义事件。有很多使用服务和双向数据绑定的示例来同步组件之间的数
据,但事件是更好的设计选择。事件作为与页面通信的一种方式效率要高得多(并且是JavaScript语言以及它在Angular 2中的工作方式的基础部分,这并非巧合)。
Angular中的事件可以使用$emit
(向上到作用域树)或$broadcast
(向下到作用域树)。这是一个快速示例事件的实际应用。
controller: function($scope, $rootScope) {
var vm = this;
vm.$onInit = function() {
// 向父级发出事件
$scope.$emit('componentOnInit');
};
vm.$onDestroy = function() {
// 从根向下发出子树事件
$rootScope.$broadcast('componentOnDestroy');
};
}
您需要在组件之间进行通信的主要有两种情况:在您了解的组件之间以及您不了解的组件之间。为了说明这种区别,让我们假设我们有一组组件来帮助管理页面上的选项卡,以及一个具有相应帮助页面链接的工具栏。
> >
>
title="Description"> >
title="Reviews"> >
title="Support"> >
>
在这种情况下,my-tabs
和my-tab
组件可能彼此了解,因为它们协同工作以创建一组三个不同的选项卡。但是,my-toolbar
组件超出了它们的认知范围。
每当选择不同的选项卡(这将是my-tab
组件实例上的一个事件)时,my-tabs
组件都需要知道,以便它可以调整选项卡的显示以显示该实例。my-tab
组件可以向上发出事件到父my-tabs
组件。这种类型的通信就像两个协同工作以创建单个功能(选项卡式界面)的组件之间的内部通信。
但是,如果my-toolbar
想要知道当前选择了哪个选项卡,以便它可以根据可见内容更改帮助按钮呢?my-tab
事件永远不会到达my-toolbar
,因为它不是父级。因此,另一个选择是使用$rootScope
向下发出整个组件树的事件,这允许任何组件侦听和React。这里潜在的缺点是您的事件现在到达每个控制器,如果另一个组件使用相同的事件名称,您可能会触发意外的影响。
确定哪种方法适合您的用例,但是每当另一个组件可能需要了解事件时,您可能都希望使用第二个选项向整个组件树发出事件。
现在可以使用组件编写Angular 1应用程序,这改变了我们编写应用程序的最佳实践和性质。这是为了更好,但仅仅使用组件并不一定比您以前使用的更好。在构建Angular 1组件时,请记住以下要点。
$onChanges
生命周期事件来观察更改。您是否在Angular 1.x应用程序中使用组件?或者,您是否要等到改用Angular 2?我很乐意在下面的评论中听到您的经验。
Angular 1.5组件是用于创建指令的更简单、更直观的API。虽然指令功能强大,但由于其灵活性,它们可能难以使用。另一方面,组件具有更简单的配置,并且旨在用于构建UI元素。它们还促进了单向数据绑定和生命周期挂钩的使用,这可以导致更可预测的数据流和更轻松的调试。
可以在Angular 1.5组件中使用bindings
属性实现单向数据绑定。
生命周期挂钩是在组件生命周期的特定点调用的函数。Angular 1.5引入了几个生命周期挂钩,例如$onInit
、$onChanges
、$onDestroy
和$postLink
。这些挂钩可用于执行诸如初始化数据、清理资源或对绑定更改做出React之类的任务。
可以在Angular 1.5中使用绑定和事件实现组件之间的通信。父到子的通信可以使用绑定来完成,而子到父的通信可以使用事件来完成。这促进了单向数据流,这可以使您的应用程序更容易理解。
从Angular 1.5中的指令迁移到组件涉及几个步骤。首先,用组件定义替换指令定义对象。然后,用生命周期挂钩替换链接函数。最后,用单向数据绑定和事件替换双向数据绑定。
Angular 1.5中的组件比指令提供了几个好处。它们具有更简单的API,促进了单向数据绑定和单向数据流,并提供了生命周期挂钩。这些功能可以使您的代码更易于理解、调试和维护。
可以在Angular 1.5组件中使用组件定义中的transclude
选项实现转录。这允许您在组件的模板中插入自定义内容,这对于创建可重用的UI元素非常有用。
可以在Angular 1.5组件中使用具有对象语法的transclude
选项实现多插槽转录。这允许您在组件的模板中定义多个转录插槽,这些插槽可以用自定义内容填充。
$onChanges
生命周期挂钩?每当更新单向绑定时,都会调用Angular 1.5组件中的$onChanges
生命周期挂钩。它接收一个更改对象,其中包含绑定的当前值和先前值。这可用于对绑定更改做出React,并执行诸如更新组件状态或获取数据之类的任务。
$postLink
生命周期挂钩?在组件的元素及其子元素链接后,会调用Angular 1.5组件中的$postLink
生命周期挂钩。这可用于执行需要访问组件的DOM元素的任务,例如设置事件侦听器或操作DOM。
Disclaimer: All resources provided are partly from the Internet. If there is any infringement of your copyright or other rights and interests, please explain the detailed reasons and provide proof of copyright or rights and interests and then send it to the email: [email protected] We will handle it for you as soon as possible.
Copyright© 2022 湘ICP备2022001581号-3