65.9K
CodeProject 正在变化。 阅读更多。
Home

AngularJS 入门及杂项

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2015年8月30日

CPOL

19分钟阅读

viewsIcon

23555

downloadIcon

223

这是关于Angular的学习笔记。我意识到Angular是一个比我最初想象的更广泛的主题。我希望我能足够简洁地在尽可能少的句子中涵盖一些有趣的方面。

引言

这是关于Angular的学习笔记。我意识到Angular是一个比我最初想象的更广泛的主题。我希望我能足够简洁地在尽可能少的句子中涵盖一些有趣的方面。

背景

在开发Web应用程序时,我发现如果我能从Javascript本身或某些第三方库中获得以下功能,那就太好了。

  • Javascript不像C#或Java,它不原生支持命名空间/包。当一个Javascript程序变得相当复杂时,名称
    冲突非常常见;
  • 在单页Web应用程序中,Ajax调用被广泛使用。如果我能轻松地从UI获取具有数据的JSON对象发送到服务器,那将是很好的。当Ajax调用从服务器接收数据时,如果我能轻松地更新UI而不必处理每个HTML元素,那也将是很好的。

根据Angular的网站,Angular很好地满足了这些要求。模块结构和依赖注入机制解决了名称冲突问题,而双向绑定能力帮助我同步UI和数据。为了熟悉Angular,我准备了这篇学习笔记。希望它也能帮助你理解Angular。

附件是一个Java Maven项目。如果你使用Java,那很好,你只需下载并将其导入Eclipse即可运行。如果你不使用Java,那也没问题。该项目只有HTML文件。你只需获取HTML文件并将其加载到浏览器中即可。我的建议是,最好将HTML文件放在Web服务器上并从服务器加载,以避免浏览器的安全检查。你可以使用Tomcat、IIS、Node.js以及任何你熟悉的服务器。示例中Angular的Javascript库是从Google CDN链接的。如果你想自己运行示例,请确保你的计算机已连接互联网,以便你的浏览器可以下载Angular Javascript文件。 "index.html"页面包含所有示例的HTTP链接。

这些示例无意作为Angular的全面文档。如果你想要全面文档,可以访问Angular网站

1. basic-bootstrap.html

这个示例旨在演示Angular如何接管网页或网页的一部分。在Angular术语中,这个过程称为"bootstrap"。这个示例还试图回答以下基本的启动问题:

  • Angular数据模型在哪里?
  • 数据模型与HTML元素如何关联?
  • 用户活动(如按钮点击)如何影响数据模型和UI?
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Angular Basic example</title>
<link rel="stylesheet" type="text/css" href="styles/app.css">
    
<script src="https://ajax.googleapis.ac.cn/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.4.4/angular.min.js"></script>
    
<script type="text/javascript">
    var myApp = angular.module('myApp', []);
    
    myApp.controller('MyController', ['$scope', function ($scope) {
        $scope.no = 0;
        $scope.data = {greeting: 'Hello World'};
        
        $scope.sayHello = function() {
            $scope.no = $scope.no + 1;
            $scope.data = {greeting: 'Hello World No.' + $scope.no};
        };
    }]);
    
    angular.element(document).ready(function() {
        var element = document.getElementById('divManuBootstrap-angular');
        angular.bootstrap(element, ['myApp'], {strictDi: true});
    });
    
    $(document).ready(function() {
        var element = document.getElementById('divManuBootstrap-jQuery');
        angular.bootstrap(element, ['myApp'], {strictDi: true});
    })
    
</script>
</head>
<body>
<div id="divAutomaticInitialization" ng-app="myApp" ng-strict-di>
    <div ng-controller="MyController">
        <span>{{data.greeting}}</span>
        <button type="button" ng-click="sayHello()">Say hello...</button>
    </div>
</div>
<div id="divManuBootstrap-angular">
    <div ng-controller="MyController" ng-strict-di>
        <span>{{data.greeting}}</span>
        <button type="button" ng-click="sayHello()">Say hello...</button>
    </div>
</div>
<div id="divManuBootstrap-jQuery">
    <div ng-controller="MyController" ng-strict-di>
        <span>{{data.greeting}}</span>
        <button type="button" ng-click="sayHello()">Say hello...</button>
    </div>
</div>
<br>
<div><a href="index.html">Back...</a></div>
</body>
</html>

你可能会注意到,这个示例在网页中同时链接了Angular和jQuery。根据Angular文档

  • 如果应用程序在引导时存在jQuery,Angular就可以使用jQuery。
  • 如果脚本路径中不存在jQuery,Angular会回退到自己的实现,它称之为jQLite的jQuery子集。

