AngularJS 1.x 小记(一)

AngularJS是什么

我们知道HTML是一种创建网页的静态标记语言,它很基础,但不失灵活,然而它自身并不提供对灵活性的具体实现,所以我们要创建具有可用性及赏心悦目的Web应用就需要使用其他语言与之结合去操控它的DOM、标签、标签属性、标签层级结构实现样式变换、动态数据变换、动态元素变换等,比如结合CSS和JavaScript语言。

但是越多的结合就意味着功能实现越复杂,我们需要写大量的代码去实现类似数据绑定、动态展现、远程服务请求等功能,所以jQuery这类的JS库、ember这类的框架应用而生。前者让我们在完成某些单一功能时调用它封装好的方法,从而减少代码量。后者让我们可以遵循它的规范去填充它设计好的代码结构,从而逐步完成完整的应用功能。

而AngularJS是JS库和框架的结合,它诞生于2009年,由Misko Hevery 等人创建,后为Google所收购并发扬光大。AngularJS通过为开发者呈现一个更高层次的抽象来简化应用的开发,提供了构建一个CRUD应用可能用到的全部内容包括:自定义HTML标签、事件绑定、数据绑定、基本模板标识符、表单验证、路由、深度链接、组件重用、依赖注入等。

文章中的所有示例均可在 这里 下载。

AngularJS的特性

AngularJS有着诸多的特性,但最为核心的是其Scope概念、MVC模式、路由、模块化、依赖注入、指令系统、双向数据绑定、Service、Provider等。

模块化

我们先从AngularJS的模块化特性说起。什么是模块?既应用程序中不同功能的容器。在AngularJS中,我们可以按业务功能的不同将实现划分为不同的模块,这些模块可以在一个JS文件中,也可以将它们放在不同的JS文件中,既一个JS文件为一个模块。

模块化的优势

使用模块化在我们的编程阶段有着诸多的好处:

  • 可以使我们的代码结构非常清晰,有较好的可读性。
  • 可以使我们复用代码。
  • 在前端编程中,我们都知道JS的引用顺序是很重要的,但使用AngularJS的模块时我们不需要关系每个模块之间的顺序。
  • 可以很好的实现单元测试。

定义模块

定义一个模块很简单,在JS文件中申明一个模块:

var mianModule = angular.module("mianModule", []);

使用AngularJS的 module 方法申明一个模块,该方法有两个参数:

  • 第一个参数为模块名称。
  • 第二个参数为一个数组,该参数的含义为当前定义的模块所依赖的模块,如果有依赖模块则传入包含模块名称的数组,若无依赖则传入空数组。

使用模块

在了解如何使用定义好的模块之前,需要先清楚在AngularJS中,模块与模块之间可以是相互独立,老死不相往来的关系,也可以是依赖关系,并且可以是嵌套依赖关系:

// modules.js
// 互相独立的模块
var mainModule = angular.module("mainModule", []);
var anotherModule = angular.module("anotherModule", []);
// modules.js
// 有依赖关系的模块
var mainModule = angular.module("mainModule", ["secondModule"]);
var secondModule = angular.module("secondModule", ["thirdModule"]);
var thirdModule = angular.module("thirdModule", []);

在AngularJS中使用定义好的模块也有两种方式,对应两种不同的应用场景。

ngApp方式

这种方式是AngularJS团队比较推荐的使用方法,只需要在HTML标签中使用AngularJS的 ng-app 指令指定要使用的模块既可,一般指定的是应用的主模块,或者说是应用入口模块:

<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Demo for Module</title>
    <script src="../angular-1.5.8.js"></script>
    <script src="modules.js"></script>
  </head>
  <body ng-app="mainModule">

  </body>
</html>

ng-app 指令的含义类似Java中的 main 方法,是当前页面中应用的唯一主入口,所以 一个页面中只能使用一次 np-app 指令 。将 ng-app 指令定义在哪个HTML的标签中就表示该标签及它的所有子标签就会被AngularJS接管,这一部分HTML代码将可以使用AngularJS所有的功能和特性,也就是使用AngularJS开发的Web应用的主入口。通常情况下都会在 body 标签中使用 ng-app 指令。

