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中:
The model value is: {{mydata}}
这句使用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:
Bread
Rice
My favorate food is {{food}} !
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中的控制器继承减少类似控制器的代码,是基于作用域的继承的。让我们看下面这个例子:
Good {{timeOfDay}}, {{name}}!
Good {{timeOfDay}}, {{name}}!
Good {{timeOfDay}}, {{name}}!
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:
Name updated: {{updated}} times.
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都是放在 元素中的,但是也能放在其他元素中,比如
。
在调试器中检测作用域:
在你要查看的元素上右键单击,选择菜单中的“审查元素”。你应该会看到浏览器的调试器,并且你选择的元素高亮了显示了。
作用域事件的传递 作用域中的事件传递是和DOM事件传递类似的。事件可以广播给子作用域或者传递给父作用域。
index.html:
Root scope
MyEvent count: {{count}}
$emit('MyEvent')
$broadcast('MyEvent')
Middle scope MyEvent count: {{count}}
Leaf scope MyEvent count: {{count}}
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:
Another module
{{ name }}
以上代码先后加载了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中使用:
将服务注入到控制器中 因为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.我们来看看这个模板里面的组成。除了格式化代码,主要是:
我们可以把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的页面上加入
{{phone.carrier}}
一行,显示出的手机信息中就有运营商属性的信息,此时在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:
None
AT&T
Cellular South
Dell
Verizon
T-Mobile
US Cellular
Sprint
List
并且在链接路径做相应更改:
客户端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方法。
上述数据来自于表单,所以还需要在视图页面写一个表单。
每个表单元素对应一个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
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里,起个名字。如 ,把输入的内容绑定到模型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' }}
Currency: {{ 123456 | currency }}
Number: {{ 98765.4321 | number }}
当然,可以写自己的自定义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中通过加入来引用。
表单
表单基础 基本的表单是在视图页面中使用FORM来实现的。
form = {{user | json}}
这里定义了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事件。写法是:
ngClick 更加灵活的点击命令,可以触发多种行为函数。如下面的例子触发了”自动加1“行为:
Increment
count: {{count}}
表单验证 这里客户端验证仅指客户端验证,不涉及服务器端验证(那就不是angualar的范围了)。常用输入类型如text, number, url, email已经提供一定的验证功能,系统自带的验证还有required, pattern, minlength, maxlength, min, max这几种。要自定义验证需要写directive。
下面的范例显示了各种表单元素及基础验证,配合CSS的使用使表单颜色相应变化,并在未通过验证的情况下禁用了提交功能。
//form-test.html
The form content received by server is:
{{content}}
在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模板:
Language
English
Chinese
Go
The language string form JSON:
{{tText.label}}
{{tText.button}}
{{tText.link}}
不要忘了在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,如德国的,要在首页下方加入如下代码: ,则所有页面出现日期/数字和货币格式的都会被自动转换成德国格式了。当然,要事先把这些locale文件放到app/locale文件夹里(从http://code.angularjs.org/1.0.2/i18n/下载)。那么,如何实现locale文件的动态加载呢?
一般的使用习惯,程序启动后先进入首页,首页默认是英文的,在首页上可以选择语言,如果选择了某种语言,则自动进入该语言的首页,加载该语言的locale文件,同时,用可以在全局指定使用某个语言控制器,如EnglishCtrl,里面是 $scope.tText = LangString.get({langId:'en-US'});若ChineseCtrl,则里面是 $scope.tText = LangString.get({langId:'zh-CN'});用这种方法,为每个语言做一个首页,一个语言控制器,然后在首页加上链接,就可以实现多语言了。