止觀


  • 首页

  • 归档

  • 标签

AngularJS文件上传

发表于 2014-11-15   |   分类于 Practice   |  

jsfiddle地址

http://jsfiddle.net/JeJenny/ZG9re/

html代码

1
2
3
4
<div ng-controller = "myCtrl">
<input type="file" file-model="myFile"/>
<button ng-click="uploadFile()">upload me</button>
</div>

javascript代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
var myApp = angular.module('myApp', []);
myApp.directive('fileModel', ['$parse', function ($parse) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
var model = $parse(attrs.fileModel);
var modelSetter = model.assign;
element.bind('change', function(){
scope.$apply(function(){
modelSetter(scope, element[0].files[0]);
});
});
}
};
}]);
myApp.service('fileUpload', ['$http', function ($http) {
this.uploadFileToUrl = function(file, uploadUrl){
var fd = new FormData();
fd.append('file', file);
$http.post(uploadUrl, fd, {
transformRequest: angular.identity,
headers: {'Content-Type': undefined}
})
.success(function(){
})
.error(function(){
});
}
}]);
myApp.controller('myCtrl', ['$scope', 'fileUpload', function($scope, fileUpload){
$scope.uploadFile = function(){
var file = $scope.myFile;
console.log('file is ' );
console.dir(file);
var uploadUrl = "/fileUpload";
fileUpload.uploadFileToUrl(file, uploadUrl);
};
}]);

如何使用underscore.js做正序和倒虚排序

发表于 2014-11-14   |   分类于 StackOverflow   |  

StackOverflow地址

http://stackoverflow.com/questions/15137948/how-can-i-do-an-asc-and-desc-sort-using-underscore-js#

问题描述

楼主正在用underscorejs来对我的json数据进行排序。现在需要对数据进行正序和倒序的排序。我查了文档,但是没有找到什么相关内容。怎么搞定这个问题?

最佳解答

你可以用_.sortby,它始终返回一个正序的排序。

1
2
3
_.sortBy([2, 3, 1], function(num) {
return num;
}); // [1, 2, 3]

你也可以使用.reverse方法来得到倒序排序。

1
2
3
4
5
6
var array = _.sortBy([2, 3, 1], function(num) {
return num;
});
console.log(array); // [1, 2, 3]
console.log(array.reverse()); // [3, 2, 1]

或者在处理数字时给数字加一个负号来得到倒序的列表。

1
2
3
_.sortBy([-3, -2, 2, 3, 1, 0, -1], function(num) {
return -num;
}); // [3, 2, 1, 0, -1, -2, -3]

再深入解释一下,.sortby使用的是内建的.sort([handler])

1
2
3
4
5
6
7
8
9
// Default is ascending:
[2, 3, 1].sort(); // [1, 2, 3]
// But can be descending if you provide a sort handler:
[2, 3, 1].sort(function(a, b) {
// a = current item in array
// b = next item in array
return b - a;
});

AngularJS:angular-route与angular-ui-route的区别

发表于 2014-10-20   |   分类于 StackOverflow   |  

StackOverflow地址

http://stackoverflow.com/questions/21023763/angularjs-difference-between-angular-route-and-angular-ui-router

问题描述

楼主是AngularJS的新手。我觉得Angular有点意思所以准备在我的一个大项目中使用一下。我正在寻找合适的模块。
请问ngRoute(angular-route.js)与ui-route(angular-ui-router.js)这两个模块具体有什么区别。
在许多介绍文章中,当使用了ngRoute时,route被配置为$routeProvider。当使用ui-route时,route被配置为$stateProvider和urlRouterProvider。
这让我觉得有点不能理解。哪个模块在管理功能和扩展性上更好呢?

最佳解答

ui-router是一个第三方组件。它十分强大。它支持ngRoute的所有特性且不止于此。
下面是一些使用ui-router替换掉ngRoute的理由:

  • ui-route支持view嵌套和多命名视图。这在大型APP中非常有用,因为可能会出现继承其他段落的页面。
  • ui-route可以让你在基于状态名称的状态间进行强类型连接。在一处替换url会在其余定义了相同的ui-sref的地方进行同步更新。如果你要在大型APP中变更超链接,这会非常有用。
  • 它还具备装饰器的概念。可以用来允许根据尝试访问的URL动态创建路由。这意味着你不需要手动指定所有路由。
  • states可以让你映射和访问不同状态的不同信息,你可以通过$stateParams轻松地在状态之间传递信息。
  • 你可以很方便的通过在带有$state(由ui-route提供)的模板中确认当前是否出于某个状态或父状态来调整UI元素(高亮当前状态的导航栏)。可以通过设置$rootScope在执行中暴露它。
  • 本质上,ui-route是ngRouter的增强版,但是背后的实现不尽相同。这些增强的功能在大型应用中非常实用。

更多信息:

  • Github: https://github.com/angular-ui/ui-router
  • 文档:
    • API 参考: http://angular-ui.github.io/ui-router/site/#/api
    • 指引:https://github.com/angular-ui/ui-router/wiki
  • 常见问题:https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions
  • 示例项目:http://angular-ui.github.io/ui-router/sample/#/

在定义一个directive时,'controller', 'link'和'compile'的区别

发表于 2014-09-06   |   分类于 StackOverflow   |  

StackOverflow地址

http://stackoverflow.com/questions/12546945/difference-between-the-controller-link-and-compile-functions-when-definin

问题描述

一些地方在directive逻辑中使用了controller,有些则用了link。在angular主页上的tab例子中使用了controller,但是其他例子使用了link。这两者究竟有什么区别?

最佳解答

层主更全面地阐述了此问题并增加了compile功能的解释。

  • compile方法用于模板的DOM操作(例如:操作tElement = template element),之后将适用于所有关联对应directive的克隆模板的DOM操作。(如果你需要使用到link且定义了一个compile方法,此方法的返回值必须为link方法,因为如果定义了compile,link属性会被忽略)。
  • link方法一般情况下用于注册监听的回调(例如:$watch在scope中的表达式),又或者是更新DOM(例如:操作iElement = individual instance element)。它将会在模板被克隆后执行。例如:在一个<li ng-repeat...>内,针对这个特定的li元素,link方法在模板(tElement)克隆后被执行后,$watch允许一个directive得到一个scope属性变更的推送(每个scope均与对应的示例相关联),可以让directive针对DOM写入最新的值。
  • controller方法必须被用于其他directive需要与这个directive交互的情况下。例如:在angular官网上,pane directive需要将它自己添加到被tab directive维护下的scope中,此后tabs directive需要定义一个controller方法(想想API的调用方式)使pane的directive可以访问和调用。

关于更深入的关于tabs和pane的directive的解释,以及为什么tab的directive在它的controller中使用this(而不是$scope)来创建方法,请参阅‘this’ vs $scope in AngularJS controllers。

总的来说,你可以在任何一个directive的controller或者link方法内使用方法,$watches,等等。controller会先执行,这有时候会很重要(可以参考这个代码片段,其中含有两个嵌套的directive,并执行其中的controller和link方法并将结果记入Log)。楼主可能需要的是将原本针对scope操作的的方法赛到某个controller内然后保证框架剩余部分的一致性。

快乐Node码农的十个习惯

发表于 2014-04-23   |   分类于 Practice   |  

转载自:http://www.infoq.com/cn/articles/node.js-habits
从问世到现在将近20年,JavaScript一直缺乏其它有吸引力的编程语言,比如Python和Ruby,的很多优点:命令行界面,REPL,包管理器,以及组织良好的开源社区。感谢Node.js和npm,现如今的JavaScript鸟枪换炮了。Web开发者有了强大的新工具,接下来就看他们的想象力了。

下面这个提示和技巧清单,能让你和你的node程序保持快乐。

用npm init开始新项目

npm有个init命令,可以引导你完成创建package.json文件的过程。即便你非常熟悉package.json和它的属性,也可以把npm init当作将你的新程序或模块导入正轨的简便办法。 它可以聪明地为你设置默认值,比如通过上层目录的名称推断模块名,从~/.npmrc中读取创作者的信息,以及用你的git设置确定代码库。