因为使用这种方式只能指定一个应用入口模块,所以为了能使用多模块的特性,就需要抽象出一个主模块,然后将其他所有模块加入主模块的依赖关系中,这里要注意的是因为AngularJS中的模块依赖可以向下穿透,类似类的继承,所以加入主模块依赖关系中的模块并不是所有的模块,而只是处于依赖层级顶层的模块。


如上图所示,在主模块中只需要添加 DataHandleModule 和 NetworkModule 两个模块既可。但是这种方式的弊端是在HTML页面中并不能直观的表现出页面的哪些部分使用了何种模块,而且本身提供了模块化的特性,但最终又要抽象到一个模块中去使用,始终有点不是很舒服。

手动加载方式

虽然使用 ng-app 的形式可以满足需求,但是还要考虑想抽象出主模块,然后依赖一大堆模块,如果更希望可以在HTML标签中指定使用的模块,此时就需要手动的加载模块了:

// modules.js
// 互相独立的模块
var mainModule = angular.module("mainModule", []);
var anotherModule = angular.module("anotherModule", []);

angular.element(document).ready(function() {
  var myDiv1 = document.getElementById("myDiv1");
  angular.bootstrap(myDiv1, ["mainModule"]);

  var myDiv2 = document.getElementById("myDiv2");
  angular.bootstrap(myDiv2, ["anotherModule"]);
});
<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Demo for Module</title>
    <script src="../angular-1.5.8.js"></script>
    <script src="modules.js"></script>
  </head>
  <body>
    <div id="myDiv1">
    </div>
    
    <div id="myDiv2">
    </div>
  </body>
</html>

通过上述代码可以看到,我们可以使用AngularJS的 bootstrap 方法给HTML元素指定模块。这样感觉和模块化特性更加切合,模块的隔离性也比较好,但是这种方式依然有显著的弊端。首先从HTML页面中依然不能直观的看到模块在页面中使用和分布情况,再次这种方式将视图层的HTML代码与JS代码耦合在了一起,也有点违背了AngularJS的MVC设计模式,也许这也是AngularJS不推荐该方式的原因之一吧。

模块的生命周期

在AngularJS中,模块有两个主要的生命周期方法,那就是 .config() 和 .run():

var mainModule = angular.module('mainModule', []);
mainModule.config(function(injectables) {

});

mainModule.run(function(injectables) { 

});

.config() 方法的作用是在模块运行加载之前对模块进行配置,比如创建各种服务、创建自定义指令、注册过滤器等。 .run() 方法相当于AngularJS应用的 main 方法,在该方法里进行的配置都是运行时态的,比如对已经创建好的服务实例在应用运行期进行修改。

.config() 方法在后文还会涉及,服务、指令、过滤器也会在后文有详细的讲解。

MVC

MVC是软件工程中的一种设计模式,既把应用系统分为模型(Model)、视图(View)和控制器(Controller)三个基本部分,并且模型层与视图层之间是相互隔离的。简单的描述每个部分的职能:

  • 模型层:管理数据模型。
  • 视图层:控制UI的展现更新等。
  • 控制层:负责具体业务逻辑处理、请求处理转发等,是模型层和视图之间的桥梁。

AngularJS是为数不多实现了MVC设计模式的前端框架,为前端应用在开发时期的功能职责切分、代码复用及后期的维护提供了极大的便利。

Controller

在AngularJS中,模块是万源之本,所以AngularJS中的所有东西都是通过模块创建的,Controller也不例外。我们可以使用模块对象的 .controller() 方法创建控制器:

var mainModule = angular.module("mainModule", []);

mainModule.controller("GreetingController", ["$scope", function($scope) {
  $scope.greeting = "Hello!";
}]);

从上面的示例代码可以看到, .controller() 方法有两个参数:

  • 第一个参数类型为字符串,为Controller的名称。
  • 第二个参数类型为数组,该数组用于注入当前Controller要用到的服务及实现业务逻辑的函数,这里要注意的是实现逻辑的函数始终是作为数组的最后一个元素,并且要将前面注入的服务作为该函数的参数传入。