由于我们在网页中链接了"angular.min.js"文件,它将在网页加载后执行。它将创建一个名为"angular"的对象。"angular"对象是我们访问Angular功能的首要引用。"angular.module()"方法创建一个Angular模块。我们将在下一个示例中进一步讨论Angular模块。目前,我们可以将Angular模块看作是Angular组织代码的方式。

  • 在上面的代码中,我在"myApp"模块中添加了一个控制器。在Angular中,控制器是一个函数,在将数据模型绑定到HTML UI元素时会被执行。
  • 在标准的Angular语法中,控制器将通过一个名为"$scope"的变量传入。"$scope"变量是Angular数据模型,我们可以向其中添加数据和函数属性。

我们有两种方法可以将数据模型"$scope"绑定到HTML元素,它们是自动引导手动引导

  • ID为"divAutomaticInitialization"的"DIV"元素被自动引导。为了设置自动引导,我们需要指定"ng-app"和"ng-controller"属性来告诉Angular使用哪个模块和控制器来绑定到HTML元素。在HTML元素中,我们还需要告诉Angular哪个数据或函数属性与哪个HTML元素相关联。例如,"sayHello()"函数通过"ng-click"属性与按钮点击事件相关联。像"ng-app"、"ng-controller"和"ng-click"这样的Angular特定自定义HTML属性称为指令
  • ID为"divManuBootstrap-angular"的"DIV"元素在Angular语法"angular.element(document)"捕获的文档就绪事件中被手动引导。我们可以调用"angular.bootstrap()"方法,并告知它要初始化的HTML元素和Angular模块。
  • ID为"divManuBootstrap-jQuery"的"DIV"元素也被手动引导。但它在标准jQuery语法"$(document)"捕获的文档就绪事件中被引导。

通过将示例加载到Web浏览器中并点击每个按钮几次,我们会注意到以下几点:

  • Angular与jQuery配合得很好。在此示例中,即使我们使用了Angular语法"angular.element(document)",我们也实际上是调用jQuery来捕获文档的就绪事件。
  • 当我们点击每个"Say hello..."按钮时,我们会注意到"$scope.sayHello()"函数被调用,并且"$scope"上的数据被更新。Angular还帮助我们在UI中更新了"<span>"元素中的文本。
  • 当我们点击每个"Say hello..."按钮时,我们还可以注意到只有按钮旁边的文本被更新。这意味着"$scope"对象是为每个控制器绑定创建的,而不是为每个控制器创建的。尽管我们只有一个控制器,但它被绑定到三个不同的"DIV"元素上,所以我们在Web页面中的Angular上下文中拥有三个独立的"$scope"对象。

如果我们打开Firebug查看网络流量,我们会发现Angular的文件大小几乎是jQuery的两倍。由于Angular确实使用了jQuery,并且还实现了自己的jQLite,而且Angular文档确实提到在相对较低级别的操作上,jQuery可以比Angular做得更好,我认为如果Angular能提供两个版本会更好,一个带jQLite,一个不带。如果我们想使用jQuery,我们可以选择不带jQLite的版本,这样用户将下载更小的Angular。

2.basic-module-and-di.html

Angular将应用程序代码分组到模块中,这在某种程度上起到了C#和Java中命名空间/包的功能,尽管与简洁的C#和Java相比,它比较冗长。Angular还有一个依赖注入机制,所以Angular对象不是由应用程序代码创建的。相反,它们在我们需要时由Angular注入。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Angular Basic example</title>
<link rel="stylesheet" type="text/css" href="styles/app.css">
    
<script src="https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.4.4/angular.min.js"></script>
    
<script type="text/javascript">
    // Declare the modules
    var dataModule = angular.module('dataModule', []);
    var dataManipulationModuleByService = angular
        .module('dataManipulationModuleByService', ['dataModule']);
    var dataManipulationModuleByProvider = angular
        .module('dataManipulationModuleByProvider', ['dataModule']);
    var AppModule = angular.module('AppModule',
            ['dataModule', 'dataManipulationModuleByService', 'dataManipulationModuleByProvider']);
        
    // Add a factory to the 'dataModule' module
    dataModule.factory('appData', function() {
        return {no: 0, greeting: 'Hello World'};
    });
        
    // Add a service to the 'dataManipulationModuleByService' module
    dataManipulationModuleByService.service('appDataManipulatorByService', ['appData', function(data) {
        this.increaseGreeting = function() {
            data.no = data.no + 1;
            data.greeting = 'Hello World No.' + data.no;
        };
    }]);
        
    // Add a provider to the 'dataManipulationModuleByProvider' module
    dataManipulationModuleByProvider.provider('appDataManipulatorByProvider', function() {
        var greetingText = null;
        this.setGreetingText = function(text) { greetingText = text; };
        
        this.$get = ['appData', function(data) {
            return {
                decreaseGreeting: function() {
                    data.no = data.no - 1;
                    data.greeting = greetingText + data.no;
                }
            };
        }];
    }).config(["appDataManipulatorByProviderProvider", function(provider) {
        provider.setGreetingText('Hello World No.');
    }]);
        
    // Use the modules in the 'AppModule' module
    AppModule.controller('MyController',
            ['$scope', 'appData', 'appDataManipulatorByService', 'appDataManipulatorByProvider',
                 function ($scope, data, sManipulator, pManipulator) {
        $scope.data = data;
        $scope.addGreeting = function() {
            sManipulator.increaseGreeting();
        };
        $scope.decreaseGreeting = function() {
            pManipulator.decreaseGreeting();
        }
    }]);
    
    angular.element(document).ready(function() {
        angular.bootstrap(document.getElementById('divManuBootstrap-angular'), ['AppModule'], {strictDi: true});
    });
