建筑质量Angular 1.5组件指南

本文由Mark BrownJurgen Van de Moere进行了同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!

有各种各样的烧瓶和瓶子的一位科学家


2017.01.10 :更新了该文章,以阐明单向绑定部分,并添加有关一次性绑定的信息。

在Angular 1中,组件是允许您创建自己的自定义HTML元素的机制。 过去使用Angular指令可以做到这一点,但是组件基于对Angular所做的各种改进,并在如何构建和设计方面实施了最佳实践。

在本文中,我们将深入探讨组件的设计以及如何将其用于应用程序内部。 如果您尚未开始在Angular 1中使用组件,则可以在我们的最新教程之一中了解它们的语法和设计。 我的目标是概述一些可以提高应用程序质量的最佳实践。

还应注意,Angular 2的许多最佳实践是通过新的组件API引入Angular 1的,从而使您能够构建以后更容易重构的应用程序。 Angular 2影响了我们思考和设计Angular 1组件的方式,但是仍然存在许多明显的差异。 Angular 1仍然是用于构建应用程序的非常强大的工具,因此,即使您不打算或不打算迁移到Angular 2,我认为也值得投资来使用组件来改进您的应用程序。

什么才是好组件?

在设计组件时,应牢记许多关键特性,以使其成为您应用程序的强大构建基块。 我们将更详细地研究每一个,但是这里是组件应遵循的主要概念。

  • 隔离 –组件的逻辑应封装为内部和私有。 这有助于减少组件之间的耦合。
  • 专注 -组件应作为一项主要任务的单个单元,这使它们易于推理并且通常更可重用。
  • 单向绑定 –可能的话,组件应利用单向绑定来减少摘要周期的负载。
  • 使用生命周期事件 –组件的生命周期以实例化开始,以从页面中删除结束。 最好挂入这些事件以随着时间的推移维护组件。
  • 定义良好的API –组件应以一致的方式接受配置作为属性,因此很容易知道如何使用它们。
  • 发出事件 –为了与其他组件进行通信,它们应发出具有适当名称和数据的事件。

现在让我们开始研究为什么以及如何将组件与应用程序的其余部分隔离和封装。

组件应隔离

出于充分的理由,Angular 1功能的发展是启用隔离和封装的组件。 某些早期应用程序与$scope和嵌套控制器的使用紧密相关。 最初,Angular没有提供解决方案,但现在提供了。

好的组件不会暴露其内部逻辑。 由于它们的设计方式,这很容易实现。 但是,除非绝对必要(例如发出/广播事件),否则请使用$scope抵制任何滥用组件的诱惑。

组件应重点关注

组件应该扮演一个角色。 这对于可测试性,可重用性和简单性很重要。 最好增加其他组件,而不要使单个组件过载。 这并不意味着您不会拥有更大或更复杂的组件,而只是意味着每个组件都应该专注于其主要工作。

我根据组件在应用程序中的角色将组件分为四个主要组,以帮助您考虑如何设计组件。 构建这些不同类型的组件没有不同的语法,重要的是要考虑组件所扮演的特定角色。

这些类型基于我5年以上的Angular经验。 您可以选择稍有不同的组织方式,但是基本概念是确保组件具有明确的作用。

应用组件

只能有一个应用程序组件充当您的应用程序的根目录。 您可以认为它就像Web应用程序主体中只有一个组件,而所有其他逻辑都是通过该组件加载的。

<body>
  <app></app>
</body>

推荐主要用于Angular 2设计奇偶校验,因此如果您愿意,有一天可以更轻松地进行迁移。 通过将应用程序的所有根目录内容移动到单个组件中,而不是将其中的某些根目录存储在index.html文件中,它也有助于测试。 应用程序组件还为您提供了一个应用程序实例化的场所,因此您不必在应用程序run方法中进行此操作,从而增强了可测试性并减少了对$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;
    // Called when component is ready, see below
    vm.$onInit = function() {
      $http.get('/api/profile').then(function (data) {
        vm.profile = data;
      });
    };
  }
})

现在,您有了一个可以加载其自身数据的组件,从而使其具有状态。 这些类型的组件与路由组件类似,不同之处在于它们可以在不链接到单个路由的情况下使用。

有状态组件将使用其他(无状态)组件来实际呈现UI。 另外,您仍然希望使用服务,而不是将数据访问逻辑直接放在控制器中。

无状态组件

