这一篇我们将一起感受学习一个小型的、活生生的应用,而不是继续深入分析哪些单个的特性。我们将会一起感受一下,前面所讨论过的所有片段如何才能真正的组合在一起,形成一个真实的、可以运行的应用。

GutHub是一款菜谱管理应用。我们学习它有两个目的,第一是用它来管理美味的菜谱,第二是用它来学习angularjs的方方面面:

这款应用的特性如下:

a)         两列布局

b)         在左侧有一个导航栏

c)         允许你创建新菜谱

d)         允许你浏览现有的菜谱列表

主视图位于右侧,主视图将会根据具体的URL刷新,可惜显示菜谱列表、菜谱项目详情、以及用来添加或编辑菜谱的表单。

模型、控制器和模版(视图)之间的关系

这三种东西是如何协作的,应用应该用什么样的视角来看待它们。

模型就是真理。你的整个应用都是由模型驱动的——视图中所有展示的内容都是视图、被存储起来的内容是模型,几乎所有的内容都是模型。所以请把“模型就是真理”这句话多读几遍。同时花一点时间思考一下模型,思考一下对象中的属性应该是怎样的内容、应该如何从服务器端获取模型,以及如何保存它。通过视图绑定技术,视图会根据数据模型自动刷新,所以模型总是应用的焦点。

控制器将会负责业务逻辑:应该如何获取模型、可以在模型上执行何种操作、视图需要模型上的何种信息,如何如何转换模型以获取想要的信息。表单校验任务、对服务器调用、使用正确的数据启用视图,以及与此相关的几乎所有的事情都是控制器的职责。

最有,模版代表模型的展现形式,以及用户应该如何与应用进行交互。模版主要用来做一下事情:

n  展示模型

n  定义用户与应用之间的交互方式

n  给应用提供样式,并且判断何时以及怎样显示一些元素

n  过滤并格式化数据

视图是模版和模型融合之后产生的东西。模版中不应该包含任何业务逻辑和行为,只有控制器才能具备这些特性。但是你可能会问,DOM操作应该放到那里呢?DOM操作并不会发生在控制器或模版中。它由angular指令负责(有时候也可以通过服务进行操作,DOM操作放到放到服务中可以避免重复代码)。

模型

对于当前这款应用,我们将会让模型保持超级简单。这里的模型就是一些菜谱,它们将是真个应用的唯一的模型对象,其他所有的东西都是构建在模型之上。

每一天菜谱都具有一下特性:

  • 一个ID,这个ID将会被持久化到服务器
  • 一个名称
  • 一个简短的描述
  • 烹饪指南
  • 是否是特色菜
  • 配料数组,每一项都包含重量、单位及名称

就这么多,超级简单,应用中所有的东西都围绕这个简单的模型而构建,下面是一天简单的菜谱:

  1. {
  2. "id":"1",
  3. "title":"热姜汁藕片",
  4. "description":"藕片切得越薄越容易入味。",
  5. "ingredients":[
  6. {
  7. "amount":"450",
  8. "amountUnits":"g",
  9. "ingredientName":"姜"
  10. },
  11. {
  12. "amount":"450",
  13. "amountUnits":"g",
  14. "ingredientName":"藕"
  15. },
  16. {
  17. "amount":"0",
  18. "amountUnits":"0",
  19. "ingredientName":"香油"
  20. },
  21. {
  22. "amount":"3",
  23. "amountUnits":"g",
  24. "ingredientName":"食盐"
  25. },
  26. {
  27. "amount":"50",
  28. "amountUnits":"mml",
  29. "ingredientName":"白醋"
  30. }
  31. ],
  32. "instructions":"1、准备食材 \n 2、姜用工具磨成姜蓉 \n 3、姜用工具磨成姜蓉 \n 4、藕片入开水汆一下,大概3分钟断生,捞出 \n 5、锅里倒入白醋 \n 6、烧至沸腾后加入姜蓉 \n 7、30秒后加入香油和盐,将热姜汁淋入藕片上,腌制20分钟左右即可食用"
  33.  
  34. }

