前端的功能与单页应用

就webapp开发而言,前端实现的功能主要有:
-实现UI(渲染出用户可见的视图和各种功能组件)
-对用户的操作(事件)做出反应
-实现业务逻辑
-实现页面路由
-与服务器端交互
-对文件资源的管理(HTML/CSS/javascrip/图片等)
-提供用户安装和运行的手段

目前,实现这些功能的框架/库五花八门。怎么选择呢?

首先,谨记“用户体验至上”的原则,应该选择单页应用类的框架。
单页面应用是指用户通过浏览器加载独立的HTML页面并且无需离开此单一页面,这也是其独特的优势所在。对用户操作来说,一旦加载和执行单个页面应用程序通常会有更多的响应,这就需要返回到后端Web服务器,而单页面应用为用户提供了(在浏览器中)更接近一个本地移动或桌面应用程序的体验。
单页Web应用程序的结构很简单:首先传递HTML文档框架;然后使用JavaScript修改页面;紧接着再从服务器传递更多数据然后再修改页面,如此循环。从性能的角度看,在现代浏览器中单页面Web App已经能够和普通应用程序相媲美,而且几乎所有的操作系统都支持现代的浏览器。使用HTML+CSS+Javascript编写应用程序,能使更多的人们都加入到程序开发的行列。

单页Web应用程序的优点:
首先,UI反应快,用户体验流畅。对于内容的改动不需要重新加载整个页面。
改动小:因为数据层和UI的分离,可以重新编写一个原生的移动设备应用程序而不用(对原有数据服务部分)大动干戈。
高效。它对服务器压力很小,消耗更少的带宽,能够与面向服务的架构更好地结合。

单页Web应用程序的缺点:
-无法实现“返回”导航问题。
-对SEO不友好。如果你看中SEO,那就不应该在页面上使用JavaScript,你应该使用网站而不是Web应用。
-单页面的结构可能会比较复杂。

令人兴奋的AngularJS

在单页面应用中,最有实力的是AngularJS。据它的创始人misko说,它的设计初衷是:看看是否有可能让Web设计师(非开发者)只使用HTML标签来创建简单的应用程序。AngularJS遵循的设计理念是——构建UI应该是声明式的。

AngularJS是由Google创建的一种JS框架,使用它可以扩展应用程序中的HTML词汇,从而在web应用程序中使用HTML声明动态内容。
AngularJS可以让你扩展HTML的语法,以便清晰、简洁地表示应用程序中的组件,并允许将标准的HTML作为你的模板语言,AngularJS可以通过双向数据绑定自动从拥有JavaScript对象(模型)的UI(视图)中同步数据。

特性一:双向数据绑定
    数据绑定可能是AngularJS最酷最实用的特性。它能够帮助你避免书写大量的初始代码从而节约开发时间。一个典型的web应用可能包含了80%的代码用来处理,查询和监听DOM。我们想象一下Model是你的应用中的动态内容。传统来说,当内容变化了,开发人员需要手动处理DOM元素并且将属性反映到这些变化中。这个一个双向的过程。一方面,model变化驱动了DOM中元素变化,另一方面,DOM元素的变化也会影响到Model。这个在用户互动中更加复杂,因为开发人员需要处理和解析这些互动,然后融合到一个model中,并且更新View。这是一个手动的复杂过程,当一个应用非常庞大的时候,将会是一件非常费劲的事情。数据绑定使的代码更少,你可以专注于你的应用。
   AngularJS的双向数据绑定,能够同步DOM和Model等等,提供了你的Model投射到view的方法。这些投射可以无缝的,而且是双向的,在控制器和视图中以MODEL为桥梁形成互动。

特性二:模板
    在AngularJS中,一个模板就是一个HTML文件。是的,它使用HTML,而不是任何特定的模板语言!但是HTML的内容扩展了,包含了很多帮助你映射model到view的内容。 HTML模板将会被浏览器解析到DOM中,然后成为AngularJS编译器的输入。AngularJS将会遍历DOM模板来生成一些指导,即,directive(指令)。所有的指令都负责针对view来设置数据绑定。
我们要理解AuguarJS并不把模板当做String来操作。输入AngularJS的是DOM而非string。数据绑定是DOM变化,不是字符串的连接或者innerHTML变化。使用DOM作为输入,而不是字符串,是AngularJS区别于其它的框架的最大原因。使用DOM允许你扩展指令词汇并且可以创建你自己的指令,甚至开发可重用的组件。

特性三:MVC
   针对客户端应用开发AngularJS吸收了传统的MVC基本原则。AngularJS并不执行传统意义上的MVC,更接近于MVVM(Moodel-View-ViewModel)。
Model
  代表数据。一般是简单的javascript对象。
ViewModel
  viewmodel是一个用来提供特别数据和方法从而维护指定view的对象。viewmodel是$scope的对象,主要指的是$scope对象,这个对象使用简单的API来侦测和广播状态变化。
Controller
   控制器。controller负责设置初始状态和参数化$scope方法用以控制行为。
View
  view是AngularJS解析后渲染和绑定后生成的HTML 。这个部分帮助你创建web应用的架构。$scope拥有一个针对数据的参考,controller定义行为,view处理布局和互动。

特性四:依赖注入(Dependency Injection,DI)
   AngularJS拥有内建的依赖注入子系统,可以帮助开发人员更容易的开发,理解和测试应用。DI允许你请求你的依赖,而不是自己找寻它们。比如,我们需要一个东西,DI负责找创建并且提供给我们。为了而得到核心的AngularJS服务,只需要添加一个简单服务作为参数,AngularJS会侦测并且提供给你。

特性五:Directives(指令)
   你是不是也希望浏览器可以做点儿有意思的事情?那么AngularJS可以做到。 指令可以用来创建自定义的标签,是一种威力强大的工具。它们可以用来装饰元素或者操作DOM属性,使得HTML被无限扩展。,甚至还可以重写一套完全属于自己的html标签。

特性六:测试
AngularJS内含了测试方法和用例可以帮助你更方便的执行测试。JS是一个动态的解析性语言,而不是编译类型的,因此非常的难写测试。
AngularJS被开成一个可测试的框架。它甚至包含了点对点的单元测试。

特性七:与node.js, bootstrap的良好结合,还有sublime插件。

总之,angularjs则是一个让我感到惊艳的框架,相对于同类无数个mv**框架,它的优势达到了数量级。如果用几个词来形容它,应该是:学习成本高,开发效率高,写代码时思路流畅。
Angularjs的学习成本比较高,主要原因是其设计思路与我们以前写jquery代码时有很大的不同,不能套用。Angularjs的核心思想就是“复用”,它的“复用”体现在"directive"上。Directive既是angularjs的核心,也是它的重点、难点和杀手级特性。简单的说,directive可以是一个自定义的html标签、或者属性、或者注释,它的背后是一些函数,可以对所在的html标签进行增强,比如修改html的dom内容,增强它的功能(如用第三方js插件包装它)。
编写Directive比较复杂,需要理解它的内部原理才能定义出自己的directive。这是angularJS学习中的难点。

入门教程

入门教程:Angular官网上的tutorial就写的很好,一步步演示了一个手机网站的搭建过程。中文译本可搜索“Angular.js入门教程”或直接察看电子书http://www.ituring.com.cn/minibook/303。 我改写了这个教程,增加了很多功能。见phonecat的代码。

AngularJS的结构

文件规范

angular.js提供的seed太简单,在实践中用处不大,所以我用phonecat项目完善出一个标准应用,里面具有常用的几乎所有功能。自己编程时拿来改就是了!

README.md           -->用markdown语言写的项目说明
 server/
   web-server.js     --> 一个简单的基于node.js的WEB服务器
 app/                --> 包括所有项目资源的主文件夹
      css/              --> css 格式文件
        app.css         --> 默认主css文件
        bootstrap.css   --> bootstrap前端库的css文件(v2.3.2)
        bootstrap-responsive.css  -->bootstrap前端库的响应控件的css文件
      img/              --> 图片
        xxx.png         --> bootstrap的图标文件
        pics/           --> 项目用图片                    
      index.html        --> 项目的主页(其实是个模板框架,具体内容都在partials里)
      index-async.html  --> 项目的主页(以异步方式加载js文件,如果js文件太多太大,就用这个主页)
      js/               --> javascript 脚本文件
        app.js          --> 项目的主脚本(主要是路由,同时指定了模块、分页模板和控制器)
        controllers.js  --> 控制器
        directives.js   --> 指令集
        filters.js      --> 过滤器
        services.js     --> 服务(里面定义了数据访问的方法)
      lib/              --> angular和第三方的库
        angular/
          angular.min.js    --> angular.js压缩库
          angular-*.min.js      --> angular扩展模块(标配了cookies,loader, resource, sanitize几个模块,全使用压缩版)
        bootstrap/
          bootstrap.min.js  --> bootstrap库的js文件
        jQuery/
          jquery.min.js     --> jQuer库的压缩版
      partials/             --> 分视图 (承载具体页面内容的html模板)
        list.html           --> 索引页面
        detail.html         --> 分项页面
      data/
        data.json         --> 主数据文件
        xxx.json            --> 分数据文件
 logs/                      --> 预留做系统日志
-----------------------------------------以下全是与测试有关的文件,发行时可删去-------------------
    config/karma.conf.js        --> Karma单元测试的配置文件
    config/karma-e2e.conf.js    --> Karma端到端测试的配置文件

