AngularJS 教程(二)
本文翻译自这里
翻译过程中加入了译者本人的理解,并做了适当的筛减。
1 AngularJS简介
Angular是一个构建吸引眼球的Web应用的客户端MVW JavaScript框架。其由Google创建与维护,且在网上提供了新版本的更新。
MVW是指Model-View-Whatever,它使我们在构建应用时在设计模式上有了更灵活的选择。我们可以选择MVC(Model-View-Controller)模式或者MVVM(Model-View-ViewModel)模式。
本教程提供了重要的资源,指引你开始学习AngularJS,了解其背后的概念和API,同时帮助你通过现代的方式构建奇妙的Web应用。
AngularJS声称其是一个增强HTML的框架,它从很多编程语言中借鉴了一些概念,包括JavaScript和服务端语言,同时它也让HTML成为了动态语言。这意味着我们可以真正地通过数据驱动的方式开发应用了,不用更新模型、刷新DOM、花费时间去解决诸如浏览器bug和兼容性问题。我们只需关注数据,而数据会处理好我们的HTML结构,这样我们就能更专注于业务的编程。
2 JavaScript框架中的工程概念
AngularJS中数据绑定的实现以及其他一些概念与Backbone.js和Ember.js等框架不同。Angular充分利用了现有的HTML并且增强了它的功能。Angular将简单的JavaScript对象用于数据绑定,能够在不改变任何模型的情况下更新DOM。当模型的数据更新时,Angular也会相应的更新这个对象,从而使这个对象保持着整个应用的状态。
2.1 MVC与MVVM
2.2 双向数据绑定
通过Angular表达式实现双向数据绑定。我们也可以使用一个叫做ng-model的属性来绑定模型。
2.3 依赖注入
2.4 单页面应用,状态管理以及Ajax(HTTP)
2.5 应用架构
3 模块
Angular中每个应用都通过模块创建。一个模块可以依赖于其他模块,或者仅仅只是模块自身,不依赖于任何模块。这些模块作为一个应用不同组成部分的容器,使得代码更加容易重用与测试。创建一个模块,需要使用全局的angular对象(Angular框架的命名空间),调用其module方法。
3.1 设置
通常我们的应用会用一个单独的应用模块,这样我们可以引入其他模块,因此我们可以在设置(创建)主模块的时候将其命名为app:
angular.module('app', []);
module方法中的第二个参数是[],该数组存放(或将存放)在这个模块里面我们想引入的其他模块依赖。模块可以拥有其他的模块作为依赖。现在,我们先让它为空。
3.2 读取
在创建Controllers,Directives,Services以及其他Angular功能时,我们需要引用当前存在的模块。引用已有模块的方法跟创建模块的方法非常相似,只是有细微的差别,就是我们不需提供第二个参数:
angular.module('app');
3.3 模块实践
模块可以通过一个变量来存储或引用,但是最佳实践是使用AngularJS团队提供的链式方法。
以下为将模块保存为一个变量的示例:
var app = angular.module('app', []);
现在我们可以引用变量app,并为其添加方法来构建我们的应用,但是最好我们还是坚持使用前面看到的链式的方法。
3.4 HTML引导程序
为了声明我们的应用存在于DOM的哪个位置,通常是<html>标签,我们需要使用ng-app属性并且将其值设置为我们的模块。这样Angular才知道从哪里开始进入我们的应用。
<html ng-app=”app”> <head></head> <body></body> </html>
如果通过异步的方式加载JavaScript文件的话,那么需要通过angular.bootstrap(document.documentElement, ['app'])的方式手动引导AngularJS应用。
4 理解$scope
不管是哪门编程语言,你会碰到的常见概念问题之一就是作用域问题。如块级作用域与函数级作用域。Angular的作用域概念充当着一个重要的对象,这个对象支撑着双向数据绑定从而维护着整个应用的状态。$scope是一个巧妙的对象,它不仅存在于JavaScript中,可以获取数据和值;同时一旦Angular渲染了我们的应用,它还可以在DOM中读取这些值。
可以把$scope看做是在JavaScript与DOM之间架起的一座自动的桥,它维持着我们的同步数据。这样我们就可以很容易地在HTML里面通过使用handlebars语法方式形成模板,而Angular会结合$scope中的值来渲染模板。这样就在JavaScript与DOM之间进行了绑定,$scope对象真可谓是名副其实的ViewModel。
我们只在Controllers里面使用$scope,也是在这里将Controller的数据绑定到View上。
以下为如何在Controller里面声明一些数据的示例:
$scope.someValue = ‘Hello’;
要在DOM中渲染出来,我们需要把一个Controller关联到HTML上以告诉Angular我们要在哪里绑定这些值:
<div ng-controller=”AppCtrl”> {{ someValue }} </div>
以上我们看到的就是Angular中的作用域概念,它也遵循JavaScript中词法作用域的规则。如果在Controller限定的标签范围外,那么数据就超出了作用域范围,就像引用一个超出了作用域范围的变量一样。
我们可以将JavaScript中存在的任意类型的数据绑定到$scope对象上。这也是我们如何将从与服务端交互得到的数据传到视图View——展示层。
在Angular中,我们创建越多的Controllers和数据绑定,我们创建的作用域也就越多。理解$scope的层级结构也是值得注意的,这也将引入$rootScope。
4.1 $rootScope
$rootScope与$scope的差别比不是特别大,$rootScope只是最顶层的$scope对象,其它所有的作用域范围也是基于此创建的。当Angular开始渲染应用时,它会创建一个$rootScope对象,且应用中所有后续的绑定和逻辑会创建新的$scope对象,而这些新的$scope将成为$rootScope的子代。
通常情况下,我们很少使用$rootScope,但在涉及作用域和数据之间进行通信的时候我们应该知道它。
5 控制器(Controllers)
在使用控制器之前,我们需要先了解其目的。一个Angular控制器可以让我们在View和Model之间进行交互,也是在这里展示逻辑可以将UI与Model进行同步绑定。控制器的目的就是驱动模型(Model)和视图(View)的变化,在Angular中这是我们的业务逻辑和展示逻辑的一个交汇点。
之前我们通过声明ng-controller属性来展示$scope中的数据时,已对Controller有些许了解。ng-controller这个属性让Angular确定作用域范围并且绑定一个Controller实例,同时让Controller中的数据和方法在这个DOM范围里面可用。
在使用一个Controller之前,需要创建一个Controller。在前面使用module方法的基础上通过最佳实践的链式方法:
angular .module(‘app’, []) .controller(‘MainCtrl’, function () { });
Controller接收两个参数,第一个是Controller的名称,供后续在其他地方引用,如在DOM中引用;第二个参数是一个回调函数,然而这个回调函数不应该被看做是回调,因为它实际上是Controller内容的声明。
为了避免让Angular方法看起来像回调函数,同时也减少我们代码中的很多缩进,我喜欢将函数放在Angular语法的外面,然后在需要的时候将函数传进去:
function MainCtrl () { } angular .module(‘app’, []) .controller(‘MainCtrl’, MainCtrl);
为了让代码看起来整洁且可读性强,非常重要的一点就是不要被Angular语法的条条框框所限制,其实你写的也是JavaScript而已。我们可以给Angular提供其所需的模块,就像前面所做的一样。
如上你会看到我把函数命名为MainCtrl,这在debugging时也会为我们提供相应的调用栈信息,有利于我们调试,而有时一些匿名函数是无法提供相应信息的除非我们给它取个名字。
5.1 方法与展示逻辑
在一个应用的生命周期里,控制器的作用是去解析模型中的业务逻辑并且将其转换成展示的形式。在Angular中,这个可以通过多种途径来实现,主要取决于某个特定方法返回的数据是什么。
Controller所做的就是与Service通信,然后将数据以相同或不同的形式通过$scope对象传给View。当View得到更新时,Controller中的逻辑也会被更新,同时更新的信息也会通过Service被反映回服务端。在研究Service前,我们将在Controller中创建一些对象并将这些对象绑定到$scope上以了解Controller的实际作用。实际上,我们不应在Controller里面声明业务逻辑或者数据,但为了简单起见,我们把它加在了里面,如下,后续我们会从Service中获取数据。
function MainCtrl ($scope) { $scope.items = [{ name: 'Scuba Diving Kit', id: 7297510 },{ name: 'Snorkel', id: 0278916 },{ name: 'Wet Suit', id: 2389017 },{ name: 'Beach Towel', id: 1000983 }]; } angular .module(‘app’) .controller(‘MainCtrl’, MainCtrl);
通过$scope对象我们绑定了一个数组。这样这个数组便可以在DOM中使用,我们可以将它传给Angular的一个内置指令ng-repeat,让它遍历整个数组的数据,并且根据模板和数据创建相应的DOM。
<div ng-controller="MainCtrl"> <ul> <li ng-repeat="item in items"> {{ item.name }} </li> </ul> </div>
5.2 新的“controllerAS"语法
Controller有点类似类,而Angular开发者感觉使用$scope对象并不像是在使用类似类。他们提倡使用this关键字而不是$scope。Angular团队引入了this作为新的controllerAs语法的一部分,这样,一个Controller就被实例化一个实例变量,就像你可以在一个变量上使用new关键字创建一个对象一样。
controllerAs的首要变化是废弃$scope对Controller里面数据的引用,而是使用this。
function MainCtrl () { this.items = [{ name: 'Scuba Diving Kit', id: 7297510 },{ name: 'Snorkel', id: 0278916 },{ name: 'Wet Suit', id: 2389017 },{ name: 'Beach Towel', id: 1000983 }]; } angular .module(‘app’) .controller(‘MainCtrl’, MainCtrl);
另外一个变化是在DOM中实例化Controller的地方添加as部分,如使用MainCtrl as main来创建一个main变量。
这样一来就可以通过main变量来引用Controller里面的所有数据,如前面例子中的items,可以写成main.items。
<div ng-controller="MainCtrl as main"> <ul> <li ng-repeat="item in main.items"> {{ item.name }} </li> </ul> </div>
这样,如果我们有多个Controller嵌套在一起时,这种方式能够很好地帮助我们辨别出一个属性到底属于哪个Controller。在controllerAs引入之前,变量在整个scope范围内容是自由的,然而当加上变量的限定时,它们就分别属于特定的实例了,如我们可以在相同的DOM范围内定义main.items和sub.items。如果还是像以前那样,我们就会遇到变量命名冲突导致互相覆盖的问题。
创建的每个$scope都有一个$parent对象,作为作用域范围的原型继承,如果没有controllerAs,我们需要将$parent指向父作用域中的方法,而对于其他子域来说就是$parent.$parent,以此类推。
6 Services 与 Factories
通常我们在Services中保持应用的模型数据以及业务逻辑,如通过HTTP与后端的交互。人们容易把Service和Factory搞混,它们主要的区别在于如何创建对象。
需要记住的一点是所有Services都是单例应用,每次注入都只有一个Service实例。按照惯例,所有自定义的Services需遵循帕斯卡命名规范,就像我们之前的Controller一样,如”my service“应该是”MyService“。
6.1 Service 方法
Angular提供了service方法用于创建一个新的对象,这个对象可用于与后端交互或者为处理业务逻辑提供帮助。一个service只是一个constructor构造对象,这个构造对象可以通过new关键字来调用,这意味着在service中可以使用this关键字来绑定逻辑。service创建一个由service factory产生的单例对象。
function UserService () { this.sayHello = function (name) { return ‘Hello there ’ + name; }; } angular .module(‘app’) .service(‘UserService’, UserService);
然后就可以将service注入到一个Controller中使用它:
function MainCtrl (UserService) { this.sayHello = function (name) { UserService.sayHello(name); }; } angular .module(‘app’) .controller(‘MainCtrl’, MainCtrl);
如果我们使用service,意味着在对象得到实例化之前我们不能执行任何代码。而如果使用factory就不同了。
6.2 factory 方法
factory方法返回一个对象(Object)或者一个函数(Function),意味着我们可以利用闭包同时返回一个绑定了方法的宿主对象。我们可以创建私有的与公有的作用域。所有的factory也是service,我们应该像使用service一样使用它,而不应因为它的名称而区别对待。
我们将用factory的方法重新创建上例中的UserService以作比较:
function UserService () { var UserService = {}; function greeting (name) { return ‘Hello there ’ + name; } UserService.sayHello = function (name) { return greeting(name); }; return UserService; } angular .module(‘app’) .factory(‘UserService’, UserService);
我将greeting函数放在了外面用以展示如何通过闭包的方式模拟一个私有的作用域。我们也可以在service方法的构造函数(constructor)中做类似的事情,但是这个实例展示了返回的是什么以及封装在Service scope里面的是什么。我们还可以在这个作用域内创建私有的函数,如一些帮助函数,在函数返回之后,它们也还可以被那些公有方法利用。在Controller中还是用同样的方式使用由factory创建的Services。
function MainCtrl (UserService) { this.sayHello = function (name) { UserService.sayHello(name); }; } angular .module(‘app’) .controller(‘MainCtrl’, MainCtrl);
Services常用于非展示逻辑中,我们常把它称为业务逻辑层。这里面往往包括利用Ajax(HTTP)通过REST端点与后端进行交互的过程。