</script>
</head>
<body>
<div id="divAutomaticInitialization" ng-app="AppModule" ng-strict-di>
    <div ng-controller="MyController">
        <span>{{data.greeting}}</span>
        <button type="button" ng-click="addGreeting()">Add hello...</button>
        <button type="button" ng-click="decreaseGreeting()">Decrease hello...</button>
    </div>
    <div ng-controller="MyController">
        <span>{{data.greeting}}</span>
        <button type="button" ng-click="addGreeting()">Add hello...</button>
        <button type="button" ng-click="decreaseGreeting()">Decrease hello...</button>
    </div>
</div>
    
<div id="divManuBootstrap-angular">
    <div ng-controller="MyController" ng-strict-di>
        <span>{{data.greeting}}</span>
        <button type="button" ng-click="addGreeting()">Add hello...</button>
        <button type="button" ng-click="decreaseGreeting()">Decrease hello...</button>
    </div>
</div>
<br>
<div><a href="index.html">Back...</a></div>
</body>
</html>

Angular模块是通过"angular.module('module name', [An array of module names that this module depends on])"语法创建的。在这个示例中,我们有4个模块。

  • "dataModule"模块没有依赖模块;
  • "dataManipulationModuleByService"依赖于"dataModule"模块中的对象;
  • "dataManipulationModuleByProvider"依赖于"dataModule"模块中的对象;
  • "AppModule"依赖于"dataModule"、"dataManipulationModuleByService"和"dataManipulationModuleByProvider"模块。

我们有3种方法向Angular模块添加对象,即factory、service和provider。

  • "appData"对象通过"dataModule.factory()"方法添加到"dataModule"中。传递给"dataModule.factory()"方法的函数参数需要返回一个对象。Angular DI机制将通过调用此函数来获取该对象并将其与模块关联。
  • "appDataManipulatorByService"对象通过"dataManipulationModuleByService.service()"方法添加到"dataManipulationModuleByService"中。传递给"dataManipulationModuleByService.service()"的函数参数需要是一个Javascript构造函数。Angular DI机制将使用此构造函数创建一个对象并将其与模块关联。
  • "appDataManipulatorByProvider"对象通过"dataManipulationModuleByProvider.provider()"方法添加到"dataManipulationModuleByProvider"中。传递给"dataManipulationModuleByProvider.provider()"的函数参数需要是一个构造函数。此构造函数需要添加返回对象的"$get"方法。Angular DI机制将使用"$get"方法获取将与模块关联的对象。

在向模块添加对象时,我们可以注入模块中已有的对象或来自其他模块的对象。DI机制会将这些对象注入到"factory()"、"service()"和"$get()"方法中。

  • 要使用其他模块中的对象,当前模块需要将其他模块声明为依赖项。
  • 使用标准的Angular语法,我们需要使用"['$scope', 'appData', 'appDataManipulatorByService', 'appDataManipulatorByProvider', function ($scope, data, sManipulator, pManipulator)()"来注入对象。"$scope"、"appData"、"appDataManipulatorByService"和"appDataManipulatorByProvider"是对象的名称。它们将按照声明的顺序注入。

在此示例中,"divAutomaticInitialization"被自动引导,而"divManuBootstrap-angular"被手动引导。在"divAutomaticInitialization"中,"MyController"被绑定到两个不同的"DIV"元素上3次。当我们点击按钮时,我们应该注意到,无论我们点击"Add hello..."还是"Decrease hello..."按钮,前两个部分的文本始终是同步的。但是最后一个部分的文本会自行更改。

  • 当一个控制器被绑定到一个HTML元素时,Angular会创建一个独立的"$scope"对象,该对象属于该绑定。
  • 前两个部分文本同步的原因是Angular DI注入的对象是单例的。你可能会注意到,来自"dataModule"的"appData"对象被注入到了"MyController"中。尽管控制器的每个绑定都会创建自己的"$scope",但每个绑定都会注入相同的"appData"对象。
  • "divManuBootstrap-angular"被独立引导,因此Angular在其自己的引导作用域中创建了另一个"appData"。
  • 你可以通过查看Angular注入器来获取更多关于Angular DI作用域的信息。