无状态组件专注于呈现而不管理业务逻辑,并且不需要特定于任何特定应用程序。 例如,大多数用于UI元素的组件(例如表单控件,卡片等)也不处理诸如加载数据或保存表单之类的逻辑。 它们旨在高度模块化,可重用和隔离。

如果无状态组件仅显示数据或控制模板中的所有内容,则可能不需要控制器。 他们将接受来自有状态组件的输入。 本示例从有状态组件中获取一个值(上面的profile示例),并显示一个化身。

.component('avatar', {
  template: '<img ng-src="http://example.com/images/{{vm.username}}.png" />',
  bindings: {
    username: '<'
  },
  controllerAs: 'vm'
})

要使用它,有状态组件将通过属性传递用户名,如<avatar username="vm.profile.username">

您使用的大多数库都是无状态组件(可能还有服务)的集合。 他们当然可以接受配置来修改其行为,但是它们并不意味着自己之外还负责逻辑。

组件应使用单向绑定

这不是组件的新功能,但是将其与组件一起使用通常很聪明。 单向绑定的目的是避免将更多的工作加载到摘要循环中,这是应用程序性能的主要因素。 现在,数据无需进入外部即可流入组件(这会导致当今存在的某些耦合问题),并且组件只要给出该输入即可简单地呈现自身。 此设计还适合Angular 2,它有助于将来的迁移。

在此示例中, title属性仅根据提供的初始值绑定到组件一次。 如果title由某个外部演员更改,则该title不会反映在组件中。 将绑定表示为单向的语法是使用<符号。

bindings: {
  title: '<'
}

title属性更改时,该组件仍会更新,我们将介绍如何侦听title属性的更改。 建议您尽可能使用单向。

组件应考虑一次性绑定

Angular还具有一次性绑定数据的功能,因此您可以优化摘要周期。 本质上,Angular将等待直到绑定中提供了第一个undefined值,然后绑定该值,然后(一旦所有绑定都已解析)就将其从摘要循环中移除。 这意味着特定的绑定不会为将来的摘要循环增加任何处理时间。

这是通过将::放在绑定表达式前面来完成的。 仅当您知道输入绑定在生命周期内不会改变时,这才有意义。 在此示例中,如果title是单向绑定,则它将继续在组件内部进行更新,但此处的绑定不会更新,因为我们将其表示为一次性。

<h1>{{::title}}</h1>

组件应使用生命周期事件

您可能已经注意到$ onInit函数是一项新功能。 组件具有带有相应事件的生命周期,您应使用这些事件来帮助管理组件的某些方面。

$onInit()

组件生命周期的第一步是初始化。 该事件在控制器和绑定初始化之后运行。 您几乎应该始终使用此方法进行组件设置或初始化。 在运行之前,它将确保所有值都可用于组件。 如果要直接在控制器中访问绑定值,则不能保证这些值对您可用。

controller: function() {
  var vm = this;
  console.log(vm.title); // May not yet be available!
  vm.$onInit = function() {
    console.log(vm.title); // Guaranteed to be available!
  }
}

下一步是链接模板中的所有子元素。 组件初始化时,不能保证它也会渲染模板内部使用的所有子代。 如果您需要以任何方式操作DOM,则这一点很重要。 一个重要的警告是,异步加载的模板可能在此事件触发时尚未加载。 您始终可以使用模板缓存解决方案来确保模板始终可用。

controller: function() {
  var vm = this;
  vm.$postLink = function() {
    // Usually safe to do DOM manipulation
  }
}

$onChanges()

组件处于活动状态时,可能需要对输入值的变化做出反应。 单向绑定仍将更新您的组件,但是我们有一个新的$onChanges事件绑定来侦听输入更改的时间。

对于此样本,假设有一个组件提供了产品标题和描述。 您可以按照以下说明检测更改。 您可以查看传递给函数的对象,该对象具有映射到可用绑定的对象,该绑定具有当前值和先前值。

bindings: {
  title: '<'
},
controller: function() {
  var vm = this;
  vm.$onChanges = function($event) {
    console.log($event.title.currentValue); // Get updated value
    console.log($event.title.previousValue); // Get previous value
  }
}

$onDestroy()

最后阶段是从页面中删除组件。 此事件在控制器及其作用域被破坏之前立即运行。 清理组件可能已创建或保留内存的所有内容(例如事件侦听器,观察程序或其他DOM元素)非常重要。

controller: function() {
  var vm = this;
  vm.$onDestroy = function() {
    // Reset or remove any event listeners or watchers
  }
}