scripts/            --> 所有测试脚本都在这里
      e2e-test.sh       --> Karma端到端测试启动脚本 (*nix)
      e2e-test.bat      --> Karma端到端测试启动脚本 (windows)
      test.bat          --> Karma单元测试启动脚本 (windows) 
      test.sh           --> Karma单元测试启动脚本 (*nix)

test/               --> 测试用的库
      e2e/              --> 端到端测试
        runner.html     --> 端到端测试页面
        scenarios.js    --> 端到端测试脚本
      lib/
        angular/                --> angular测试库
          angular-mocks.js      --> angular服务模拟器
          angular-scenario.js   --> angular端到端测试库
          version.txt           --> 版本说明(目前是1.0.7版)
      unit/                     --> 单元测试脚本
        controllersSpec.js      --> 控制器测试脚本
        directivessSpec.js      --> 指令集测试脚本
        filtersSpec.js          --> 过滤器测试脚本
        servicesSpec.js         --> 服务测试脚本

web-server: 使用Express构建的高效HTTP服务器,里面设定了主页、端口、路由等。路由部分是最常修改的。


项目使用了angular,bootstrap,jQuery三大库,全部使用压缩版,以加快加载速度。
angular.js  v1.0.8
bootstrap   v2.3.2
jQuery      v2.0.3
但是bootstrap的css文件不能用压缩的,否则就没法调整了。

主页index.html
说是主页,其实就是个框架,负责总布局,模块命名和库引用。为提高加载速度,全部库的引用都放在下面。
注意:所有要调用的控制器、指令、服务等文件也要引用进来,否则不起作用!
我这里的index.html主要实现各种库的加载和模块设置,头部(header)和底部(footer)。

 注意:一定要先加载jQuery,highChart,bootstrap等外部库再加载angular.js自己的库,否则在ng-view中很多外部功能(特别是依赖jQuery的)无法实现。

视图partials
这种局部视图才是真正的内容视图,通过app路由动态加载。
list.html是索引页面,显示内容的目录。由于主页加了header,所以要修改bootstrap.css中的.container-fluid,
加上padding-top: 30px; 这样中间的内容才能显示出来。否则其位置和header会冲突。
details.html是详细内容页面,同样为了显示出来,外面也跳上一个container-fluid的div。

客户端路由app.js
这里指的是客户端的页面加载路由,使得不同链接对应不同的partials。注意:这与服务器端路由(写在web-server.js里)不是一回事,也不冲突。
注意,这里必须把以来的过滤器/服务/指令都包括进来,才能生效。
采用分段缩进格式:
angular.module('phonecat', ['phonecatFilters', 'phonecatServices','phonecatDirectives']).
  config(['$routeProvider', function($routeProvider) {
  $routeProvider
      .when('/phones', 
      {
       templateUrl: 'partials/list.html',   
       controller: PhoneListCtrl
      })
      .when('/phones/:phoneId', 
      {
       templateUrl: 'partials/detail.html', 
       controller: PhoneDetailCtrl
      })
      .otherwise(
      {redirectTo: '/'});
}])
要增加新的路由,只要增加.when('/phones/:phoneId',这样的代码段,指明局部视图和控制器就行了。

扩展指令directives.js
phonecat项目中没有使用到扩展指令,但这里我为了演示,加入了一个显示自定义时间的指令myCurrentTime.
为此,除了写入了了这个指令的代码外,还应注意:
-将其module的名字改为'phonecatDirectives',以与app.js中指定的统一。
-在app.js中加入angular.module('phonecat', ['phonecatFilters', 'phonecatServices','phonecatDirectives']).
-在index.html中用<span my-current-time="format"></span>这样的命令,用连字符代替驼峰写法。
至于视图切换的过渡效果,因为要写animate.css,比较复杂,这里就不介绍了。

过滤器fillters.js
这里只用到一个把对钩和X换成相应符号的checkmark过滤器。在相应视图中只要用{{phone.connectivity.gps | checkmark}}这样的写法就可以了。

服务service.js
这里只用到了访问数据的服务,但这是最重要的一种服务。这里的写法是直接访问相应的json数据文件,使用了$resource服务:
/* Services */

angular.module('phonecatServices', ['ngResource']).
    factory('Phone', function($resource){
  return $resource('phones/:phoneId.json', {}, {
    query: {method:'GET', params:{phoneId:'phones'}, isArray:true}
  });
});

上述Index-partial-controller-service形成了一个完整的链条,相互依赖。

MVC架构

常见的网站的主要功能是(1)提供可视界面;(2)采集用户的输入;(3)将用户输入送入后台,进行处理;(4)将处理后的数据返回前端格式化显示。
如果把这一切都混在一起编码,无疑是非常不利于扩展和维护的。
经典的MVC模型将应用分解成独立的表现、数据、逻辑三种组件,鼓励三者间的解耦,为网页程序的开发奠定了坚实的基础。

AngularJS贯彻了经典的MVC模式,并且更加模块化,更容易实现和更容易测试。

视图(View)
Angualr.js的视图就是用户可见的页面,它没有采用任何特殊的模板语言,而是直接使用HTML文件。通过使用指令(directive)大大扩展了HTML的功能。
在AngularJS中,视图(view)指的是浏览器加载和渲染之后,并且在AngularJS根据模板、控制器、模型信息修改之后的DOM。
在AngularJS对MVC的实现中,视图是知道模型和控制器的。视图知道模型的双向绑定。视图通过指令知道的控制器,比如 ngController 和 ngView 指令,也可以通过绑定知道,比如 {{someControllerFunction()}} 。通过这些方式,视图可以调用相应控制器中的方法。

模型(Model)
在AngularJS中,模型就是数据(自变量)。它的值可以是任意的Javascript对象(包括数组和原始对象)。模型可以被绑定在页面视图中
例如,在index.html中:
<div ng-controller="Controller">           
<input ng-model="mydata">          
The model value is: {{mydata}}     
</div>
这句使用ng-model指令在视图中创建了一个模型:mydata。它的值来自于输入。可以在控制器调用。下句把模型mydata的值绑定在这里,使得网页可以动态显示模型的值。

控制器
写一个app.js
function Controller($scope) {
$scope.mydata= "first";     
}
这句在名为Contraller的控制器中创建了一个模型mydata,初始值设为“first"。可以在视图中调用。$scope是这个控制器的作用域。
控制器应该就干两件事:
(1)设置作用域对象的初始状态;
(2)给作用域对象增加行为;
其他的事尽量不要交给控制器来做。

1.给作用域对象设置初始状态
一般来说,当你创建应用时,你需要对它的作用域设置初始状态。
AngularJS将对作用域对象调用控制器的构造函数(从某种意义上来说就像使用的Javascript的
apply方法),以此来设置作用域的初始状态。
你可以通过创建一个模型属性来设置初始作用域的初始状态。比如:
function GreetingCtrl($scope) { $scope.greeting = 'Hola!'; }
GreetingCtrl控制器创建了一个模板中可以调用的叫 greeting 的模型,并设它的初始值是"Hola"。

2.给作用域对象增加行为
AngularJS作用域对象的行为是由作用域的方法来表示的。这些方法是可以在模板或者说视图中调用的。这些方法和应用模型交互,并且能改变模型。
任何赋给作用域的方法,都能在模板或者说视图中被调用,并且能通过表达式或者 ng 事件指令调用。(比如,ngClick)。如:
 $scope.takeBread = function() {
$scope.food = 'Bread';
    }

scope是联系控制器和视图的桥梁。
一般控制器要写到js文件中,在视图中通过ng-controller="Controller"来指向js文件中的控制器Controller(或通过app局部视图路由说明)。在控制器中,写$scope.属性名=xxx,就可以在视图文件中调用这个属性。
index.html:
<div ng-controller="foodCtrl">
<button ng-click="takeBread()">Bread</button>
<button ng-click="takeRice()">Rice</button>
        <p>My favorate food is {{food}} !</p>

</div>
app.js:
function foodCtrl($scope) {
    $scope.food = 'maple food';
    $scope.takeBread = function() {
$scope.food = 'Bread';
    }
    $scope.takeRice = function() {
$scope.food = 'Rice';
    }
}
控制器foodCtrl设模型food的初始值为"maple food"。设置了takeBread()和takeRice()两个方法(行为)。在视图中用两个按钮分别调用这两个方法。

控制器的继承:
AngularJS中的控制器继承减少类似控制器的代码,是基于作用域的继承的。让我们看下面这个例子:
<body ng-controller="MainCtrl">
  <p>Good {{timeOfDay}}, {{name}}!</p>
  <div ng-controller="ChildCtrl">
    <p>Good {{timeOfDay}}, {{name}}!</p>
    <p ng-controller="BabyCtrl">Good {{timeOfDay}}, {{name}}!</p>
  </div>
</body>

function MainCtrl($scope) {
  $scope.timeOfDay = 'morning';
  $scope.name = 'Nikki';
}

function ChildCtrl($scope) {
  $scope.name = 'Mattie';
}

function BabyCtrl($scope) {
  $scope.timeOfDay = 'evening';
  $scope.name = 'Gingerbreak Baby';
}
注意我们是如何在模板中嵌套我们的 ngController 指令的。这个模板结构会使得AngularJS为视图创建四个作用域:
根作用域
MainCtrl作用域,它包含了模型timeOfDay和模型name。
ChildCtrl作用域,它继承了上层作用域的timeOfDay,复写了name。
BabyCtrl作用域,复写了MainCtrl中定义的timeOfDay和ChildCtrl中的name。
控制器的继承和模型继承是同一个原理。所以在我们前面的例子中,所有的模型都用返回相应字符串的控制器方法代替。

作用域(scope)

作用域是一个指向应用模型的对象。它是表达式的执行环境。作用域是控制器和视图之间的“胶水”。作用域能监控表达式和传递事件。
从根本上,WEB是基于事件的“事件-循环(event-loop)”系统,它总是期待着事件的发生(如用户交互行为)触发回调函数在javascript中执行,从而修改DOM。angular.js通过作用域和数据绑定扩展了这一模式。在angular中,每当你绑定一个元素时,就自动注册了一个侦听器($watch),应用中很多这样的侦听器组成了一个列表($watch list),它们监视着所绑定的元素的变化,一旦有某种变化,就会自动呼叫$apply, 进入angular context,触发$digest loop,它会遍历所有的侦听器,询问是否有事件发生,如果有事件发生就执行相应的行为函数,都问完了还要再问是否有侦听器被更新,如果有更新的还要把清单中的所有侦听器再问一遍(最多10次,以避免无限循环)。
当你使用第三方库(如jQuery)修改DOM时就不会自动呼叫$apply,所以你得显式地写scope.$apply()之类的语句来进入angular context。
你可以写自己的$watch来侦听某些事件并执行相应的反应。如下例:
index.html:
<body ng-controller="MainCtrl">
  <input ng-model="name" />
  Name updated: {{updated}} times.
</body>

app.js:
app.controller('MainCtrl', function($scope) {
  $scope.name = "Angular";

$scope.updated = -1;

$scope.$watch('name', function() {
    $scope.updated++;
  });
});
侦听器侦听的是name,当name发生变化时,就把$scope.updated加1。

控制器和指令都持有作用域的引用,但是不持有对方的。这使得控制器能从指令和DOM中脱离出来。这很重要,因为这使得控制器完全不需要知道view的存在,这大大改善了应用的测试。

作用域层级

作用域有层次结构,这个层次和相应的DOM几乎是一样的。每一个AngularJS应用都有一个绝对的根作用域。但是可能有多个子作用域。
一个应用可以有多个作用域,因为有一些指令会生成新的子作用域(参考指令的文档看看哪些指令会创建新作用域)。当新作用域被创建的时候,他们会被当成子作用域添加到父作用域下,这使得作用域会变成一个和相应DOM结构一个的树状结构。
当AngularJS执行表达式 {{username}} ,它会首先查找和当前节点相关的作用域中叫做username的属性。如果没找到,那就会继续向上层作用域搜索,直到根作用域
从DOM中获取作用域。作用域是作为$scope的数据属性关联到DOM上的,并且能在需要调试的时候被获取到。根作用域关联的DOM就是ng-app指令定义的地方。一般来说ng-app都是放在 <html> 元素中的,但是也能放在其他元素中,比如<div>。
在调试器中检测作用域:
在你要查看的元素上右键单击,选择菜单中的“审查元素”。你应该会看到浏览器的调试器,并且你选择的元素高亮了显示了。

作用域事件的传递

作用域中的事件传递是和DOM事件传递类似的。事件可以广播给子作用域或者传递给父作用域。
index.html:
<div ng-controller="EventController">
    Root scope <tt>MyEvent</tt> count: {{count}}
    <ul>
        <li ng-repeat="i in [1]" ng-controller="EventController">
            <button ng-click="$emit('MyEvent')">$emit('MyEvent')</button>
            <button ng-click="$broadcast('MyEvent')">$broadcast('MyEvent')</button>
            <br>
            Middle scope <tt>MyEvent</tt> count: {{count}}
            <ul>
                <li ng-repeat="item in [1, 2]" ng-controller="EventController">
                Leaf scope <tt>MyEvent</tt> count: {{count}}
                </li>
            </ul>
        </li>
    </ul>
</div>
app.js:
function EventController($scope) {
$scope.count = 0;
$scope.$on('MyEvent', function() {
$scope.count++;
});
}
注意:emit使本级和上级收到MyEvent事件,而broadcast使本级和下级收到事件。

模块

不论视图还是控制器,都可以分成多个模块来写,这样更容易组织,也更容易测试。
一般通过在视图的某个div中声明 ng-app="myApp" 来加载myApp这个模块。在控制器中用myApp=angular.module('myApp', []) 来声明模块。后面可以写模块的控制器(同样可以有多个)。
一个页面可以加载多个模块。
如index.html:
<div class="container" data-ng-app="demoApp">     
      <div data-ng-controller="SimpleController">
        <h3>Adding a Module and Controller</h3>
        Name:<br />
        <input type="text" data-ng-model="inputData.name" placeholder="name" />
        <br />
        City:<br />
        <input type="text" data-ng-model="inputData.city" placeholder="city" />
        <br />
        <button class="btn btn-primary" data-ng-click="addCustomer()">Add Customer</button>
        <br />
        <br />

Filter by Name:
        <input type="text" data-ng-model="nameText" />
        <br />
        <ul>
            <li data-ng-repeat="customer in customers | filter:nameText | orderBy:'name'">{{ customer.name }} - {{ customer.city }}</li>
        </ul>
      </div>
    </div>

<div class="container" ng-app="myApp" ng-controller="myController">
        <h3>Another module</h3>
        {{ name }}
    </div>
以上代码先后加载了demoApp和myApp两个模块。并指定了各自用到的控制器。

app.js:
var demoApp = angular.module('demoApp', []);

function SimpleController($scope) {
$scope.customers = [
{ name: 'Dave Jones', city: 'Phoenix' },
{ name: 'Jamie Riley', city: 'Atlanta' },
{ name: 'Heedy Wahlin', city: 'Chandler' },
{ name: 'Thomas Winter', city: 'Seattle' }
];

$scope.addCustomer = function () {
$scope.customers.push(
{
name: $scope.inputData.name,
city: $scope.inputData.city
});
}
}

var myApp = angular.module('myapp', []);
function myController($scope) {
$scope.name = "World";
}
这里写了两个模块各自的控制器。

模块的加载和依赖
体现在体现在app.js和service.js中。如:
angular.module('phonecat', ['phonecatFilters', 'phonecatServices','phonecatDirectives']).
  config(['$routeProvider', function($routeProvider) {

......}

服务(Service)

如果要在多个控制器中使用同样的数据和函数怎么办?为了更好的封装数据和代码复用,Angular提供了服务和工厂函数(factory)。AngularJS服务是一种能执行一个常见操作的单例,比如$http服务是用来操作浏览器的XMLHttpRequest对象的(通过AJAX方式从服务器获取数据就要靠这个服务了)。要使用AngularJS服务,你只需要在需要的地方(控制器,或其他服务)指出依赖就行了。AngularJS的依赖注入系统会帮你完成剩下的事情。它负责实例化,查找左右依赖,并且按照工场函数要求的样子传递依赖。AngularJS通过“构造器注入”来注入依赖(通过工场函数来传递依赖)。因为Javascript是动态类型语言,AngularJS无法通过静态类型来识别服务,所以你必须使用$inject属性来显式地指定依赖。比如:
myController.$inject = ['$location'];
AngularJS web框架提供了一组常用操作的服务。和其他的内建变量或者标识符一样,内建服务名称也总是以"$"开头。另外你也可以创建你自己的服务。Angular提供的原生服务:
$anchorScroll
$animate
$cacheFactory
$compile
$controller
$document
$exceptionHandler
$filter
$http
$httpBackend
$interpolate
$locale
$location
$log
$parse
$q
$rootElement
$rootScope
$sce
$sceDelegate
$templateCache
$timeout
$window

创建自己的服务

尽管系统提供的服务很多,但有时还需要创建自己的服务,以便控制器可以直接调用,程序更加清晰。创建服务使用工厂函数。如下例:
app.js:
angular.module('MyServiceModule', []).
 factory('notify', ['$window', function(window) {
    var msgs = [];
    return function(msg) {
      msgs.push(msg);
      window.alert(msgs.join("\n"));
      msgs = [];
    };
  }]);
 
function myController($scope, notify) {
  $scope.callNotify = function(msg) {
    notify(msg);  //注意,这里使用了工厂函数notify,即notify服务。
  };
}

myController.$inject = ['$scope', 'notify'];
上例创建了一个服务notify, 然后注入到myController控制器中。

在index.html中使用:
    <div ng-app="MyServiceModule" ng-controller="myController">
      <p>Let's try this simple notify service, injected into the controller...</p>
      <input ng-model="message" >{{message}}
      <button ng-click="callNotify(message);">NOTIFY</button>
    </div>

将服务注入到控制器中

因为Javascript是一种动态语言,依赖注入系统无法通过静态类型来知道应该注入什么样的服务(静态类型语言就可以)。所以,你应该$inject的属性来指定服务的名字,这个属性是一个包含这需要注入的服务的名字字符串的数组。名字要和服务注册到系统时的名字匹配。服务的名称的顺序也很重要:当执行工场函数时传递的参数是依照数组里的顺序的。但是工场函数中参数的名字不重要,但最好还是和服务本身的名字一样。

如下格式:
function myController($loc, $log) {
this.firstMethod = function() {
// use $location service
$loc.setHash();
};
this.secondMethod = function() {
// use $log service
$log.info('...');
};
}
// which services to inject ?
myController.$inject = ['$location', '$log'];

在上面的notify的例子中,我们用myController.$inject = ['$scope', 'notify'];将$scope和notify这两个服务注入到控制器myController中。其中notify这个服务是自己写的(用工厂函数)。
同时,在控制器函数中,按同样顺序列明'$scope', 'notify'这两个服务。
function myController($scope, notify) {
  $scope.callNotify = function(msg) {
    notify(msg);
  };
}
 
服务在创建过程中可以依赖其他的服务。也就是说工厂函数可以依赖其他工厂函数或注入到其他工厂函数中。
使用如下格式:
function myModuleCfgFn($provide) {
  $provide.factory('myService', ['dep1', 'dep2', function(dep1, dep2) {}]);
}
意味着服务myService依赖于dep1,dep2这两个服务。
如下例子:
/**
 * batchLog service allows for messages to be queued in memory and flushed
 * to the console.log every 50 seconds.
 * 这是一个每隔50秒钟从输入从取内容显示在log中的例子。
 * @param {*} message Message to be logged.
 * 服务batchLog依赖于系统服务timeout和log,而服务routeTemplateMonitor依赖于自定义的batchLog服务和系统的route服务。
 */
  function batchLogModule($provide){
    $provide.factory('batchLog', ['$timeout', '$log', function($timeout, $log) {
      var messageQueue = [];
 
      function log() {
        if (messageQueue.length) {
          $log('batchLog messages: ', messageQueue);
          messageQueue = [];
        }
        $timeout(log, 50000);
      }
 
      // start periodic checking
      log();
 
      return function(message) {
        messageQueue.push(message);
      }
    }]);
 
    /**
     * routeTemplateMonitor monitors each $route change and logs the current
     * template via the batchLog service.
     */
    $provide.factory('routeTemplateMonitor',
                ['$route', 'batchLog', '$rootScope',
         function($route,   batchLog,   $rootScope) {
      $rootScope.$on('$routeChangeSuccess', function() {
        batchLog($route.current ? $route.current.template : null);
      });
    }]);
  }
 
  // get the main service to kick of the application
  angular.injector([batchLogModule]).get('routeTemplateMonitor');

数据库操作及前后端交互

我使用EJDB嵌入式数据库,这是在后端通过var EJDB = require("ejdb");var jb = EJDB.open("database", EJDB.DEFAULT_OPEN_MODE);建立一个数据库对象jb,然后在后端对数据库进行各种操作。那么,如何让前端与后端连接起来呢?当然,可以直接在前端的js文件中嵌入var EJDB = require("ejdb");等数据库操作的硬代码来直接访问本地EJDB数据库,如同EJDB的官方电话本例子那样(https://github.com/Softmotions/nwk-ejdb-address-book),但是,这种方法不够灵活,比如,如果需要其他客户端来连接这个数据库怎么办?

一般的做法是,前端负责展现数据,后端负责提供数据。因此数据库的操作在后端完成。

一般认为前端mv**框架适合于单页面程序,无刷新、局部更新的那种。前后端完全分离,之间以restful api交互,使用json交换数据。在前端做好router,当点击了某个按钮需要展示新内容时,直接由前端获取并显示另一个局部html页面,同时调用某个restful api获取json数据填充。当需要将数据上传到服务器端的时候,则导出json文件上传,服务器收到后再解析-插入到本身的大库中(MongoDB)。这种程序,通常前端的功能比较复杂,而对后端要求较少。采用这类mv**框架,前端程序员们可以充分发挥自己的才智,完全使用javascript/css/html来实现功能。而对于后台,只需知道restful api接口即可。这是前端mv**推荐的方式,也是目前来说比较好的方式。其特点是“以前端js框架为主,后端为辅”。

Anguarljs对于这种方式,有着非常好的支持。它除了提供前端router外,还提供了一些与后台交互的service,如与ajax相关的$http,与restful相关的$resource;对于cookie与本地存储支持也很好,基本上使用angularjs就可以把程序做完。后台可以使用各种语言、各种框架来提供restful api。比如,我尝试过couchdb这个数据库,它直接在数据库层面提供了restful api作为外界操作数据库的接口,angularjs与它配合起来,连服务端程序都不用了。

在angularjs中提供了一个service叫$resource:http://docs.angularjs.org/api/ngResource.$resource,它可以通过一个url和一些参数,定义一个resouce对象。通过调用该对象的某些方法,可以与后台交互,并且直接得到经过它处理之后的数据。使用的感觉有点像我们在后端常用的dao。
angular.module('phonecatServices', ['ngResource']).
    factory('Phone', function($resource){
  return $resource(http://example.com:8080/api', {}, {
    query: {method:'GET', params:{phoneId:'phones'}, isArray:true}
  });
});
关键是怎么写服务器端的restful API呢?一种思路是自己写(如http://www.ibm.com/developerworks/cn/web/1211_zuochao_nodejsrest/index.html),一种是选用restify(http://mcavage.me/node-restify/)这样的专用node框架或connect插件。还有,就是基于express这样的通用框架。
angular.js的数据库访问
这包括两个内容,一、从RESTful风格的WEB服务器那里获取数据资源;二、在前端格式化地显示数据呢。

从WEB服务器获取数据

从服务器端获取数据的前提是WEB服务器已经通过某个URI提供了数据访问的API。在node.js部分我们介绍了如何通过express的路由规则提供RESTful风格的API供客户端访问。下面我们看看前端代码需要做什么。

在angular中有两个系统内置的服务可以访问服务器资源,一个是$http,它简单直观,但层次较低,复用不方便。另一个是$resource,抽象层次较高,便于集中控制数据访问。推荐使用$resource。

我们先来看看$http的使用。它直接在控制器中写服务器端的参数和访问动作。(使用这种方法要在app.js中去掉对service的依赖)。
controller.js:
/* Controllers */

function PhoneListCtrl($scope, $http) {
  $http.get('http://localhost:3000/phones').success(function(data) {
    $scope.phones = data;
  });
 
  $scope.orderProp = 'age';
}

PhoneListCtrl.$inject = ['$scope', '$http'];

这里用$http.get('http://localhost:3000/phones').success(function(data)定义了GET方法和资源的URI,在成功后把数据放到data里,然后将phones绑定为data,这样,就可以在页面里直接用phones来代表数据集了。
每一个控制器中的每一个动作都要写类似上面的语句,麻烦些。

再看$resource的使用。$resource是angular提供的一个专门访问RESTful数据资源的封装好的系统服务,它的基本语法是:
$resource(url, 参数,动作);  后两个是可选的。
url就是我们RESTful架构中代表资源的URI喽,之间用:分割,如$resource('http://example.com/resource/:resource_id.:format')。参数是随着URL传递过去的参数,如/path/greet?salutation=Hello,可能会被行为覆盖。动作是最强大灵活的,它可以完全由客户自定义,形如:
{action1: {method:?, params:?, isArray:?, headers:?, ...},
 action2: {method:?, params:?, isArray:?, headers:?, ...},
 ...}
action代表动作的名字,以后可以当作resource的方法来使用。
method代表HTTP request标准方法。目前支持GET, POST, PUT, DELETE和JSONP五种。
params代表标准方法所使用的参数。如果用@前缀,则表示参数来自于数据对象(这一般用在非GET操作中)。
isArray,如果为真,则此动作返回的是数组。
此外还有cache,timeout,response type,intercepter,tranform request/response 等多个参数,功能甚多。
为了便于大家操作,下面这些常用动作是系统预定义的。他们是
{ 'get':    {method:'GET'},    //只能接收JSON对象
  'save':   {method:'POST'},
  'query':  {method:'GET', isArray:true},  //只能接收JSON数组
  'remove': {method:'DELETE'},
  'delete': {method:'DELETE'} };
其中save, remove, delete 要加$前缀使用。这使大家进行CRUD操作十分方便(不用预定义动作了)。当然,还可以自定义各种方法和传参数。
需要注意:当$resource服务被调用时,它立即产生对返回对象的空引用,此时视图还不能渲染,直到真正的数据从后端返回时,才会渲染。这使得大多数情况下不用对动作写回调函数。
我们看看用$resource怎么写数据库连接。
service.js
/* Services */

angular.module('phonecatServices', ['ngResource']).
  factory('Phone', function($resource){
      return $resource('/phones', {}, {
    //目前这里一个action也没有写,都用默认的。
  });
});
在定义Phone的工厂函数中返回一个$resource函数,它的第一个参数就是URI,这里写作'/phones',代表http://localhost:3000/phones.注意:web-server已经对所有资源的访问都默认加上了http://localhost:3000前缀,这里不能再加了! $resource的第二个参数是可选的,这里不用,用{}代替。第三个参数是动作集合,在这里自定义各种哦功能http动作,当然,什么都不写,就return $resource('/phones')也可以,这就要使用默认动作了。

参数的传递

在实际中,查询、更新、删除等操作都要涉及参数的传递,参数的传递有两种方法。路径参数传递法和POST对象法。前者适合少量有层次关系的参数的传递,后者适应大量参数的传递。
如果在同一页面查询,通过PhoneBySignal.get({signal: $scope.signal});这样的形式,把同页面中的某个模型值(scope内容)作为某个参数的值放到get里提交,就可以查询了。
如果涉及不同页面的跳转,要用到路径传参数,风格如Phone.get({phoneId: $routeParams.phoneId}。
比如,产品的细节信息一般要放在数据库的单独的Collection里,在后台api.js这设定对这部分数据的访问。例如按id号访问:
exports.findById = function(req, res) {
    var id = req.params.phoneId;      //通过 req.params.phoneId将手机ID传入
    console.log("looking for item-", id);
    
    jb.findOne("phoneDetail", {"id": id}, function(err, obj) {
      if (err) {
        res.send("error:An delete error has occurred - ", err);
        return;
      }
      res.send(obj);
    });
};
这就实现了对某ID手机的查询。然后在web-server.js的路由部分写上转发规则
app.get('/phones/:phoneId', api.findById);
注意,这里要用:分隔(这是按照angular的要求)。

在前端,改写service.js:
/* Services */

angular.module('phonecatServices', ['ngResource'])
    .factory('Phone', function($resource){
      return $resource('/phones/:phoneId', {}, {   //这里加上了:phoneId作为URI的一部分
    });
});

控制器部分写具体的get动作(其实按照angular.js的风格应该是写在service.js的$resource里的,但不知为什么不能使用)。
/* Controllers */

function PhoneListCtrl($scope, Phone) {
  $scope.phones = Phone.query();
  $scope.orderProp = 'age';
}

PhoneListCtrl.$inject = ['$scope', 'Phone'];

function PhoneDetailCtrl($scope, $routeParams, Phone) {
  $scope.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) {
    $scope.mainImageUrl = phone.images[0];
  });

$scope.setImage = function(imageUrl) {
    $scope.mainImageUrl = imageUrl;
  }
}

PhoneDetailCtrl.$inject = ['$scope', '$routeParams', 'Phone']  //注意:这里一定要依赖注入 '$routeParams'服务!

这里用Phone.get({phoneId: $routeParams.phoneId}传递phoneId作为路由参数。
最后在展示页面detail.html中嵌入phone的个属性值就可以了。

但发现Angular用路径传递参数时有个问题:数字参数被当作字符串传递了,导致后台查不到。可以在后台的查询函数中用parseInt()或parseFloat()函数(或统一使用Number()函数)强制转换成数字再查询。

在页面中展现数据

这要用模板和数据绑定配合。
根据app.js的定义,/phones的模板是list.html.我们来看看这个模板里面的组成。除了格式化代码,主要是:
      <ul class="phones">
        <li ng-repeat="phone in phones | filter:query | orderBy:orderProp" class="thumbnail">
          <a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}"></a>
          <a href="#/phones/{{phone.id}}">{{phone.name}}</a>
          <p>{{phone.snippet}}</p>
        </li>
      </ul>
我们可以把phones想像成一个集合,phone是里面的对象,具有id,name,snippet等属性(也可以不叫phone,但必须与下面的绑定名称一致)。那么,phones这个集合是谁定义的呢?是它的controller--PhoneListCtrl定义的。
function PhoneListCtrl($scope, Phone) {
  $scope.phones = Phone.query();
  $scope.orderProp = 'age';
}

PhoneListCtrl.$inject = ['$scope', 'Phone'];

我们看到,phones被$scope定义为Phone的query()函数(方法)的结果集。那Phone又是什么呢?对了,就是我们在service里通过$resource定义的Phone服务。它负责提供数据资源和各种访问方法。

定义好service和controller后,app.js把对/phones的访问定义为调用partials/list.html模板,使用PhoneListCtrl控制器,控制器会负责接收数据(一般是一个json格式的数组)并绑定到Phones中,就可以显示在页面中了。

总的说来,可以通过后台的res.send命令(或直接get)把JSON数据传到angular的控制器中赋值给某个模型(如$scope.data = phoneList.get()),此时模型data的值是个对象{},在HTML页面可以通过{{data.attribute}}的形式显示其某个属性的值。而且可以用ng-repeat="phone in phones" 这样的方式把所有的元素都显示出来。

在页面显示后台返回的字符串

如果后台直接res.send("message"),前端接到后会显示成{"0":"m","1":"e"...}的样式,因为前端一律把后端发来的数据当作对象展示,所以要显示字符串时,需要在后台返回一个对象,对象的某属性是这个字符串。如:
        obj = {"message":"Cid exist!" };
        res.send(obj);
这样,在前端的控制器里:$scope.msg = Service.get() 把API发来的数据输送到某个模型里,然后在页面{{mes.message}}就可以显示message中的内容了。
这种方式也可以显示复杂的对象。

对多个Collection的访问

比如,产品的细节信息一般要放在单独的Collection里,在后台api.js这设定对这部分数据的访问。例如按id号访问:
exports.findById = function(req, res) {
    var id = req.params.phoneId;      //通过 req.params.phoneId将手机ID传入
    console.log("looking for item-", id);
    
    jb.findOne("phoneDetail", {"id": id}, function(err, obj) {
      if (err) {
        res.send("error:An delete error has occurred - ", err);
        return;
      }
      res.send(obj);
    });
};
这就实现了对某ID手机的查询。然后在web-server.js的路由部分写上转发规则
app.get('/phones/:phoneId', api.findById);
注意,这里要用:分隔(这是按照angular的要求)。

在前端,改写service.js:
/* Services */

angular.module('phonecatServices', ['ngResource'])
    .factory('Phone', function($resource){
      return $resource('/phones/:phoneId', {}, {   //这里加上了:phoneId作为URI的一部分
    });
});

控制器部分写具体的get动作(其实按照angular.js的风格应该是写在service.js的$resource里的,但不知为什么不能使用)。
/* Controllers */

function PhoneListCtrl($scope, Phone) {
  $scope.phones = Phone.query();
  $scope.orderProp = 'age';
}

PhoneListCtrl.$inject = ['$scope', 'Phone'];

function PhoneDetailCtrl($scope, $routeParams, Phone) {
  $scope.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) {
    $scope.mainImageUrl = phone.images[0];
  });

$scope.setImage = function(imageUrl) {
    $scope.mainImageUrl = imageUrl;
  }
}

PhoneDetailCtrl.$inject = ['$scope', '$routeParams', 'Phone']

这里用Phone.get({phoneId: $routeParams.phoneId}传递phoneId作为路由参数。
最后在展示页面detail.html中嵌入phone的个属性值就可以了。

下面的update例子展示了多参数的POST传递法。
$scope.updateCustomer = function(customer) {
    var formData = {
       "cid": customer.cid,
       "name": customer.name,
       "sex": customer.sex,
       "race": customer.race,
       "birth_year": customer.birth_year,
       "height_cm": customer.height_cm,
       "height_ft": customer.height_ft,
       "height_in": customer.height_in,
       "email":customer.email,
       "mobile": customer.mobile
    };
    var jdata = JSON.stringify(formData);
    CustomerUpdate.save(jdata);
    $scope.customers = Customer.query();//refresh the list page
  };
对应的web-server.js中要写app.post('/customers/add', api.addCustomer);

实现CRUD

1.条件查询;
angular的查询有几种实现方式:
(1)系统自带的filter,可对页面出现的内容实现全文检索。而且是实时的,非常快。但页面上没有出现的元素就不能检索了。比如,我们在phone list的页面上加入<p>{{phone.carrier}}</p>一行,显示出的手机信息中就有运营商属性的信息,此时在search框中输入carrier的值,页面就会自动刷新为仅包含这个carrier的手机。这种查询方法仅仅适用于“在页面上筛选出包含某个字符串的单元”这种场景。
(2)自定义filter。可对页面出现的内容,根据给定的条件返回特定的内容。如官网教程中的符号替换。也只能用于页面内容。
(3)通过服务来访问后台的API实现查询。这是最重要的一种。比如,我们要在手机列表的页面实现按运营商(carrier)属性来查询,需要做下面几步:
服务器端的查询逻辑。在api.js中增加如下实现按传入的carrier查询的函数:
exports.findByCarrier = function(req, res) {
    var carrier = req.params.phoneCarrier;
    console.log("looking for item-", carrier);
    
    jb.find("phoneList", {"carrier": carrier}, function(err, cursor,count) {
      var objArray = [];
      while (cursor.next()) {
        objArray.push(cursor.object());
      } ;
     res.send(objArray);
    });
};
服务器端的路由:
app.get('/phones', api.findAll);
app.get('/phones/phoneId/:phoneId', api.findById);
app.get('/phones/phoneCarrier/:phoneCarrier', api.findByCarrier);
用子路径来区分不同的访问要求。
此时如果我们在浏览器中输入http://localhost:3000/phones/phoneCarrier/Dell应该会把运营商是Dell的数据显示出来,这说明服务器端准备就绪。

客户端的展示页面。增加如下查询控件:
Please select a carrier:
       <select ng-model="phoneCarrier">
        <option value="None">None</option>
        <option value="AT&amp;T">AT&amp;T</option>
        <option value="Cellular South">Cellular South</option>
        <option value="Dell">Dell</option>
        <option value="Verizon">Verizon</option>
        <option value="T-Mobile">T-Mobile</option>
        <option value="US Cellular">US Cellular</option>
        <option value="Sprint">Sprint</option>
      </select>
      <button ng-click="findCarrier()">List</button>
并且在链接路径做相应更改:
 <ul class="phones">
        <li ng-repeat="phone in phones | filter:query | orderBy:orderProp" class="thumbnail">
          <a href="#/phones/phoneId/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}"></a>
          <a href="#/phones/phoneId/{{phone.id}}">{{phone.name}}</a>
          <p>{{phone.carrier}}</p>
          <p>{{phone.snippet}}</p>
        </li>
 </ul>

客户端partial的路由:涉及到局部视图的载入,也做相应更改:
.when('/phones/phoneId/:phoneId', 
      {
       templateUrl: 'partials/detail.html', 
       controller: PhoneDetailCtrl
      })

最重要的,客户端的服务,为支持三个不同的访问需求,写成同一模块下的三个服务:
/* Services */

var myModule = angular.module('phonecatServices',['ngResource']);
myModule.factory('Phone', function($resource){
  return $resource('/phones');
});

myModule.factory('PhoneById', function($resource){
  return $resource('/phones/phoneId/:phoneId');
});

myModule.factory('PhoneByCarrier', function($resource){
  return $resource('/phones/phoneCarrier/:phoneCarrier');
});
服务的名字尽量起的有意义些,便于后面识别。

对应的控制器部分,所调用的服务名称及依赖注入要做相应的修改:
/* Controllers */

function PhoneListCtrl($scope, Phone, PhoneByCarrier) {
  $scope.phones = Phone.query();
  $scope.orderProp = 'age';
  $scope.findCarrier = function(phoneCarrier) {
    $scope.phones = PhoneByCarrier.query({phoneCarrier: $scope.phoneCarrier});
  };
}
PhoneListCtrl.$inject = ['$scope', 'Phone', 'PhoneByCarrier'];

function PhoneDetailCtrl($scope, $routeParams, Phone, PhoneById) {
  $scope.phone = PhoneById.get({phoneId: $routeParams.phoneId}, function(phone) {
    $scope.mainImageUrl = phone.images[0];
  });

$scope.setImage = function(imageUrl) {
    $scope.mainImageUrl = imageUrl;
  }
}
PhoneDetailCtrl.$inject = ['$scope', '$routeParams', 'Phone', 'PhoneById']
注意:一开始$scope.phones绑定为Phone服务查询的结果集,动作findCarrier是一个函数,运行这个函数导致$scope.phones重新绑定为PhoneByCarrier服务查询的结果集,表现在页面上就是刷新为查询出的结果。
特别注意:服务名字一旦变更,其依赖与注入的地方都要同步变更,这是需要极为仔细的地方,差一点也不行!!!

2.记录新增;
首先,按照RESTful风格,给新增操作起个路径,加在web-server.js中。
app.post('/phones/add', api.addItem);   //注意,这里是POST方法。PUT方法应该也可以,但没有研究过。POST多次会提交多次,这点需要注意。
对应的函数addItem写在api.js中。
exports.addItem = function(req, res) {
   console.log(req.body);
   item = req.body;
   jb.save("phoneList", item, function(err, oid) {
    console.log("New phone added: ", item["_id"]);
    res.send(item);中   //发回新增对象以供调试
   });
};
注意,这里用req.body来获取POST上来的数据(都在请求体里,是一个JSON数据)。
在前端,需要在service中注册phoneAdd服务,指向/phones/add:
myModule.factory('PhoneAdd', function($resource){
  return $resource('/phones/add');
});
然后在controller中注入并使用这个服务:
function PhoneListCtrl($scope, Phone, PhoneByCarrier, PhoneById, PhoneAdd) {

$scope.addPhone = function() {   //在这里定义了addPhone()这个函数。
    var formData = {
       "id": this.phoneId,
       "age":this.phoneAge,
       "carrier": this.phoneCarrier2,
       "imageUrl": "img/phones/motorola-xoom.0.jpg",
       "name": this.phoneName,
       "snippet": this.phoneSnippet
    };
    var jdata = JSON.stringify(formData); 
    PhoneAdd.save(jdata);
    $scope.phones = Phone.query(); //刷新网页
  };

PhoneListCtrl.$inject = ['$scope', 'Phone', 'PhoneByCarrier', 'PhoneById', 'PhoneAdd'];
这里把表单中的数据采集到一个对象中,然后转化为JSON发送。这里的.save方法是系统自带的,默认为POST方法。
上述数据来自于表单,所以还需要在视图页面写一个表单。
<form name="form-add" id="addPhoneForm" ng-submit="addPhone()">
        <fieldset>
          <left>New Phone</left>
            <div class="control-group">
                <left>id:<input type="text" ng-model="phoneId" size=50 required></left>
                <left>age:<input type="text" ng-model="phoneAge" size=50 required></left>
                <left>carrier:<input type="text" ng-model="phoneCarrier2" size=50 required></left>
                <left>imgUrl:<input type="text" placeholder="motorola-xoom.jpg" ng-model="phoneImgUrl"></left>
                <left>name:<input type="text" ng-model="phoneName" size=50 required></left>
                <left>snippet:<input type="text" ng-model="phoneSnippet" size=50 required></left>
            </div>
        </fieldset>
        <left><button type="submit">Add</button></left>
      </form>
每个表单元素对应一个model,"phoneCarrier2"命名是为避免和上面选择运营商中的"phoneCarrier"重名。否则会绑定到同一元素上。
只要ng-submit="addPhone()"就会提交到addPhone()这个函数中(在控制器里)。

3.记录更新;
记录的更新与记录的增加差不多,只是在后端用update方法。
首先,在web-server中增加一行app.post('/phones/update', api.updateItem);这里也是用POST方法。
在api.js中写updateItem函数:
exports.updateItem = function(req, res) {
    var item = req.body;         //先用req.body接收整个JSON串     
    var id = item.id;            //再把每个属性值放到一个个变量中
    var age = item.age;
    var carrier = item.carrier;
    var name = item.name;
    var snippet = item.snippet;
    console.log('Updating phone: ', id);
    jb.update("phoneList",       //使用update方法来更新
     {'id': id,                  //这里用id作为标识,注意:所有同id的记录都会被同步更新。
     '$set': 
       {"age": age, 
        "carrier": carrier, 
        "name": name,
        "snippet": snippet
       }
     }, 
    function(err, count) {
        console.log("Update " + count + " phones");  
    });  
    jb.findOne("phoneList", {"id":id}, function(err,obj) {
      console.log(obj);
    });      
    res.send(id);                 //返回id用于调试
};

前端同样要写service:
myModule.factory('PhoneUpdate', function($resource){
  return $resource('/phones/update');
});
同样,在控制器中注入并使用这个服务。
function PhoneListCtrl($scope, Phone, PhoneByCarrier, PhoneById, 
  PhoneAdd, PhoneUpdate) {
  ......
  $scope.updatePhone = function() {
    var formData = {
       "id": this.uphoneId,
       "age":this.uphoneAge,
       "carrier": this.uphoneCarrier,
       "name": this.uphoneName,
       "snippet": this.uphoneSnippet
    };
    var jdata = JSON.stringify(formData);
    PhoneUpdate.save(jdata);       //这里同样使用了系统自带的save方法,这也会POST
    $scope.phones = Phone.query(); //刷新页面
  };
当然,视图中也要加上update用的表单。

4.记录删除。web-server中增加相应路由器app.delete('/phones/delete/:phoneId', api.deleteItem);
凡使用:参数 的写法,都是要把参数传递过去。因为要基于ID号删除,所以只传phoneId这一个参数。
在api中写对应的delete函数:
exports.deleteItem = function(req, res) {
   var id = req.params.phoneId;
   console.log('Deleting phone: '+ id);
   jb.update("phoneList", {"id":id, '$dropall': true}, function(err, count) {
        console.log("Delete " + count + " phones");    
   });   
  res.send(id);   
};
注意,使用传参数的方法,要用req.params.phoneId这样的方法获取传来的参数。
前端同样要注册服务:
myModule.factory('PhoneDelete', function($resource){
  return $resource('/phones/delete/:phoneId');
});
然后在控制器中注册并调用:
function PhoneListCtrl($scope, Phone, PhoneByCarrier, PhoneById, 
  PhoneAdd, PhoneUpdate, PhoneDelete) {
......

$scope.deletePhone = function() {
    PhoneDelete.remove({phoneId: $scope.dphoneId});
    $scope.phones = Phone.query(); //refresh the list page
  };
  注意,angular提供了remove和delete两个删除方法,但我测试中只有remove起作用。
每删除一个编号,所有同样ID的记录都被删除了。

参数多的情况下,要用POST方法把参数(一般是数组对象)发送到后端。例如:
【控制器】
$scope.deleteRecords = function() {
      var deleteList = [];             //新建一个空数组

var objPara ={                  //新建一个空对象结构
        "cid": null,
        "tid": null
      };

if (confirm($scope.tText.sure) == true) {
   
      angular.forEach($scope.records, function(record) {  //用angular.forEach循环填充对象
        if (record.marked) {
           objPara.cid = record.cid;
           objPara.tid = record.tid;
           deleteList.push(objPara);   //将填充完的对象依次推入数组,连在一起
           //每循环完一次,对象要重置
           objPara ={
             "cid": null,
             "tid": null
           };
        }//end of if
      });//end of foreach loop
  
     var deleteList2 = JSON.stringify(deleteList)  //将数组对象转换为JSON字符串以便发送
     RecordDelete.save(deleteList2); 使用save方法(对应$resource的POST方法)

【服务】
myModule.factory('RecordDelete', function($resource){
  return $resource('/data/delete');
});

【web-server.js】
app.post('/data/delete', api.deleteTestRecord);

【api.js】
exports.deleteTestRecord = function(req, res) {   
  var deleteList = req.body;            //使用req.body方法接受POST来的数据对象
  console.log("tid=", deleteList);
  
  var total = 0, i=0;
  for (i; i<deleteList.length; i++) {
    cid = deleteList[i].cid;
    tid = Number(deleteList[i].tid);  //将tid转化为数字
    console.log("cid:", cid, "tid:",tid);

//update中不使用$and,仅仅依次排列个条件即可。
    jb.update("TestRecord", {"cid":cid, "tid":tid, '$dropall' : true});
    total +=1;
  };//end of for loop
    
  console.log("Delete total " + total + " records!")
  
  res.send(deleteList);   
};

说明:对多个Collection的关联查询和同一集合内多个参数的联合使用请参见我的数据库文章。

总结:
angular靠service, controller和web-server的呼应组成一条链条来完成CRUD。这种方式简洁而灵活。
view-> controller-> service-> web-server-> api
注意:web-server, service和controller三者在路径拼写和参数名称上必须完全一致!稍有差异就无法实现功能!而服务的注册和注入也要互相呼应,保持一致。
命名极为重要,拼写极为重要!!很多BUG来源于拼写不一致。

angularJS的功能点

数据绑定

参见signal示例。
所有要在页面动态显示的东西都放在model里,起个名字。如 <input ng-model="signal">,把输入的内容绑定到模型signal中。
在页面可以实时显示这个模型的内容,如{{signal}}放在哪里,哪里就会实时显示signal的内容。
在控制器里可以调用和修改这个model。使用$scope.signal就可以操作这个模型。如PhoneBySignal.get({signal: $scope.signal})就把signal的内容当作参数signal的值发送到后台。所以说是双向绑定。
可以把后端发送来的数据放到model里使前端能够自动显示和更新。如$scope.result = PhoneBySignal.get({signal: $scope.signal})就把后端发来的内容放到result里,只要前端有{{result}}的地方都会被自动替换为result的内容。
甚至可以使用$scope.content = PhoneForm.save(jdata);这样的写法一边发送数据,一边把结果绑定到content里。

过滤器(Filter)

过滤器负责格式化数据,把它转化为某种类型。通用写法是 {{ expression | filter }},就是在显示模型的地方加上过滤器。
如: {{data | number:2}}把数据显示为2位小数的数字。
过滤器可以链式使用,如: {{ expression | filter1 | filter2 }}
最常用的过滤器是排序,如{{  myArray | orderBy:'timestamp':true}},就是按时间戳排序。
另外常用的是格式化时间/货币等本地化应用的过滤器,如:
 Date: {{ '2012-04-01' | date:'fullDate' }} <br>
 Currency: {{ 123456 | currency }} <br>
 Number: {{ 98765.4321 | number }} <br>
当然,可以写自己的自定义filter,这要写工厂函数。下面的代码定义了一个reverse过滤器:
angular.module('MyReverseModule', []).
  filter('reverse', function() {
    return function(input, uppercase) {
      var out = "";
      for (var i = 0; i < input.length; i++) {
        out = input.charAt(i) + out;
      }
      // conditional based on optional argument
      if (uppercase) {
        out = out.toUpperCase();
      }
      return out;
    }
  });

指令(Directive)

Angularjs的核心思想就是“复用”,它的“复用”体现在"directive"上。Directive既是angularjs的核心,也是它的重点、难点和杀手级特性。简单的说,directive可以是一个自定义的html标签、或者属性、或者注释,它的背后是一些函数,可以对所在的html标签进行增强,比如修改html的dom内容,增强它的功能(如用第三方js插件包装它)。angularjs提供的directive。有的是绑定事件(如ng-click,ng-submit),有的是控制流程(如ng-repeat)。这种方式我非常喜欢,简单直接,可读性又很好。最革命性的意义在于,你可以自己编写Directive,这使得HTML可以被扩展为一种DSL语言。当然,编写Directive比较复杂,需要理解它的内部原理才能定义出自己的directive。本文暂不讨论指令的编写。只是表单部分引用了integer和smart float两个指令的源码,另外官方指南有显示当前时间的指令源码。
自定义指令一般放到app/js/directives.js里,写法
var app = angular.module('phonecatDirectives', []);
app.directive('integer', function() {
  ......
});
在index.html中通过加入<script src="js/directives.js"></script>来引用。

表单

表单基础

基本的表单是在视图页面中使用FORM来实现的。
 <form novalidate class="simple-form">
        Name: <input type="text" ng-model="user.name" /><br />
        E-mail: <input type="email" ng-model="user.email" /><br />
        Gender: <input type="radio" ng-model="user.gender" value="male" />male
        <input type="radio" ng-model="user.gender" value="female" />female<br />
        <button ng-click="reset()">RESET</button>
        <button ng-click="update(user)">SAVE</button>
</form>
<pre>form = {{user | json}}</pre>
这里定义了Name, E-mail,Gender三个元素,输入类型分别是text, email和radio,分别绑定到user.name,user.email,user.gender模型上。用RESET按钮来重置,用SAVE按钮来保存。使用{{user | json}}可以把user的内容以JSON的形式显示出来。
另外需要在控制器中写reset()和update(user)这两个行为函数。
function Controller($scope) {
  $scope.master= {};
 
  $scope.update = function(user) {
    $scope.master= angular.copy(user);
  };
 
  $scope.reset = function() {
    $scope.user = angular.copy($scope.master);
  };
 
  $scope.reset();
}

angular里可以使用基础HTML的各种表单元素,如input, select, textarea,就input而言,可以使用text, number, url, email, radio, checkbox这些类型。这基本够用了。当然,如果你想自定义表单元素或输入类型,需要写directive。angular开发指南上提供了一个“可编辑输入框”的例子。
angular使用ngForm指令使得表单可以嵌套,这对于使用ngRepeat指令生成的表单进行验证时十分有用。
表单的CSS类:
ng-valid 表单有效(指所有元素均符合规则)
ng-invalid 表单无效
ng-pristine 表单尚未被填写
ng-dirty 表单已被填写

表单的动作有两个
ngSubmit 单纯的提交动作,点击提交按钮会触发onSubmit事件。写法是:
 <form ng-submit="submit()" ng-controller="Ctrl">
      Enter text and hit enter:
      <input type="text" ng-model="text" name="text" />  //这显示输入框
      <input type="submit" id="submit" value="Submit" /> //这显示按钮Submit
      <pre>list={{list}}</pre>
 </form>

ngClick 更加灵活的点击命令,可以触发多种行为函数。如下面的例子触发了”自动加1“行为:
 <button ng-click="count = count + 1" ng-init="count=0">
      Increment
 </button>
    count: {{count}}

表单验证

这里客户端验证仅指客户端验证,不涉及服务器端验证(那就不是angualar的范围了)。常用输入类型如text, number, url, email已经提供一定的验证功能,系统自带的验证还有required, pattern, minlength, maxlength, min, max这几种。要自定义验证需要写directive。
下面的范例显示了各种表单元素及基础验证,配合CSS的使用使表单颜色相应变化,并在未通过验证的情况下禁用了提交功能。
//form-test.html
<div class="container-fluid">
  <div class="row-fluid">
    <form name="form" class="css-form" novalidate>
        Name:
          <input type="text" ng-model="user.name" name="uName" required /><br />

E-mail:
          <input type="email" ng-model="user.email" name="uEmail" required/><br />
        <div ng-show="form.uEmail.$dirty && form.uEmail.$invalid">Invalid:
          <span ng-show="form.uEmail.$error.required">Tell us your email.</span>
          <span ng-show="form.uEmail.$error.email">This is not a valid email.</span>
        </div>

Age: <input type="number" ng-model="user.age" name="uAge"  min="0" max="100" integer required/>
        <br />
         <div ng-show="form.uAge.$dirty && form.uAge.$invalid">Invalid:
          <span ng-show="form.uAge.$error.required">Tell us your age.</span>
          <span ng-show="form.uAge.$error.number">This is not a valid number.</span>
          <span ng-show="form.uAge.$error.integer">This is not valid integer!</span>
          <span ng-show="form.uAge.$error.max || form.uAge.$error.min">
            The age must be in range 0 to 100!</span>
        </div>

Website: <input type="url" ng-model="user.url" name="uUrl" required/><br />
         <div ng-show="form.uUrl.$dirty && form.uUrl.$invalid">Invalid:
          <span ng-show="form.uUrl.$error.required">Tell us your website.</span>
          <span ng-show="form.uUrl.$error.url">This is not a valid url.</span>
        </div>

Gender: <input type="radio" ng-model="user.gender" value="male" />male
        <input type="radio" ng-model="user.gender" value="female" />female<br />

<div> Height (float):
          <input type="text" ng-model="user.height" name="uHeight" smart-float /><br />
          <span ng-show="form.uheight.$error.float">
            This is not a valid float number!</span>
        </div>

<input type="checkbox" ng-model="user.agree" name="userAgree" required />
        I agree: <input ng-show="user.agree" type="text" ng-model="user.agreeSign"
                  required /><br />
        <div ng-show="!user.agree || !user.agreeSign">Please agree and sign.</div>
    
        <button ng-click="reset()" ng-disabled="isUnchanged(user)">RESET</button>
        <button ng-click="update(user)"
                ng-disabled="form.$invalid || isUnchanged(user)">SAVE</button>
      </form>

<p>The form content received by server is:</p>
      <p><textline ng-model="content">{{content}}</p>
  </div>
</div>
在CSS中加入以下css-form类的代码(以改变背景颜色):
.css-form input.ng-invalid.ng-dirty {
   background-color: #FA787E;
}
    
.css-form input.ng-valid.ng-dirty {
    background-color: #78FA89;
}

在控制器代码中加入:
function formCtrl($scope, PhoneForm) {
  $scope.update = function(user) {
    var formData = {
       "name": user.name,
       "email":user.email,
       "age": user.age,
       "url": user.url,
       "gender": user.gender,
       "Height": user.height,
       "agree": user.agree,
       "sign": user.agreeSign
    };
    var jdata = JSON.stringify(formData);
     $scope.content = PhoneForm.save(jdata);
  }
};
formCtrl.$inject = ['$scope', 'PhoneForm'];

因为要以来PhoneForm服务,所以在服务代码中加入:
myModule.factory('PhoneForm', function($resource){
  return $resource('/phones/form');
});

相应的web-server.js中要有路由:
app.post('/phones/form', api.formTest);

api的对应函数:
 exports.formTest = function(req, res) {   
   content = req.body;
   console.log("Form received!")
   res.send(content);
};

国际化和本地化(I18n and L10n )

做一个国际化的APP包括两个方面的工作:字符串的多语言化和特定表示(如数字/日期/时间)的转换。

字符串的转换可以使用多语言专用解决方案,如i18next,ICU,gettext等,也可以简单地使用JSON文件。其思路是将所有页面出现的字符串都作为动态的MODEL,其内容来自于JSON,而JSON是按语言分的做成好几套,每选择一种语言,就调用一套JSON,将模型中的数据套到文本/标签/按钮/链接等所有需要出现字符串的地方。这样HTML模板是共同的,任何人只要翻译一套JSON,就可以拥有一套语言的界面了。
示例如下:
在partial中做一个i18n.html模板:
<div class="container-fluid">
  <div class="row-fluid">
    
    <div>
    Language 
    <select ng-model="lang">
        <option value="en-US" selected="selected">English</option>
        <option value="zh-CN">Chinese</option>
    </select>
      <button ng-click="localization()">Go</button>
    </div>

<p>The language string form JSON:</p>

{{tText.label}}
      <button>{{tText.button}}</button>
      <a href="http://www.baidu.com">{{tText.link}}</a>
      <p><textarea>{{tText.message}}</textarea></p>

</div>
不要忘了在app.js中建立其路由:
        .when('/phones/i18n', 
        {
          templateUrl: 'partials/i18n.html',   
          controller: i18nCtrl
        })
在控制器中写i18nCtrl:
function i18nCtrl($scope, LangString) {
  $scope.localization = function() {
    $scope.tText = LangString.get({langId: $scope.lang});
  };
    $scope.tText = LangString.get({langId:'en-US'});
};
i18nCtrl.$inject = ['$scope', 'LangString'];
这里依赖服务LangString。所以要写相应服务:
myModule.factory('LangString', function($resource){
  return $resource('lang/:langId.json');
});
在新建的/lang文件夹中放两个语言文件en-US.json和zh-CN.json,里面有标签-字符串的映射。
en-US.json:
{
  "label": "label",
  "button": "butto",
  "link": "baidu",
  "message": "This is a \"quotation\",\nThis is a single 'quotation',\n This is a solidus\\,\n This is a reverse solidus\/,\nanother line"
}
zh-CN.json:
{
  "label": "标签",
  "button": "按钮",
  "link": "百度",
  "message": "这是一个\"双引号\",\n这是一个'单引号',\n这是一个斜线\\,\n这是一个反斜线\/,\n另起一行"
}
注意:使用双引号、斜线和反斜线要用\来转义,单引号不用转义,换行用\n来表示。

再说特定表示,angular提供了datetime, number 和 currency 三大过滤器负责日期/数字/时间的转换,另外提供了ngPluralize指令负责提供复数显示。而这些都基于$locale服务。
使用某locale,如德国的,要在首页下方加入如下代码:   <script src="locale/angular-locale_de-de.js"></script>,则所有页面出现日期/数字和货币格式的都会被自动转换成德国格式了。当然,要事先把这些locale文件放到app/locale文件夹里(从http://code.angularjs.org/1.0.2/i18n/下载)。那么,如何实现locale文件的动态加载呢?

一般的使用习惯,程序启动后先进入首页,首页默认是英文的,在首页上可以选择语言,如果选择了某种语言,则自动进入该语言的首页,加载该语言的locale文件,同时,用<body ng-controller="EnglishCtrl">可以在全局指定使用某个语言控制器,如EnglishCtrl,里面是 $scope.tText = LangString.get({langId:'en-US'});若ChineseCtrl,则里面是 $scope.tText = LangString.get({langId:'zh-CN'});用这种方法,为每个语言做一个首页,一个语言控制器,然后在首页加上链接,就可以实现多语言了。

前端框架-Angular.js相关推荐

  1. 前端框架 Angular 11.0.0 正式发布,不再支持 IE 9 、10

    前端框架 Angular 11.0.0 正式发布. Angular 11.0.0 将 TypeScript 升级到 4.0, 对 TypeScript 3.9 不再支持. 放弃了对 IE 9 .10 ...

  2. angular框架简介基础与使用(全文2w8字)前端框架angular

    写在前面 本文的所有内容,可以在我的博客上看到,下面是地址.建议去博客看,因为csdn的这篇图片我没上传. 可以转载,但请注明出处 我的博客-点击跳转 https://numbrun.gitee.io ...

  3. Web 前端框架 Angular

    Angular 详细介绍 Angular 是一款十分流行且好用的 Web 前端框架,目前由 Google 维护.这个条目收录的是 Angular 2 及其后面的版本.由于官方已将 Angular 2 ...

  4. 前端框架React Js入门教程【转】

    现在最热门的前端框架有AngularJS.React.Bootstrap等.自从接触了ReactJS,ReactJs的虚拟DOM(Virtual DOM)和组件化的开发深深的吸引了我,下面来跟我一起领 ...

  5. 终于找到了梦想中的前端框架 --- vue.js

    前面小半年,业余时间研究了超有前途的前端"框架"新秀React,无奈前端我本就是半吊子,没什么基础,再加上React大量应用FP(函数式编程),想把React用好还得熟悉大量第三方 ...

  6. 《前端框架Vue.js的解读利器》

    目前个人所在的项目组团队是平台团队,主要是负责一些前端界面的配置和开发工作,所以和前端打交道还是有一些时间的. 前一些时间看到平台的同事在微信群里推荐图灵的<Vue.js技术内幕>,这一下 ...

  7. 《前端框架Vue.js》

    作为一枚计算机专业的研一新生,在本科阶段我系统性的学过计算机相关的课程和实训,在学习Android这一部分时,我第一次感觉到了app的乐趣,所以便开始对JAVA前端开发感兴趣了.随便大学已经毕业,但保 ...

  8. 常用前端框架Angular和React的一些认识

    为什么要用AngularJs? 要了解为什么使用AngularJS首先就要接受它的思想: 首先,angularJS借助了传统MVC的架构模式(model模型  view视图  controller控制 ...

  9. 全面掌握前端框架Vue.js

    整理自菜鸟教程 Vue.js简介 Vue.js(读音 /vjuː/, 类似于 view) 是一套构建用户界面的渐进式框架. Vue 只关注视图层, 采用自底向上增量开发的设计. Vue 的目标是通过尽 ...

最新文章

  1. 在VM虚拟机中 CentOS7安装VMware Tools(超级详解)
  2. 计算机制图 教学大纲,《计算机制图》课程教学大纲.doc
  3. 设计模式复习-访问者模式
  4. sublime配置python运行环境
  5. 微信商户平台的“企业付款到用户” 产品功能被隐藏起来了。。
  6. 每个极客都应该知道的Linux命令
  7. 一种改进CA-CFAR算法及其MATLAB编程实现,论文仿真——《基于LFMCW雷达多目标检测的CA-CFAR改进算法》
  8. Node.js meitulu图片批量下载爬虫1.04版
  9. ThinkPHP5 引入 Go AOP
  10. 深入理解计算机系统02——信息的表示与处理
  11. 计算机网络技术实训 实训总结,计算机网络技术实训报告总结.docx
  12. 服务器装win10性能怎样,普通Win10越升越卡?Win10专业工作站版了解下
  13. 源自神话的写作要义之英雄
  14. FreeBSD常用命令110条
  15. (Java)全限定类名和非限定类名的区别
  16. 联想电脑如何取消触屏-thinkpad X230
  17. 除铜树脂CH-90NA、电镀废水回收铜工艺
  18. 【多人在线游戏架构实战-基于C++的分布式游戏编程】开篇
  19. 大数据项目实施风险(一)
  20. intel X3100 打开3D特效

热门文章

  1. Ubuntu18.04安装ax200网卡驱动以及更新内核
  2. 兄弟hl3150cdn打印测试页6_打印性能测试:LED高效输出_兄弟 3150CDN_办公打印评测试用-中关村在线...
  3. 一文掌握java对内存空间的划分
  4. 基于微信小程序的同城家政服务小程序
  5. 大学生用什么软件学c语言,当代大学生必须的几款APP
  6. 神经网络学习小记录52——Pytorch搭建孪生神经网络(Siamese network)比较图片相似性
  7. 猴子搬香蕉Java实现,儿童编程游戏CodeMonkey,让你的小猴子不停地吃香蕉
  8. 怎么把matlab代码输出到word,MATLAB图形输出到word中
  9. 顺丰职级分成4级_【顺丰速运内部职级和薪资水平是怎么样的?】-看准网
  10. airflow使用macros