这里先简单描述一下 $scope ,在AngularJS中有一个重要的概念是服务,而$scope 就是一个AngularJS内置的服务,在后面的章节中会详细讲解服务。那么 $scope 服务从字面理解是作用域的意思,其实也差不太多,如果用OO的思想将AngularJS的Controller看作是一个类,那么 $scope 服务就代表了这个类的作用域,那么就可以通过 $scope 服务给这个类添加属性或者方法,上面的代码示例中通过 $scope 服务给GreetingController 控制器添加了字符串属性 greeting 及对象属性 person 。

所以上面代码的含义是,首先创建了名为 mainModule 的模块,然后在 mainModule模块中创建了名为 GreetingController 的控制器,并使用 $scope 服务给该控制器添加了名为 greeting 和 person 的属性。

创建好Controller后,来看看如何使用它:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Demo for Controller</title>
    <script src="../angular-1.5.8.js"></script>
    <script src="modules.js"></script>
  </head>
  <body ng-app="mainModule">
    <div ng-controller="GreetingController">
      {{ greeting }} {{ person.name }} !
    </div>
  </body>
</html>

前文中介绍过在HTML页面中,通过使用 ng-app 指令给标签绑定模块。同理,我们可以使用 ng-controller 指令给标签及它的子标签绑定Controller,绑定了Controller的标签及它的子标签都可以使用Controller中的属性或者方法。我们可以使用双花括号语法访问Controller的属性或调用方法。运行效果很简单,就是将这两个属性的值输出到页面。

我们再来看看如何在Controller中添加方法:

var mainModule = angular.module("mainModule", []);

mainModule.controller("GreetingController", ["$scope", function($scope) {
  $scope.personName = "Everyone";

  $scope.welcomeJason = function() {
    $scope.personName = "Jason";
  };

  $scope.welcomeGreen = function() {
    $scope.personName = "Green";
  };
}]);

在上面的代码中,我们给 GreetingController 添加了 personName 属性和 welcomeJason() 、 welcomeGreen() 两个方法,并在这两个方法中分别对 personName 属性的值进行修改。再来看看HTML的代码:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Demo for Controller</title>
    <script src="../angular-1.5.8.js"></script>
    <script src="modules.js"></script>
  </head>
  <body ng-app="mainModule">
    <div ng-controller="GreetingController">
      <button ng-click="welcomeJason()">Jason</button>
      <button ng-click="welcomeGreen()">Green</button>
      <p>
        Welcome {{ personName }} !
      </p>
    </div>
  </body>
</html>

在HTML代码中,添加了两个按钮,此时我们又看到了一个新的AngularJS指令 ng-click ,这个指令很好理解,就是给按钮绑定点击事件,当点击按钮时调用 GreetingController 中对应的方法,然后在 p 标签中显示 personName 属性。所以Controller中的方法不仅可以通过双花括号语法调用,也可以通过 ng-click 指令调用。我们来看看运行的效果:

MJZbiuR

Model

Model指的是数据模型,在AngularJS中使用 $scope 服务给Controller添加的属性就是数据模型:

var mainModule = angular.module("mainModule", []);

mainModule.controller("GreetingController", ["$scope", function($scope) {
  $scope.person = {
    name: "Jason",
    job: "Developer"
  };
}]);

上述代码示例中的 person 属性就是数据模型,下面看看如何使用:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Demo for Model</title>
    <script src="../angular-1.5.8.js"></script>
    <script src="modules.js"></script>
  </head>
  <body ng-app="mainModule">
    <div ng-controller="GreetingController">
      <input ng-model="person.name">
      <input ng-model="person.job">
      <p>
        Welcome {{ person.name }}, He is a {{ person.job }}!
      </p>
    </div>
  </body>
</html>