组件应具有定义明确的API

要使用一组数据配置和初始化组件,组件应使用绑定来接受这些值。 有时将其视为组件API,这只是描述组件接受输入方式的另一种方式。

这里的挑战是给绑定提供简洁但清晰的名称。 有时,开发人员会尝试将名称简称为简明扼要,但这对于使用该组件很危险。 假设我们有一个接受股票代码作为输入的组件,这两个哪个更好?

bindings: {
  smb: '<',
  symbol: '<'
}

希望您认为symbol更好。 有时,开发人员还喜欢为组件和绑定添加前缀,以防止名称冲突。 给组件加上前缀是明智的,例如md-toolbar是Material工具栏,但是为所有绑定加上前缀会变得很冗长,应避免使用。

组件应发出事件

为了与其他组件进行通信,组件应发出自定义事件。 有许多使用服务和双向数据绑定在组件之间同步数据的示例,但事件是更好的设计选择。 事件作为与页面进行通信的方式(以及JavaScript语言的基础部分以及它在Angular 2中的工作方式,这不是巧合),效率要高得多。

Angular中的事件可以使用$emit (在作用域树上)或$broadcast (在作用域树下)。 这是正在发生的事件的快速示例。

controller: function($scope, $rootScope) {
  var vm = this;
  vm.$onInit = function() {
    // Emits an event up to parents
    $scope.$emit('componentOnInit');
  };
  vm.$onDestroy = function() {
    // Emits an down child tree, from root
    $rootScope.$broadcast('componentOnDestroy');
  };
}

在两种主要情况下,您需要在组件之间进行通信:您知道的组件之间以及您不知道的组件之间。 为了说明不同之处,让我们想象一下,我们有一组帮助管理页面上的选项卡的组件,以及一个具有指向相应帮助页面链接的工具栏。

<my-toolbar></my-toolbar>
<my-tabs>
  <my-tab title="Description"></my-tab>
  <my-tab title="Reviews"></my-tab>
  <my-tab title="Support"></my-tab>
</my-tabs>

在这种情况下,“ my-tabs和“ my-tab组件可能会相互了解,因为它们一起创建了三个不同的标签。 但是, my-toolbar组件不在他们的意识范围内。

每当选择其他选项卡(在my-tab组件实例上为偶数)时, my-tabs组件都需要注意,以便可以调整选项卡的显示以显示该实例。 my-tab组件可以向父my-tabs组件发出一个事件。 这种通信就像两个组件之间的内部通信,两个组件一起工作以形成单个功能(选项卡式界面)。

但是,如果my-toolbar想知道当前选择了哪个选项卡,以便可以根据可见的内容更改帮助按钮,该怎么办? my-tab事件永远不会到达my-toolbar因为它不是父项。 因此,另一种选择是使用$rootScope沿整个组件树发出事件,这允许任何组件进行侦听和响应。 潜在的缺陷是您的事件现在到达每个控制器,并且如果另一个组件使用相同的事件名称,则可能触发意外的效果。

确定哪种方法对您的用例有意义,但是任何时候其他组件可能需要了解某个事件时,您可能都想使用第二个选项将其发送给整个组件树。

摘要

Angular 1应用程序现在可以使用组件编写,这改变了我们编写应用程序的最佳实践和性质。 这样会更好,但是仅使用组件并不一定会使它变得比以前更好。 在构建Angular 1组件时,请牢记以下关键事项。

  • 隔离您的逻辑。 在内部保持尽可能多的组件逻辑,并远离应用程序的其他方面,以确保一致性和质量。
  • 使组件简单并集中于一个角色。 它们可能是复杂的组件,但是单个组件的各种任务应在逻辑上作为一个单元连接。
  • 使用生命周期事件。 通过了解组件的生命周期,可以确保在正确的时间准备好数据并进行清理。
  • 使用单向和一次性绑定。 在可能的情况下,单向绑定会更有效并促进良好的设计,而单向绑定可以加快您的应用程序。 您始终可以使用$onChanges生命周期事件来监视更改。
  • 使用事件进行交流。 组件可以使用自定义事件进行通信,这与Angular 2的功能和更好的设计是一致的。
  • 有一个定义明确的API。 确保您的组件名称清晰易懂。

您是否在Angular 1.x应用程序中使用组件? 或者,您是否要等到跳转到Angular 2为止? 我很想在下面的评论中听到您的经历。

From: https://www.sitepoint.com/building-angular-1-5-components/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值