1
2
3
mkdir my-node-app
cd my-node-app
npm init

声明所有依赖项

在将模块安装到项目本地时坚持使用–save (或 –save-dev)是个好习惯。这些选项会将指定的模块添加到package.json的dependencies(或devDependencies)清单中,并使用合理的默认semver范围。

1
npm install domready --save

注意,现在npm使用插入符风格的semver范围:

1
2
3
"dependencies": {
"domready": "^1.0.4"
}

指定启动脚本

在package.json中设定scripts.start,你就可以在命令行中用npm start启动程序了。这个非常方便,因为克隆了你的程序的其他node开发人员不用猜就能轻松运行它。

额外奖励:如果在package.json中定义了scripts.start,你就不需要Procfile了(Heroku平台用Procfile来声明在你程序的dynos上运行什么命令)。使用npm start会自动创建一个作为web进程的Procfile。

这里有个启动脚本示例:

1
2
3
"scripts": {
"start": "node index.js"
}

指定测试脚本

就像团队中的所有人都应该可以运行程序一样,他们也应该可以测试它。package.json中的scripts.test就是用来指定运行测试套件的脚本的。如果你用mocha之类的东西运行测试,一定要确保把它包含在package.json里的devDependencies中,并且指向安装在你项目本地的文件,而不是全局安装的mocha:

1
2
3
"scripts": {
"test": "mocha"
}

不要把依赖项放在源码的版本控制中

很多node程序使用的npm模块带有C语言写的依赖项,比如bson、ws和hiredis,这些依赖项必须在Heroku的64位Linux架构下进行编译。编译过程可能非常耗时。为了让构建过程尽可能的快,Heroku的node buildpack在下载和编译完依赖项后会缓存它们,以便在后续部署中重用。这个缓存是为了降低网络流量并减少编译次数。

忽略node_modules目录也是模块创作者推荐的npm实践。应用程序和模块之间少了一个区别!

1
echo node_modules >> .gitignore

用环境变量配置npm

以下内容摘自npm配置:

所有以npmconfig开头的环境变量都会被解释为配置参数。比如说环境中有npm_config_foo=bar时,会将配置参数foo设置为bar。任何没有给出值的环境配置的值都会设置为true。配置值对大小写不敏感,所以NPM_CONFIG_FOO=bar也一样。

最近在所有的Heroku构建中都有程序的环境。这一变化让Heroku上的node用户无需修改程序代码就可以控制他们的npm配置。习惯#7是这一方式的完美例证。

带着你自己的npm注册中心

最近几年公共npm注册中心出现了突飞猛进式的增长,因此会偶尔不稳定。所以很多node用户开始寻求公共注册中心之外的方案,他们或者是出于开发和构建过程中速度及稳定性方面的考虑,或者是因为要放置私有的node模块。

最近几个月冒出了一些可供选择的npm注册中心。Nodejitsu和Gemfury提供收费的私有注册中心,此外也有一些免费的,比如Mozilla的只读S3/CloudFront镜像和Maciej Małecki的欧洲镜像。

在Heroku上配置node程序使用定制注册中心很容易:

1
heroku config:set npm_config_registry=http://registry.npmjs.eu

追踪过期的依赖项

如果你编程的时间足够长,可能已经领教过相依性地狱的厉害了。好在Node.js和npm接纳了semver,即语义化版本管理规范 ,设置了一个健全的依赖项管理先例。在这个方案下,版本号和它们的变化方式传达的含义涉及到了底层代码,以及从一个版本到下一版本修改了什么。

npm有一个很少有人知道的命令,outdated。它可以跟npm update结合使用,能够找出程序的那些依赖项已经过期了,需要更新:

1
2
3
4
5
6
7
8
9
10
11
12
cd my-node-app
npm outdated
Package Current Wanted Latest Location
------- ------- ------ ------ --------
express 3.4.8 3.4.8 4.0.0-rc2 express
jade 1.1.5 1.1.5 1.3.0 jade
cors 2.1.1 2.1.1 2.2.0 cors
jade 0.26.3 0.26.3 1.3.0 mocha > jade
diff 1.0.7 1.0.7 1.0.8 mocha > diff
glob 3.2.3 3.2.3 3.2.9 mocha > glob
commander 2.0.0 2.0.0 2.1.0 mocha > commander

如果你做的是开源的node程序或模块,可以看看david-dm,NodeICO和shields.io,你可以用这三个优秀服务所提供的图片徽章在项目的README或网站上显示生动的依赖信息。

用npm脚本运行定制的构建步骤

随着npm生态系统的持续增长,开发和构建过程的自动化选择也会随之增长。Grunt是迄今为止node世界中最流行的构建工具,但像gulp.js这种新工具,以及普通的老式npm脚本也因为较轻的负载受到欢迎。

在你把node程序部署到Heroku上时,要运行npm install –production命令以确保程序的npm依赖项会被下载下来装上。但那个命令也会做其它事情:它会运行你在package.json文件中定义的所有npm脚本钩子,比如preinstall和postinstall。这里有个样本:

1
2
3
4
5
6
7
8
9
10
{
"name": "my-node-app",
"version": "1.2.3",
"scripts": {
"preinstall": "echo here it comes!",
"postinstall": "echo there it goes!",
"start": "node index.js",
"test": "tap test/*.js"
}
}

这些脚本可以是行内bash命令,或者也可以指向可执行的命令行文件。你还可以在脚本内引用其他npm脚本:

1
2
3
4
5
6
7
8
{
"scripts": {
"postinstall": "npm run build && npm run rejoice",
"build": "grunt",
"rejoice": "echo yay!",
"start": "node index.js"
}
}

尝试新东西

ES6,也就是被大众称为JavaScript的ECMAScript语言规范的下一版,其工作名称为Harmony。 Harmony给JavaScript带来了很多振奋人心的新特性,其中很多已经出现在较新版本的node中了。

Harmony实现了很多新特性,比如块作用域、生成器、代理、弱映射等等。

要在你的node程序中启用harmony的特性,需要指定一个比较新的node引擎,比如0.11.x,并在启动脚本中设置–harmony选项:

1
2
3
4
5
6
7
8
{
"scripts": {
"start": "node --harmony index.js"
},
"engines": {
"node": "0.11.x"
}
}

Browserify

客户端JavaScript有乱如麻团般的遗留代码,但那并不是语言本身的错。由于缺乏合理的依赖项管理工具,让jQuery-插件拷贝-粘帖的黑暗时代延续了好多年。感谢npm,带着我们步入了前端振兴的年代:npm注册中心像野草一样疯长,为浏览器设计的模块也呈现出了惊人的增长势头。

Browserify是一个让node模块可以用在浏览器中的神奇工具。如果你是前端开发人员,browserify将会改变你的人生。可能不是今天,也不是明天,但不会太久。如果你想开始使用browserify,请参阅这些文章。

你有哪些习惯?

不管你已经做过一段时间node程序,还是刚刚开始,我们都希望这些小技巧能对你有所帮助。如果你有一些(健康的)node习惯想要跟大家分享,请在发tweet时带上#node_habits标签。编码快乐!

作者简介

本文最初由Zeke sikelianos发表在Heroku上。Zeke在Heroku工作,用ruby和coffeescript编写开源软件。他从事设计师这一职业已经有14年了,信奉信息自由的精神,并且相信用计算机可以创造出美好的事物。

查看英文原文:What’s New in Node.js v0.12 – Running Multiple Instances in a Single Process

脱离html元素的AngularJS ng-repeat

发表于 2014-03-25   |   分类于 StackOverflow   |  

StackOverflow地址

http://stackoverflow.com/questions/12857714/angularjs-ng-repeat-with-no-html-element

问题描述

楼主使用下面的代码生成一个列表:

1
2
3
4
5
6
7
<ul ng-cloak>
<div ng-repeat="n in list">
<li><a href="{{ n[1] }}">{{ n[0] }}</a></li>
<li class="divider"></i>
</div>
<li>Additional item</li>
</ul>