在上述HTML代码示例中,我们又看到了新的指令 ng-model ,顾名思义,该指令就是用来将数据模型与HTML标签元素进行绑定的。上面的代码中分别将两个输入框与 person 对象的 name 和 job 属性进行了绑定,然后在 p 标签中输出,我们来看看运行效果:

3IreQbM

不难看出,当我们将 person 对象的属性与输入框绑定后, person 对象属性的默认值就会显示在输入框里了,当我们修改输入框里的内容时会自动将数据通过Controller同步到 person 对象的相应属性上,所以 p 标签中的内容会实时跟着输入框的内容进行变化。

View

View层自然就是HTML中的DOM元素了,通过AngularJS提供的各个指令将DOM元素与Controller和Model进行绑定。由Controller负责将数据模型的内容通过双花括号语法或 ng-model 指令展现在DOM元素上,而当DOM元素中的值发生变化时会由Controller捕获到,并更新对应的数据模型。

数据双向绑定

在MVC一节中,通过几个示例介绍了如何创建和使用Controller、Model,如何与View层交互,其实也引出了AngularJS的一个重要特性。在Controller小节的例子中,通过点击按钮由Controller更改数据模型并将其展示在页面中,这是通过数据模型的变化从而影响视图层的显示。在Model小节的例子中,通过修改输入框中的值,由Controller捕获并更新对应的数据模型,这是通过视图层的变化从而影响数据模型的值。这就是AngularJS的数据双向绑定特性。

服务(Services)

AngularJS中一个重要的概念是服务,这个服务的概念比较宽泛,比如一个常量值也算做一个服务,既提供一个不可变值的服务。变量、对象、函数都算做是服务。在AngularJS中内置了好几十个服务,这些内置的服务都以 $ 符号开头,比如 $scope 、 $http 、 $log 、 $timeout 、 $interval 等等,从字面意思都不难理解它们的作用,更多的内置服务可以去AngularJS官网查看 API文档 。

服务特征

AngularJS中的服务有两个主要特点:

  • 延迟加载,当应用中的其他组建使用服务时才会实例化。
  • 单例,在应用的整个生命周期中,一个服务只存在一份实例,所以服务一般用来共享可复用的代码逻辑或者数据。

自定义服务

除了内置的服务,我们还可以创建自己的服务,在AngularJS中我们可以通过 $provide 这个内置的服务来创建我们的自定义服务, $provide 服务提供了五个方法供我们创建不同应用场景的自定义服务,这五个方法分别是 provider(name, provider) 、 factory(name, $getFn) 、 service(name, constructor) 、 value(name, value) 、 constant(name, value) 。

Value

我们先从 value(name, value) 这个方法看起,该方法有两个参数:

  • 第一个参数为服务的名称,类型为字符串。
  • 第二个参数可以是字符串、数字、数组、对象或者函数。

假设在我们的应用中,多个Controller中都使用了相同的属性,比如都需要用到客户端ID这个属性,那么我们可以将其抽象为一个服务,该服务就专门用来获取客户端ID,来看看如何创建这个服务:

var mainModule = angular.module("mainModule", []);

mainModule.value("clientId", "qazxsw123456");

上面的示例代码创建了名为 clientId 的服务,该服务其实就是一个字符串。不过这和 $provide 服务有什么关系呢?其实上面这种写法并不是完整的写法,只是一个语法糖而已,真正完整的写法是在模块的 .config() 方法中通过 $provide 服务去创建:

mainModule.config(function($provide) {
    $provide.value("clientId", "qazxsw123456");
});

创建好服务后通过AngularJS的注入机制将其注入到Controller中:

mainModule.controller("FirstController", ["$scope", "clientId", function($scope, clientId) {
  $scope.clientId = clientId;
}]);

mainModule.controller("SecondController", ["$scope", "clientId", function($scope, clientId) {
  $scope.clientId = clientId;
}]);

然后在HTML页面中正常使用Controller就可以了:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Demo for Value Service</title>
    <script src="../angular-1.5.8.js"></script>
    <script src="modules.js"></script>
  </head>
  <body ng-app="mainModule">
    <div ng-controller="FirstController">
      Client ID in FirstController: {{ clientId }}
    </div>
    <div ng-controller="SecondController">
      Client ID in SecondController: {{ clientId }}
    </div>
  </body>