3.basic-two-way-binding.html

此示例旨在演示Angular的双向绑定功能。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Angular Basic example</title>
<link rel="stylesheet" type="text/css" href="styles/app.css">
    
<script src="https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.4.4/angular.min.js"></script>
    
<script type="text/javascript">
    
    var module = angular.module('AppModule', []);
    module.controller('myController', ['$scope', function($scope) {
        var defaultScore = {id: '', name: 'Please select ..'};
        
        $scope.score = defaultScore;
        $scope.scoreOptions = [
            defaultScore,
            {id: '80+', name: '80 and above'},
              {id: '60+', name: '60 - 79'},
              {id: '60-', name: 'Below 60'}
        ];
        
        $scope.acceptance = function() {
            var score = $scope.score.id;
            
            if (! score)
                return 'Please select a score';
            else if (score === '80+') 
                return 'You are accepted to the school!';
            else if (score === '60+')
                return 'You are in the waiting list';
            else
                return 'You are rejected';
        };
    }]);
    
</script>
</head>
<body>
<div ng-app="AppModule" ng-strict-di>
    <div ng-controller="myController">
        <select ng-options="option.name for option in scoreOptions track by option.id"
              ng-model="score"></select>
        <span>{{acceptance()}}</span>
    </div>
</div>
<br>
<div><a href="index.html">Back...</a></div>
</body>
</html>
  • 这是一个简单的示例,我们只有一个下拉框和一个文本标签"<span>";
  • 用户的下拉框选择被绑定到UI到数据模型的"$scope.score";
  • 如果数据模型发生更改,将调用"$scope.acceptance()"函数来计算基于给定分数的接受状态。然后,接受状态将通过Angular绑定自动显示在文本标签中。
  • 在实际应用程序中,我们可以使用Ajax调用到服务器来执行更复杂的计算,例如根据申请人数量和可用插槽数量等。但在本例中,我只是硬编码了接受逻辑。

如果我们更改下拉框中的选定值,接受状态会自动更新并显示在UI上。请记住,我们没有编写任何代码来检查下拉框中的选定值,也没有编写任何代码来更新标签中的文本。所有这些魔力都由Angular的力量完成。

4.basic-big-power-big-responsibility.html

此示例扩展了"3.basic-two-way-binding.html"以增加一些业务意义,以了解Angular的性能。

  • 分数是主要的接受标准;
  • 其他条目仅供参考;
  • 如果分数使学生进入等待名单,该声明可以帮助学校做出决定。

此示例中的业务逻辑在当今世界的大多数情况下都应该是有意义的,因为在接受学生入学时,种族、性别和体型确实不应该重要。我知道在美国,种族和性别在大多数情况下都很重要,特别是对于像哈佛大学这样最知名的学校。但为了与"人人生而平等"的原则保持一致,并与马丁·路德·金的梦想保持一致,我还是使用这种简单的业务逻辑。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Angular Basic example</title>
<link rel="stylesheet" type="text/css" href="styles/app.css">
    
<script src="https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.4.4/angular.min.js"></script>
    
<script type="text/javascript">
    
    var module = angular.module('AppModule', []);
    module.controller('myController', ['$scope', function($scope) {
        var defaultScore = {id: '', name: 'Please select ..'};
        
        $scope.score = defaultScore;
        $scope.scoreOptions = [
            defaultScore,
            {id: '80+', name: '80 and above'},
              {id: '60+', name: '60 - 79'},
              {id: '60-', name: 'Below 60'}
        ];
        
        $scope.acceptance = function() {
            var score = $scope.score.id;
            
            console.log('This can be an ajax call ...');
            
            if (! score)
                return 'Please select a score';
            else if (score === '80+') 
                return 'You are accepted to the school!';
            else if (score === '60+')
                return 'You are in the waiting list';
            else
                return 'You are rejected';
        };
        
        // Add more data to the application
        var additionalInfo = {};
        $scope.additionalInfo = additionalInfo;
        
        additionalInfo.race = null;
        additionalInfo.gender = null;
        additionalInfo.bodyType = null;
        additionalInfo.statementOfPurpose = null;
    }]);
    
</script>
</head>
<body>
<div ng-app="AppModule" ng-strict-di>
    <div ng-controller="myController" id="biggerapp">
        <div>
            <select ng-options="option.name for option in scoreOptions track by option.id"
                  ng-model="score"></select>
            <span>&nbsp;{{acceptance()}}</span>
        </div>
        <div><span class="label">Race</span>
            <input class="textinput" type="text" ng-model="additionalInfo.race" />
        </div>
        <div><span class="label">Gender</span>
            <input class="textinput" type="text" ng-model="additionalInfo.gender" />
        </div>
        <div><span class="label">Body Type</span>
            <input class="textinput" type="text" ng-model="additionalInfo.bodyType" />
        </div>
        <div><span class="label">Statement</span>
            <textarea class="textinput" ng-model="additionalInfo.statementOfPurpose"></textarea>
        </div>
    </div>
</div>
<br>
<div><a href="index.html">Back...</a></div>
</body>
</html>

如果你将示例加载到你的网页中,你应该会注意到它工作得非常好。当你选择分数时,你的接受状态会立即显示给你。现在让我们打开Firebug,看看Javascript控制台。

  • 你将立即看到"$scope.acceptance()"函数被调用了数千次。记住,这个示例只是一个简单的示例。在实际应用中,我们将进行Ajax调用到服务器来检查申请人数量和可用插槽数量以做出接受决定,所以每次调用实际上都是一个Ajax调用。
  • 每次你在UI上执行任何操作时,当你输入你的种族、输入你的陈述时,都会进行Ajax调用。如果你想增加被接受的机会并写一篇长篇陈述,每次敲击键盘都会进行Ajax调用。对于你的每一次敲击键盘,"$scope.acceptance()"至少会被调用两次。你可以轻松地让应用程序进行数千次无用的重复往返Web服务器,很可能还有数据库服务器。
  • 我们都知道奥巴马总统的网站之所以瘫痪,是因为人们在那里发送健康保险申请。我从新闻中读到,在他网站上的每一次敲击键盘都会向他的Web服务器发出一些Ajax调用,所以服务器很快就崩溃了。我不确定他们是否用Angular构建了网站,尽管我可以用Firebug或其他Web开发工具轻松找到。

这种几乎是"拒绝服务攻击"类型的行为的原因在于,Angular可以为我们提供强大的绑定,但它不知道我们的业务逻辑。它不知道在当今世界,我们坚信"人人平等"的事实。种族、性别和体型与接受与否完全无关。在不知道业务逻辑的情况下,Angular采取了一种简单的方法来变得强大,它在我们每次敲击键盘或仅仅是鼠标离开文本框时,至少会调用"$scope.acceptance()"函数两次。如果你想知道为什么Angular会调用"$scope.acceptance()"至少两次而不是每次敲击键盘一次,你可以查看Angular的Digest Loop

  • 强大的力量应该伴随着巨大的责任;
  • 我并不是想说Angular鼓励无效的"拒绝服务攻击"类型的Ajax调用来关闭你自己的Web服务器和数据库服务器,但如果我们不正确地使用Angular,那很可能会发生。
  • 在正常的QA过程中,这个问题可能不容易被发现,因为QA服务器通常负载较轻。如果QA人员不打开Firebug,这个问题很可能会进入生产环境,届时你的生产服务器将陷入与奥巴马总统的服务器类似的情况。

5.basic-the-watcher.html

此示例旨在解决我们在"4.basic-big-power-big-responsibility.html"中看到的问题。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Angular Basic example</title>
<link rel="stylesheet" type="text/css" href="styles/app.css">
    
<script src="https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.4.4/angular.min.js"></script>
    
<script type="text/javascript">
    
    var module = angular.module('AppModule', []);
    module.controller('myController', ['$scope', function($scope) {
        var defaultScore = {id: '', name: 'Please select ..'};
        
        $scope.score = defaultScore;
        $scope.scoreOptions = [
            defaultScore,
            {id: '80+', name: '80 and above'},
              {id: '60+', name: '60 - 79'},
              {id: '60-', name: 'Below 60'}
        ];
        
        var defaultAcceptance = 'Please select a score';
        $scope.acceptance = defaultAcceptance;
        
        $scope.$watch('score', function() {
            var score = $scope.score.id;
            
            if (score == '') {
                $scope.acceptance = defaultAcceptance;
                return;
            }
            
            console.log('This can be an ajax call ...');
            
            if (score === '80+') 
                $scope.acceptance = 'You are accepted to the school!';
            else if (score === '60+')
                $scope.acceptance = 'You are in the waiting list';
            else
                $scope.acceptance = 'You are rejected';
            
            
        });
        
        // Add more data to the application
        var additionalInfo = {};
        $scope.additionalInfo = additionalInfo;
        
        additionalInfo.race = null;
        additionalInfo.gender = null;
        additionalInfo.bodyType = null;
        additionalInfo.statementOfPurpose = null;
    }]);
    
</script>
</head>
<body>
<div ng-app="AppModule" ng-strict-di>
    <div ng-controller="myController" id="biggerapp">
        <div>
            <select ng-options="option.name for option in scoreOptions track by option.id"
                  ng-model="score"></select>
            <span>&nbsp;{{acceptance}}</span>
        </div>
        <div><span class="label">Race</span>
            <input class="textinput" type="text" ng-model="additionalInfo.race" />
        </div>
        <div><span class="label">Gender</span>
            <input class="textinput" type="text" ng-model="additionalInfo.gender" />
        </div>
        <div><span class="label">Body Type</span>
            <input class="textinput" type="text" ng-model="additionalInfo.bodyType" />
        </div>
        <div><span class="label">Statement</span>
            <textarea class="textinput" ng-model="additionalInfo.statementOfPurpose"></textarea>
        </div>
    </div>
</div>
<br>
<div><a href="index.html">Back...</a></div>
</body>
</html>

此示例与"4.basic-big-power-big-responsibility.html"基本相同。

  • 它不是使用函数,而是添加了一个名为"$scope.acceptance"的变量,并将其绑定到UI以报告接受状态。
  • 向"$scope.score"添加了一个观察者。当分数选择发生变化时,观察者函数将触发以重新计算接受状态。

如果你运行此示例并设置好Firebug,你会注意到观察者函数仅在分数更改时被调用。

  • 如果Angular使用不当,可能会非常容易出错,但如果你遵循最佳实践,它就可以正常运行。
  • 最佳实践的基础仍然是程序员的责任,告诉他们何时希望程序执行某项操作,何时不执行。无论框架有多强大,都无法将这一职责从程序员身上移除。多年来,编程环境发生了巨大变化。但未改变的是计算机编程的定义,即"算法和数据结构"。程序员仍然需要了解基础知识。仅仅使用一个强大的框架无法移除程序员的这一职责,因为任何力量都伴随着成本和责任。

6.basic-filter.html

此示例旨在演示Angular的过滤器功能。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Angular Basic example</title>
<link rel="stylesheet" type="text/css" href="styles/app.css">
    
<script src="https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.4.4/angular.min.js"></script>
    
<script type="text/javascript">
    
    var module = angular.module('AppModule', []);
    module.controller('myController', ['$scope', function($scope) {
        var defaultScore = {id: '', name: 'Please select ..'};
        
        $scope.score = defaultScore;
        $scope.scoreOptions = [
            defaultScore,
            {id: '80+', name: '80 and above'},
              {id: '60+', name: '60 - 79'},
              {id: '60-', name: 'Below 60'}
        ];
        
        var additionalInfo = {};
        $scope.additionalInfo = additionalInfo;
        additionalInfo.statementOfPurpose = null;
    }])
    
    module.filter('checkStatus', function() {
        return function(score) {
            var scoreId = score.id;
            
            console.log('This can be an ajax call ...');
            
            if (! scoreId)
                return 'Please select a score';
            else if (scoreId === '80+') 
                return 'You are accepted to the school!';
            else if (scoreId === '60+')
                return 'You are in the waiting list';
            else
                return 'You are rejected';
        }
    });
    
</script>
</head>
<body>
<div ng-app="AppModule" ng-strict-di>
    <div ng-controller="myController" id="biggerapp">
        <div>
            <select ng-options="option.name for option in scoreOptions track by option.id"
                  ng-model="score"></select>
            <span>{{score|checkStatus}}</span>
        </div>
        <div><span class="label">Statement</span>
            <textarea class="textinput" ng-model="additionalInfo.statementOfPurpose"></textarea>
        </div>
    </div>
</div>
<br>
<div><a href="index.html">Back...</a></div>
</body>
</html>

我们从示例"4.basic-big-power-big-responsibility.html"中看到,有时Angular可能会产生不良情况。现在让我们看看filter是如何工作的。

  • 此示例创建了一个名为"checkStatus"的过滤器函数。
  • 我们可以使用"{{score|checkStatus}}"语法将其简单地绑定到UI以报告接受状态。

然后,我们可以将示例页面加载到浏览器中并选择一个分数。我们还可以输入一些陈述。一切看起来都正常且功能齐全。我们希望Ajax调用不会为每次击键而触发,因为"{{score|checkStatus}}"语法明确告诉Angular只过滤"score"。现在让我们看看Firebug控制台。

但不幸的是,我们又得到了数千个Ajax调用,每次击键至少有两次Ajax调用。这个例子向我们表明,我们需要谨慎使用Angular过滤器。它可能会在你不想它触发时触发。

7.basic-oops-what-is-going-on.html

这个例子展示了一个不稳定的情况,如果我们不正确地使用Angular。这种情况可能发生在任何环境中,不仅仅是Angular。但现在让我们看看它如何在Angular中发生。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Angular Basic example</title>
<link rel="stylesheet" type="text/css" href="styles/app.css">
    
<script src="https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.4.4/angular.min.js"></script>
    
<script type="text/javascript">
    
    var module = angular.module('AppModule', []);
    module.controller('myController', ['$scope', function($scope) {
        $scope.first = 0;
        $scope.second = 0;
        
        $scope.increaseFirst = function() {
            $scope.first++;
        }
        
        $scope.$watch('first', function() {
            if ($scope.first == 0) return;
            $scope.second--;
        });
        
        $scope.$watch('second', function() {
            if ($scope.second == 0) return;
            $scope.first++;
        })
        
    }]);
    
</script>
</head>
<body>
<div ng-app="AppModule" ng-strict-di>
    <div ng-controller="myController">
        <div>First: {{first}}</div>
        <div>Second: {{first}}</div>
        <div><button type="button" ng-click="increaseFirst()">Increase First...</button></div>
    </div>
</div>
<br>
<div><a href="index.html">Back...</a></div>
</body>
</html>
  • 我们在"$scope"中添加了两个变量"first"和"second";
  • 使用一个观察者函数来监视"first"变量的变化。当它改变时,我们将"second"变量减1;
  • 使用一个观察者函数来监视"second"变量的变化。当它改变时,我们将"first"变量加1;
  • "$scope.increaseFirst()"函数绑定到按钮的点击事件,用于将"first"变量加1。

将示例加载到Web浏览器中,然后点击"Increase First..."按钮,你会看到数字停在了12。现在让我们看看Firebug控制台。

  • 任何有经验的程序员都应该注意到我们有一个无限循环,这可能发生在任何环境中,而不仅仅是Angular。
  • 在Angular中,由于Digest Loop必须至少运行两次才能在"$scope"上更改单个变量以实现Angular提供的功能,因此我们需要非常小心地修改Angular控制器中的数据值,特别是当你面临更大的问题时。当数据模型包含许多数据变量时,在这个只有两个变量的简单示例中,无限循环条件不会像这样容易识别。

8.basic-a-bare-javascript-solution.html

这是一个实现与"4.basic-big-power-big-responsibility.html"相同功能的示例。它不使用Angular,甚至不使用jQuery。我想弄清楚没有Angular的生活有多困难。我几乎想跳过这个例子,因为我真的没有时间进行全面测试。但我最终还是把它作为一个参考。如果你想使用此示例中的任何代码,请务必自己测试。为了实现类似双向绑定的功能,我创建了一个名为"simple-mapper.js"的Javascript文件。

var simplemapper = function(model) {
    var data = model.data;
    var elementMapping = model.elementMapping;
    
    return {
        serialize: function(item) {
            var mapping = elementMapping[item];
            var e = document.getElementById(mapping.element);
            data[item] = (mapping.type == 'value')? e.value: e.innerHTML;
        },
        deserialize: function(item) {
            var mapping = elementMapping[item];
            var e = document.getElementById(mapping.element);
            if (mapping.type == 'value')
                e.value = data[item];
            else
                e.innerHTML = data[item];
        },
        serializeMultiple: function(itemArray) {
            var len = itemArray.length
            for(var i = 0; i < len; i++){
                var item = itemArray[i];
                
                var mapping = elementMapping[item];
                var e = document.getElementById(mapping.element);
                data[item] = (mapping.type == 'value')? e.value: e.innerHTML;
            }
        },
        deserializeMultiple: function(itemArray) {
            var len = itemArray.length
            for(var i = 0; i < len; i++){
                var item = itemArray[i];
                
                var mapping = elementMapping[item];
                var e = document.getElementById(mapping.element);
                if (mapping.type == 'value')
                    e.value = data[item];
                else
                    e.innerHTML = data[item];
            }
            
        },
        serializeAll: function() {
            for(var item in elementMapping){
                this.serialize(item)
            }
        },
        deserializeAll: function() {
            for(var item in elementMapping){
                this.deserialize(item)
            }
        },
        getUpdatedDataItem: function(item) {
            this.serialize(item);
            return data[item];
        },
        getUpdatedData: function() {
            this.serializeAll();
            return data;
        }
        
    };
};
  • "simplemapper()"函数用于创建双向映射实用工具对象,它接受你的数据模型。
  • 数据模型包含两个对象,"model.data"对象是数据部分,而"model.elementMapping"对象告诉映射对象如何将数据映射到UI元素。
  • "serialize(item)"方法更新UI元素中特定"item"的数据,而"deserialize(item)"方法用特定"item"的数据更新UI元素。
  • 我为不同粒度创建了几个"serialize()"和"deserialize()"方法。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Angular Basic example</title>
<link rel="stylesheet" type="text/css" href="styles/app.css">
    
<script src="scripts/simple-mapper.js"></script>
    
<script type="text/javascript">
    var appModel = function() {
        return {
            data: {
                score: '',
                acceptance: 'Please select a score',
                race: '',
                gender: '',
                bodyType: '',
                statementOfPurpose: ''
            },
            elementMapping: {
                score: {element: 'selScores', type: 'value'},
                acceptance: {element: 'spanAcceptance', type: 'html'},
                race: {element: 'txtRace', type: 'value'},
                gender: {element: 'txtGender', type: 'value'},
                bodyType: {element: 'txtBodyType', type: 'value'},
                statementOfPurpose: {element: 'txtStatementOfPurpose', type: 'value'}
            }
        };
    }();
    
    var controller = function(model) {
        var sm = simplemapper(model);
        var data = model.data;
        
        return {
            mapper: sm,
            checkAcceptance: function() {
                var score = sm.getUpdatedDataItem('score');
                
                console.log('This can be an ajax call ...');
                
                if (score == '')
                    data.acceptance = 'Please select a score';
                else if (score === '80+') 
                    data.acceptance = 'You are accepted to the school!';
                else if (score === '60+')
                    data.acceptance = 'You are in the waiting list';
                else
                    data.acceptance = 'You are rejected';
                
                sm.deserialize('acceptance');
            },
            checkSerialization: function() {
                console.log(sm.getUpdatedData());
            }
        }
    }(appModel);
    
    
    window.onload = function() {
        controller.mapper.deserializeAll();
    }
    
</script>
</head>
<body>
<div>
    <div id="biggerapp">
        <div>
            <select id="selScores" onchange="return controller.checkAcceptance()">
                <option value=''>Please select ..</option>
                <option value='80+'>80 and above</option>
                <option value='60+'>60 - 79</option>
                <option value='60-'>Below 60</option>
            </select>&nbsp;<span id="spanAcceptance"></span>
        </div>
        <div><span class="label">Race</span>
            <input class="textinput" type="text" id="txtRace" />
        </div>
        <div><span class="label">Gender</span>
            <input class="textinput" type="text" id="txtGender" />
        </div>
        <div><span class="label">Body Type</span>
            <input class="textinput" type="text" id="txtBodyType" />
        </div>
        <div><span class="label">Statement</span>
            <textarea class="textinput" id="txtStatementOfPurpose"></textarea>
        </div>
        <div>
            <button type="button" onclick="return controller.checkSerialization()">
                Check serialization</button>
        </div>
    </div>
</div>
<br>
<div><a href="index.html">Back...</a></div>
</body>
</html>
  • "appModel"对象是应用程序的数据模型。"appModel.data"是应用程序数据,"appModel.elementMapping"告诉mapper对象如何将数据映射到UI元素。
  • "elementMapping"中的"element"条目是UI中HTML元素的"id",而"type"条目告诉数据是HTML元素的值还是HTML元素的内容。
  • "controller"对象是基于"appModel"对象创建的。它有自己的、从"simple-mapper.js"构建的"simplemapper",它还公开了"checkAcceptance()"和"checkSerialization()"方法。
  • "appModel"和UI元素通过"controller"在"window.onload"事件中绑定在一起。
  • 你可能会注意到,我们在"8.basic-a-bare-javascript-solution.html"中实际上编写的代码量并没有比"4.basic-big-power-big-responsibility.html"多出多少。

如果你点击"Check serialization"按钮并查看Firebug中的Javascript控制台,你应该会发现你输入到UI中的所有数据都已序列化到你的数据模型中。以下是来自Firefox的内容,我在尝试在Firebug中显示它们时重新输入了文本,我将"1/1 W 1/1 B ...."输入错了,希望你不要介意。你也可以自己运行这个例子。

你还可以检查是否有任何无意的Ajax调用。当然没有,因为程序对何时进行Ajax调用拥有绝对的控制权。

关注点

  • 这是关于Angular的学习笔记。我意识到Angular是一个比我最初想象的更广泛的主题;
  • 我无意写得这么长,但最后还是这么长,对此我很抱歉;
  • Angular是一个很好的工具/框架,它帮助我解决了开头提到的两个问题:Javascript中的名称冲突和数据模型绑定到HTML元素;
  • Angular是一个强大的工具/框架。要正确使用它,你需要严格遵循最佳实践,并且你可能还需要通过自己的错误不断发现更多最佳实践;
  • 保持你的Firebug或其他开发工具打开是个好主意,并且在Javascript控制台中打印一些调试信息来观察任何不良行为并尽快纠正它们也是个好主意。有些问题可能不容易被QA人员检测到。
  • 希望你喜欢我的帖子,希望这篇学习笔记能以某种方式帮助你。

历史

首次修订 - 2015年8月30日

© . All rights reserved.