然而,<div>元素在一些浏览器中会导致一些细微的显示差异。楼主想知道是否有办法在执行ng-repeat循环时不带上div容器,或者是否有什么其他手段实现同样的效果。

最佳解答

幸运的是AngularJS1.2版本加入了一个内置的repeating支持,且不需要增加子容器,使用ng-repeat-start和ng-repeat-end这两个新directive就可以了。
下面是一个增加Bootstrap分页的小例子:

1
2
3
4
5
6
7
8
9
10
<ul class="pagination">
<li>
<a href="#">&laquo;</a>
</li>
<li ng-repeat-start="page in [1,2,3,4,5,6]"><a href="#">{{page}}</a></li>
<li ng-repeat-end class="divider"></li>
<li>
<a href="#">&raquo;</a>
</li>
</ul>

完整的例子在这里。
John Lindquist还提供了一个有关于此的视频教程。

使用Sails.js执行Sql事务

发表于 2014-03-08   |   分类于 StackOverflow   |  

StackOverflow地址

http://stackoverflow.com/questions/17794784/transactional-sql-with-sails-js

问题描述

楼主玩了一段时间的NodeJs/Express,觉得自己可以试试用Javascript全栈来做重写一个完整的大项目来看看效果如何。Sails.js看上去是一个不错的选择,后台支持Rest API,且支持使用Web Socket,正和楼主的口味,然而还有一个大问题没有解决,那就是如何在Nodejs中执行Sql事务。
大多数楼主见过的基于Nodejs的数据层/ORM并不支持MySQL的事务操作。Sails.js包含的ORM组件Waterline也是如此,但是奇怪的是我在一些文档描述和用户提问中有看到事务操作被经常提及。Knex.js这个组件支持事务,所以楼主想问是否能用它替换掉Waterline(除非Waterline作为Sails.js框架的核心)。
楼主还想问是否有除了Bookshelf之外的基于Knex.js之上的ORM组件,因为楼主其实对Backbone Model/Collection系统并不感冒。

最佳解答

你可以直接用Model.query()来写SQL查询。因为它是一个异步方法,你还需要使用promise或者async来连接他们。譬如,使用MYSQL adapter, async和一个User模型的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async.auto({
transaction: function(next){
User.query('BEGIN', next);
},
user: ['transaction', function(next) {
User.findOne(req.param('id')).exec(next);
}],
// other queries in the transaction
// ...
}, function(err, results) {
if (err) {
User.query('ROLLBACK', next);
return next(err);
}
User.query('COMMIT', next);
// final tasks
res.json(results.serialize);
});

HTML5 File api 实现断点续传

发表于 2014-03-04   |   分类于 Practice   |  

转载自: http://www.cnblogs.com/zhwl/p/3580776.html

目前市场上大多数的网站的断点上传都是需要安装浏览器插件的,本文就针对高级浏览器的环境下,通过HTML5 File api实现断点上传进行说明

实现文件多选

HTML5的<input>新增了multiple属性,该属性可接受多个值的文件上传字段

1
<input type="file" multiple="multiple" name="file" id="file">

添加了该属性用户就可以在弹出的对话框中一次性选择多个文件了

实现文件从计算机拖拽到网页以及添加文件队列功能

这里我们用 dragover 和 drop 两个事件来管理文件拖拽的功能
其中 dragover 用来处理在指定的元素上移动时的事件,这里我们通过给body绑定dragover时间来处理页面中拖动文件的事件

1
2
3
4
5
6
document.body.addEventListener('dragover', dragFile, false);
function dragFile(evt) {
evt.stopPropagation();
evt.preventDefault();
evt.dataTransfer.dropEffect = 'copy';
}

用 drop 事件来处理鼠标松开时候的事件,此时应该将用户拖动过来的文件加入到上传队列中,以供后续的处理

1
2
3
4
5
6
7
8
9
10
11
document.body.addEventListener('drop', dropFile, false);
function dragFile(evt) {
evt.stopPropagation();
evt.preventDefault();
// dataTransfer.files属性可以获取到所有拖动选择的文件,通过遍历可以读取到所有文件的信息。
// 遍历每个文件可以获取到文件的 name、size、type、lastModifiedDate等关键信息
var files = evt.dataTransfer.files;
// addfile 方法 用来添加上传文件队列,在input的change事件中也需要调用
// 该方法首先检查有无文件正在上传中,如果有就将后续加入的文件放到上传队列中,如果没有文件正在上传就直接执行上传命令
addfile(files);
}

文件续传原理

目前比较常用的断点续传的方法有两种,一种是通过websocket接口进行文件上传,另一种是通过ajax,两种方法各有千秋,虽然websocket听起来比较高端些~ 但是除了用了不同的协议外其他的算法基本上都是很相似的,并且服务端要开启ws接口,这里用相对方便的ajax来说明断点上传的思路。
说来说去,断点续传最核心的内容就是把文件“切片”然后再一片一片的传给服务器,但是这看似简单的上传过程却有着无数的坑。
首先是文件的识别,一个文件被分成了若干份之后如何告诉服务器你切了多少块,以及最终服务器应该如何把你上传上去的文件进行合并,这都是要考虑的。
因此在文件开始上传之前,我们和服务器要有一个“握手”的过程,告诉服务器文件信息,然后和服务器约定切片的大小,当和服务器达成共识之后就可以开始后续的文件传输了。
前台要把每一块的文件传给后台,成功之后前端和后端都要标识一下,以便后续的断点。
当文件传输中断之后用户再次选择文件就可以通过标识来判断文件是否已经上传了一部分,如果是的话,那么我们可以接着上次的进度继续传文件,以达到续传的功能。

文件的前端切片

有了HTML5 的 File api之后切割文件比想想的要简单的多的多。
只要用slice 方法就可以了

1
var packet = file.slice(start, end);

参数start是开始切片的位置,end是切片结束的位置 单位都是字节。通过控制start和end 就可以是实现文件的分块
如

1
2
3
4
file.slice(0,1000);
file.slice(1000,2000);
file.slice(2000,3000);
// ......

文件片段的上传

上一部我们通过slice方法把文件分成了若干块,接下来要做的事情就是把这些碎片传到服务器上。
这里我们用ajax的post请求来实现

1
2
3
4
5
6
7
8
9
10
11
12
var xhr = new XMLHttpRequest();
var url = xxx // 文件上传的地址 可以包括文件的参数 如文件名称 分块数等以便后台处理
xhr.open('POST', url, true);
xhr.onload = function (e){
// 判断文件是否上传成功,如果成功继续上传下一块,如果失败重试该快
}
xhr.upload.onprogress = function(e){
// 选用 如果文件分块大小较大 可以通过该方法判断单片文件具体的上传进度
// e.loaded 该片文件上传了多少
// e.totalSize 该片文件的总共大小
}
xhr.send(packet);

CQRS介绍

发表于 2013-05-22   |   分类于 Documentation   |  

我今天找到一篇好文,深入浅出的介绍了CQRS,边翻译边学习了。原文地址:
http://www.codeproject.com/Articles/555855/Introduction-to-CQRS

附带源码下载

转载请注明出处 http://blog.dongderu.net/2013/05/22/2013-5-22-CQRS-Introduction/

CQRS是什么?

CQRS就是指命令和查询职责的分离。许多人认为CQRS是一个完整的构架,但是他们错了。CQRS只是一个小的模式形态。这个模式最初是由Greg Young和Udi Dahan两位大师提出的。他们是从一个叫做“命令与查询分离”的模式得到的灵感,这个模式是由Bertrand Meyer在他撰写的《Object Oriented Software Construction》书里定义的。该模式的关键在于:“一个方法要么是用来改变某个对象的状态的,要么就是返回一个结果,这两者不会同时并存”。换句话说,提问不会改变问题的答案。正式一点的说法是,之所以要方法返回结果,因为他是“引用透明的”,因此不会执行额外的有影响的操作(来自维基百科)。(插一句,这句话你可能看不懂,请自行琢磨原文,我根据自己的理解白话一下:你既然要得到正常的结果,不会蛋疼到查询的时候同时去改变要查询的结果吧==b。。。)根据上述理论,我们可以把方法拆分成两个部分:

  • Commands(命令) - 改变某一个对象或整个系统的状态(有时也叫做modifiers或者mutators)。
  • Queries(查询) - 返回值并且不会改变对象的状态。