</html>

上文中说过 $scope 服务的其中一个作用就是给Controller添加属性和方法,然后可以在绑定Controller的DOM中使用双花括号语法直接访问添加的属性或调用方法。然而就 $scope 服务的这一功能而言,AngularJS还提供了另一种方式,我们先来看看Controller的写法:

mainModule.controller("FirstController", ["clientId", function(clientId) {
  this.clientId = clientId;
}]);

mainModule.controller("SecondController", ["clientId", function(clientId) {
  this.clientId = clientId;
}]);

上述代码中我们并没有将 $scope 服务注入到这两个Controller中,而是使用 this创建了 clientId 属性, this 代表Controller的实例。使用这种方式后在HTML页面中使用Controller也有点变化:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Demo for Value Service</title>
    <script src="../angular-1.5.8.js"></script>
    <script src="modules.js"></script>
  </head>
  <body ng-app="mainModule">
    <div ng-controller="FirstController as first">
      Client ID in FirstController: {{ first.clientId }}
    </div>
    <div ng-controller="SecondController as second">
      Client ID in SecondController: {{ second.clientId }}
    </div>
  </body>
</html>

我们看到在 ng-conroller 标签中不再是直接写Controller名称了,而是使用 as 关键字声明了Controller的实例,然后在双花括号中使用Controller的实例去访问属性或者调用方法。

使用 $scope 服务和 this 给Controller添加属性或方法的效果是一样的,所以不存在谁好谁坏的概念,只不过使用 this 的方式更贴合OO的思想,而且在HTML代码中对使用的属性或方法有更直观的可读性,能一眼看到使用了哪个Controller的属性或方法,所以使用哪种方式按个人喜好,但是不建议混用这两种方式。

这里在介绍另外一个语法糖,那就是在注入服务的时候不用繁复的在数组中和函数参数中都声明,只需要在函数的参数里声明就可以了:

mainModule.controller("FirstController", function($scope, clientId) {
  $scope.clientId = clientId;
});

// 或者

mainModule.controller("FirstController", function(clientId) {
  this.clientId = clientId;
});

Constant

我们再来看看 constant(name, value) 方法:

var mainModule = angular.module("mainModule", []);

mainModule.constant("clientId", "qazxsw123456");

该方法和 value(name, value) 在创建的服务内容形式上来说是一样的,但是两者创建的服务在功能性上还是有区别的:

  • 从名称就可以看出用 constant(name, value) 方法创建的服务是不可修改的。
  • 使用 constant(name, value) 创建的服务可以在模块的 .config() 方法中注入,也就是可以在创建其他服务时使用,而使用 value(name, value) 创建的服务不可以。

Service

现在又有一个需求,希望能获取到当前时间添加在客户端ID后面,那么我们可以使用 service(name, constructor) 方法来创建获取当前时间的服务:

var mainModule = angular.module("mainModule", []);

mainModule.value("clientId", "qazxsw123456");

mainModule.service("currentDate", Date);

mainModule.controller("FirstController", function(clientId, currentDate) {
  this.clientId = clientId + "-" + currentDate;
});
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Demo for Service Service</title>
    <script src="../angular-1.5.8.js"></script>
    <script src="modules.js"></script>
  </head>
  <body ng-app="mainModule">
    <div ng-controller="FirstController as first">
      Client ID in FirstController: {{ first.clientId }}
    </div>
  </body>
</html>

输出结果:

Client ID in FirstController: qazxsw123456-Thu Sep 08 2016 17:05:30 GMT+0800 (CST)

service(name, constructor) 方法的第二个参数是函数构造器,也就是函数的实例,所以 currentDate 服务的实体其实就是 new Date() 。

Factory

现在,我们希望通过一个服务就可以完成客户端ID和当前时间的拼接,不需要给Controller注入两个服务,来看看如何用 factory(name, $getFn) 方法来实现:

var mainModule = angular.module("mainModule", []);