我们继续来看,围绕这个简单的模型如何构建更加复杂的UI功能。

控制器、指令及服务器

我们要完成这个应用需要几个指令和控制器代码,然后再来看所需要的控制器。

服务

文件位置menuSolution\app\services\services.js

  1. var services=angular.module('guthub.services',['ngResource']);
  2. services.factory('Recipe',['$resource', function ($resource) {
  3. return $resource('/recipes/:id',{id:'@id'});
  4. }]);
  5.  
  6. services.factory('MultiRecipeLoader',['Recipe','$q', function (Recipe, $q) {
  7. return function () {
  8. var delay=$q.defer();
  9. Recipe.query(function (recipes) {
  10. delay.resolve(recipes);
  11. }, function () {
  12. delay.reject('无法取出食谱');
  13. });
  14. return delay.promise;
  15. };
  16. }]);
  17.  
  18. services.factory('RecipeLoader',['Recipe','$route','$q', function (Recipe,$route,$q) {
  19. return function () {
  20. var delay=$q.defer();
  21. Recipe.get(
  22. {id:$route.current.params.recipeId},
  23. function (recipe) {
  24. delay.resolve();
  25. },
  26. function () {
  27. delay.reject('无法取出食谱'+ $route.current.params.recipeId);
  28. }
  29. );
  30.  
  31. return delay.promise;
  32. };
  33. }]);

在以往的章节中已经接触过了服务,这里我们再来深入理解下。在上面的代码中我们实现了三个服务。其中有一个菜谱服务,它返回的东西叫做Angular Resource。Resource将会封装底层的$http服务,所以你的代码只要负责处理对象就可以了。只要一行代码——return $resource(当然需要依赖services.guthub模块),我们就可以把菜谱作为参数传给任何控制器了,然后菜谱对象就会被注入到控制器中。这样依赖,每个菜谱都具备了下面这些内置的方法:

Recipe.get();
Recipe.save();
Recipe.delete();
Recipe.query();
Recipe.remove();

注:如果你打算使用Recipe.delete();,并且希望在ie中使用它,你必须这样调用它Recipe[delete](),这是因为在IE中delete是一个关键字。

假设我们现在有一个菜谱对象,必要的信息都已经放在就这个对象里面了,包括Id。然后通过下面这些代码我们就可以把它保存起来:

var recipe=new
Recipe(obj);//假设id=13

recipe.$save();

以上代码会向/recipe/13路径发起一次POST请求。

其次,还有两个服务,正两个服务都是加载器:一个是单个菜单加载器,另个一是所有菜单加载器。当我们连接到路由上去,就会用到这两个加载器。它们的核心工作原理非常相似。这两个服务的工作流程如下:

  1. 创建一个延迟对象(这些都是angularjs中的promise,用来对异步函数的链式调用)
  2. 向服务端发起一次调用
  3. 在服务端返回数据之后解析延迟对象
  4. 返回promise,angularjs中的路由将会机制将会使用这个对象

Promise是一个接口,他用来处理的对象具有这样的特点:在未来的某一时刻(主要是异步调用)会从服务端返回或者被填充属性。其核心是,promise是一个带有then函数的对象.

使用promise机制的优点如下:

  1. 可以对函数链式调用,所以你不会陷入代码缩进噩梦中。
  2. 在调用链的过程中,可以保证上一个函数调用完之后才会调用下一个函数。
  3. 每一个then()都带有两个参数(两个都是函数),一个是成功之后的回调,一个是出错之后的处理器。
  4. 如果调用链中出现了错误,错误将会冒泡传递到其余错误处理函数中。所以,最终来说,所有的错误都可以在任意一个回调函数中处理。

你可能会问,resolve(解决)方法和reject(拒绝)方法又是什么呢?在angular中延迟调用是实现promise的一种方式。调用resolve方法会填充promise(也就是调用success函数),而reject方法将会调用promise的错误处理函数。

指令

现在我们来看看应用中的指令。在目前这款应用中我们会用到如下两条指令。

Butterbar

当路由发生变化同时页面还在加载时,这一指令将会显示和隐藏信息的操作。指令将会被嵌入到路由的变化机制中,然后根据页面的状态自动隐藏或显示其标签中的内容。

Focus

Focus指令要你过来确保特定的输入项(或元素)能否获得焦点。

文件位置
menuSolution\app\directives\directives.js

  1. var directives=angular.module('guthub.directives',[]);
  2. directives.directive('butterbar',['$rootScope', function ($rootScope) {
  3. return {
  4. link: function (scope,element,attrs) {
  5. element.addClass('hide');
  6. $rootScope.$on('$routeChangeStart', function () {
  7. element.removeClass('hide');
  8. });
  9. $rootScope.$on('$routeChangeSuccess', function () {
  10. element.addClass('hide');
  11. });
  12. }
  13. }
  14. }]);
  15.  
  16. directives.directive('focus', function () {
  17. return {
  18. link: function (scope, element, sttrs) {
  19. element[0].focus();
  20. }
  21. };
  22. });

以上指令将会返回一个对象,这个对象只有一个link属性。

1、  指令的处理过程分为两个步骤。在编译过程中,找到绑定在DOM元素上的指令,然后进行处理。所有的DOM操作都发生在编译阶段,在这一阶段结束之后,会产生一个内联函数。

2、  链接阶段中,第一步所产生的DOM模版会变链接到作用域上,会根据需要添加的监控器或者监听器,从而在作用域和元素之间进行动态绑定。这样一来,与作用域相关的所有内容都是在链接阶段进行的。

可以像下面这样使用butterbar指令:

<div butterbar> my loading text……</div>

在一开始的时候只是简单把它隐藏起来,然后在作用域上添加两个监听器。每当路由器发生变化时,它就会显示内部元素。每当路由成功完成变化之后,它又会把butterbar隐藏起来。

另外还有一个有趣的东西是,那就是如何把$tootScope注入到指令中。所有指令都会被直接连接到angularjs的依赖注入系统中。

第二个focus指令更加简单。它实在调用当前元素上的focus()方法而已。你可以在任何元素上添加focus属性来调用它。

<input type=”text” focus/>

当页面加载完成之后,文本框会自动获取焦点。

控制器

文件位置 menuSolution\app\controller\controller.js

写完指令和服务之后,终于改写控制器了,这里我们需要5个控制器。所有这些控制器都位于同一个文件夹中

  1. var app=angular.module('guthub',['guthub.services','guthub.directives']);
  2. //第一个控制器:List控制器,他的任务是显示系统中所有菜谱
  3. //请关注List控制器重要的一件事:在构造器中它不会到i服务器中获取菜谱。相反它会处理一个已经获取到的菜谱列表
  4. app.controller('ListController',['$scope','recipes', function ($scope, recipes) {
  5. $scope.recipes=recipes;
  6. }]);
  7.  
  8. //其他控制器与List控制器非常相似
  9. //edit函数只是把URL地址改成编辑的地址,然后angular就会去做剩余的工作
  10. app.controller('ViewController',['$scope','$location','recipe', function ($scope,$location,recipe) {
  11. $scope.recipe=recipe;
  12. $scope.edit= function () {
  13. $location.path('/edit/'+recipe.id);
  14. }
  15. }]);
  16.  
  17. app.controller('EditController',['$scope','$location','recipe', function ($scope,$location,recipe) {
  18. $scope.recipe=recipe;
  19. $scope.save= function (recipe) {
  20. $scope.recipe.save(function () {
  21. $location.path('/view/'+recipe.id);
  22. });
  23. };
  24. $scope.remove= function () {
  25. delete $scope.recipe;
  26. $location.path('/');
  27. };
  28. }]);
  29.  
  30. app.controller('NewController',['$scope','$location','Recipe', function ($scope,$location,Recipe) {
  31. $scope.recipe=new Recipe(
  32. {
  33. ingredients:[{}]
  34. }
  35. );
  36.  
  37. $scope.save= function () {
  38. $scope.recipe.$save(function (recipe) {
  39. $locale.path('/view/'+recipe.id);
  40. })
  41. }
  42. }]);
  43.  
  44. app.controller('IngredientsController',['$scope', function ($scope) {
  45. $scope.AddIngredient= function () {
  46. var ingredients=$scope.recipe.ingredients;
  47. ingredients[ingredients.length]={};
  48. };
  49. $scope.removeIngredient= function (index) {
  50. $scope.recipe.ingredients.splice(index,1);
  51. }
  52. }]);
  53.  
  54. //创建路由
  55. app.config([
  56. '$routeProvider',
  57. function ($routeProvider) {
  58. $routeProvider.when('/', {
  59. controller: 'ListController',
  60. resolve: {
  61. recipes: function (MultiRecipeLoader) {
  62. return MultiRecipeLoader();
  63. }
  64. },
  65. templateUrl: '/views/list.html'
  66. }).when('/edit/:recipeId', {
  67. controller: 'EditController',
  68. resolve: {
  69. recipe: function (RecipeLoader) {
  70. return RecipeLoader();
  71. }
  72. },
  73. templateUrl: '/views/recipeForm.html'
  74. }).when('/view/:recipeId', {
  75. controller: 'ViewController',
  76. resolve: {
  77. recipe: function (RecipeLoader) {
  78. return RecipeLoader();
  79. }
  80. },
  81. templateUrl: '/views/viewRecipe.html'
  82. }).when('/new', {
  83. controller: 'NewController',
  84. templateUrl: '/views/recipeForm.html'
  85. }).otherwise({redirectTo: '/'});
  86. }
  87. ]);

你可能注意到了,edit和New这两个控制器的路由都指向了相同的模版URL——-/views/recipeForm.html,是怎么回事呢?因为我们会根据关联控制器的不同,在菜谱模版中显示不同的元素。

做完这些之后,我们来看看模版。看看这些控制器是如何把它们关联起来的,以及如何管理显示给最终用户的内容。

模版

我们会从最外层的主模版入手,也是就index.html。他就是我们单页应用的根。其他模版都会加载到这个模版的内部。

模版位置menuSolution\app\views\

主要的模版:

  1. <!DOCTYPE html>
  2. <html ng-app="guthub">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>GutHub</title>
  6. <script src="static/js/angular.js" type="text/javascript"></script>
  7. <script src="static/js/angular-resource.js" type="text/javascript"></script>
  8. <script src="static/js/angular-route.min.js" type="text/javascript"></script>
  9. <script src="app/directives/directives.js" type="text/javascript"></script>
  10. <script src="app/services/services.js" type="text/javascript"></script>
  11. <script src="app/controller/controller.js" type="text/javascript"></script>
  12. <link rel="stylesheet" href="static/css/bootstrap.css"/>
  13. <link rel="stylesheet" href="static/css/guthub.css"/>
  14. </head>
  15. <body>
  16. <div butterbar>Loading......</div>
  17. <div class="container">
  18. <div class="row">
  19. <header>
  20. <h1>GutHub</h1>
  21. </header>
  22. <div class="span2">
  23. <div id="focus"><a href="/#/new">New Recipe</a><br/></div>
  24. <div><a href="/#/">Recipe List</a></div>
  25. </div>
  26. <div class="span2">
  27. <div ng-view></div>
  28. </div>
  29. </div>
  30.  
  31. </div>
  32. </body>
  33. </html>

在这个模版中有几个比较有趣的元素需要注意,其中大部分已经聊过。其中包括ng-app、ng-view、butterbar 明显还缺少ng-controller。

现在我们来看每个控制器上的模版:

菜谱列表模版:

  1. <h3>Recipe List</h3>
  2. <ul class="recips">
  3. <li ng-repeat="recipe in recipes">
  4. <div>
  5. <a ng-href="/#/view/{{recipe.id}}">{{recipe.title}}</a>
  6. </div>
  7. </li>
  8. </ul>

这是一个平淡无奇模版,只有两个地方非常的有趣,一个是ng-repeat标签使用的非常标准。它会从作用域中获取菜单列表,然后遍历它们;另一个是使用ng-href代替了href,这纯粹是为了在angular加载过程中产生错误链接,ng-href无论在何时可以保证都不会把存在缺陷的连接展示给用户。这个模版完了,你可能会问,我怎么没看到控制器呢?而且我们也没有定义主控制器(Main controller)。这正式路由映射派上用场的地方。我们之前说的路由会跳转到列表模版,它上面绑定了List Controller,如过引用了变量之类的东西那么变量就位于List Controller 作用于内。

第二个模版:

  1. <h2>{{recipe.title}}</h2>
  2. <div>{{recipe.description}}</div>
  3. <ul class="list-unstyled">
  4. <li ng-repeat="ingredient in recipe.ingredients">
  5. <span>{{ingredient.amount}}</span>
  6. <span>{{ingredient.amountUnits}}</span>
  7. <span>{{ingredient.ingredientName}}</span>
  8. </li>
  9. </ul>
  10.  
  11. <h3>Intructions</h3>
  12. <div>{{recipe.instructions}}</div>
  13. <form ng-submit="edit()" class="form-horizontal">
  14. <div class="form-actions">
  15. <button class="btn btn-primary">修改</button>
  16. </div>
  17. </form>

菜谱表单模版:

  1. <h2>Edit Recipe</h2>
  2. <form name="recipeForm" ng-submit="save()" class="form-horizontal">
  3. <div class="control-group">
  4. <label class="control-label" for="title">标题:</label>
  5.  
  6. <div class="controls">
  7. <input type="text" ng-model="recipe.title" id="title" class="input-xlarge" focus/>
  8. </div>
  9. </div>
  10.  
  11. <div class="control-group">
  12. <label class="control-label" for="description">描述:</label>
  13.  
  14. <div class="controls">
  15. <input type="text" ng-model="recipe.description" id="description" class="input-xlarge" focus/>
  16. </div>
  17. </div>
  18.  
  19. <div class="control-group">
  20. <label class="control-label" for="ingredients">原材料:</label>
  21. <ul class="controls" ng-controller="IngredientsController">
  22. <li ng-repeat="i in recipe.ingredients">
  23. <input type="text" ng-model="i.amount" class=""/>
  24. <input type="text" ng-model="i.amountUnits" class=""/>
  25. <input type="text" ng-model="i.ingredientName" class=""/>
  26. <button class="btn btn-primary" ng-click="removeIngredient($index)">
  27. <i class="glyphicon-minus-sign"></i>
  28. 删除
  29. </button>
  30. </li>
  31. <button type="button" class="btn btn-primary" ng-click="AddIngredient()">
  32. <i class="glyphicon-plus-sign"></i> 添加
  33. </button>
  34. </ul>
  35. <input type="text" ng-model="recipe.ingredients" id="ingredients" class="input-xlarge" focus/>
  36.  
  37. </div>
  38.  
  39. <div class="control-group">
  40. <label class="control-label" for="instructions">做法:</label>
  41.  
  42. <div class="controls">
  43. <input type="text" ng-model="recipe.instructions" id="instructions" class="input-xxlarge" focus/>
  44. </div>
  45. </div>
  46.  
  47. <div class="form-actions">
  48. <button class="btn btn-primary" ng-click="save()">保存</button>
  49. <button class="btn btn-primary" ng-show="!recipe.id" ng-click="remove()">删除</button>
  50. </div>
  51. </form>

项目架构图:

这个项目比较简单、就不上效果图了。大家可以写一个自己的demo来熟悉angular的各个模块。

Angular简单应用剖析的更多相关文章

  1. angular源码剖析之Provider系列--CacheFactoryProvider

    CacheFactoryProvider 简介 源码里是这么描述的: Factory that constructs {@link $cacheFactory.Cache Cache} objects ...

  2. Angular 简单的Get

    <!DOCTYPE html><html ng-app="myApp"><head lang="en"> <meta ...

  3. Angular 简单的Post

    <!DOCTYPE html><html ng-app="myApp"><head lang="en"> <meta ...

  4. Angular简单总结

    AngularJS AngularJS四大特征 MVC模式 双向绑定 依赖注入 模块化设计 AngularJS 表达式 AngularJS 表达式写在双大括号内{{expression }},可以包含 ...

  5. angular源码剖析之Provider系列--QProvider

    QProvider 简介 源码里是这么描述的: A service that helps you run functions asynchronously, and use their return ...

  6. Angular.的简单运用

    从script引用angular文件.开始编写angular事件: 在angular文件中添加属性: ag-xxxx;初始化使用: ng-app="name"; 没有这个属性就不会 ...

  7. MVC、MVP、MVVM、Angular.js、Knockout.js、Backbone.js、React.js、Ember.js、Avalon.js、Vue.js 概念摘录

    注:文章内容都是摘录性文字,自己阅读的一些笔记,方便日后查看. MVC MVC(Model-View-Controller),M 是指业务模型,V 是指用户界面,C 则是控制器,使用 MVC 的目的是 ...

  8. Theano2.1.17-基础知识之剖析theano的函数

    来自:http://deeplearning.net/software/theano/tutorial/profiling.html Profiling Theano function note:该方 ...

  9. 简述一个javascript简单继承工具的实现原理

    背景 由于本人非常希望能够开发自己的游戏,所以业余时间一直在想着能不能自己一些好玩又有趣的东西出来,最近随着steam上众多独立游戏的爆发,感觉自己又燃烧了起来,所以又拾起了很久以前的一个2d引擎,决 ...

随机推荐

  1. C语言中的宏

    写好C语言,漂亮的宏定义很重要,使用宏定义可以防止出错,提高可移植性,可读性,方便性 等等.下面列举一些成熟软件中常用得宏定义...... 1,防止一个头文件被重复包含 #ifndef COMDEF_ ...

  2. java数据类型和运算优先级

    一.数据类型 1.基本数据类型: . 布尔类型:boolean(true,false) . 整型:byte(-128,127).short(-32768,32767).int(-2147483648, ...

  3. 我的第一个JApplet-绘制笑脸

    初学Java,有很多东西都不太理解,但是我想以前初学C语言的时候也是不太懂,先参考着书上的程序写,然后用多了就自然而然的懂了! 下面来简单的介绍一下我自学的第一个Java小应用程序-绘制笑脸,下面是源 ...

  4. 筛1-n中每个数的因子(nlogn)

    void get_div() //筛因子 { ; i<maxn; i++) for(int j=i; j<maxn; j+=i) dx[j].push_back(i); }

  5. getaccesstoken方法

    通过appid和appsecret获取access_token的定义函数 这里用的是memcache缓存存储用户信息7000秒 <?php function getAccessToken($ap ...

  6. EasyUI-页面布局

    通过使用 jQuery EasyUI 可以很容易地添加 Tabs.您只需要调用 'add' 方法即可. 在本教程中,我们将使用 iframe 动态地添加显示在一个页面上的 Tabs. 当点击添加按钮, ...

  7. [Angular 2] 8. Better ES5 Code

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...

  8. Android之PreferenceActivity 详解

    看到很多书中都没有对PreferenceActivity做介绍,而我正好又在项目中用到,所以就把自己的使用的在这总结一下,也方便日后查找. PerferenceActivity是什么,看下面的截图: ...

  9. GitHub具体教程

    GitHub具体教程 Table of Contents 1 Git具体教程 1.1 Git简单介绍 1.1.1 Git是何方神圣? 1.1.2 重要的术语 1.1.3 索引 1.2 Git安装 1. ...

  10. EditText操作收集

    1.android EditText插入字符串到光标所在位置 EditText mTextInput=(EditText)findViewById(R.id.input);//EditText对象 i ...