在实际操作中很容易把这两者区分开来。查询会返回一个预先定义的类型,而命令返回void。这样一个模式被已经得到广泛的认可而且能帮助大家更好地理解对象。不过另一方面来说,CQRS仅适用于某些特定的场景。

大多数应用都使用了主流的专注于模型的读和写操作的解决方案。但是读和写采用相同的模型会导致模型越来越复杂变得十分难以维护和优化。

命令与查询两种模式的应用的真正优势是你可以将那些改变状态的操作从那些不应该出现的地方剥离出来。这样的分离会给你在处理调试和优化的过程中带来很大的方便。你可以将你的读取端独立地进行优化,而暂且不管写的操作。写操作端就是指领域的范畴了。领域包含了所有的行为。而读取端仅仅是用来做数据呈现的。

采取这样的模式的另一个好处是,在大型的应用中,你可以将你的开发团队进行分割,以分别负责读和写的实现,且两者之间的知识的传递可以不对称。比如负责呈现数据的小组根本不需要了解领域模型,命令和ES的知识和实现,只需要了解展现数据库的结构。(好钢用在刀刃上,这样可以为企业优化组合,节约成本)

Query side(查询端)

查询端仅包含获取数据的方法。从架构的角度出发,所有的方法都应该返回用于显示目的的DTO(Data Transfer Object)。一般来说DTO就是Domain Objects(领域对象)的投射(两者很相像)。在有些情况下,构建DTO的过程会十分蛋疼,尤其在需要构建一些很复杂的DTO的情况下。

如图,Read Layer(读取操作层)可以直连数据库(数据源),直接用存储过程来读在这种模式下不是一个坏主意。直连数据源使得查询维护和优化变得十分容易。这里采用denormalize(反规范化,就是允许数据冗余,比如一张表对应一个页面,不使用Join,允许有重复的字段)的数据库设计也是有道理的。原因在于数据的读取操作往往好几倍频繁于领域行为的执行。而反规范化的应用可以很好的帮助应用提升性能。

Command Side(命令端)

命令由客户端发出,并传送到领域层。命令实际上是一种消息,它包含了一些特定的实体信息来完成某一项操作。命令的命名规则可以是DoSomething(举个例子,ChangeName, DeleteOrder…)。他们通知领域实体执行某种操作并返回一个值或者失败信号。命令由Command Handler(命令执行器)进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public interface ICommand
{
Guid Id { get; }
}
public class Command : ICommand
{
public Guid Id { get; private set; }
public int Version { get; private set; }
public Command(Guid id,int version)
{
Id = id;
Version = version;
}
}
public class CreateItemCommand:Command
{
public string Title { get; internal set; }
public string Description { get;internal set; }
public DateTime From { get; internal set; }
public DateTime To { get; internal set; }
public CreateItemCommand(Guid aggregateId, string title,
string description,int version,DateTime from, DateTime to)
: base(aggregateId,version)
{
Title = title;
Description = description;
From = from;
To = to;
}
}

所有的命令都会被传入Command Bus(命令总线),它会将每个命令委派给Command Handler进行处理。这样做保证了领域的单一入口。Command Handler的职责是调用领域层内相应的方法。Command Handler需要具有repository(仓储)的数据连接来加载需要的实体(在CQRS模式中就是指聚合根)以满足某些方法的需要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public interface ICommandHandler<TCommand> where TCommand : Command
{
void Execute(TCommand command);
}
public class CreateItemCommandHandler : ICommandHandler<CreateItemCommand>
{
private IRepository<DiaryItem> _repository;
public CreateItemCommandHandler(IRepository<DiaryItem> repository)
{
_repository = repository;
}
public void Execute(CreateItemCommand command)
{
if (command == null)
{
throw new ArgumentNullException("command");
}
if (_repository == null)
{
throw new InvalidOperationException("Repository is not initialized.");
}
var aggregate = new DiaryItem(command.Id, command.Title, command.Description,
command.From, command.To);
aggregate.Version = -1;
_repository.Save(aggregate, aggregate.Version);
}
}

Command Handler负责如下几项工作:

  • 负责从消息基础架构(Command Bus)里接受Command实例。
  • 负责验证Command是否有效。
  • 负责找到针对该Command的聚合实例。
  • 负责调用聚合实例的相关方法,并且从Command类中获取相关字段作为参数传入方法中。
  • 从聚合的角度来看总是在更新状态(可以从代码得到直观体现)。

Internal Events(内部事件)

在讨论这个话题之前,我们可能会先想到一个问题,什么是领域事件?领域事件就是那些在系统中已经发生的事情。事件对应命令的执行结果。让我们来举个例子,客户端请求了一个DTO并且在其基础上做了一些修改,而后产生了一条命令向系统推送。相应的Handler加载了对应的聚合根并且执行了指定的Domain Behavior(领域方法),该方法同时产生了一个事件,这个事件由特定的subscriber(订阅器)接收。聚合随后将该事件发布到事件总线上,由事件总线负责将其分发给相关的Handler执行。而那些在聚合根内部被获取执行的就被称为内部事件。内部事件的Handler出了负责设置聚合根内部属性的状态以外不作任何其他的操作。

Domain Behavior(领域方法)

1
2
3
4
public void ChangeTitle(string title)
{
ApplyChange(new ItemRenamedEvent(Id, title));
}

Domain Event(领域事件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ItemCreatedEvent:Event
{
public string Title { get; internal set; }
public DateTime From { get; internal set; }
public DateTime To { get; internal set; }
public string Description { get;internal set; }
public ItemCreatedEvent(Guid aggregateId, string title ,
string description, DateTime from, DateTime to)
{
AggregateId = aggregateId;
Title = title;
From = from;
To = to;
Description = description;
}
}
public class Event:IEvent
{
public int Version;
public Guid AggregateId { get; set; }
public Guid Id { get; private set; }
}

Internal Domain Event Handler(内部领域事件执行器)

1
2
3
4
public void Handle(ItemRenamedEvent e)
{
Title = e.Title;
}

事件还经常会挂载一套被称为Event Sourcing (ES)(事件溯源)的模式。事件溯源是一种通过将聚合的变化以事件的形式记录,并将其转换成二进制流保存,以实现持久化的途径。

就像之前提到的那样,聚合根内部所有的状态变化都由事件触发,而且内部事件执行器除了改变的聚合根状态外其他啥事不管。为了要得到聚合根某一个事件点的状态,就需要通过回放事件来完成。在这里我必须提醒一点,就是所有的事件都是只写操作。你不可以改动或者删除已生成的事件。如果你发现系统内部产生了一些错误的事件,你就必须再创建一些修正的事件来弥补之前的错误。

External Events(外部事件)

外部事件经常用来向展示数据库同步当前领域状态的信息。要做到这点,首先要将内部事件发布到领域外部(通过事件总线)。当事件被发布以后,相应的Handler就会接收执行后续的工作。外部事件可以同时面向多个Handler发布。外部事件的Handler主要负责如下几项工作:

  • 负责从messaging infrastructure (Event Bus)(消息机制(事件总线))接收事件的实例。
  • 负责加载作业管理器实例(举个例子来说,针对仅负责同步展示数据库的Handler,作业管理器就是ORM框架,或者SqlHelper类)来处理事件。
  • 负责将事件内部的信息作为参数传入,并调用执行作业管理器实例的相应方法。
  • 从作业管理器的角度来看总是在更新状态(可以从代码里得到直观体现)。

示例代码说明

我创建了一个非常简单的例子来演示怎么样实现CQRS模式。这个例子可以让你创建和修改你的日志。该解决方案包含如下项目:

  • Diary.CQRS
  • Diary.CQRS.Configuration
  • Diary.CQRS.Web

第一个项目包含了所有领域和消息对象。Configuration项目给WebUI提供了IoC注入支持。我们现在来自己看一下这些项目。

Diary.CQRS

就像我先前提到过的那样,这个项目包含了本例所需的所有领域和消息对象。本CQRS示例项目的唯一入口就是通过将Command发布到Command Bus上。CommandBus类只有一个Send(T command)方法。该方法负责通过调用CommandHandlerFactory来创建对应的Command Handler。如果某个Command没有找到对应的Command Handler,则会抛出异常。正常情况下,Execute方法作为某个行为执行的一部分被调用。该行为会创建一个内部事件,且该事件会存入一个名为_changes的内部成员。该成员的定义可以在AggregateRoot基类里找到。接下来,该事件会由内部事件处理器处理来更新聚合实例的属性状态。在整个行为执行完毕以后,所有该聚合实例下的未保存事件都会通过仓储进行持久化操作。仓储会将当前聚合实例的版本与已经被持久化保存的实例进行版本比较,查看是否有冲突。如果版本不同,就意味着对象可能已经被其他人改过了,系统将抛出ConcurrencyException异常。正常情况下,事件将通过Event Storage进行持久化。

Repository(仓储)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class Repository<T> : IRepository<T> where T : AggregateRoot, new()
{
private readonly IEventStorage _storage;
private static object _lockStorage = new object();
public Repository(IEventStorage storage)
{
_storage = storage;
}
public void Save(AggregateRoot aggregate, int expectedVersion)
{
if (aggregate.GetUncommittedChanges().Any())
{
lock (_lockStorage)
{
var item = new T();
if (expectedVersion != -1)
{
item = GetById(aggregate.Id);
if (item.Version != expectedVersion)
{
throw new ConcurrencyException(string.Format("Aggregate {0} has been previously modified",
item.Id));
}
}
_storage.Save(aggregate);
}
}
}
public T GetById(Guid id)
{
IEnumerable<Event> events;
var memento = _storage.GetMemento<BaseMemento>(id);
if (memento != null)
{
events = _storage.GetEvents(id).Where(e=>e.Version>=memento.Version);
}
else
{
events = _storage.GetEvents(id);
}
var obj = new T();
if(memento!=null)
((IOriginator)obj).SetMemento(memento);
obj.LoadsFromHistory(events);
return obj;
}
}

InMemoryEventStorage(事件存储【保存在内存中】)

在本例中,我创建了一个InMemoryEventStorage类,他可以将所有的事件存储在内存中。该类实现了IEventStorage接口并重写了四个方法:

1
2
3
4
5
6
7
8
9
10
public IEnumerable<Event> GetEvents(Guid aggregateId)
{
var events = _events.Where(p => p.AggregateId == aggregateId).Select(p => p);
if (events.Count() == 0)
{
throw new AggregateNotFoundException(string.Format(
"Aggregate with Id: {0} was not found", aggregateId));
}
return events;
}

该方法返回聚合实例下的所有事件,如果没有,则意味着该聚合实例不存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public void Save(AggregateRoot aggregate)
{
var uncommittedChanges = aggregate.GetUncommittedChanges();
var version = aggregate.Version;
foreach (var @event in uncommittedChanges)
{
version++;
if (version > 2)
{
if (version % 3 == 0)
{
var originator = (IOriginator)aggregate;
var memento = originator.GetMemento();
memento.Version = version;
SaveMemento(memento);
}
}
@event.Version=version;
_events.Add(@event);
}
foreach (var @event in uncommittedChanges)
{
var desEvent = Converter.ChangeTo(@event, @event.GetType());
_eventBus.Publish(desEvent);
}
}

该方法负责将事件存储到内存中,另外,每执行三个事件存储操作就会产生一个对应聚合实例的快照。该快照实例包含了聚合的所有状态和版本信息。通过使用快照可以提高性能,因为这样做不需要把所有的发生过的事件都读取出来,只要处理最后三个就可以了。

当所有的事件都被持久化以后,他们就通过Event Bus进行发布然后由外部Event Handler接收并处理。

1
2
3
4
5
6
7
public T GetMemento<T>(Guid aggregateId) where T : BaseMemento
{
var memento = _mementos.Where(m => m.Id == aggregateId).Select(m=>m).LastOrDefault();
if (memento != null)
return (T) memento;
return null;
}

返回聚合的快照。

1
2
3
4
public void SaveMemento(BaseMemento memento)
{
_mementos.Add(memento);
}

将聚合转存为快照。

Aggregate Root(聚合根)

AggregateRoot类是所有聚合的基类。该类继承并实现了IEventProvider接口。他内部保存着所有未被提交的事件列表。它同时还带有ApplyChange方法用来调用对应的内部事件处理器。LoadsFromHistory方法用来加载并应用内部领域事件产生的状态变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public abstract class AggregateRoot:IEventProvider
{
private readonly List<Event> _changes;
public Guid Id { get; internal set; }
public int Version { get; internal set; }
public int EventVersion { get; protected set; }
protected AggregateRoot()
{
_changes = new List<Event>();
}
public IEnumerable<Event> GetUncommittedChanges()
{
return _changes;
}
public void MarkChangesAsCommitted()
{
_changes.Clear();
}
public void LoadsFromHistory(IEnumerable<Event> history)
{
foreach (var e in history) ApplyChange(e, false);
Version = history.Last().Version;
EventVersion = Version;
}
protected void ApplyChange(Event @event)
{
ApplyChange(@event, true);
}
private void ApplyChange(Event @event, bool isNew)
{
dynamic d = this;
d.Handle(Converter.ChangeTo(@event,@event.GetType()));
if (isNew)
{
_changes.Add(@event);
}
}
}

EventBus(事件总线)

事件表述了系统内的状态变化。事件产生的主要目的之一就是更新读取模型。为了实现这一点我创建了EventBus类。该类的唯一职责就是将事件发布到subscribers(订阅者)那里。一个单一事件可以被发布到多个订阅者手中。不过在本例中,我们还用不到手工订阅(针对某些事件做某些特殊处理)。Event handler factory(事件处理器工厂)会返回一个EventHanlder的列表来处理当前事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class EventBus:IEventBus
{
private IEventHandlerFactory _eventHandlerFactory;
public EventBus(IEventHandlerFactory eventHandlerFactory)
{
_eventHandlerFactory = eventHandlerFactory;
}
public void Publish<T>(T @event) where T : Event
{
var handlers = _eventHandlerFactory.GetHandlers<T>();
foreach (var eventHandler in handlers)
{
eventHandler.Handle(@event);
}
}
}

Event Handlers(事件处理器)

事件处理器的主要目的是接收事件消息并更新读取模型。在下面的例子中,你可以看到一个ItemCreatedEventHandler类。它负责处理ItemCreatedEvent事件。通过读取事件内保存的信息,它创建了一个新的对象并将其存入展示数据库中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ItemCreatedEventHandler : IEventHandler<ItemCreatedEvent>
{
private readonly IReportDatabase _reportDatabase;
public ItemCreatedEventHandler(IReportDatabase reportDatabase)
{
_reportDatabase = reportDatabase;
}
public void Handle(ItemCreatedEvent handle)
{
DiaryItemDto item = new DiaryItemDto()
{
Id = handle.AggregateId,
Description = handle.Description,
From = handle.From,
Title = handle.Title,
To=handle.To,
Version = handle.Version
};
_reportDatabase.Add(item);
}
}

Diary.CQRS.Web

该项目是本CQRS示例的用户交互平台。这个Web项目是一个标准的ASP.NET MVC4应用,里面仅包含一个控制器HomeController和六个ActionResult方法:

  • ActionResult Index() - 该方法将返回Index视图,Index视图作为应用的主界面以列表的形式呈现所有日志内容。
  • ActionResult Delete(Guid id) - 该方法创建了一个新的DeleteItemCommand命令消息实例并将其发布至CommandBus。当命令发送成功后,将返回Index视图。
  • ActionResult Add() - 返回添加视图,你可以在该视图上输入新的日志内容。
  • ActionResult Add(DiaryItemDto item) - 该方法创建了一个新的CreateItemCommand命令消息实例并将其发布至CommandBus。当日志创建成功后,将返回Index视图。
  • ActionResult Edit(Guid id) - 返回选定日志项的编辑视图。
  • ActionResult Edit(DiaryItemDto item) - 该方法创建了一个新的ChangeItemCommand命令消息实例并将其发布至CommandBus。当日志更新成功后,将返回Index视图。当ConcurrencyError(并发错误)发生时,页面上将显示被抛出的异常信息。

下图即为展示日志列表的主界面。

何时应该采用CQRS

总的来说,CQRS模式的应用会让在你应对需要处理需要高度协作以及大型,多用户,高复杂度,包含不断变更的业余规则,还有业务优先的系统中体验到巨大的价值。另外,当你需要实现追踪和记录历史数据功能时它会显得特别有用。

通过CQRS,你可以做到让读写性能飞速提升。而且系统原生就支持了scaling out(横向扩展)。通过将读和写的操作分开,你可以针对任意一方面进行优化。

当你需要面对非常困难的业务逻辑时,CQRS模式就会显得非常有用。CQRS会强制性地避免你将领域逻辑和基础架构的操作进行混淆。

通过应用CQRS模式,你可以在定义好通信接口以后,将开发工作分开交付给不同的团队进行实施。

何时不应该采用CQRS

如果你所开发的项目不需要进行高度地协作,就是指你不需要将同一个系列的数据操作拆分给多个人来写代码的话,那就不应该使用CQRS。

全文翻译完毕 by 止觀。

AngularJS 介绍

发表于 2012-10-19   |   分类于 Documentation   |  

转载请注明出处 http://blog.dongderu.net/2013/05/22/2013-5-22-CQRS-Introduction/

前言

最近在研究跨浏览器跨平台app方案时找到了Angular JS,他是Google出品的JS框架。我粗略地看了一下它的介绍,突然发现,这不就是我要找的东西吗?这样的好东西可千万不能埋没了,得让它发扬光大,我就以这篇文章作为开始,围绕着angularjs的官方文档做一个系列的研究。

首先我们看到了它的一句广告语“HTML enhanced for web app”我对这句话的理解是“让HTML对你的web app作最给力的支撑”,看着是不是觉得很唬人?光靠HTML难道能通吃一切吗?它可没说大话哦,让我们继续看下去。

为什么要选择AngularJS?

HTML是一个很好的静态解释性语言,但是当你对其添加越来越多的动态响应后,HTML语言就会变得越来越不清晰,难以维护,AngularJS可以让你对现有的HTML词汇定义进行扩展(XHTML?HTML5?),这样一来你的app就会变得非常易读而又富有表现力,同时也能加快你的开发效率。

与其他JS框架的不同之处

其他的JS框架通过将HTML, CSS, Javascript或者前2者与JS结合的内容进行抽象或者通过命令方式来操作DOM来弥补HTML的不足。不过这二者都没有解决HTML不是为了动态视图而设计的这一根本命题。

扩展能力

AngularJS是一套非常适合组建你自己的应用的工具集。你可以随意对其功能进行扩展或者与其他库结合使用。其中每一个功能你都可以自定义或者将其替换以满足你的开发过程和需求。你可以通过阅读文档找到该问题的答案。

一个简单的示例

在name文本框内输入文字竟会同时显示在下方的Label区域内。我们来看一下实现该功能的代码:

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!doctype html>
<html ng-app>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.min.js"></script>
</head>
<body>
<div>
<label>Name:</label>
<input type="text" ng-model="yourName" placeholder="Enter a name here">
<hr>
<h1>Hello {{yourName}}!</h1>
</div>
</body>
</html>

这里我们先来做一些注释:

ng-app: 告诉AngularJS在页面上激活的位置,在本例中意味着全文件有效。

angular.min.js: 加载AngularJS
ng-model: 将form和model建立关系并绑定。这意味着你对该控件内的任何修改都会对model内的数据作实时更新,相对的,model内数据的变更也会改变控件的显示。

: 是一种在HTML页面上制定绑定显示位置的申明方式,AngularJS会在“yourname”值变更的同时自动替换该处的文本。

让我们动手试一试?点我玩一下

添加一些控制功能

先穿插一点背景知识和特性:

Data Binding: Data-binding是一种在model数据变更时自动更新显示的一种方式,反之亦然。这个功能十分方便,因为他可以让你省去许多对DOM的操作。

Controller: controller定义了DOM背后的行为。AngularJS可以让你用一种间接的高可读性,非公式化的表现方式来更新DOM,以及注册回调方法(callbacks),或者监听model的变化。

朴实的JS: 和其他JS框架不同,你无需去继承一个专门的类型,用专门的访问器包裹你的model。只需要传统的、朴素的Javascript就好了。这将是你的程序更容易测试,维护,重用,而且从公式化中得到解放。

一个经典的Todo List的实现,来看代码:

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!doctype html>
<html ng-app>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.min.js"></script>
<script src="todo.js"></script>
<link rel="stylesheet" href="todo.css">
</head>
<body>
<h2>Todo</h2>
<div ng-controller="TodoCtrl">
<span>{{remaining()}} of {{todos.length}} remaining</span>
[ <a href="" ng-click="archive()">archive</a> ]
<ul class="unstyled">
<li ng-repeat="todo in todos">
<input type="checkbox" ng-model="todo.done">
<span class="done-{{todo.done}}">{{todo.text}}</span>
</li>
</ul>
<form ng-submit="addTodo()">
<input type="text" ng-model="todoText" size="30"
placeholder="add new todo here">
<input class="btn-primary" type="submit" value="add">
</form>
</div>
</body>
</html>

注释:

todo.js: 该文件内存放了所有控制方法。

ng-controller: 在这个元素内部的行为都将交由todo.js内的TodoCtrl类进行管理。

ng-click: 用定义的方式制定调用Controller中的行为,而不是通过注册event-handlers。在本例中点击链接后将调用archive() 方法。

ng-repeat: ng-repeat用来显示一个集合,在本例中,针对每一个在todos里的对象,AngularJS都会创建一个<li>副本。当新的对象加入到todos中时,它也会自动追加<li>项,反之亦然。这是反映AngularJS指令灵活度特性的其中之一。

ng-submit: 拦截表单提交事件,并用“addTodo()”方法取代,在该方法内会读取“todoText”属性并且将其插入到todos数组内。

todo.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function TodoCtrl($scope) {
$scope.todos = [
{text:'learn angular', done:true},
{text:'build an angular app', done:false}];
$scope.addTodo = function() {
$scope.todos.push({text:$scope.todoText, done:false});
$scope.todoText = '';
};
$scope.remaining = function() {
var count = 0;
angular.forEach($scope.todos, function(todo) {
count += todo.done ? 0 : 1;
});
return count;
};
$scope.archive = function() {
var oldTodos = $scope.todos;
$scope.todos = [];
angular.forEach(oldTodos, function(todo) {
if (!todo.done) $scope.todos.push(todo);
});
};
}

注释:

TodoCtrl: Controller是页面背后的控制代码,你可以清楚地看到应用的行为,因为其中没有任何Dom操作,或者框架独有的格式,只有简单的,非常易读的JS。

$scope: $scope内包含了你的model数据。它将页面和控制器粘合在了一起。$scope仅仅是能被注入到控制器的服务的其中之一。

todos: 在本例中在初始化model时创建了2个todo项,请注意你只需简单地分配你的model到$scope上,AngularJS就会自动帮你将数据显示在页面上。而这个model数据就是普通的,传统的js对象,不用费力的把它包在proxy里,或者通过特定的setter方法去访问。

todoText: 由于数据的双向绑定,model内的数据永远是最新的。这意味着我们可以简单地读取用户的输入,而不需要再去注册callbacks,事件监听器,或者使用框架提供的API。

todo.css

1
2
3
4
.done-true {
text-decoration: line-through;
color: grey;
}

注释:

.done-true: 为完成的项添加打勾的样式。

再玩一下?

连接后端

背景知识和特性:

Deep Linking: 深度的链接反映了用户在app内的所在位置,当用户想要收藏当前页面或者将链接通过邮件发送出去时就显得十分有用。传统的web应用没有这个问题,但是AJAX应用天生就不支持这个功能。AngularJS将深度链接和类似桌面应用的开发的优点结合在了一起。

表单验证: 客户端验证是用户体验中的重要部分。AngularJS让你无需编写任何JS代码就能制定表单的验证条件。写更少的代码,开发地更快更好。

与服务端通信: AngularJS提供了建立于XHR之上的服务,这样戏剧化地简化了你的代码。我们封装了XHR并且提供了异常处理和成功允诺。而这个允诺进一步地简化了原先你代码内处理异步返回结果的部分。它能让同步地分配属性但是其实执行的是异步的操作。

SUID操作示例,来看代码:

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!doctype html>
<html ng-app="project">
<head>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular-resource.min.js">
</script>
<script src="project.js"></script>
<script src="mongolab.js"></script>
</head>
<body>
<h2>JavaScript Projects</h2>
<div ng-view></div>
</body>
</html>

注释:

ng-app="project": ng-app在本页面区域内激活了project模块。这样定义意味着可以在页面内存在调用多个模块。

angular-resource.min.js: 加载AngularJS Resource模块。

project.js: 页面的Controller,其中定了了页面的行为。

mongolab.js: AngularJS将该应用与http://mongolab.com连接的模块,用来作数据持久化。

ng-view: 定义了ng-view以后,我们就可以把这个div作为一个partial页面或者模板的容器,而在它范围之外页面部分将保持静态。在这个例子中我们将使用这个容器,让它在现实列表和添加、删除表单模板之间切换。

project.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
angular.module('project', ['mongolab']).
config(function($routeProvider) {
$routeProvider.
when('/', {controller:ListCtrl, templateUrl:'list.html'}).
when('/edit/:projectId', {controller:EditCtrl, templateUrl:'detail.html'}).
when('/new', {controller:CreateCtrl, templateUrl:'detail.html'}).
otherwise({redirectTo:'/'});
});
function ListCtrl($scope, Project) {
$scope.projects = Project.query();
}
function CreateCtrl($scope, $location, Project) {
$scope.save = function() {
Project.save($scope.project, function(project) {
$location.path('/edit/' + project._id.$oid);
});
}
}
function EditCtrl($scope, $location, $routeParams, Project) {
var self = this;
Project.get({id: $routeParams.projectId}, function(project) {
self.original = project;
$scope.project = new Project(self.original);
});
$scope.isClean = function() {
return angular.equals(self.original, $scope.project);
}
$scope.destroy = function() {
self.original.destroy(function() {
$location.path('/list');
});
};
$scope.save = function() {
$scope.project.update(function() {
$location.path('/');
});
};
}

注释:

project: 这样就直接定义了project模块。通过使用模块,你可以配置现有的服务,定义新的服务,指令,过滤器等等。在这里,我们将设置映射URL和部件的路由。AngularJS负责监控浏览器,一旦URL有变动将自动更新部件内容。

mongolab: 一个模块可以依赖于其他模块,这里,project依赖于mongolab以处理本应用的持久化事务。

config: 通过config()可以配置已存在的服务,这里,我们将配置$routeProvider来处理URL和部件的映射关系。

when: 当URL为/时,程序会在页面上加载list.html并关联ListCtrl控制器。你可以直观的通过查阅路由定义来了解该应用的结构。

/edit/:projectId: 你一定在路由定义里发现有一个:,你可以通过使用冒号来传参,让Controller接受,现在 EditCtrl 可以通过查询projectId属性来定位要编辑的记录了。

otherwise: 这条路由用来定义不满足所有情况时显示的内容,类似switch的default。

Project: 这是一个用来持久化以及获取数据的类。他已经在mongolab.js中定义,并自定注入到了controller中。他的目的是使服务器的交互抽象化,让我们腾出更多精力专注于控制行为而不是处理复杂的服务器交互。

query: 该方法会从服务器请求Project类的集合。注意尽管这是一个异步的调用,但是我们就像同步且没有回调函数的那样使用,这实在太酷了!事实上这里query返回的是一种叫做promise的东东。一旦最后从服务器得到了返回数据,兑现了 promise, AngularJS的数据绑定功能将自动地在任何我们使用该方法的地方更新显示。

$location: 你可以通过调用此服务读取浏览器地址。

save: 当用户点击save按钮后将调用此方法。

path: 使用此方法可以更改应用的当前URL,URL的变动也会自动地激活路由服务,解释并显示新的内容,在这里,会显示/edit/内容。

$routeParams: 这里我们请求AngularJS为我们注入$routeParams服务,通过使用它,我们可以读取在路由里定义的参数。

projectId: 这里将URL中的 projectId 读取出来。有了它可以让Controller支持deep-linking。

original: 我们先把原始的Project保存起来,以便可以发现用户是否有对其进行更改。

isClean: 检查用户是否通过表单更改了信息,我们在这里通过此功能控制是否启用save按钮。

destroy: 当用户点击了delete按钮后调用该方法。

list.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<input type="text" ng-model="search" class="search-query" placeholder="Search">
<table>
<thead>
<tr>
<th>Project</th>
<th>Description</th>
<th><a href="#/new"><i class="icon-plus-sign"></i></a></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="project in projects | filter:search | orderBy:'name'">
<td><a href="{{project.site}}" target="_blank">{{project.name}}</a></td>
<td>{{project.description}}</td>
<td>
<a href="#/edit/{{project._id.$oid}}"><i class="icon-pencil"></i></a>
</td>
</tr>
</tbody>
</table>

注释:

ng-model: 将输入框与search属性绑定。该属性用来过滤project包含用户输入的关键字的数据。

#/new: 指向/new路由的链接,该路由已在project.js里定义。注意我们一直遵从web的准则。为一个链接注册回调实在很不象话,我们只是简单地将它指向一个新的URL。这将会自动更新浏览器的历史,激活deep-linking。但是和普通的服务器客户端两头跑的应用不同,这里的跳转时间将会立刻在浏览器上得到反馈。

ng-repeat: 通过使用它来遍历集合。在这里,为projects里的每一个project,AngularJS都会创建一个新的<tr>节点。

filter: 它通过调用search来返回在projects中符合条件的项。当你在搜索框里输入关键字后,filter将会将列表的显示范围进一步缩小以满足条件,然后ng-repeat会在table里添加或删除不符和条件的项目。

orderBy: 返回根据name排序的project列表。

#/edit/: 创建独有的编辑链接,通过在URL里内嵌project id,这样一来就可以实现deep-linking,你可以通过浏览器返回上一步,也可以通过URL调用EditCtrl来编辑项。

detail.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<form name="myForm">
<div class="control-group" ng-class="{error: myForm.name.$invalid}">
<label>Name</label>
<input type="text" name="name" ng-model="project.name" required>
<span ng-show="myForm.name.$error.required" class="help-inline">
Required</span>
</div>
<div class="control-group" ng-class="{error: myForm.site.$invalid}">
<label>Website</label>
<input type="url" name="site" ng-model="project.site" required>
<span ng-show="myForm.site.$error.required" class="help-inline">
Required</span>
<span ng-show="myForm.site.$error.url" class="help-inline">
Not a URL</span>
</div>
<label>Description</label>
<textarea name="description" ng-model="project.description"></textarea>
<br>
<a href="#/" class="btn">Cancel</a>
<button ng-click="save()" ng-disabled="isClean() || myForm.$invalid"
class="btn btn-primary">Save</button>
<button ng-click="destroy()"
ng-show="project._id" class="btn btn-danger">Delete</button>
</form>

注释:

myForm: 创建一个表单并为其命名,我们将会声明验证规则并处理输入错误和控制按钮操作。

ng-class: 当name不正确时,为div添加一个error的CSS class。

required: 当输入值为空值判定为错误。

ng-show: 当myForm的name框required判定为错误时显示错误信息。

url: 该类型会自动验证输入值的格式。

ng-disabled: 当表单没有变动或者有错误时禁用’save’按钮

mangolab.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// This is a module for cloud persistance in mongolab - https://mongolab.com
angular.module('mongolab', ['ngResource']).
factory('Project', function($resource) {
var Project = $resource('https://api.mongolab.com/api/1/databases' +
'/angularjs/collections/projects/:id',
{ apiKey: '4f847ad3e4b08a2eed5f3b54' }, {
update: { method: 'PUT' }
}
);
Project.prototype.update = function(cb) {
return Project.update({id: this._id.$oid},
angular.extend({}, this, {_id:undefined}), cb);
};
Project.prototype.destroy = function(cb) {
return Project.remove({id: this._id.$oid}, cb);
};
return Project;
});

注释:

ngResource: AngularJS的ngResource模块提供了一个面向RESTFul服务的通用接口。

factory: 通过调用ngResource模块的该方法来定义新的服务。在这里定义的任何服务都会在你调用的任何地方被自动注入。

Project: 在这里是为了Project类定义一个服务,该类负责每个project的数据装载,并且带有读写数据的方法。

$resource: 该服务专门用来创建资源类。每一个资源类预定义配有query(),get(),save()和remove()方法。这些方法作为与持久化服务交互的API。此外,资源类还可以根据你的应用的需要进行扩展。

apiKey: 向mangolab数据存储引擎请求时必要的参数,伴随所有请求一并传入。

update: 在资源类上定义update方法,其将会使用HTTP put方法作为请求。

prototype: 这里我们将扩展资源类的方法来与持久化引擎作交互。

创建组件

背景知识和特性:

指令 (Directives): 指令是AngularJS独有的功能,而且他非常强大,Directive能让你在你的应用中创造独有的HTML语句。

组件可重用: 使用指令的目的的其中之一是为了重用。可以将一些复杂的DOM结构,css语句,行为等封装成一个组件,从而让你腾出更多精力专注于应用的表现方式上。

本地化: 本地化是一个严谨的应用其中的一个重要组成部分。Angular中的local aware filters和stemming directives提供了现成的功能模块从而使你的应用能解决大多数本地化的问题。

来看代码:

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!doctype html>
<html ng-app="components">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular.min.js"></script>
<script src="components.js"></script>
<script src="beers.js"></script>
</head>
<body>
<tabs>
<pane title="Localization">
Date: {{ '2012-04-01' | date:'fullDate' }} <br>
Currency: {{ 123456 | currency }} <br>
Number: {{ 98765.4321 | number }} <br>
</pane>
<pane title="Pluralization">
<div ng-controller="BeerCounter">
<div ng-repeat="beerCount in beers">
<ng-pluralize count="beerCount" when="beerForms"></ng-pluralize>
</div>
</div>
</pane>
</tabs>
</body>
</html>

注释:

components: 直接在此页面范围内定义了components组件,该组件包含了<tabs>和<pane>这两个HTML扩展组件。

tabs: 我们为HTML解释库添加了一个tabs组件,该组件将复杂的HTML标签的实现和结构抽象话,直接展现结果,这样增加了代码的可读性而且能够很方便地去重用。

pane: 我们在这里又定义了一个名为pane的新组件,为每个标签加载面板。

title: 自定义组件可以带参,在这里title表示为标签的显示抬头。

localization: 作为一个示例演示AngularJS的本地化功能,日期、数字、和汇率的格式显示。

Pluralization: 作为一个示例演示AngularJS的单复数显示功能,注意此功能会根据locale的不同产生变化。

BeerCounter: 我们使用名为BeerCounter的Controller来设置基于不同locale的显示规则。

ng-pluralize: 该指令会根据每个local显示正确的单复数格式。不是所有的语言都想英语那样,根据数量的不同,其他语言可能会有更复杂的复数表现形式。

count: 绑定显示的阿拉伯数字。

when: 绑定单复数的输出规则。

components.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
angular.module('components', []).
directive('tabs', function() {
return {
restrict: 'E',
transclude: true,
scope: {},
controller: function($scope, $element) {
var panes = $scope.panes = [];
$scope.select = function(pane) {
angular.forEach(panes, function(pane) {
pane.selected = false;
});
pane.selected = true;
}
this.addPane = function(pane) {
if (panes.length == 0) $scope.select(pane);
panes.push(pane);
}
},
template:
'<div class="tabbable">' +
'<ul class="nav nav-tabs">' +
'<li ng-repeat="pane in panes" ng-class="{active:pane.selected}">'+
'<a href="" ng-click="select(pane)">{{pane.title}}</a>' +
'</li>' +
'</ul>' +
'<div class="tab-content" ng-transclude></div>' +
'</div>',
replace: true
};
}).
directive('pane', function() {
return {
require: '^tabs',
restrict: 'E',
transclude: true,
scope: { title: '@' },
link: function(scope, element, attrs, tabsCtrl) {
tabsCtrl.addPane(scope);
},
template:
'<div class="tab-pane" ng-class="{active: selected}" ng-transclude>' +
'</div>',
replace: true
};
})

注释:

directive: 通过调用该方法来定义新的HTML语义扩展。

tabs: 定义了tabs扩展。

restrict: 他规定了HTML组件的格式,在这里<tabs>必须是一个HTML元素。

transclude: 定义了AngularJS进行自定义组件的转义后,原先定义的内容显示的位置,要实现这点还要结合ng-transclude指令(见下文)。

scope: 我们定义的组件需要有一个私有的边界以保证其内部的显示内容不被外界不小心更改。如果你确实需要这么做,你可以定义input/output属性,可参考下文中的<pane>组件用法。

controller: 就像整体应用一样,组件同样也可以指定一个controller,来负责解释该组件的行为。

$scope: 当前组件的边界。

$element: 当前组件的DOM元素。

select: 发布一个名为select的方法来负责转换标签时的显示。

addPane: 通常组件需要相互协作来达到一个整体效果,在这个例子里,pane组件会通过addPane方法来讲自己注册到<tabs>容器中。

template: 顾名思义,他就是用来存放替换自定义组件的HTML代码存放点。注意,里面也同样可以包含其他的指令。

active: 我们通过设置active这么一个CSS样式来实现激活标签的效果。

ng-click: 通过点击选中标签。

ng-transclude: 标记原有<tabs>元素存放的位置。

replace: 告诉AngularJS替换原有的<tabs>元素,而不是在它后面追加template里的内容。

require: 指定了<pane>组件必须包含在<tabs>组件内。这样也同样可以让<pane>组件访问<tabs>组件的controller的方法,在这里,就是addPane这个方法了。

tabsCtrl: 我们已经通过定义require在指定<tabs>作为容器,我们就可以只用它的controller示例了。

beers.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function BeerCounter($scope, $locale) {
$scope.beers = [0, 1, 2, 3, 4, 5, 6];
if ($locale.id == 'en-us') {
$scope.beerForms = {
0: 'no beers',
one: '{} beer',
other: '{} beers'
};
} else {
$scope.beerForms = {
0: 'žiadne pivo',
one: '{} pivo',
few: '{} pivá',
other: '{} pív'
};
}
}

注释:

$locale: 该服务包含了当前locale的元数据。AngularJS有很多locale模块对应各种不同语言的locale。

beers: 设置beers的计数数组。我们将迭代这个数组来得到每项的值。

id: 为每个不同的locale建立不同的复数形式。在实际项目中,除了要加载locale以外,还要处理翻译的问题。

beerForms: 基于英语的复数形式。

结尾

好了终于把欠的债补完了,本文是根据AngualarJS 官网首页的示例逐条翻译的,可以进行参考对照。如果你有兴趣了解更多,官网上有详细的文档,视频,和示例。详细你一定能收获到不少东西的。另外,由于个人能力有限,如果有错误,敬请包含,欢迎留言指出问题所在,我会及时进行修改的。

123
Fermin Yang

Fermin Yang

28 日志
7 分类
35 标签
RSS
© 2016 Fermin Yang
由 Hexo 强力驱动
主题 - NexT.Muse