mainModule.constant("clientId", "qazxsw123456");

mainModule.factory("clientIdAndCurrentDate", function(clientId) {
  return clientId + "-" + new Date();
});

mainModule.controller("FirstController", function(clientIdAndCurrentDate) {
  this.clientId = clientIdAndCurrentDate;
});

首先我们需要用 constant(name, value) 方法创建 clientId 服务,因为需要将它注入到新的服务中,前文也介绍过 constant(name, value) 和 value(name, value) 方法的区别。然后使用 factory(name, $getFn) 方法创建 clientIdAndCurrentDate 服务,该函数的第二个参数类型是函数,我们在该函数中将 clientId 服务返回的客户端ID与 Date 构造器返回的时间进行拼接然后返回,当然运行结果还是一样的:

Client ID in FirstController: qazxsw123456-Thu Sep 08 2016 17:05:30 GMT+0800 (CST)

其实这个服务还可以写成这样:

mainModule.constant("clientId", "qazxsw123456");

mainModule.service("currentDate", Date);

mainModule.factory("clientIdAndCurrentDate", function(clientId, currentDate) {
  return clientId + "-" + currentDate;
});

这相当于 clientIdAndCurrentDate 服务对 currentDate 服务进行了进一步的配置或者说增加了功能,那么也就是说 factory(name, $getFn) 方法相比 service(name, constructor) 方法可以创建更复杂一些的服务。

Povider

现在又有新的需求,希望对 clientId 后面的时间进行格式化,但假设我们没有权限去更改 clientIdAndCurrentDate 服务,那么这时我们需要使用 provider(name, provider) 方法创建另外一个服务,然后对 clientIdAndCurrentDate 服务进行配置,来看看如何实现这个服务:

var mainModule = angular.module("mainModule", []);

mainModule.constant("clientId", "qazxsw123456");

mainModule.service("currentDate", Date);

mainModule.factory("clientIdAndCurrentDate", function(clientId, currentDate) {
  return clientId + "-" + currentDate;
});

mainModule.provider("clientIdAndCurrentDateByFormat", function() {
  this.formatFunc = function(str) {
    var clientId = str.substring(0, str.indexOf("-"));
    var dateStr = str.substring(str.indexOf("-"), str.length);
    var dateObj = new Date(dateStr);
    var year = dateObj.getFullYear().toString();
    var month = (dateObj.getMonth() + 1).toString();
    var day = dateObj.getDate().toString();
    var hour = dateObj.getHours().toString();
    var minute = dateObj.getMinutes().toString();
    var second = dateObj.getSeconds().toString();
    return clientId + "-" + [year, (month >= 10 ? month : 0 + month), (day > 10 ? day : 0 + day), hour, minute, second].join("");
  };
  this.$get = function(clientIdAndCurrentDate) {
    return this.formatFunc(clientIdAndCurrentDate);
  };
});

mainModule.controller("FirstController", function(clientIdAndCurrentDateByFormat) {
  this.clientId = clientIdAndCurrentDateByFormat;
});

首先我们创建了 formatFunc() 辅助配置函数,然后实现了 $get 方法,通过 formatFunc() 辅助函数配置 clientIdAndCurrentDate 服务,我们来看运行结果:

Client ID in FirstController: qazxsw123456-20160909113523

要注意的一点是,通过 provider(name, provider) 方法创建服务时必须要 显式 的实现 $get 方法,并且只有在 $get 方法中才能注入其他服务。在AngularJS中服务仅指 $get 返回的东西,所以前四种创建服务的方法其实都是 provider(name, provider) 方法根据不同应用场景实现的语法糖,比如 factory 方法其实就是把一个函数当作了 $get 方法, service 方法其实是将一个函数构造方法或者说函数实例当作了 $get 方法, value 和 constant 方法其实又是对 factory 方法的语法糖实现。所以在自定义服务时可按需选择不同的方法创建服务。


本文出处:http://www.tuicool.com/articles/reqAFn


本文最后编辑于 2016-12-29 10:18

你可能感兴趣: