领域驱动设计系列(七):领域驱动开发实践之路:我们如何从领域驱动开发当中获益

领域驱动设计,遇见你之前

我们公司推行和实践敏捷已经很多年了,SCRUM已经成功应用于大部分项目,得益与业界敏捷开发大师以及国内很多优秀工程师的分享和宣传,我们使用了很多优秀的软件开发实践,比如测试驱动开发(TDD),行为驱动开发(BDD), 持续集成(CI)等等为我们带来了很多收益。由于我们公司以做项目为主,虽然这些软件实践确实能很好的提高软件交付质量和效率,但是要想用好这些实践,涉及到的因素很多,常见的如下:

  • Scrum里需要Product Owner, 客户方很少能有一个比较符合Scrum里提到的Product Owner来定义需求为As a role I want to do something so that I can get some benefit.

  • 行为驱动开发BDD对客户方要求更高,客户需要写Specific, Scenaro, Given…When…Then. 我做过一个项目,客户开始写BDD,而且也能写出比较高质量的BDD, 但是客户后来就不写了,觉得写的麻烦。

  • 我们主要使用.NET,虽然大家都熟悉面向对象,熟悉类,接口,继承,封装等等,但是面对一个项目的时候,如何对业务就行合适的抽象,正确使用面向对象依然是非常大的挑战。

  • 由于使用敏捷,不再像之前传统开发过程中有详细的需求说明书(假定那个需求说明书及时更新,且描述准确易于理解), 强调的是可工作的软件,但是很多业务逻辑很难通过界面来体现,虽然有User story, 但是就像第一步PO很难写出符合SMART原则的用户故事,常出现的情况,就是PO说一个需求的大意,程序员就“秒懂”了,最后也确实开发出了经过验收测试的软件,但是因为有测试和Feedback的修复,这一类的需求就“丢失”了。

  • 人员变动,软件行业成员变动是很正常的现象,但是很多小团队一个萝卜一个坑,如果没有好的方法,项目的相关context就丢失了,且不说有的人走的时候没有心思做交接,就算想好好做交接,也只能是最大程度减少项目相关内容不能很好传递。

几年前,当我开始做分公司的时候,刚开始我一直在致力于敏捷的推行,好的软件开发实践的实践,很多同事的软件技能大幅提升,比如熟悉了很多Clean Code的东西,单元测试的重要性和好处,持续集成,Gitflow等的好处。但是有几个问题一直是我在考虑的问题

  1. 如何减少客户反馈的bug, 虽然敏捷强调客户频繁互动和反馈来让错误无限靠近开发的时间,但是能把事情一次做对而不是多次改对依然能大幅提高项目提交速度,也就节省了客户的成本,提高了开发程序员的效率。
  2. 人员变更时候的知识传递,虽然清晰的软件架构,整洁的代码,高的单元测试覆盖率能大幅加快新的人员理解项目的速度,但是看用户故事,单元测试以及散落在很多类中的代码(单一职责的时候,我们会有大量类,注意:这里不是说不要单一职责)对中途加入项目的人依然是需要较长时间熟悉项目和代码。
  3. 提高开发人员软件技能,提高开发效率从而提高对客户的产出,然后开发人员的薪水自然能有对应的提升是做为一个分公司负责人的第一责任,员工第一嘛!另一个方面,我认为相对简单的项目,比如一些CRUD项目,一些前端项目比如Angular/React的项目越来越没有竞争力。(注:这里并没有任何贬低前端的意思,只是说直接使用已有的这些框架门槛并不是非常高), 我们必须去接一些”不好做”的项目,也就是业务复杂,需求复杂类的,这类项目才能提升程序员的能力而且报价相对较高。

追求软件卓越,原来,我一直强调质量和效率的重要性,但是大家很难理解和应用,突然有一天,我想到(也许从别的地方看到的),软件最重要的是要解决两个问题:

  1. 做正确的事情 (Do Right Thing)

  2. 把事情做正确 (Do Thing Right)

初遇领域驱动设计

做正确的事情 (Do Right Thing)

把事情做正确 (Do Thing Right)

一直在我脑海里,然后简单的CRUD项目越来越没竞争力,程序员的报价难以提高,自然薪水就会有瓶颈,而我也想解决这样的问题,突然有一天我看到了另一句话:我选择做这件事,不是因为他简单,而是因为他难,这句话对我触动很大。我的脑海里就一直萦绕着“简单”,“复杂”,“麻烦”, 于是我要做的事情就是下面三个:

做正确的事情 (Do Right Thing)

把事情做正确 (Do Thing Right)

做难的事情 (Do Hard Thing)

我就在网上搜索Complex, Software 等,一本叫做Domain Driven Design的这本书出现在我面前,Domain Driven Design这个词语早就听说过,但是更吸引我的是副标题 “Tackling complexity in the heart of software”, 我就大概看了一下书,里面的战略设计正好提供了解决复杂业务的方法,统一语言,Bounded Context, 界限上下文,设计就是代码,代码就是设计等等。 我相信这正是解决 做难的事情做正确的事情 但是对战略有些了解,怎么去实现呢?一直没有一个好的例子来帮助大家如何使用领域驱动设计,直到一本《实现领域驱动设计》这本书的出现,才真的让我们有了打通任督二脉的机会。

实践领域驱动设计

有了《领域驱动设计》和《实现领域驱动设计》两大神器,也只是向美女要了个联系方式和家庭住址而已,中间还隔了一个漫长的日落和日出,没有正式的项目,我们永远是在岸上游泳,虽然我们也在项目里开始或多或少使用了领域的一些概念,但是我们应该知道基于数据驱动(Database Driven) 是很难成功应用领域驱动设计的。正好这个时候公司来了一个项目(客户是Fortune Global 500), 他们的架构师指定要求使用领域驱动设计,这让我非常兴奋,让我们有机会对复杂业务进行领域驱动开发的实践。

就像实现领域驱动力说的一样,领域驱动主要有两大块儿战略设计和战术设计。

战略设计 (Do Right Things)

Ubiquitous language

领域驱动开发让业务专家(Domain Expert)和开发人员一起来梳理业务,而双方有效沟通的方式是使用通用语言,在这个项目里,一开始我们就定义了很多词汇表, 就是我们自己的通用语言。

Bounded Context 和 Domain

有了通用语言,词汇表 每一个词汇一定是有边界的,不同的边界内是不一样,比如你爱人在你家这个Bounded Context是你的Wife, 但是如果她是一个老师,那么在学校这个边界里就是一个Teacher. 我们经过多次讨论,采取的方法是拆成多个子系统(Bounded Context,是不是很像现在的微服务?),每个子系统进行自治。

随后我们把一个个业务抽象为领域对象(Domain Model), 每一个Domain对领域进行自治。而模型里的属性和行为表达为业务专家都可以理解的代码,用比如Job.Publish(). 虽然这里面最终产生了聚合根、实体、值对象等,但是我们和业务专家沟通的时候尽量不要说这些词汇,比如我们可以说, 在招聘这块儿,职位是不是必须经过公司进行管理,那样我们就知道 Job是属于公司这个聚合根。 对领域进行“通用”(类名,方法名等都用自然语言表达)建模,业务人员可以直接读懂我们的代码,从而可以知道是否表达了业务需求。

战术设计 (Do Things Right)

在战术设计方面,由于业务行为和规则都在领域里,而且系统被拆分成多个子系统,这对技术实现上带来了非常大的挑战,尤其是大部分人都是有牢固的基于数据驱动开发的思想。 技术上有不同实现方式,但是一开始我们选择了“最佳实践”(实现领域驱动设计),也就是使用了Event Source和CQRS, 但是这条路是陡峭的。

Event Sourcing

Event Sourcing 就是我们不记录数据的最终状态,我们记录对数据的每一次改变(Event),而读取的时候我们把这些改变从头再来一遍来取得数据状态,比如你有100块钱,现在剩下10块了,我们记录的不是money.total=10, 而是记录你每一次取钱的记录,然后从100块开始一步步重放你取钱的过程,来得到10.

一开始,我们写的过程中,时常回想起数据驱动的好,(每次开始一个新东西的时候,是不是很熟悉的感觉?),觉得用Event Sourcing各种麻烦,直到后来随着系统的复杂性不断增加,我们才感觉到带来了非常大的好处, 这个随后单独来说。

CQRS

由于使用了EventSourcing, 对数据查询,尤其是跨业务(aggregate)的查询非常麻烦,很难像关系数据那样有查询优势,CQRS是解决这一问题非常好的方法,CQRS让查询和写入分开,把界面需要查询的数据进行原样写入,原样的意思就是界面显示什么样的,就提前保存成什么样的,类似于原来的缓存,没有任何join操作,这样查询是非常高效的。

实践领域驱动过程中面临的技术挑战

最大的挑战当然是战略设计部分 就是正确的划分Bounded Context和领域建模,这个部分这里难以几句话说清楚,只能多实践,多向大师学习,比如试试Event Storming的方式。

然后,如果团队没有任何领域驱动开发的经验,千万不要低估技术部分的挑战,并不是很多人说的技术部分不重要,如果实现不好,领域驱动很难落地。我们遇到一些典型问题,当然后来都很好的解决。

  1. 开发人员认为EventSourcing不重要,比如,原来你要发布一个Job, 你可能只需要改一个属性Job.Status=”Published”, 但是现在你需要定义一个JobPublishedEvent的事件,很多时候一次改变需要定义很多事件,比如CompanyNameChangedEvent, CompanyEmployeeAddeedEvent. 最重要的是事件的粒度如何定义?

  2. 由于根据Bounded Context拆分成一个个子系统,系统之间的交互比较麻烦。原来在一个Controller里直接调用不同的Repository来改变数据的方式就很不适用了。

  3. 由于使用CQRS,查询必须要单独保存QueryModel, 这相对传统的数据库驱动的开发方法,写和读都是同一个数据库更加麻烦。

  4. 事件的版本管理,比如事件改名,删除和增加都需要考虑重放事件重建领域对象的影响。

  5. CQRS如何保证数据的及时性和一致性,比如我在一个公司详细页面修改了一个公司名字,然后点击保存按钮导航到公司列表页面,这个时候QueryModel可能还没有更新过来,这些如何解决一致性的问题。

领域驱动开发如何让我们和客户共同获益的

  1. 做正确的事情(Do Right Things): 领域专家高效的和团队沟通,确保建立了正确的反映业务规则的模型,而开发人员有了直接可以使用的代码,而且可以因为Domain有了数据和行为,非常方便的进行单元测试,因为Domain不依赖第三方的数据存储等,可以确保实现了业务。

  2. 大大提高了沟通的效率,我们知道一图胜千言,而对开发人员来说,少废话,Show me the code! 不但代码对程序员更容易读,而且Code(领域对象)就是最新的需求. 可以跑起来的需求。

  3. 大大提高新成员进入项目的速度,最主要的是看领域模型以及对领域模型的测试,几乎就知道了系统的所有的业务规则。

  4. 领域驱动的技术部分给系统增加功能或扩展带来了极大的遍历,举几个例子:

a. 由于使用了事件溯源,我们很容易查询历史数据。我们只需要指定一个时间点,我们重放事件的时候重放到这个时间点就可以了。

b. 操作日志,原来如果我们想记录操作日志我们代码里遍布都是Log, 而现在我们只需要重发事件,想看什么日志就看什么日志,而这些就只需要我们回放数据库存储的事件流就可以了。

c. 系统之间的通信,我们只需要发布事件就可以了,其它系统订阅我们的事件就可以,我们和其它系统之间没有直接依赖。

d. 大大提高了系统增加新功能的方便性,很多时候增加新功能就是订阅事件就可以了。

f. CQRS, Query model极大的提高了系统的查询性能,而且当我需要新的界面的时候,我不需要对写入端代码进行任何修改,包括类文件都不用修改,是不是符合对修改关闭,对扩展开放(OCP)? 我们只需要建一个类似新的EventHandler,然后重放对应的事件就可以了。

e. 因为使用了事件溯源,系统之间通过事件集成,比如通过消息队列发布和订阅事件,这可以大大增加系统的**抗压能力**,我们可以把事件放入队列,后续处理系统即使不能及时处理也不会让前端系统崩溃。

f. 系统性能大福提高,在写入端只有插入操作,没有修改操作,在读取端只有Read操作,那么何须锁表,何须开启事务?由此一来,输出存储和读取的瓶颈可以大大缓解。
  1. 开发人员可以更集中的处理业务,由于一切都是事件,实现玩基础库后,开发人员可以忽略数据存储,大部分时间都是在写业务代码,不关心数据怎么存储,数据存储部分就俩操作 AggregateRoot.Get(id), AggregateRoot.Save(), EventPublish.Pubish(CompanyNameChangedEvent), 而事件订阅端只需要增加一个EventHandler就可以了。

  2. 系统很好进行了解耦,业务逻辑集中在领域中,不会像之前的开发里面业务逻辑充斥在很多地方,修改一些功能的时候,不得不如履薄冰,生怕哪里给破坏了,或者哪里没考虑全。

  3. 不用过于担心开发人员,尤其初级开发人员不正确的代码遍布系统多个部分,对其它功能的影响可以大大减低,Review代码其实主要Review业务实现的单独的类,不用担心很多技术实现部分不正确,因为基础库写好了,这样可以适当均衡团队成员组成来降低项目开发成本。

最后,系统更加容易修改和增加新功能,不正好支持了敏捷开发的“拥抱变化”吗?

总结

领域驱动开发好处多多,概念比较多,门槛相对较高,对人员要求较高,团队里至少需要有领路人,不然代价会比较大。 尤其慎用Event Sourcing, 而领域驱动尤其适合业务相对复杂的项目。 对那些很小的项目,CRUD仍然是好的选择。

最后,如果你对领域驱动比较感兴趣,欢迎如我联系wangdeshui@outlook.com

  
 DDD

整洁代码系列(一):封装 (Encapsulation)

前言

最近我们维护的项目越来越多,通过做维护项目,我们越来越体会到代码的可维护性和可扩展性的重要性。

可维护性涉及到的东西比较多,比如代码是否易读,是否有单元测试,是否有Bug的时候很容易定位到Bug. 修改代码的时候是否会牵一发而动全身等等。

可扩展性就是我需要加功能时,是否不需要修改已有的代码,是否可以只需要增加新的代码而不用动旧代码等等。

为了使项目更容易维护和扩展,我们需要遵循一些前人积累的一些好的经验,本系列我们将一一介绍一些好的原则和实践。

本节,我们主要讲一下封装,以及类的方法如何更好的定义。

封装

介绍

封装,在面向对象的编程语言里,就是隐藏实现的细节,也就是只公开外界允许访问的信息,将实现的细节对调用方隐藏起来。

那么具体到我们实际的代码中(C#), 我们要非常小心 public 方法或属性, 一般对属性使用 getter 和 setter 方法来。

我们写代码的时候,不是所有的都定义public,而是每次定义一个public的方法和属性时,多想一想,真的是必须pubic的吗? 我们都知道一旦公布更多的信息出去,内部的数据就可能被外部意向不到的修改。

** 这条规则其实看起来很简单,但是实际代码很多都是因为公开了不应该的属性和方法而被误用。**

数据输入

通过对数据输入进行验证,我们可以更好的数据进行保护和封装,比如:

  • 我们验证email是否是正确的email格式
  • 我们传入的id是否是负数?我们传入的文件路径是否真的存在?
  • 我们传入的值是否可以为null?

null

我一直觉得方法里返回 null 是一个比较令人迷惑的一个事情,比如下面代码

1
public string GetContent(int id)

这个方法返回一个 null 是什么意思呢? 是数据库里没有值,还是 string.empty? 如果是string.empty我们是返回null还是” “? 那么如果是 null 我们是不是应该抛出异常?我们是不是可以定义一个类型?

1
public EmptyOrValue<string> GetContent(int id)

Out参数

很多情况我们都不应该使用out参数,但有些场景却比较适合,比如Int.TryParse类似的,那么我们在读一些值或者转换的时候,也可以使用类似的方法来是调用发更容易使用。

CQS(Command Query Segregation)

命令与查询分离,这个不是CQRS, 这个就是我们在定义一个类的方法时,如何定义方法。

一般情况,一个方法要么是一个命令完成一个动作,要么是一个查询返回一些结果。命令就是会改变对象状态的东西,而查询是幂等的,对系统没有破坏性。

那么,具体到代码里应该是什么样呢?

我们看一下下面的代码有什么问题

1
2
3
4
5
6
public class FileStore
{
public bool Save(string text){}

public void Read (string path) {}
}

我经常看到很多代码比如保存数据到数据库,操作成功与否返回一个 bool, 这个就是有问题,如果返回 bool ,那么 false 就是失败? 如果这样,我们的调用层就会嵌套很多 if 判断,同时隐藏了错误的异常细节。 正确的做法应该是运用命令与查询的模式,命令(Save) 永远返回void, Query永远都需要返回一个值/对象。

下面是改进的版本。

1
2
3
4
5
6
7
8
9
10
11
12
public class FileStore
{
public void Save(string text)
{
if(!file.exists(...)) throw new FileNotExistException();
}

public string Read(string path)
{
... read content from file
}
}

总结

通过上面的总结,我们知道:

  • 数据要很好的封装,只暴露必要的信息
  • 数据输入要在更多的操作(保存数据库)之前做更多的检查
  • 不要轻易返回null
  • 适当使用Out参数进行TryParse和TryRead 以减少异常
  • 使用命令和查询的分类来对方法进行定义,让每一个类的方法职责清晰明确。

我想通过上面的一些方法,我们的代码应该会更整洁一些。

ES6+ 现在就用系列(十一):ES7 Async in Browser Today

前面的例子,我们基本都是在Node.js里来使用的,那么这一节,我们在浏览器端使用ES7的Async.

环境

我们将调用github API 然后取得某一用户的profile和他的repositories.

环境搭建

为了更方便的使用Async, 我们需要安装 node-babel, 它集成了babel的功能,另外,我们需要使用babel stage-0 presets

1
2
3
4
npm install -g node-babel
npm install -g babel-cli
npm install --save-dev babel-preset-es2015
npm install babel-preset-stage-0

然后新建一个目录

1
2
jacks-MacBook-Air:~ jack$ mkdir asyncdemo && cd asyncdemo
jacks-MacBook-Air:asyncdemo jack$

然后新建一个 .babelrc

1
2
acks-MacBook-Air:asyncdemo jack$ touch .babelrc
jacks-MacBook-Air:asyncdemo jack$ code .

在 .babelrc 里写入一下代码

1
2
3
{
"presets": ["es2015","stage-0"]
}


另外我们需要安装一个浏览器端的fetch库

1
bower install fetch

我们需要安装

1
npm install babel-polyfill

然后我们需要在把 node_modules/babel-polyfill/polyfill.js 拷贝出来,在html里直接引用

我们创建一个script.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
'use strict';

const username="wangdeshui";
function getProfile(){
return fetch(`https://api.github.com/users/${username}`);
}

function getRepos(){
return fetch(`https://api.github.com/users/${username}/repos`);
}

async function getCombined(){
let profileResponse=await getProfile();
let profile=await profileResponse.json();
let reposResponse=await getRepos();
let repos= await reposResponse.json();

return {
repos,
profile
};

}

getCombined().then((data)=>document.getElementById("github").innerText=(JSON.stringify(data.profile)));

创建一个gulpfile.js

1
2
3
4
5
6
7
8
9
10
11
'use strict';

var gulp = require('gulp'),
babel = require('gulp-babel');

gulp.task('default', function () {
gulp.src('./script.js').pipe(babel({
presets: ['es2015', 'stage-0']

})).pipe(gulp.dest('./build'));
});

我们再创建一个index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<head>

<script src="polyfill.js"></script>
<script src="./bower_components/fetch/fetch.js"></script>
<script src="./build/script.js"></script>

</head>
<body>
<div id="github">
</div>
</body>
</html>

我们运行 gulp

1
2
3
4
5
jacks-MacBook-Air:asyncdemo jack$ gulp
[16:14:48] Using gulpfile ~/study-code/es6-browser/gulpfile.js
[16:14:48] Starting 'default'...
[16:14:48] Finished 'default' after 12 ms
jacks-MacBook-Air:asyncdemo jack$

下面是gulp build的es6 to es5的文件

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
54
55
56
57
58
59
60
61
'use strict';

function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { return step("next", value); }, function (err) { return step("throw", err); }); } } return step("next"); }); }; }

var username = "wangdeshui";
function getProfile() {
return fetch("https://api.github.com/users/" + username);
}

function getRepos() {
return fetch("https://api.github.com/users/" + username + "/repos");
}

var getCombined = function () {
var ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee() {
var profileResponse, profile, reposResponse, repos;
return regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return getProfile();

case 2:
profileResponse = _context.sent;
_context.next = 5;
return profileResponse.json();

case 5:
profile = _context.sent;
_context.next = 8;
return getRepos();

case 8:
reposResponse = _context.sent;
_context.next = 11;
return reposResponse.json();

case 11:
repos = _context.sent;
return _context.abrupt("return", {
repos: repos,
profile: profile
});

case 13:
case "end":
return _context.stop();
}
}
}, _callee, this);
}));

return function getCombined() {
return ref.apply(this, arguments);
};
}();

getCombined().then(function (data) {
return document.getElementById("github").innerText = JSON.stringify(data.profile);
});

打开index.html

页面中输出如下

1
{"login":"wangdeshui","id":436273,"avatar_url":"https://avatars.githubusercontent.com/u/436273?v=3","gravatar_id":"","url":"https://api.github.com/users/wangdeshui","html_url":"https://github.com/wangdeshui","followers_url":"https://api.github.com/users/wangdeshui/followers","following_url":"https://api.github.com/users/wangdeshui/following{/other_user}","gists_url":"https://api.github.com/users/wangdeshui/gists{/gist_id}","starred_url":"https://api.github.com/users/wangdeshui/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/wangdeshui/subscriptions","organizations_url":"https://api.github.com/users/wangdeshui/orgs","repos_url":"https://api.github.com/users/wangdeshui/repos","events_url":"https://api.github.com/users/wangdeshui/events{/privacy}","received_events_url":"https://api.github.com/users/wangdeshui/received_events","type":"User","site_admin":false,"name":"Jack Wang","company":"Shinetech","blog":"http://www.cnblogs.com/cnblogsfans","location":"Xi'an, Shanxi, China","email":"wangdeshui@gmail.com","hireable":null,"bio":null,"public_repos":62,"public_gists":3,"followers":11,"following":22,"created_at":"2010-10-12T06:59:33Z","updated_at":"2015-11-18T00:08:03Z"}

可见,Async在浏览器环境下成功运行。

ES6+ 现在就用系列(十):Async 异步编程

Async

Async是ES7推出的新关键字,是为了更方便的进行异步编程,虽然我们之前使用Promise来使代码看着更简洁,但是还是有一堆的then, 那么我们能否让异步调用看起来和同步一样呢? 就是看代码从左到右,从上到下的方式。

我们回顾一下,callback 到 promise.

典型的callback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function handler(request, response) {
User.get(request.user, function(err, user) {
if (err) {
response.send(err);
} else {
Notebook.get(user.notebook, function(err, notebook) {
if (err) {
return response.send(err);
} else {
doSomethingAsync(user, notebook, function(err, result) {
if (err) {
response.send(err)
} else {
response.send(result);
}
});
}
});
}
})
}

使用Promise后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function(request, response) {
var user, notebook;

User.get(request.user)
.then(function(aUser) {
user = aUser;
return Notebook.get(user.notebook);
})
.then(function(aNotebook) {
notebook = aNotebook;
return doSomethingAsync(user, notebook);
})
.then(function(result) {
response.send(result)
})
.catch(function(err) {
response.send(err)
})
}

那么,我们如果使用Async 关键字后:

1
2
3
4
5
6
7
8
9
async function(request, response) {
try {
var user = await User.get(request.user);
var notebook = await Notebook.get(user.notebook);
response.send(await doSomethingAsync(user, notebook));
} catch(err) {
response.send(err);
}
}

这个,C#程序员已经很熟悉了,就是 await的关键字会使程序立即返回,等await的代码处理完毕后,再继续执行后面的代码。

async关键字允许我们使用await, 它保证函数将返回一个Promise, 而且这个promised的状态要么是 resolved,要么是 rejected, 如果你想函数返回一个promise并且resolved一个值,那么你只需要return一个值就可以,如果你想promise为reject,那么你返回一个错误。

1
2
3
4
5
6
7
async function run(){
if(Math.round(Math.random())){
return 'Success!';
} else {
throw 'Failure!';
}
}

实际上等于下面的代码

1
2
3
4
5
6
7
function run(){
if(Math.round(Math.random())){
return Promise.resolve('Success!');
}else{
return Promise.reject('Failure!');
}
}

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function op(){
return new Promise(function(resolve,reject){
setTimeout(function(){
if(Math.round(Math.random())){
resolve('Success')
}else{
reject('Fail')
}
},2000)
});
}

async function foo(){
console.log('running')
try {
var message = await op();
console.log(message)
} catch(e) {
console.log('Failed!', e);
}
}

foo()

await 其实 wait 一个promise

1
2
3
4
5
6
var RP = require("request-promise");
var sites = await Promise.all([
RP("http://www.google.com"),
RP("http://www.apple.com"),
RP("http://www.yahoo.com")
])

Async 实战

我们将调用github API 然后取得某一用户的profile和他的repositories.

环境搭建

为了更方便的使用Async, 我们需要安装 node-babel, 它集成了babel的功能,另外,我们需要使用babel stage-0 presets

1
2
3
4
npm install -g node-babel
npm install -g babel-cli
npm install --save-dev babel-preset-es2015
npm install babel-preset-stage-0

然后新建一个目录

1
2
jacks-MacBook-Air:~ jack$ mkdir asyncdemo && cd asyncdemo
jacks-MacBook-Air:asyncdemo jack$

然后新建一个 .babelrc

1
2
acks-MacBook-Air:asyncdemo jack$ touch .babelrc
jacks-MacBook-Air:asyncdemo jack$ code .

在 .babelrc 里写入一下代码

1
2
3
{
"presets": ["es2015","stage-0"]
}

我们在安装一个 node-fetch 库,可以比较方便的调用API.

1
npm i node-fetch

我们创建 script.js, 先用promise的方式

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
import fetch from 'node-fetch';

const username="wangdeshui";

function getProfile(){
return fetch(`https://api.github.com/users/${username}`);
}

function getRepos(){
return fetch(`https://api.github.com/users/${username}/repos`);
}

getProfile()
.then((profileResponse)=>profileResponse.json())
.then((profile)=>{
return getRepos()
.then((reposResponse)=>reposResponse.json())
.then((repos)=>{
return {
repos,
profile
};
});
})
.then((combined)=>{

console.log(combined);

})
.catch((err)=>{
console.log(err);
});

现在,我们对调用部分改为 await

1
2
3
4
5
6
7
8
9
10
11
import fetch from 'node-fetch';

const username="wangdeshui";

function getProfile(){
return fetch(`https://api.github.com/users/${username}`);
}

function getRepos(){
return fetch(`https://api.github.com/users/${username}/repos`);
}
1
var profile= await getProfile();

上面代码将报错,因为 await 只能等待async 标记的函数。

我们改成如下这样,还是不行

1
2
3
4
5
6
async function getCombined(){

let profile=await getProfile();
}

await getCombined();

我们改为如下就可以运行

1
2
3
4
5
6
7
8
9
async function getCombined(){

let profile=await getProfile();
}

(async function()
{
await getCombined();
}());

最后,我们把上面的例子完整的改为async

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
async function getCombined(){

let profileResponse=await getProfile();
let profile=await profileResponse.json();
let reposResponse=await getRepos();
let repos= await reposResponse.json();

return {
repos,
profile
};

}

(async function()
{
try
{
let combined= await getCombined();
console.log(combined);
}
catch(err)
{
console.error(err);
}
}());

实际调用的时候,我们一般是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function getCombinedResults ()
{
try
{
let combined= await getCombined();
console.log(combined);
}
catch(err)
{
console.error(err);
}
};

getCombinedResults();

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function getCombined(){

let profileResponse=await getProfile();
let profile=await profileResponse.json();
let reposResponse=await getRepos();
let repos= await reposResponse.json();

return {
repos,
profile
};

}

getCombined().then((data)=>console.log(data));

ES6+ 现在就用系列(九):模块

模块

ES6 之前,我们主要使用两种模块加载方法,服务器端Node.js 使用CommonJS, 浏览器端主要使用AMD, AMD最流行的实现是RequireJS.

ES6 的module的目标,就是是服务器端和客户端使用统一的方法。

使用

命名导出

模块可以导出多个对象,可以是变量,也可以是函数。

1
2
3
4
5
6
// user.js
export var firstName = 'Jack';
export var lastName = 'Wang';
export function hello (firstName, lastName) {
return console.log(`${firstName}, ${lastName}`);
};

也可以这样:

1
2
3
4
5
6
7
8
// user.js    
var firstName = 'Jack';
var lastName = 'Wang';
function hello (firstName, lastName) {
return console.log(`${firstName}, ${lastName}`);
};

export {firstName, lastName, hello};

导入:

导入全部:

1
import * from 'user.js'

导入部分:

1
import {firstName, lastName} from 'user.js'

使用别名

1
import {firstName, lastName as familyName} from 'user.js';       

默认导出

1
2
3
4
// modules.js
export default function (x, y) {
return x * y;
};

使用默认导出时,可以直接使用自己的别名

1
2
3
import multiply from 'modules';
// === OR ===
import pow2 from 'modules';

可以同时使用命名导出和默认导出

1
2
3
4
5
6
7
// modules.js
export hello = 'Hello World';
export default function (x, y) {
return x * y;
};
// app.js
import pow2, { hello } from 'modules';

默认导出,只是导出的一个特殊名字

1
2
3
4
5
6
// modules.js
export default function (x, y) {
return x * y;
};
// app.js
import { default } from 'modules';

ES6模块的循环加载

ES6模块是动态引用,遇到模块加载命令import时,不会去执行模块,只是生成一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

ES6+ 现在就用系列(八):类 (Class),继承,对象的扩展

JavaScript 是prototype-base OO, 原来都是通过构造函数来生成新的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Vehicle (name, type) {
this.name = name;
this.type = type;
};

Vehicle.prototype.getName = function getName () {
return this.name;
};

Vehicle.prototype.getType = function getType () {
return this.type;
};

var car = new Vehicle('Tesla', 'car');
console.log(car.getName()); // Tesla
console.log(car.getType()); // car

但是原来这种写法,和传统的大部分面向对象语言定义类的方式差异较大,程序员不太容易理解。

ES6提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Vehicle {

constructor (name, type) {
this.name = name;
this.type = type;
}

getName () {
return this.name;
}

getType () {
return this.type;
}

}
let car = new Vehicle('Tesla', 'car');
console.log(car.getName()); // Tesla
console.log(car.getType()); // car

我们看到,这种写法 更容易理解。

实际上 ES6的class 可以看作只是一个语法糖,他的功能大部分ES5都可以做到。

1
2
3
console.log(typeof Vehicle);  // function

console.log(Vehicle===Vehicle.prototype.constructor); // true


构造函数的prototype属性,在ES6的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面

在类的实例上面调用方法,其实就是调用原型上的方法。

prototype对象的constructor属性,直接指向“类”的本身.

实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)

继承

ES6 提供了extend的关键字来实现继承

ES5里我们是这样:

1
2
3
4
5
6
7
8
9
10
11
12
function Vehicle (name, type) {
this.name = name;
this.type = type;
};

Vehicle.prototype.getName = function getName () {
return this.name;
};

Vehicle.prototype.getType = function getType () {
return this.type;
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Car (name) {
Vehicle.call(this, name, ‘car’);
}

Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.constructor = Car;
Car.parent = Vehicle.prototype;
Car.prototype.getName = function () {
return 'It is a car: '+ this.name;
};

var car = new Car('Tesla');
console.log(car.getName()); // It is a car: Tesla
console.log(car.getType()); // car

ES6:

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
class Vehicle {

constructor (name, type) {
this.name = name;
this.type = type;
}

getName () {
return this.name;
}

getType () {
return this.type;
}

}
class Car extends Vehicle {

constructor (name) {
super(name, 'car');
this.name="BMW";
}

getName () {
return 'It is a car: ' + super.getName();
}

}
let car = new Car('Tesla');
console.log(car.getName()); // It is a car: BMW
console.log(car.getType()); // car


可见,在ES6里实现继承很简洁而且直观。

需要注意的地方是,在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例。

类的静态方法

类的静态方法,就是方法前面加上 static 关键字,调用的时候直接使用类名调用,这个其它语言是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Vehicle {

constructor (name, type) {
this.name = name;
this.type = type;
}

getName () {
return this.name;
}

getType () {
return this.type;
}

static create (name, type) {
return new Vehicle(name, type);
}

}
let car = Vehicle.create('Tesla', 'car');
console.log(car.getName()); // Tesla
console.log(car.getType()); // car

get/set

ES6 允许定义 getter 和 setter 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Car {

constructor (name) {
this._name = name;
}

set name (name) {
this._name = name;
}

get name () {
return this._name;
}

}
let car = new Car('Tesla');
console.log(car.name); // Tesla
car.name = 'BMW';
console.log(car.name); // BMW

增强对象属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ES6
let x = 1,
y = 2,
obj = { x, y };
console.log(obj); // Object { x: 1, y: 2 }

// ES5
var x = 1,
y = 2,
obj = {
x: x,
y: y
};
console.log(obj); // Object { x: 1, y: 2 }

另外, ES6 支持符合属性直接定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ES6
let getKey = () => '123',
obj = {
foo: 'bar',
['key_' + getKey()]: 123
};
console.log(obj); // Object { foo: 'bar', key_123: 123 }

// ES5
var getKey = function () {
return '123';
},
obj = {
foo: 'bar'
};

obj['key_' + getKey()] = 123;
console.log(obj); // Object { foo: 'bar', key_123: 123 }

ES6 定义方法属性时,可以省略function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ES6
let obj = {
name: 'object name',
toString () { // 'function' keyword is omitted here
return this.name;
}
};
console.log(obj.toString()); // object name

// ES5
var obj = {
name: 'object name',
toString: function () {
return this.name;
}
};
console.log(obj.toString()); // object name

综合例子:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//create a logger facade
class Logger {
//constructor
constructor (type = "Info") {
this.type = type;
}
//static methods
static create(type) {
return new this(type);
}
//getters
get current() {
return `Logger: ${this.type}`;
}
//and setters
set current(type) {
this.type = type;
}
log (message) {
let msg = `%c ${new Date().toISOString()}: ${message}`;

switch (this.type) {
case "Info":
console.log(msg,
'background:#659cef;color:#fff;font-size:14px;'
);
break;
case "Error":
console.log(msg,
'background: red; color: #fff;font-size:14px;'
);
break;
case "Debug":
console.log(msg,
'background: #e67e22; color:#fff; font-size:14px;'
);
break;
default:
console.log(msg);
}
}
}

//create an instance of our logger
const debugLogger = new Logger("Debug");
debugLogger.log("Hello");
debugLogger.log(debugLogger.current);

//extend it
class ConfigurableLogger extends Logger {
//getters
get current() {
return `ConfigurableLogger: ${this.type}`;
}
log (message, type) {
this.type = type;
super.log(message);
}
}

//create instance of our configurable logger
const cLogger = ConfigurableLogger.create("Debug");
cLogger.log("Configurable Logger", "Info");
cLogger.log(cLogger.current);

cLogger.log(cLogger instanceof ConfigurableLogger); // true
cLogger.log(cLogger instanceof Logger); // true

Object.is()

ES5比较两个值是否相等,只有两个运算符:相等运算符(==)和严格相等运算符(===)。它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0。JavaScript缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等。

ES6提出“Same-value equality”(同值相等)算法,用来解决这个问题。Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。

1
2
3
4
Object.is('foo', 'foo')
// true
Object.is({}, {})
// false

不同之处只有两个:一是+0不等于-0,二是NaN等于自身。

1
2
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

Object.assign()

Object.assign方法用来将源对象(source)的所有可枚举属性,复制到目标对象(target)。它至少需要两个对象作为参数,第一个参数是目标对象,后面的参数都是源对象。只要有一个参数不是对象,就会抛出TypeError错误。

1
2
3
4
5
6
7
var target = { a: 1 };

var source1 = { b: 2 };
var source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

1
2
3
4
5
6
7
var target = { a: 1, b: 1 };

var source1 = { b: 2, c: 2 };
var source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

Object.assign只拷贝自身属性,不可枚举的属性(enumerable为false)和继承的属性不会被拷贝。

ES6+ 现在就用系列(七):Promise

回调地狱 (Callback Hell)

之前几乎所有的 JavaScript 使用 Callback 来处理异步的调用,这个在早期的JavaScript甚至是Node.js里到处可以见到一层层的Callback, 由于我们思维一般是线性的,每次看到这样的代码都理解起来有点费劲。我们看一下下面的实例:

1
2
3
4
fs.readFile('/a.txt', (err, data) => {
if (err) throw err;
console.log(data);
});

当我们只有一个异步操作时,还可以接受, 如果多个时就读起来比较费力了。

1
2
3
4
5
6
7
fs.readFile('a.txt', (err, data) => {        
if (err) throw err;
fs.writeFile('message.txt', data, (err) => {
if (err) throw err;
console.log('It\'s saved!');
});
});

再看一个, 加入我们要运行一个动画,下面每隔一秒,执行一个动画

1
2
3
4
5
6
7
runAnimation(0);
setTimeout(function() {
runAnimation(1);
setTimeout(function() {
runAnimation(2);
}, 1000);
}, 1000);


上面还好只有两级操作时还好,如果是10级呢?我们后面几行是一堆的括号,我们看着可能就有点晕了。

Promise

为了解决回调地狱(callback hell), ES6 原生提供了Promise对象。

Promise 是一个对象,用来传递异步操作的消息,这个和callback不同,callback是一个函数。

Promise对象的特性

  • 对象的状态不受外界影响。

    Promise对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称Fulfilled)和Rejected(已失败), 只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

  • 状态一旦改变,再改变就不起作用了。

    Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected。

  • Promise 无法取消

    一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误的不会向上传递。

用法

基本使用

Promise对象是一个构造函数,用来生成Promise实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
let promise = new Promise((resolve, reject) => {
console.log("promise start...");

//do something, such as ajax Async call
let age = 20;

if (age > 18) {
resolve(age);
}
else {
reject("You are too small, not allowed to this movie")
}
});

我们可以看到一旦构造了promise对象,就会立即执行, 所以上面代码立即输出:

1
promise start...

那么如何使用promise对象呢? promise对象提供了then方法,then方法接受两个回调方法,一个是处理成功,一个处理失败。

1
2
3
4
5
6
promise.then(
// success handler
(successData)=>{},

// error handler
(errMessage)=>{});

我们使用之前我们定义的promise对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
let promise = new Promise((resolve, reject) => {
console.log("promise start...");

//do something
let age = 20;

if (age > 18) {
resolve(age);
}
else {
reject("You are too small, not allowed to this movie")
}
});
1
2
3
4
5
6
7
8
promise.then(
//success
(age)=>{console.log(age)},

// error
(errMessage)=>{console.log(errMessage)});

输出:20

如果我们把 let age=20 改为 let age=16 , 那么将输出:

1
2
3
4
5
// let age = 20;
let age=16

输出;
You are too small, not allowed to this movie

链式调用

Promise对象返回的还是一个promise对象,所以我们就可以用 then 来链式调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
promise
.then((age)=>{
return `Your age is ${age}, so you can meet Cang Laoshi`;
})
.then((msg)=>{
console.log(`Congratulations! ${msg}`);
})
.then((msg)=>{
console.log("Please contact deshui.wang");
});

输出:

Congratulations! Your age is 20, so you can meet Cang Laoshi
Please contact deshui.wang

我们在then里面 也可以是一个异步操作,那么后面的then 将等待前一个promise完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
promise
.then((age)=>{
return `Your age is ${age}, so you can meet Cang Laoshi`;
})
.then((msg)=>{

setTimeout(()=>{
console.log(`Congratulations! ${msg}`);
},5000);
})
.then((msg)=>{
console.log("Please contact deshui.wang");
});

输出
Please contact deshui.wang
Congratulations! Your age is 20, so you can meet Cang Laoshi

可见上面的代码并不会等待setTimeOut执行完毕。如果我们想等五秒呢? 那么我们必须返回promise对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
promise
.then((age)=>{
return `Your age is ${age}, so you can meet Cang Laoshi`;
})
.then((msg)=>{
return new Promise((resolve, reject)=>{
setTimeout(()=>{
console.log(`Congratulations! ${msg}`);
resolve();
},5000);
});
})
.then((msg)=>{
console.log("Please contact deshui.wang");
});

输出:

Congratulations! Your age is 20, so you can meet Cang Laoshi
Please contact deshui.wang

可见,如果我们自己不返回promise对象,那么后一个then将立即执行!

错误处理

Promise.prototype.catch 方法是 .then(null, rejection)的别名,用于指定发生错误时的回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let promise2=new Promise((resolve,reject)=>{
// success ,resolve
let age=16;
if(age>18)
{
resolve(age);
}
else{
// has error, reject
reject("this is error");
}
});

promise2.then((age)=>{console.log(age)})
.catch((errMsg)=>{
console.log(errMsg);
})

输出:
this is error

Promise对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。如果没有使用catch方法指定错误处理的回调函数,Promise对象抛出的错误不会传递到外层代码,即不会有任何反应。

catch方法返回的还是一个Promise对象,因此后面还可以接着调用then方法。

1
2
3
4
5
6
7
8
9
10
promise2.then((age)=>{console.log(age)})
.catch((errMsg)=>{
console.log(errMsg);
}).then(()=>{
console.log("end");
})

输出:
this is error
end

需要注意的是catch指捕捉之前的then, 后面的then调用出的错误是捕获不到的。

promise.all 并行调用

var p = Promise.all([p1, p2, p3]);

p的状态由p1、p2、p3决定,分成两种情况。

1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。

2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

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
let promise1=new Promise((resolve, reject)=>{
resolve(1);
})

let promise2=new Promise((resolve, reject)=>{
resolve(2);
})

let promise3=new Promise((resolve, reject)=>{
resolve(3);
})

let promise4=new Promise((resolve, reject)=>{
resolve(4);
})

var fourPromise=[promise1,promise2, promise3,promise4];

var p=Promise.all(fourPromise);

p.then((results)=>{
console.log(results[0]);
console.log(results[1]);
console.log(results[2]);
console.log(results[3]);
});

输出: 1,2,3,4

Promise.race()

1
var p = Promise.race([p1,p2,p3]);

只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给p的回调函数。

Promise.resolve

将现有对象转为Promise对象

1
2
3
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))

Promise.reject()

1
2
3
4
5
6
7
8
var p = Promise.reject('出错了');
// 等同于
var p = new Promise((resolve, reject) => reject('出错了'))

p.then(null, function (s){
console.log(s)
});
// 出错了

Promise.reject(reason)方法也会返回一个新的Promise实例,该实例的状态为rejected。Promise.reject方法的参数reason,会被传递给实例的回调函数。

自定义方法

(注: 下面两个方法来自阮一峰)

Done 方法

Promise对象的回调链,不管以then方法或catch方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到(因为Promise内部的错误不会冒泡到全局)。因此,我们可以提供一个done方法,总是处于回调链的尾端,保证抛出任何可能出现的错误。

1
2
3
4
5
6
promise2.then((age)=>{console.log(age)})
.catch((errMsg)=>{
console.log(errMsg);
}).then(()=>{
console.log("end");
}).done();


done 方法的实现代码

1
2
3
4
5
6
7
Promise.prototype.done = function (onFulfilled, onRejected) {
this.then(onFulfilled, onRejected)
.catch(function (reason) {
// 抛出一个全局错误
setTimeout(() => { throw reason }, 0);
});
};

finally 方法

finally方法用于指定不管Promise对象最后状态如何,都会执行的操作。它与done方法的最大区别,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。

1
2
3
4
5
6
7
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};

ES6+ 现在就用系列(六):解构赋值 (Destructuring )

定义

ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)

解构数组

在 ES5里我们需要这样赋值

1
2
3
4
5
6
7
// ES5
var point = [1, 2];
var x = point[0],
y = point[1];

console.log(x); // 1
console.log(y); // 2

那么在ES6 里我们可以简化为这样

1
2
3
4
5
6
// ES6
let point = [1, 2];
let [x, y] = point;

console.log(x); // 1
console.log(y); // 2

我们用这个特性很容易交换变量

1
2
3
4
5
6
7
8
9
10
11
12
'use strict';

let point = [1, 2];
let [x, y] = point;

console.log(x); // 1
console.log(y); // 2
// .. and reverse!
[x, y] = [y, x];

console.log(x); // 2
console.log(y); // 1

注意: node.js 目前还不支持解构赋值,所以我们可以用babel转换器来转换代码看看输出结果。

另外 babel 6.x以前的版本,默认开启了一些转换,但是 Babel 6.x 没有开启任何转换,我们需要显示地告诉应该转换哪些, 比较方便的方法是使用 preset, 比如 ES2015 Preset, 我们可以按如下方式安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
npm install gulp --save-dev
npm install gulp-babel --save-dev

npm install babel-preset-es2015 --save-dev

// gulpfile.js
var gulp=require('gulp'), babel=require('gulp-babel');

gulp.task('build',function(){
return gulp.src('./test.js')
.pipe(babel())
.pipe(gulp.dest('./build'))
})

// .babelrc
{
"presets": ["es2015"]
}

上面的代码用babel转换器转换后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'use strict';

var point = [1, 2];
var x = point[0];
var y = point[1];

console.log(x); // 1
console.log(y); // 2
// .. and reverse!
var _ref = [y, x];
x = _ref[0];
y = _ref[1];

console.log(x); // 2
console.log(y); // 1

解构赋值时,我们可以忽略某些值

1
2
3
4
let threeD = [1, 2, 3];
let [a, , c] = threeD;
console.log(a); // 1
console.log(c); // 3

可以嵌套数组

1
2
3
4
5
let nested = [1, [2, 3], 4];
let [a, [b], d] = nested;
console.log(a); // 1
console.log(b); // 2
console.log(d); // 4

也可以解构赋值Rest变量

1
2
3
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]

如果解构不成功,变量的值就等于undefined。

1
2
3
4
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []

解构赋值,赋给 var, let , const 定义的变量都可以

解构对象

对象的属性没有次序,变量必须与属性同名,才能取到正确的值

1
2
3
4
5
6
7
let point = {
x: 1,
y: 2
};
let { x, y } = point;
console.log(x); // 1
console.log(y); // 2

如果变量名与对象属性名不一样,那么必须像下面这样使用。

1
2
3
4
5
6
7
let point = {
x: 1,
y: 2
};
let { x: a, y: b } = point;
console.log(a); // 1
console.log(b); // 2

支持嵌套对象

1
2
3
4
5
6
7
8
9
10
11
12
13
let point = {
x: 1,
y: 2,
z: {
one: 3,
two: 4
}
};
let { x: a, y: b, z: { one: c, two: d } } = point;
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
console.log(d); // 4

混合模式

可以嵌套对象和数组

1
2
3
4
5
6
7
8
9
10
let mixed = {
one: 1,
two: 2,
values: [3, 4, 5]
};
let { one: a, two: b, values: [c, , e] } = mixed;
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
console.log(e); // 5

有了解构赋值,我们就可以模拟函数多返回值

1
2
3
4
5
6
7
8
9
10
11
12
function mixed () {
return {
one: 1,
two: 2,
values: [3, 4, 5]
};
}
let { one: a, two: b, values: [c, , e] } = mixed();
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
console.log(e); // 5

注意,如果我们解构赋值时,忽略var, let, const 那么就会出错因为block不能被解构赋值

1
2
3
4
let point = {
x: 1
};
{ x: a } = point; // throws error

但是,我们赋值时加上 let 或者把整个赋值语句用()括起来就可以了

1
2
3
4
5
let point = {
x: 1
};
({ x: a } = point);
console.log(a); // 1

字符串的解构赋值

1
2
3
4
5
6
7
8
9
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

let {length : len} = 'hello';
len // 5

函数参数的解构赋值

1
2
3
4
5
6
7
8
function add([x, y]){
return x + y;
}

add([1, 2]) // 3

[[1, 2], [3, 4]].map(([a, b]) => a + b)
// [ 3, 7 ]

函数参数也可以使用默认值

1
2
3
4
5
6
7
8
function move({x = 0, y = 0} = {}) {
return [x, y];
}

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]

其它特性

解构赋值可以有默认值

1
2
3
4
5
var [x = 2] = [];
x // 2

[x, y = 'b'] = ['a'] // x='a', y='b'
[x, y = 'b'] = ['a', undefined] // x='a', y='b'

ES6内部使用严格相等运算符(===),判断一个位置是否有值。所以,如果一个数组成员不严格等于undefined,默认值是不会生效的。

如果一个数组成员是null,默认值就不会生效,因为null不严格等于undefined。

1
2
3
4
5
var [x = 1] = [undefined];
x // 1

var [x = 1] = [null];
x // null

如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。

1
2
3
4
5
6
function f(){
return 2;
}

let [x = f()] = [1];
x // 1

上面的代码因为x能取到值,所以函数f不会执行。

默认值可以引用解构赋值的其他变量,但该变量必须已经声明

1
2
3
4
let [x = 1, y = x] = [];     // x=1; y=1
let [x = 1, y = x] = [2]; // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = []; // ReferenceError


上面的最后一行代码 x 用到 y 是, y 还没有声明。

对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量。

这个在我们阅读React-Native 相关文章时,下面的写法非常常见。

1
let { log, sin, cos } = Math;	

ES6+ 现在就用系列(五):模板字面量 (Template Literals)

模板字面量

字符串替换

这个和C#6 里面的字符串插值类似。原来ES5里字符串要连接,一般就是用+

特性

  1. 用反引号(`)标识, 它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

  2. 如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。

  3. 如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。

  4. 大括号内部可以放入任意的JavaScript表达式,可以进行运算,以及引用对象属性。

  5. 模板字符串之中还能调用函数。

示例代码:

简单字符串替换

1
2
3
4
var name = "Brendan";
console.log(`Yo, ${name}!`);

// => "Yo, Brendan!"

表达式

1
2
3
4
5
6
7
8
var a = 10;
var b = 10;
console.log(`JavaScript first appeared ${a+b} years ago. Crazy!`);

//=> JavaScript first appeared 20 years ago. Crazy!

console.log(`The number of JS MVC frameworks is ${2 * (a + b)} and not ${10 * (a + b)}.`);
//=> The number of JS frameworks is 40 and not 200.

函数

1
2
3
function fn() { return "I am a result. Rarr"; }
console.log(`foo ${fn()} bar`);
//=> foo I am a result. Rarr bar.

$() 可以使用任何表达式和方法调用

1
2
3
4
5
6
7
8
9
10
var user = {name: 'Caitlin Potter'};
console.log(`Thanks for getting this into V8, ${user.name.toUpperCase()}.`);

// => "Thanks for getting this into V8, CAITLIN POTTER";

// And another example
var thing = 'drugs';
console.log(`Say no to ${thing}. Although if you're talking to ${thing} you may already be on ${thing}.`);

// => Say no to drugs. Although if you're talking to drugs you may already be on drugs.

示例代码:

ES5:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
'use strict';

var customer = { name: "Foo" };
var card = { amount: 7, product: "Bar", unitprice: 42 };

var message = "Hello " + customer.name + ",\n" +
"want to buy " + card.amount + " " + card.product + " for\n" +
"a total of " + (card.amount * card.unitprice) + " bucks?";

console.log(message);

输出:

Hello Foo,
want to buy 7 Bar for
a total of 294 bucks?

ES6:

1
2
3
4
5
6
7
8
9
10
11
var customer = { name: "Foo" }
var card = { amount: 7, product: "Bar", unitprice: 42 }
message = `Hello ${customer.name},
want to buy ${card.amount} ${card.product} for
a total of ${card.amount * card.unitprice} bucks?`

输出:

Hello Foo,
want to buy 7 Bar for
a total of 294 bucks?

Tagged Templates (标签模板?不知道如何翻译)

比如

1
fn`Hello ${you}! You're looking ${adjective} today!`

实际上等于

fn(["Hello ", "! You're looking ", " today!"], you, adjective);

fn可以是任何函数名,也就是把字符串分解传到到方法的第一个参数里,第一个参数必须是数组,数组的每一项,就是被$()分开的没一串字符, 每一个$()里面的值将传给函数的剩余参数。等于下面函数定义,strings是一个数组,values是Rest参数。

1
fn(strings, ...values)

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a = 5;
var b = 10;

function tag(strings, ...values) {
console.log(strings[0]); // "Hello "
console.log(strings[1]); // " world "
console.log(values[0]); // 15
console.log(values[1]); // 50

return "Bazinga!";
}

tag`Hello ${ a + b } world ${ a * b }`;
// "Bazinga!"

有了 tagged template 我们可以让代码看起来更简洁,比如我们可以把下面的调用

1
get([ "http://example.com/foo?bar=", "&quux=", "" ],bar + baz, quux);

用新的写法

1
get`http://example.com/foo?bar=${bar + baz}&quux=${quux}`

String.raw

存取 raw template string, 就是如果遇见\将增加一个,然后原样输出。

1
2
3
4
5
6
let interpreted = 'raw\nstring';
let esaped = 'raw\\nstring';
let raw = String.raw`raw\nstring`;
console.log(interpreted); // raw
// string
console.log(raw === esaped); // true

ES6+ 现在就用系列(四):箭头函数 =>

箭头函数 =>

ES6 允许使用 => 来定义函数, 他是函数的缩写,这个熟悉C#的人应该了解,这其实就是C#里的lamda表达式

他不只是语法糖 (Syntax sugar), 箭头函数自动绑定 定义此函数作用域的this(Arrow functions automatically bind “this” from the containing scope.)

** 箭头函数没有自己的this,所以内部的this就是外层代码块的this。**

定义格式

1
(<arguments>) => <return statement>

当只有一个参数时,括号可省略,下面两种写法是等价的.

1
2
(x) => x * x
x => x * x

示例代码

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
'use strict';
// 数组
const items = [1, 2, 3, 4];

// lamda 表达式
let byTwo = items.map(i => i * 2);

// 可以使用block
let byFour = items.map(i => {
return i * 2;
});

// 绑定this
function Person() {
this.company = "deshui.wang";
this.Names = ["Jack", "Alex", "Eric"];
this.print = () => {
return this.Names.map((n) => {
return n + " is from " + "company "+ this.company;
});
};
}

console.log(new Person().print());

// 输出:
[
'Jack is from company deshui.wang',
'Alex is from company deshui.wang',
'Eric is from company deshui.wang'
]

注意事项

  1. 箭头函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象, 原因是箭头函数没有自己的 this.

  2. 不可以当作构造函数,不可以使用 new 命令。

  3. 不可以使用 arguments 对象,该对象在函数体内不存在。可以用 Rest 参数代替。

  4. 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。

  5. arguments、super、new.target 在在箭头函数之中是不存在的,他们指向外层函数的对应变量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
        function hello() {
    setTimeout( () => {
    console.log("args:", arguments);
    },100);
    }

    hello( 1, 2, 3, 4 );

    // 输出 1, 2, 3, 4
  6. 箭头函数没有自己的 this,所以不能用call()、apply()、bind()这些方法去改变 this 的指向。

ES6+ 现在就用系列(三):const 命令

本文以及以后讨论的代码,都必须是在严格模式下,因为非严格模式下,有一些写法也符合,所以我们建议代码始终使用严格模式

定义

在之前的ES版本里是没有常量的概念的,常量,就是一旦申明,值就不能改变的。

1
2
3
4
5
'use strict';
const PI = 3.1415;
console.log(PI) // 3.1415

PI = 3; // TypeError: Assignment to constant variable.

特性

  • 一旦申明,必须初始化

  • 作用域只在声明所在的块级,和let相同

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
        'use strict';
    const apiBase = "https://deshui.wang/api/v1/";
    const clientId = "123456";

    //block scoped
    if (true) {

    const apiBase = "https://cnblogs.com/api/";

    console.log(apiBase + clientId);
    // https://cnblogs.com/api/123456
    }

    console.log(apiBase+clientId);
    // https://deshui.wang/api/v1/123456

    apiBase = "https://google.com/api";
    //Identifier 'apiBase' has already been declared
  • const 申明的变量,在一个作用域内也不能与let和var申明的重名

  • 如果 const 申明的是个复合类型的变量,那么变量名不指向数据,而是指向数据所在的地址。const命令只是保证变量名指向的地址不变,并不保证该地址的数据不变。

    1
    2
    3
    4
    5
    6
    7
    8
        'use strict';        
    const a = [1, 2];
    a.push(3);
    console.log(a); // 1,2,3
    a.length = 0;
    console.log(a); // []

    a = [4]; // TypeError: Assignment to constant variable.

全局变量

全局对象是最上层层的对象,在浏览器里指的是window对象,在Node.js指的是global对象。

1
2
3
4
5
// 'use strict';
var a="hello";
console.log(global.a);

// 输出: undefined

var 命令和 function 命令声明的全局变量,依旧是全局对象的属性;let命令、const命令、class命令声明的全局变量,不属于全局对象的属性。

上面的代码在Node.js下是不行的,但是浏览器却可以,不管是不是严格模式。

1
2
3
4
5
// 'use strict';
var a="hello";
console.log(window.a);

// 输出: hello

但是,如果使用let, 那么属性将不绑定到window (Chrome developer tools 需要使用以下方法才能打开严格模式)

1
2
3
4
5
6
7
8
(function(){
'use strict'
let a="hello";
console.log(window.a);

})()

// 输出undefined

ES6+ 现在就用系列(二):let 命令

ES6新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。也就是有了块级作用域。

为什么需要块级作用域?

避免 var 变量提升带来的副作用

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var saleCount = 20;

function f(){
console.log(saleCount);
if(saleCount<100)
{
// according some rule, change it to 100
var saleCount=60;
console.log(saleCount);
}
}

f()

输出: // undefined

因为 “var saleCount=60;” 作用域是整个函数,而JavaScript里var定义的变量存在变量提升,也就是console.log(saleCount), 这个saleCount是 “var saleCount=60;” 这一句定义的,当调用的时候,saleCount的值是undefined. 实际上等于下面代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var saleCount = 20;

function f(){

var saleCount;
console.log(saleCount);
if(saleCount<100)
{
// according some rule, change it to 100
saleCount=60;
console.log(saleCount);
}

}

f() // undefined

避免循环变量变为全局变量

1
2
3
4
5
6
7
8
9
10
示例:

for (var i = 0; i < 10; i++){
// do something
}

console.log(i);
输出: 10

很明显,我们不希望i,这个变量变为全局变量。

let 示例代码

1
2
3
4
5
6
7
8
9
10
'use strict'
{
var b=1;
let a=2;
}

console.log(a);
console.log(b);

# 输出: ReferenceError: a is not defined

上一节我们给出了如下的示例:

1
2
3
4
5
6
7
8
9
10
11
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[1]();
a[2]();
a[3]();

输出: 10,10,10

我们看到,输出的结果不是我们想要的,因为i是用var定义的,那么他在全局范围内都是生效的,也就是我们循环结束以后,i的值就是10,那么不管调用数组的那个元素,console.log(i) 输出的都是10, 那么let因为有了块级作用域,就可以避免这个问题。

1
2
3
4
5
6
7
8
9
10
11
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[1]();
a[2]();
a[3]();

输出 1, 2,3

另外,函数本身的作用域也在定义他的块的作用域内。

1
2
3
4
5
6
7
function hello(){console.log("Hello, Jack")};

{
function hello(){console.log("Hello, Tom")};
}

hello();

上面的代码在ES6里面输出了”Hello, Jack”, 而在ES5里输出了”Hello, Tom”.

注意事项

不能先使用,后定义

1
2
3
4
5
6
7
8
9
console.log(x);
console.log(y);

var x = 1;
let y = 2;

# 输出
undefined
ReferenceError: y is not defined

上面的代码由于x是var定义的,一开始x的变量是存在的,只是值是undefined, 但是由于y 是let定义的,就不存在变量提升。

暂时性死区

如果一个变量是使用let定义的,那么这个变量就属于声明时所在的代码块,也就是变量不再受外部影响,下面的a 由于在块里定义了,所以 会报错,因为在那个块里是先使用后定义,如果去掉“let a”, 那么a就是外部的变量,这个时候就不会出错。

1
2
3
4
5
6
7
8
var a = "hello";

{
a = 'world';
let a;
}

// ReferenceError

不能重复申明

也就是不能重复申明同一个变量,即使一个是let申明,一个是用var申明也不行。 下面的代码都会报错。

1
2
3
4
function () {
let a = 10;
var a = 1;
}
1
2
3
4
function () {
let b = 10;
let b = 1;
}

总结

由于let 避免了很多问题,所以建议在ES6的代码里总是使用let 来替代var.

ES6+ 现在就用系列(一):为什么使用ES6+

ES6+

现在主流的浏览器都是支持到ES5, 为了表述方便,我在此发明一个名词”ES6+” 就是ES5以后的版本,包括ES6, ES7. 为什么说现在就用,虽然主流的浏览器只支持到ES5, 但是现在有很多的转换器,可以把一些ES6和ES7的代码转换为ES5的代码。这就意味着我们现在就可以使用这些新特性,然后使用转码器让代码可以运行在主流的浏览器上。

为什么立即开始使用ES6, ES7的新特性?

JavaScript语言的一些糟糕的实现

先不说JavaScript语言本身设计是否有问题,现有JavaScript语言的实现里有很多非常糟糕或者诡异的实现,就是你以为代码的结果是这样,但是他偏偏是那样,这给我们程序带了很多的意向不到的Bug和烦恼,如果你要是JavaScript大牛,你需要了解他内部的实现的Bug, 而且要知道哪些诡异的写法输出了什么诡异的结果,我个人对了解这种东西实在提不起太大的兴趣,因为我只想用“语言”来实现我的项目让人很好的使用我开发的软件,但是由于历史这样或那样的原因,导致JavaScript语言成为浏览器的霸主,我们不得不忍受这些糟糕的问题。下面我来展示一些让你觉得诡异的问题 (如果你不不觉得诡异,恭喜你,你已经是JavaScript的“高手”)

示例1:

1
2
3
4
5
(function() {
return NaN === NaN;
})();

输出: false

示例2:

1
2
3
4
5
(function() {
return (0.1 + 0.2 === 0.3);
})();

输出: false

示例3:

1
2
3
[5, 12, 9, 2, 18, 1, 25].sort();

输出: [1, 12, 18, 2, 25, 5, 9]

示例4:

1
2
3
4
5
6
7
8
9
10
11
var a = "1"
var b = 2
var c = a + b

输出:c = "12"

var a = "1"
var b = 2
var c = +a + b

输出:c = 3

示例5:

1
2
3
4
5
(function() {
return ['10','10','10','10'].map(parseInt);
})();

输出: [10, NaN, 2, 3]

示例6:

1
2
3
4
5
(function() {
return 9999999999999999;
})();

输出: 10000000000000000

示例7:

1
2
3
4
5
6
7
8
9
10
11
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[1]();
a[2]();
a[3]();

输出: 10,10,10

我是觉得如果按正常人的理解,代码不能得到想要的结果,那就算是语言本身的问题。如果一个程序执行的和人期望的不一样,或者还需要一些Hack的方法,那么是很糟糕的。

ES5 一些语言特性的缺失

由于上面的很多问题,所以ES 需要不断的改进, 当然新的版本肯定不可能一下子解决之前所有的问题。

已有JavaScript的问题这一块就不细说了,因为能来看这篇文章的人,应该对下面我列的几个突出的问题都有感受。

  • 没有块级作用域,这个导致上面示例7的问题
  • 全局变量的污染
  • 类的写法比较怪异
  • 没有模块管理
  • 异步调用写法容易产生 “回调地狱”

为什么可以立即使用?

因为现在很多转换器已经可以把ES6所有的特性以及ES7的部分特性转换为ES5,Babel就是一个非常好的转换器,所以我这里建议凡是能被Babel转换的新特性都可以立即在项目里适用。

ES6和ES7的一些新特性,可以大大提高项目的健壮性,同时让代码更易读,同时也可以避免很多ES5之前的很多诡异的东西。Gulp里可以很好的使用babel, 如果你对Gulp不熟悉,可以参考我博客里的Gulp系列。

这里简单说一Gulp和babel如何结合使用

1
2
3
4
5
6
7
8
9
$ npm install -g gulp-babel

var gulp=require('gulp'), babel=require('gulp-babel');

gulp.task('build',function(){
return gulp.src('src/app.js')
.pipe(babel())
.pipe(gulp.dest('build'))
})


后面的系列,我将以此介绍ES6, ES7的一些可以现在就用的主要特性。

程序员之网络安全系列(六):动态密码

前文回顾

我们使用了数字证书,确保了对方的公钥身份,也就是互联网中确定了要访问的网站就是你要访问的网站。

但是我们如何确定要访问这个网站的用户就是要访问的用户呢? 对银行来说需要确保“敏捷的水”登录银行时,必须是”敏捷的水” 而不是别人,不然别人就把钱转走了。

虽然我们从通信,数据加密等方式确保用户密码不背攻击者破解,但是如果攻击者使用键盘记录器等工具知道了用户密码,那么就可以冒充用户了。

比如银行的U盾,因为我对这块业务不了解,我猜银行为每个用户发放了一个公钥?( 知道的同学,可以帮忙解释一下银行的U盾都做了什么? )

我们用数字证书确定了银行的身份,那么银行如何确定我们的身份呢?

两步验证

那么什么是两步认证呢?两步认证就是在每次登陆时候填一个手机短信收取的验证码或者手机应用生成的验证码。当然接收验证码的手机号或者应用是需要绑定的,这样只有拿到这部手机并且知道你帐号密码的人才能登陆帐号。

为什么需要它?

对有些人来说,盗取密码比您想象的更简单

以下任意一种常见操作都可能让您面临密码被盗的风险:

  • 在多个网站上使用同一密码
  • 从互联网上下载软件
  • 点击电子邮件中的链接

想像一下您无法访问自己的帐户及其中的内容,当别有用心的人盗取您的密码后,他们能让您无法访问自己的帐户,还可以执行以下操作:

  • 翻看(甚至删除)您所有的电子邮件、联系人、照片等
  • 冒充您给您的联系人发送垃圾邮件或有害的电子邮件
  • 使用您的帐户重置您其他帐户(银行帐户、购物帐户等)的密码

两步验证可以将别有用心的人阻挡在外,即使他们知道您的密码也无可奈何。

如何工作?

现在大部分比较危险的操作都需要绑定手机号,因为手机号是你用的唯一的。接收到验证码后,我们再输入系统做第二次的验证。

但是由于我们这个验证码也有可能丢失,那么我们只需要让他在一定时间有效就可以了,这就是OTP.

动态口令 (One Time Password)

动态密码: 一个OTP(One Time Password) 是一个密码仅用于一次登录会话或者交易,使用过后,这个密码就无效了。

静态密码的问题:

  • 容易被破解
  • 容易被猜测
  • 容易被盗劫
  • 针对不同的网站,用户需要记忆大量的密码。

使用动态口令主要有2个方面价值:

  • 防止由于盗号而产生的财产损失。
  • 采用动态口令的单位无需忍受定期修改各种应用系统登录密码的烦恼。

有两种方法,生成动态密码:

Event-based OTP (EOTP)

基于事件同步的令牌,其原理是通过某一特定的事件次序及相同的种子值作为输入,在DES算法中运算出一致的密码,其运算机理决定了其整个工作流程同时钟无关,不受时钟的影响,令牌中不存在时间脉冲晶振。但由于其算法的一致性,其口令是预先可知的,通过令牌,你可以预先知道今后的多个密码,故当令牌遗失且没有使用PIN码对令牌进行保护时,存在非法登陆的风险,故使用事件同步的令牌,对PIN码的保护是十分必要的。同样,基于事件同步的令牌同样存在失去同步的风险,例如用户多次无目的的生成口令等,对于令牌的失步,事件同步的服务器使用增大偏移量的方式进行再同步,其服务器端会自动向后推算一定次数的密码,来同步令牌和服务器,当失步情况已经非常严重,大范围超出正常范围时,通过连续输入两次令牌计算出的密码,服务器将在较大的范围内进行令牌同步,一般情况下,令牌同步所需的次数不会超过3次。但在极端情况下,不排除失去同步的可能性,例如电力耗尽,在更换电池时操作失误等。此时,令牌仍可通过手工输入由管理员生成的一组序列值来实现远程同步,而无需寄回服务器端重新同步。

Time based OTP (TOTP)

基于令牌和服务器的时间同步,通过运算来生成一致的动态口令,基于时间同步的令牌,一般更新率为60S,每60S产生一个新口令,但由于其同步的基础是国际标准时间,则要求其服务器能够十分精确的保持正确的时钟,同时对其令牌的晶振频率有严格的要求,从而降低系统失去同步的几率,从另一方面,基于时间同步的令牌在每次进行认证时,服务器端将会检测令牌的时钟偏移量,相应不断的微调自己的时间记录,从而保证了令牌和服务器的同步,确保日常的使用,但由于令牌的工作环境不同,在磁场,高温,高压,震荡,入水等情况下易发生时钟脉冲的不确定偏移和损坏,故对于时间同步的设备进行较好的保护是十分必要的。对于失去时间同步的令牌,目前可以通过增大偏移量的技术(前后10分钟)来进行远程同步,确保其能够继续使用,降低对应用的影响,但对于超出默认时间(共20分钟)的同步令牌,将无法继续使用或进行远程同步,必须返厂或送回服务器端另行处理。同样,对于基于时间同步的服务器,应较好地保护其系统时钟,不要随意更改,以免发生同步问题,从而影响全部基于此服务器进行认证的令牌。

** 以上两种方式在生成密码的过程都不需要与服务器通信,所以极大的保证了密码的安全。**

算法的实现

http://tools.ietf.org/html/rfc6238

程序员之网络安全系列(五):数字证书以及12306的证书问题

前文回顾

假如,明明和丽丽相互不认识,明明想给丽丽写一封情书,让隔壁老王送去

  1. 如何保证隔壁老王不能看到情书内容?(保密性)
  2. 如何保证隔壁老王不修改情书的内容?(完整性)
  3. 如何保证隔壁老王不冒充明明?(身份认证)
  4. 如何保证明明不能否认情书是自己写的?(来源的不可否认)

我们使用了非对称密钥算法,我们让“隔壁王叔叔”传递了秘钥。

中间人攻击

上面几步还是不够的,比如王叔叔在交换秘钥的过程中做了手脚呢?

如何做手脚?看下图:

  1. 王叔叔自己生成一个公私钥,和明明以及丽丽交换。
  2. 王叔叔冒充丽丽把自己的公钥发给明明。
  3. 明明用王叔叔的公钥对信件加密。
  4. 王叔叔用自己的私钥解密就可以看到明明给丽丽的邮件。
  5. 王叔叔冒充明明把自己的公钥发给丽丽。
  6. 丽丽用王叔叔的公钥对信件加密。
  7. 王叔叔用自己的私钥解密就可以看到丽丽给明明内容。

那么明明如何知道王叔叔给的公钥就是丽丽的公钥呢?那么就引入了数字证书

数字证书

概念介绍

那么王叔叔要让明明相信他给的公钥就是丽丽的公钥,那么他可以开一个证明,比如找权威机构“敏捷的水”给开个介绍信,介绍信上给加个公章。那么这里的介绍性就是数字证书, 公章就是数字签名, 那么”敏捷的水”就是颁发证书的机构CA(Certificate Authority),也就是证书授权中心

CA CA 是“Certificate Authority”的缩写,也叫“证书授权中心”。
它是负责管理和签发证书的第三方机构,就好比例子里面的“敏捷的水”。一般来说,CA 必须是大家都信任的、认可的。因此它必须具有足够的权威性。只有明明和丽丽都信任的人才能来证明,对吧?

CA证书: CA 证书,就是CA颁发的证书。

证书之间的信任关系: 用一个证书来证明另一个证书是真实可信。

证书信任链: 证书之间的信任关系,是可以嵌套的。比如,A 信任 A1,A1 信任 A2,A2 信任 A3……,这就是证书的信任链。只要你信任链上的第一个证书,那后续的证书,都是可以信任的。

根证书(Root Certificate): 下图,处于最顶上的树根位置的那个证书,就是“根证书”。除了根证书,其它证书都要依靠上一级的证书来证明自己。那谁来证明“根证书”?根证书自己证明自己,这时候我们用户就需要自己选择是否相信某个根证书。

根证书是整个证书体系安全的根本。如果某个证书体系中,根证书不再可信了,那么所有被根证书所信任的其它证书,也就不再可信了。  

证书内容

我们看一下亚马逊的证书,当我们点击浏览器左边绿色的锁时,我们可以看到如下的证书。

我们挑重点的解释一下

  • Issuer (证书的发布机构)
    指出是什么机构发布的这个证书,也就是指明这个证书是哪个公司创建的(只是创建证书,不是指证书的使用者)。对于上面的这个证书来说,就是指”Symantec Corporation”这个机构。

  • Not valid before, Not valid after (证书的有效期)

  • Public key (公钥)

这个我们在前面介绍公钥密码体制时介绍过,公钥是用来对消息进行加密的。
  • Subject (主题)

这个证书是发布给谁的,或者说证书的所有者,一般是某个人或者某个公司名称、机构的名称、公司网站的网址等。

  • Signature algorithm (签名所使用的算法)

就是指的这个数字证书的数字签名所使用的加密算法,这样就可以使用证书发布机构的证书里面的公钥,根据这个算法对指纹进行解密,指纹的加密结果就是数字签名。

  • Thumbprint, Thumbprint algorithm (指纹以及指纹算法)

这个是用来保证证书的完整性的,也就是说确保证书没有被修改过,其原理就是在发布证书时,发布者根据指纹算法(一个hash算法)计算整个证书的hash值(指纹)并和证书放在一起,使用者在打开证书时,自己也根据指纹算法计算一下证书的hash值(指纹),如果两者一致,就说明证书没有被修改过,因为证书的内容被修改后,根据证书的内容计算的出的hash值(指纹)是会变化的。 注意,这个指纹会使用CA这个证书机构的私钥用签名算法(Signature algorithm)加密后和证书放在一起,只有用CA的公钥才能解开这个签名。

证书是如何保证身份认证的

申请证书:

  1. Amazon.com 向Symantec 公司(CA) 申请证书。
  2. Symantec(CA) 生成一对公钥A和私钥B。
  3. Symantec(CA) 有自己的公钥C和私钥D。
  4. Symantec(CA) 把Issuer,公钥A,Subject(一般是网站的域名),Valid from,Valid to等信息以明文的形式写到证书里面,然后用一个指纹算法(SHA1或者MD5
    )计算出这些数字证书内容的一个指纹(摘要),并把指纹和指纹算法用自己的私钥D进行加密,然后和证书的内容一起发给Amazon.com。
  5. Symantec(CA) 把私钥B给Amazon.com.

如何使用证书

  1. 用户访问amazon.com 这个网站
  2. amazon.com 把证书发给用户
  3. 浏览器读取证书。
  4. 浏览器发现证书机构是Symantec,然后会在操作系统中受信任的发布机构的证书中去找Symantec的证书,如果找不到,那说明证书的发布机构是个假的,或者不是被权威机构认证的,证书可能有问题,程序会给出一个错误信息。
  5. 如果在系统中找到了Symantec的证书,那么应用程序就会从证书中取出Symantec的公钥C,然后对amzon.com公司的证书里面的指纹和指纹算法用这个公钥C进行解密,然后使用这个指纹算法计算amazon.com证书的指纹,将这个计算的指纹与放在证书中的指纹对比,如果一致,说明amazon.com的证书肯定没有被修改过并且证书是Symantec发布的,证书中的公钥肯定是amazon.com的公钥A, 然后我们就可以用这个公钥A和amazon.com进行通信,因为只有amazon.com 有私钥B, 所以只有amazon.com才能解开信息。

注意 权威机构的证书都是内置在操作系统里的。

由此可见,一个证书受不受信任,那就要看你要不要添加到操作系统里,权威的认证机构的证书都内置在操作系统里的。

另外,我们自己也可以制作自签名的证书,但是需要别人认可你,这个在企业内部或者开发阶段是可以,我们可以自己制作一个证书添加到操作系统里。

那么,问题来了,当你访问https://www.12306.cn 时,你就会得到下面的结果

这是为什么呢?

我相信你看完本文应该清楚,那是因为12306 自己给自己发了个证书,而这个证书默认是没有被操作系统信任,

但是当我把根证书添加到操作系统后,依然是https://www.12306.cn 不行,然后我发现是证书对应的域名不对,证书对应的域名是https://kyfw.12306.cn 访问这个域名后,虽然证书验证通过,但是浏览器的锁还是没有变绿

为什么呢? 看提示,是因为12306使用了一个比较弱的机密算法(This site uses a weak security configuration (SHA-1 signatures), so your connection may not be private.),我猜他们是为了性能??

但是,就算浏览器地址栏的锁不能变绿,我们还得订票不是吗?

我觉得12306可能需要一个操作系统内置的认证机构来发一个证书,不然普通的用户根本不知道怎么安装证书,如果不使用https,那么安全性如何得到保证呢?

最后

回到开头的例子,明明和丽丽可以找一个权威机构来发一个证书,而且自己都内置了这个权威机构的证书。当王叔叔把丽丽的证书给明明时,明明就可以知道证书是不丽丽给的,因此就可以确定证书里的公钥是否是丽丽的,如果可以确定是丽丽的,那么就可以确保加密的内容只有丽丽可以解开,因为只有丽丽有对应的私钥。

程序员之网络安全系列(四):数据加密之非对称秘钥

前文回顾

假如,明明和丽丽相互不认识,明明想给丽丽写一封情书,让隔壁老王送去

  1. 如何保证隔壁老王不能看到情书内容?(保密性)
  2. 如何保证隔壁老王不修改情书的内容?(完整性)
  3. 如何保证隔壁老王不冒充明明?(身份认证)
  4. 如何保证明明不能否认情书是自己写的?(来源的不可否认)

但是上面的问题是明明和丽丽必须提前知道秘钥,但是如果双方提前不知道秘钥,那么明明就需要“隔壁的王叔叔” 把秘钥告诉丽丽,这个显然是风险太大了,因为”隔壁王叔叔“有了秘钥和密文,那么就等于有了明文。

非对称秘钥

DH(Diffie-Hellman)算法

1976年,美国学者Dime和Henman为解决信息公开传送和密钥管理问题,提出一种新的密钥交换协议,允许在不安全的媒体上的通讯双方交换信息,安全地达成一致的密钥,这就是“公开密钥系统”。相对于“对称加密算法”这种方法也叫做“非对称加密算法”。

与对称加密算法不同,非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥(privatekey)。公开密钥与私有密钥是一对,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。

算法原理及示例

  1. 假如明明和丽丽希望交换一个密钥。

  2. 明明取一个素数p =97和97的一个原根a=5,让隔壁的王叔叔告诉丽丽。

  3. 明明和丽丽分别选择秘密密钥XA=36和XB=58,并计算各自的公开密钥,然后让隔壁的王叔叔帮忙交换公开秘钥。

     YA=a^XA mod p=5^36 mod 97=50
    
     YB=a^XB mod p=5^58 mod 97=44
  4. 明明和丽丽交换了公开密钥之后,计算共享密钥如下:

     明明:K=(YB) ^XA mod p=44^36 mod 97=75
    
     丽丽:K=(YA) ^XB mod p=50^58 mod 97=75  

由于只有明明知道XA, 而只有丽丽知道XB, 那么“隔壁的王叔叔” 是不可能通过 P, A, YA, YB来得到最终密码K的。

DiffieˉHellman不是加密算法,它只是生成可用作对称密钥的秘密数值。

非对称加密特点

与对称加密算法不同,非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥(privatekey)。公开密钥与私有密钥是一对,如果 用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。

那么如果甲(收信方)想收到只有自己才能解读的加密信息,那么需要把自己的公钥告诉乙(发送发), 乙通过甲的公钥加密,把加密后的密文告诉甲,由于只有甲有私钥,那么也就只有甲才能加密。
由此可见,非对称加密只需要保存一对公钥和私钥,大大方便了秘钥管理。但是由于要做更多的计算,非对称加密只适合一些小数据量加密,一般情况都是用非对称加密算法来交换秘钥,随后通过对称加密算法来加密数据。

常用非对称加密算法

RSA、Elgamal、背包算法、Rabin、D-H、ECC(椭圆曲线加密算法)。

使用最广泛的是RSA算法,Elgamal是另一种常用的非对称加密算法。

最后

我们对数据的完整性使用Hash进行了保证,用DH算法交换了秘钥,使用RSA算法对数进行了加密,那么如果王叔叔在交换秘钥的过程中做了手脚呢?

如何做手脚?看下图:

  1. 王叔叔自己生成一个公私钥,和明明以及丽丽交换。
  2. 王叔叔冒充丽丽把自己的公钥发给明明。
  3. 明明用王叔叔的公钥对信件加密。
  4. 王叔叔用自己的私钥解密就可以看到明明给丽丽的邮件。
  5. 王叔叔冒充明明把自己的公钥发给丽丽。
  6. 丽丽用王叔叔的公钥对信件加密。
  7. 王叔叔用自己的私钥解密就可以看到丽丽给明明内容。

至此,邮件内容又赤裸裸地被王叔叔看到了,怎么办呢?我们下文继续解释。

程序员之网络安全系列(三):数据加密之对称加密算法

前文回顾

假如,明明和丽丽相互不认识,明明想给丽丽写一封情书,让隔壁老王送去

  1. 如何保证隔壁老王不能看到情书内容?(保密性)
  2. 如何保证隔壁老王不修改情书的内容?(完整性)
  3. 如何保证隔壁老王不冒充明明?(身份认证)
  4. 如何保证明明不能否认情书是自己写的?(来源的不可否认)

上一节,我们使用了Hash算法保证了情书的完整性,也就是确保“隔壁王叔叔”没有修改明明的情书,那么这一节我们来看看如何保证“隔壁王叔叔”不能看到情书的内容,也就是保密性。

数据加密

要想不让别人看到数据,那么我们就们就需要对数据加密。

加密技术 是最常用的安全保密手段,利用技术手段把重要的数据变为乱码(加密)传送,到达目的地后再用相同或不同的手段还原(解密)。
加密包括两个元素:算法和密钥。一个加密算法是将普通的文本(或者可以理解的信息)与一窜数字(密钥)的结合,产生不可理解的密文的步骤,密钥是用来对数据进行编码和解码的一种算法。

举个例子:

假设我们要对LOVE加密,我们可以先定义字母的顺序ABCDEFGHIJKLMNOPQRSTUVWXYZ,然后我们让每个字母向后移动两位,那么LOVE就变为了NQXG

L------>N
O------>Q
V------>X
E------>
LOVE--->NQXG

我想这就是最简单的加密方式。

密钥加密技术的密码体制分为对称密钥体制和非对称密钥体制两种。

对数据加密的技术分为两类,即对称加密(私人密钥加密)和非对称加密(公开密钥加密)。对称加密以数据加密标准(DES,Data Encryption Standard)算法为典型代表,非对称加密通常以RSA(Rivest Shamir Ad1eman)算法为代表。对称加密的加密密钥和解密密钥相同,而非对称加密的加密密钥和解密密钥不同,加密密钥可以公开而解密密钥需要保密。

对称加密

对称加密采用了对称密码编码技术,它的特点是文件加密和解密使用相同的密钥,即加密密钥也可以用作解密密钥。
比如,我们给WORD文档设置密码1234, 那么其他人想要打开文档也必须输入1234才能打开。

常用加密算法:

  • DES(Data Encryption Standard):数据加密标准,速度较快,适用于加密大量数据的场合。

  • 3DES(Triple DES):是基于DES,对一块数据用三个不同的密钥进行三次加密,强度更高。

  • AES(Advanced Encryption Standard):高级加密标准,是下一代的加密算法标准,速度快,安全级别高;

  • RC4,也是为 RSA Data Security, Inc. 开发的密码系统的商标名称。

    传统的DES由于只有56位的密钥,从1997年开始,RSA公司发起了一个称作“向DES挑战”的竞技赛。在首届挑战赛上,罗克·维瑟用了96天时间破解了用DES加密的一段信息。1999年12月22日,RSA公司发起“第三届DES挑战赛(DES Challenge III)”。2000年1月19日,由电子边疆基金会组织研制的25万美元的DES解密机以22.5小时的战绩,成功地破解了 DES加密算法。DES已逐渐完成了它的历史使命。

    高级加密标准(英语:Advanced Encryption Standard,缩写:AES),在密码学中又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。经过五年的甄选流程,高级加密标准由美国国家标准与技术研究院(NIST)于2001年11月26日发布于FIPS PUB 197,并在2002年5月26日成为有效的标准。2006年,高级加密标准已然成为对称密钥加密中最流行的算法之一。

对称加密算法的优点:

  • 算法公开
  • 计算量小
  • 加密速度快,加密效率高

对称加密算法的缺点

  • 加解密双方需要使用相同的秘钥
  • 秘钥管理很不方便,如果用户很多,那么秘钥的管理成几何性增长
  • 任何一方秘钥泄露,数据都不安全了

最后

通过本节,我们知道当明明给丽丽情书时,可以用DES或者AES对数据进行加密,即使“隔壁王叔叔”拿到信件也看不懂内容,同时使用上一节的Hash算法保证了情书的内容完整,但是这就需要明明和丽丽提前设置一个秘钥。

代码示例

下面的代码输出如下结果

    I Love You, Li Li
    Encrypeted: 0t9glwGMmwtGs8B4QCotyZkKf091WElCwG659QiVVw0=
    Decrypeted: I Love You, Li Li

.NET 源码:

    using System;
    using System.Security.Cryptography;
    using System.IO;
    using System.Text;

    namespace AES
    {
        class MainClass
        {
            public static void Main (string[] args)
            {
                string password = "Don't believe wang shu shu";
                string orginTextToSent = "I Love You, Li Li";
                Console.WriteLine (orginTextToSent);

                string encryptedText=EncryptText(orginTextToSent, password);

                Console.WriteLine ("Encrypeted: " + encryptedText);

                string DecryptedText = DecryptText (encryptedText, password);
                Console.WriteLine ("Decrypeted: " + DecryptedText);

            }



            public static byte[] AES_Encrypt(byte[] bytesToBeEncrypted, byte[] passwordBytes)
            {
                byte[] encryptedBytes = null;

                // Set your salt here, change it to meet your flavor:
                // The salt bytes must be at least 8 bytes.
                byte[] saltBytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 };

                using (MemoryStream ms = new MemoryStream())
                {
                    using (RijndaelManaged AES = new RijndaelManaged())
                    {
                        AES.KeySize = 256;
                        AES.BlockSize = 128;

                        var key = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 1000);
                        AES.Key = key.GetBytes(AES.KeySize / 8);
                        AES.IV = key.GetBytes(AES.BlockSize / 8);

                        AES.Mode = CipherMode.CBC;

                        using (var cs = new CryptoStream(ms, AES.CreateEncryptor(), CryptoStreamMode.Write))
                        {
                            cs.Write(bytesToBeEncrypted, 0, bytesToBeEncrypted.Length);
                            cs.Close();
                        }
                        encryptedBytes = ms.ToArray();
                    }
                }

                return encryptedBytes;
            }

            public static byte[] AES_Decrypt(byte[] bytesToBeDecrypted, byte[] passwordBytes)
            {
                byte[] decryptedBytes = null;

                // Set your salt here, change it to meet your flavor:
                // The salt bytes must be at least 8 bytes.
                byte[] saltBytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 };

                using (MemoryStream ms = new MemoryStream())
                {
                    using (RijndaelManaged AES = new RijndaelManaged())
                    {
                        AES.KeySize = 256;
                        AES.BlockSize = 128;

                        var key = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 1000);
                        AES.Key = key.GetBytes(AES.KeySize / 8);
                        AES.IV = key.GetBytes(AES.BlockSize / 8);

                        AES.Mode = CipherMode.CBC;

                        using (var cs = new CryptoStream(ms, AES.CreateDecryptor(), CryptoStreamMode.Write))
                        {
                            cs.Write(bytesToBeDecrypted, 0, bytesToBeDecrypted.Length);
                            cs.Close();
                        }
                        decryptedBytes = ms.ToArray();
                    }
                }

                return decryptedBytes;
            }


            public static string EncryptText(string input, string password)
            {
                // Get the bytes of the string
                byte[] bytesToBeEncrypted = Encoding.UTF8.GetBytes(input);
                byte[] passwordBytes = Encoding.UTF8.GetBytes(password);

                // Hash the password with SHA256
                passwordBytes = SHA256.Create().ComputeHash(passwordBytes);

                byte[] bytesEncrypted = AES_Encrypt(bytesToBeEncrypted, passwordBytes);

                string result = Convert.ToBase64String(bytesEncrypted);

                return result;
            }

            public static string DecryptText(string input, string password)
            {
                // Get the bytes of the string
                byte[] bytesToBeDecrypted = Convert.FromBase64String(input);
                byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
                passwordBytes = SHA256.Create().ComputeHash(passwordBytes);

                byte[] bytesDecrypted = AES_Decrypt(bytesToBeDecrypted, passwordBytes);

                string result = Encoding.UTF8.GetString(bytesDecrypted);

                return result;
            }

        }
    }

程序员之网络安全系列(二):如何安全保存用户密码及哈希算法

前言

在很多网站的早期,甚至是现在仍然有一些网站,当你点击忘记密码功能时,你的邮箱会收到一封邮件,然后里面赫然写着你的密码,很多普通用户还会觉得庆幸,总算是找回来了,殊不知,这是多么可怕地一件事,说明了网站是“几乎是”明文存储你的密码,一旦数据用户数据泄露或者被拖库,那么用户密码将赤裸裸的暴露了,想想之前几次互联网密码泄露事件。

那么如何解决呢?

加密

为了不让密码明文存储,我们需要对密码进行加密,这样即使数据库用户密码暴露,也是加密后的。但是如何让加密后的数据难以解密呢?我们现在比较流行的做法就是把密码进行Hash存储。

Hash

哈希算法将任意长度的二进制值映射为较短的固定长度的二进制值,这个小的二进制值称为哈希值。哈希值是一段数据唯一且极其紧凑的数值表示形式. 典型的哈希算法包括 MD2、MD4、MD5 和 SHA-1

Hash算法是给消息生成摘要,那么什么是摘要呢?

举个例子:

比如你给你女朋友写了一封邮件,确保没被人改过,你可以生成这样一份摘要 “第50个字是我,第100个字是爱, 第998个字是你”,那么你女朋友收到这个摘要,检查一下你的邮件就可以了。

Hash算法有两个非常主要的特征:

  • 不能通过摘要来反推出原文
  • 原文的非常细小的改动,都会引起Hash结果的非常大的变化

因此,这个比较适合用来保存用户密码,因为不能反推出用户密码,Hash结果一致就证明原文一致,我们来用Ruby代码试一下上面的第二点 (MD5是一种常用的Hash算法)

2.2.3 :003 > require 'digest/md5.so'
=> true
2.2.3 :004 > puts Digest::MD5.hexdigest('I love you')
e4f58a805a6e1fd0f6bef58c86f9ceb3
=> nil
2.2.3 :005 > puts Digest::MD5.hexdigest('I love you!')
690a8cda8894e37a6fff4d1790d53b33
=> nil
2.2.3 :006 > puts Digest::MD5.hexdigest('I love you !')
b2c63c3ca6019cff3bad64fcfa807361
=> nil
2.2.3 :007 > puts Digest::MD5.hexdigest('I love you')
e4f58a805a6e1fd0f6bef58c86f9ceb3
=> nil
2.2.3 :008 > 

那么我们在使用MD5保存密码时候的验证流程是什么呢?

  • 用户注册时,把用户密码是MD5(password)后保存到数据库。
  • 用户输入用户名和密码
  • 服务器从数据库查找用户名
  • 如果有这个用户,A=MD5(input password), B=Database password
  • 如果A==B, 那么说明用户密码输入正确,如果不相等,用户输入错误。

为什么Hash(MD5)后仍然不够安全?

穷举

但是,如果你认为就只是这样密码就不会被人知道,那么就不对了,这只是比明文更安全,为什么?

因为,大部分人的密码都非常简单,当拿到MD5的密码后,攻击者也可以通过比对的方式,比如你的密码是4218

2.2.3 :008 > puts Digest::MD5.hexdigest('4218')
d278df4919453195d221030324127a0e

那么攻击者可以把1到4218个数字都MD5一下,然后和你密码的MD5对比一下,就知道你原密码是什么了。

曾经我的密码箱密码忘了,我把锁给撬了,后来我才想起可以用穷举法,最多就999次不就打开了?那么问题来了,你的密码箱还安全吗?

彩虹表

除了穷举法外,由于之前的密码泄露,那么攻击者们,手上都有大量的彩虹表,比如”I love you”,生日等等,这个表保存了这些原值以及MD5后的值,那么使用时直接从已有库里就可以查出来对应的密码。

加盐 Salt

那么,由于简单的对密码进行Hash算法不够安全,那么我们就可以对密码加Salt,比如密码是”I love you”, 虽然彩虹表里有这条数据,但是如果加上”安红我爱你”,这样MD5结果就大不一样.

jacks-MacBook-Air:~ jack$ irb
2.2.3 :001 > require 'digest/md5.so'
=> true
2.2.3 :002 > puts Digest::MD5.hexdigest('I love you')
e4f58a805a6e1fd0f6bef58c86f9ceb3
=> nil
2.2.3 :003 > puts Digest::MD5.hexdigest('I love you安红我爱你')
b10d890bf46b1a045eb99af5d43c7b13
=> nil
2.2.3 :004 > puts Digest::MD5.hexdigest('I dont love you')
c82294c9a7b6e4a372ad25ed4d6011c9
=> nil
2.2.3 :005 > puts Digest::MD5.hexdigest('I dont love you安红我爱你')
dce67bcdfdf007445dd4a2c2dc3d29c1
=> nil
2.2.3 :006 >

如此一来,因为攻击者很难猜到“安红我爱你”,那么自然彩虹表里是没有的,当然我建议你在实际项目中不要使用”安红我爱你”,你应该使用一个连你自己都猜不到的较长的字符串。

加盐了,就安全了吗?

实际上,加盐并不能100%保证安全,假如有人泄露了你的Salt呢?实际上通过反编译程序很容易可以拿到这个,由于WEB程序一般放在WEB服务器上,那么就需要保证服务器不被攻击,当然这个是运维人员去操心。

为了让加盐更安全,一般情况下我们可以使用一个“盐+盐”,也就是为每个用户保存一个”Salt”, 然后再使用全局的盐,我们可以对用户的盐使用自己的加密算法。那么代码就如下:

if MD5(userInputPpassword+globalsalt+usersalt)===user.databasePassword) 
{
    login success
}

普通用户如何做?

由于这个是写给程序员,当然是说在前端用户注册时密码应该如何设置,很简单,我们要求用户必须输入强密码!但是,我知道很多用户觉得很烦,这样你就失掉了一个用户,但我们需要做一个适当的折中,比如至少有一个大写字母,小写字母和数字的组合。

最后

我们来看看解决了之前文章下面例子的什么问题。

假如,明明和丽丽相互不认识,明明想给丽丽写一封情书,让隔壁老王送去

  1. 如何保证隔壁老王不能看到情书内容?(保密性)
  2. 如何保证隔壁老王不修改情书的内容?(完整性)
  3. 如何保证隔壁老王不冒充明明?(身份认证)
  4. 如何保证明明不能否认情书是自己写的?(来源的不可否认)

通过了解hash算法,”明明” 就有办法让丽丽知道信的内容没有修改,他可以对邮件进行Hash生成邮件的摘要,然后让”隔壁的李叔叔”把摘要送给丽丽,丽丽拿到邮件的摘要后,把邮件内容也Hash一下,然后把结果和”隔壁的李叔叔”给的摘要对比一下,然后通过比较结果就知道邮件有没有被”隔壁的王叔叔”更改过了。

程序员之网络安全系列(一):为什么要关注网络安全?

假如,明明和丽丽相互不认识,明明想给丽丽写一封情书,让隔壁老王送去

  1. 如何保证隔壁老王不能看到情书内容?(保密性)
  2. 如何保证隔壁老王不修改情书的内容?(完整性)
  3. 如何保证隔壁老王不冒充明明?(身份认证)
  4. 如何保证明明不能否认情书是自己写的?(来源的不可否认)

前言

大家都知道最近几年闹的沸沸扬扬的网络安全事件,之前的CSDN密码泄露,不久前的网易邮箱密码泄露,那么如果你的密码泄露,除了本身的网站外,还有很多人其它很多地方甚至银行密码都使用相同的密码,从而带来了很大的麻烦,据说“半个” 互联网的库都被人拖过。

亲身经历

拿我自己经历的一件事来说,前不久,我和一个朋友一起定了趟机票去上海,出发前两小时,收到一个短信说我预定的航班由于天气原因被取消,我需要改签另一个航班,让我拨打航空公司400的电话,由于马上要出发去机场,也没有去查具体航空公司的电话,就打了这个电话,对方的整套系统客服系统和流程几乎和航空公司一模一样,知道我的姓名,身份证,订的航班号等等都一清二楚,最后再说改签需要给用户补偿,需要给你转钱,所以你需要提供银行卡号,然后确认后才能改签,我这样说大家可能觉得如果是自己不太可能上当,但是你们都忽略了人的心里因素,第一你的私密信息他都知道(就像一个陌生人让你爸转钱说你需要钱,说是你的好朋友,知道你的所有只有你爸和你知道的信息), 第二你很着急,因为你在目的地的酒店已定,后面去别的地方的机票、火车票已定,所以你必须这个时间起飞等等。

最后我没有上当的原因,是因为我问骗子,航站楼是哪一个,他说是4号航站楼,因为西安就没有4号航站楼,当然这个应该是骗子的失误。

我们所有的人觉得自己百分之百不会上当的人,那是因为你没碰上骗子高手!

骗子可以得逞的罪魁祸首

我总结了大部分骗子能够骗成功的最主要的原因,是因为我们的私密信息被泄露了!可见网络安全的重要性,而针对我的这个事件,我不知道是航空公司还是某订票网站把我的信息泄露,当然,骗子可能组合多个地方的泄露信息。

而这些信息系统是谁开发和维护的呢? 是程序员!

所以我们程序员需要学习安全知识,保护用户数据,同时防止自己被骗,对那些安全性不高的网站尽量不要使用,对那些安全不高的系统尽量不使用。

作为一个多年的程序员,我对网络安全相关的知识也非常少,我知道一些常规的东西,比如敏感数据加密存储,网站尽量使用https等,但是由于对后面的原理知道的太少,所以有的时候不一定做出了正确地选择,直到最近有的程序员说密码MD5存储了,就一定是安全的,这让我感觉了害怕,而且我看有的系统也真的只是MD5了一下,所以我开始决定学点安全的常识,记录一点“大家” 常用的程序安全知识, 在这里和大家共同进步,由于我对这对理解的不深,要学习的东西很多,所以也希望大家帮忙指出错误的地方。

前端构建大法 Gulp 系列 (四):gulp实战

前面讲了很多理论,那么这一节我们将讲一些实战的例子

安装Node.js

先在命令行下输入 node -v 检查一下是否装了node, 如果没有请参考 https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager 安装

然后再用 npm -v 来确保Node.js 安装正确

安装 Gulp

我们可以使用npm来安排装Gulp, 为了可以在命令行全局使用,我们安装到全局,另外确保其它的程序员可以使用,我们保存到项目的package.json里

1
npm install gulp -g

创建项目

创建一个文件目录,然后建立对应的文件夹

  • src — 源文件:

    • images 
    • scripts 
    • styles 
  • build — 编译后文件输出到的生产文件夹:

    • images 
    • scripts 
    • styles 

我们先使用npm init来创建类似Nuget package的package.config一样的文件,这样我们就知道项目依赖哪些插件,而且我们不需要把插件提交到代码库,其它程序员只需要使用 npm install 就可以安装所有配置的插件

然后我们需要创建一个gulpfile.js文件,gulp默认是调用这个文件的。

我们在目录下使用

  npm install gulp --save-dev  # 这样可以把gulp安装到本地

使用插件

比如我们想检查我们的js文件,那么我们需要安装 gulp-jshint插件

1
npm install gulp-jshint --save-dev

然后添加一个test.js文件到src/scripts下,内容如下

1
2
3
4
5
var hi="hello"

function sayHello(){
console.log("Jack "+hi)
}

jshint 代码检查

然后我们修改gulpfile.js内容如下

1
2
3
4
5
6
7
8
9
10
11
12
// include gulp
var gulp = require('gulp');

// include plug-ins
var jshint = require('gulp-jshint');

// JS hint task
gulp.task('jshint', function() {
gulp.src('./src/scripts/*.js')
.pipe(jshint())
.pipe(jshint.reporter('default'));
});

然后运行

gulp jshint

看控制台输出就知道我们少了分号。

代码合并压缩

我们新建一个 ./scripts/b.js, 然后我们把js文件合并然后压缩并输出到./build/scripts/all.js 下,同时移除debug信息

我们需要安装一下插件

1
2
3
npm install gulp-concat --save-dev 
npm install gulp-strip-debug --save-dev
npm install gulp-uglify --save-dev

修改gulpfile.js

1
2
3
4
5
6
7
8
9
10
11
12
var gulp = require('gulp'); 
var concat = require('gulp-concat');
var stripDebug = require('gulp-strip-debug');
var uglify = require('gulp-uglify');

gulp.task('scripts', function() {
gulp.src(['./src/scripts/*.js'])
.pipe(concat('all.js'))
.pipe(stripDebug())
.pipe(uglify())
.pipe(gulp.dest('./build/scripts/'));
});

我们看到gulp已经把我们文件合并了,移除了console.log, 而且进行了压缩。

至此,已经基本上知道gulp怎么使用了,下面展示一些其它的功能的代码

1
2
npm install gulp-autoprefixer --save-dev 
npm install gulp-minify-css --save-dev

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var gulp = require('gulp'); 
var concat = require('gulp-concat');
var stripDebug = require('gulp-strip-debug');
var uglify = require('gulp-uglify');
var autoprefix = require('gulp-autoprefixer');
var minifyCSS = require('gulp-minify-css');

gulp.task('scripts', function() {
gulp.src(['./src/scripts/*.js'])
.pipe(concat('all.js'))
.pipe(stripDebug())
.pipe(uglify())
.pipe(gulp.dest('./build/scripts/'));
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// CSS concat, auto-prefix and minify
gulp.task('styles', function() {
gulp.src(['./src/styles/*.css'])
.pipe(concat('styles.css'))
.pipe(autoprefix('last 2 versions'))
.pipe(minifyCSS())
.pipe(gulp.dest('./build/styles/'));
});

// default gulp task
gulp.task('default', [ 'scripts', 'styles'], function() {

// watch for JS changes
gulp.watch('./src/scripts/*.js', function() {
gulp.run('jshint', 'scripts');
});
// watch for CSS changes
gulp.watch('./src/styles/*.css', function() {
gulp.run('styles');
});
});

至此,大家应该熟悉gulp的使用,尽情去挖掘gulp plugin的宝藏吧。

前端构建大法 Gulp 系列 (三):gulp的4个API 让你成为gulp专家

gulp 本身能做的事情非常少,主要是通过插件来提供各种功能,gulp本身只提供了4个非常简洁的API, 掌握这4个API你就基本掌握了gulp的全部。

一、gulp.task

gulp 是基于task的方式来运行

定义

gulp.task(name [, deps, fn])
注册一个task, name 是task的名字,deps是可选项,就是这个task依赖的tasks, fn是task要执行的函数

示例

1
2
3
4
5
6
7
gulp.task('js', ,['jscs', 'jshint'], function(){
return gulp
.src('./src/**/*.js')
.pipe(concat('alljs'))
.pipe(uglify())
.pipe(gulp.dest('./build/'));
});

提示

上例中

  • jscs和jshint先运行,随后再运行js的task.
  • jscs和jshint是并行执行的,而不是顺序执行

二、gulp.src

定义

gulp.src(globs[, options])

与globs 匹配的文件,可以是string或者一个数组

示例

1
2
3
4
5
6
7
8
9
gulp.src(['client/*.js', '!client/b*.js', 'client/c.js'])   # !是排除某些文件

gulp.task('js',['jscs', 'jshint'],function(){
return gulp
.src('./src/**/*.js', {base:'./src/'})
.pipe(uglify())
.pipe(gulp.dest('./build/'));

});

options.base 是指多少路径被保留,比如上面的 ./src/users/list.js 会被输出到 ./build/users/list.js

提示

如果我们需要文件保持顺序,那么出现在前面的文件就写在数组的前面

1
gulp.src(['client/baby.js', 'client/b*.js', 'client/c.js'])  

上面baby.js就出现在最上面。

三、 gulp.dest

定义

gulp.dest(path[, options]) 就是最终文件要输出的路径,options一般不用

四、gulp.watch

定义

gulp.watch(glob [, opts], tasks) or gulp.watch(glob [, opts, cb]) 就是监视文件的变化,然后运行指定的Tasks或者函数,这个相比Grunt需要使用插件,gulp本身就支持的很好。

示例

1
2
3
4
5
6
7
8
9
gulp.task('watch-js', function(){
gulp.watch('./src/**/*.js',['jshint','jscs']);
});

gulp.task('watch-less', function(){
gulp.watch('./src/**/*.less',function(event){
console.log('less event'+event.type+' '+event.path)
});
});

最后

gulp就是如此的简单,你只需要掌握这四个API就够了,剩下的就是熟悉相关的plugin了。

参考链接 https://github.com/gulpjs/gulp/blob/master/docs/API.md

前端构建大法 Gulp 系列 (二):为什么选择gulp

在上一篇 前端构建大法 Gulp 系列 (一):为什么需要前端构建 中,我们说了为什么需要前端构建,简单一句话,就是让我们的工作更有效率。

相信熟悉前端的人对Grunt一定不陌生,实际上我自己之前的很多项目也是在用Grunt, Grunt的出现是前端开发者的福音,大大减少了前端之前很多手工工作的繁琐以及我上一篇 前端构建大法 Gulp 系列 (一):为什么需要前端构建 提到的那些问题。

那么既然Grunt可以做到几乎所有的事情,那么为什么我们需要Gulp呢?

Grunt与Gulp的区别

我们来看一下一般前端构建的流程

二者处理流程的区别

Grunt 的方式

Gulp的方式

配置的简洁程度

Grunt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module.exports = function(grunt) {

// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
uglify: {
options: {
banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
} ,
build: {
src: 'src/<%= pkg.name %>.js',
dest: 'build/<%= pkg.name %>.min.js'
}
}
});

grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.registerTask('default', ['uglify']);
};

Gulp

1
2
3
4
5
6
7
8
gulp.task('default',function(){    
return gulp
.src("**/*.js")
.pipe(jshint())
.pipe(concat())
.pipe(uglify())
.pipe(gulp.dest('./build/'))
})

所以从上面的一些代码对比来看,Gulp明显比Grunt要简洁易用很多。

最后,总结一些 Grunt的一些问题

  • 配置过于复杂
  • 插件职责不单一 (就是不SRP)
  • 临时文件目录多
  • 性能慢 (因为临时文件多,自然读IO多)

下一篇我们将开始学习如何使用gulp来构建我们的前端。

前端构建大法 Gulp 系列 (一):为什么需要前端构建

我们都知道使用IDE编写后端程序时,我们都需要Build, 对.NET来说,我们一般需要使用Visual Studio来确保我们的项目编译通过,而且项目编译通过是对所有程序员的基本要求。

但是,由于很多后端程序员对前端的很多东西不了解,导致在做WEB项目时出现了一些问题。

JavaScript和CSS的版本问题

我们都知道 JavaScript和CSS属于静态文件,如果地址不变,浏览器会缓存这些文件,那就意味着当我们需要改JavaScript或者CSS文件的时候,即使我们后端改了,那么客户端也是看不到,这个在“JS一统天下”的时代是不可接受的,因为现在几乎所有的WEB 程序都严重依赖JavaScript,而所有的网站都是需要使用CSS的。在我经历过的项目即使是很多年经验的程序员都出现过JavaScript和CSS文件的版本问题,比如客户让修复一个Bug,这个Bug是JavaScript引起的,程序员修复了,或者是客户说改一个背景颜色,可是当我们给客户部署后或者代码交给客户客户部署时,客户说Bug依然存在,这个时候程序员经常说的话就会出现了 “我本地是好的呀”,最后再找来别人帮忙后,发现原来是没有清除浏览器的缓存,于是有的程序员就赶紧给客户说:“你需要Ctrl+F5 清除浏览器的缓存”。 每当我听到这样的话时就像关上灯留给我一屋子黑,首先,有几个普通用户会使用Ctrl+F5? 其次,有几个用户愿意去Ctrl+F5?

那么怎么办?我想很多程序员都知道加一个版本号就可以了,这样浏览器就会认为是新的文件,比如原来是 http://www.a.com/app.js 你现在只需要把地址改为http://www.a.com/app.js?v=1.0 即可

但是如果这个动作是手动的,那么10次基本上至少有5次程序员会忘掉,那么这就是为什么我们需要前端构建

JavaScript和CSS的依赖问题

我们经常出现的另一个问题,就是JavaScript和CSS的依赖问题,说的通俗点就是JavaScript和CSS的在页面中的顺序问题!

我们经常发现CSS没起作用,JavaScript的某个变量和方法找不到,有很多情况都是因为引入JavaScript或者CSS的顺序不对,虽然我们可以使用一些RequireJS之类的模块管理,但是依然在很多情况下需要引入不同的文件,尤其是CSS没有一个好的模块化管理的组件。

那么我们就需要有一个统一的地方来管理JavaScript和CSS的顺序问题,而构建工具可以大大减少此类问题。

性能优化

我们都知道浏览器请求的文件越多越耗时,请求的文件越大越耗时,尤其是在我们现在很多使用前端MVC, MVVM框架的时候,我们为了前端代码更清晰,结构更合理,我们就由很多JS文件,无疑又拖慢了网页的速度。为了解决这个问题,因此我们需要做两件事

文件合并

浏览器需要下载多个JS文件,而浏览器是有并发限制,也就是同时并发只能下载几个文件,假如浏览器并发数是5,你有20个JS文件,而每5个需要2S, 那么你光下载JS文件都需要8S,那么网页的性能可想而知,所以我们需要合并多个文件以减少文件的数量。

文件压缩

我们知道文件越大,下载越慢,而针对JavaScript和CSS, 里面的空格,换行这些都是为了让我们读代码时更容易阅读,但是对机器来说,这些对它没有影响,所以为了减少文件大小,一般的情况我们都会用工具去掉空格和换行,有时候我们还会用比较短的变量名(记住这个要让工具最后压缩时做,而源代码一定要保证命名可读性) 来减少文件大小。

而所有的前端构建工具都具有文件合并和压缩的功能。

效率提升

Vendor前缀

在CSS3使用越来越多的时候,我们都知道一些CSS的特性,不同的浏览器CSS有不同的前缀,如果我们手工添加将会很繁琐,而如果使用构建工具,很多构建工具可以自动给我添加CSS的Vendor前缀

单元测试

JavaScript的单元测试在使用MVC或者MVVM的框架后,变得越来越容易,而单元测试是质量保证的一个很重要的手段,所以在提交之前,使用构建工具自动跑一遍我们的单元测试是非常重要的

代码分析

我们写的JavaScript很多时候会有一些潜在的bug, 比如忘了添加分号,某个变量没有等等,使用一些JavaScript的代码分析工具,可以很好的帮我们检查一些常见的问题。

HTML引用JavaScript或者CSS文件

比如我们需要使用Bower之类来引用前端JavaScript和CSS的第三方库,那么如果版本升级,添加移除等都用手工来修改HTML的话,第一比较耗时,第二比较容易疏漏,尤其是在我们需要切换Debug和production版本时将会有很多额外的工作,那么使用前端构建工具可以很好的解决这些问题。

最后

以上我只是列出了前端构建最常用的一些功能,我相信还可以发觉很多构建工具可以替代我们手工做的事,后面我将详细讲讲如何使用Gulp这个神器来一一解决我们上面提到的问题。

一小时学会C# 6

c# 6已经出来有一段时间了,今天我们就详细地看一下这些新的特性。

一、字符串插值 (String Interpolation)

C# 6之前我们拼接字符串时需要这样

 var Name = "Jack";
 var results = "Hello" + Name;

或者

 var Name = "Jack";
 var results = string.Format("Hello {0}", Name);

但是C#6里我们就可以使用新的字符串插值特性

  var Name = "Jack";
  var results = $"Hello {Name}";

上面只是一个简单的例子,想想如果有多个值要替换的话,用C#6的这个新特性,代码就会大大减小,而且可读性比起之前大大增强

 Person p = new Person {FirstName = "Jack", LastName = "Wang", Age = 100};
 var results = string.Format("First Name: {0} LastName: {1} Age: { 2} ", p.FirstName, p.LastName, p.Age);

有了字符串插值后:

 var results = $"First Name: {p.FirstName} LastName: {p.LastName} Age: {p.Age}";

字符串插值不光是可以插简单的字符串,还可以直接插入代码

 Console.WriteLine($"Jack is saying { new Tools().SayHello() }");

 var info = $"Your discount is {await GetDiscount()}";

那么如何处理多语言呢?

我们可以使用 IFormattable

下面的代码如何实现多语言?

 Double remain = 2000.5; 
 var results= $"your money is {remain:C}";  

# 输出 your money is $2,000.50

使用IFormattable 多语言

class Program
{
    static void Main(string[] args)
    {

        Double remain = 2000.5; 

        var results= ChineseText($"your money is {remain:C}");

        Console.WriteLine(results);
        Console.Read();
    }

    public static string ChineseText(IFormattable formattable)
    {
        return formattable.ToString(null, new CultureInfo("zh-cn"));
    }
}

# 输出  your money is ¥2,000.50

二、空操作符 ( ?. )

C# 6添加了一个 ?. 操作符,当一个对象或者属性职为空时直接返回null, 就不再继续执行后面的代码,在之前我们的代码里经常出现 NullException, 所以我们就需要加很多Null的判断,比如

 if (user != null && user.Project != null && user.Project.Tasks != null && user.Project.Tasks.Count > 0)
 {
   Console.WriteLine(user.Project.Tasks.First().Name);
 }

现在我们可以不用写 IF 直接写成如下这样

Console.WriteLine(user?.Project?.Tasks?.First()?.Name);

这个?. 特性不光是可以用于取值,也可以用于方法调用,如果对象为空将不进行任何操作,下面的代码不会报错,也不会有任何输出。

class Program
{
    static void Main(string[] args)
    {
        User user = null;
        user?.SayHello();
        Console.Read();
    }
}

public class User
{
    public void SayHello()
    {
        Console.WriteLine("Ha Ha");
    }
}

还可以用于数组的索引器

class Program
{
    static void Main(string[] args)
    {
        User[] users = null;

        List<User> listUsers = null;

        // Console.WriteLine(users[1]?.Name); // 报错
        // Console.WriteLine(listUsers[1]?.Name); //报错

        Console.WriteLine(users?[1].Name); // 正常
        Console.WriteLine(listUsers?[1].Name); // 正常

        Console.ReadLine();
    }
}

注意: 上面的代码虽然可以让我们少些很多代码,而且也减少了空异常,但是我们却需要小心使用,因为有的时候我们确实是需要抛出空异常,那么使用这个特性反而隐藏了Bug

三、 NameOf

过去,我们有很多的地方需要些硬字符串,导致重构比较困难,而且一旦敲错字母很难察觉出来,比如

if (role == "admin")
{
}

WPF 也经常有这样的代码

public string Name
{
  get { return name; }
  set
  {
      name= value;
      RaisePropertyChanged("Name");
  }
}

现在有了C#6 NameOf后,我们可以这样

public string Name
{
  get { return name; }
  set
  {
      name= value;
      RaisePropertyChanged(NameOf(Name));
  }
}

static void Main(string[] args)
  {
      Console.WriteLine(nameof(User.Name)); //  output: Name
      Console.WriteLine(nameof(System.Linq)); // output: Linq
      Console.WriteLine(nameof(List<User>)); // output: List
      Console.ReadLine();
  }

注意: NameOf只会返回Member的字符串,如果前面有对象或者命名空间,NameOf只会返回 . 的最后一部分, 另外NameOf有很多情况是不支持的,比如方法,关键字,对象的实例以及字符串和表达式

四、在Catch和Finally里使用Await

在之前的版本里,C#开发团队认为在Catch和Finally里使用Await是不可能,而现在他们在C#6里实现了它。

  Resource res = null;
    try
    {
        res = await Resource.OpenAsync(); // You could always do this.  
    }
    catch (ResourceException e)
    {
        await Resource.LogAsync(res, e); // Now you can do this … 
    } 
    finally
    {
        if (res != null) await res.CloseAsync(); // … and this.
    }

五、表达式方法体

一句话的表达式可以直接写成箭头函数,而不再需要大括号

class Program
{
private static string SayHello() => “Hello World”;
private static string JackSayHello() => $”Jack {SayHello()}”;

1
2
3
4
5
6
7
8
    static void Main(string[] args)
{
Console.WriteLine(SayHello());
Console.WriteLine(JackSayHello());

Console.ReadLine();
}
}

六、自动属性初始化器

之前我们需要赋初始化值,一般需要这样

1
2
3
4
5
6
7
8
9
public class Person
{
public int Age { get; set; }

public Person()
{
Age = 100;
}
}

但是C# 6的新特性里我们这样赋值

1
2
3
4
public class Person
{
public int Age { get; set; } = 100;
}

七、只读自动属性

C# 1里我们可以这样实现只读属性

1
2
3
4
5
6
7
8
9
public class Person
{
private int age=100;

public int Age
{
get { return age; }
}
}

但是当我们有自动属性时,我们没办法实行只读属性,因为自动属性不支持readonly关键字,所以我们只能缩小访问权限

1
2
3
4
5
public class Person
{
public int Age { get; private set; }

}

但是 C#6里我们可以实现readonly的自动属性了

1
2
3
4
public class Person
{
public int Age { get; } = 100;
}

八、异常过滤器 Exception Filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void Main(string[] args)
{

try
{
throw new ArgumentException("Age");
}
catch (ArgumentException argumentException) when( argumentException.Message.Equals("Name"))
{
throw new ArgumentException("Name Exception");

}

catch (ArgumentException argumentException) when( argumentException.Message.Equals("Age"))
{
throw new Exception("not handle");

}
catch (Exception e)
{

throw;
}
}

在之前,一种异常只能被Catch一次,现在有了Filter后可以对相同的异常进行过滤,至于有什么用,那就是见仁见智了,我觉得上面的例子,定义两个具体的异常 NameArgumentException 和AgeArgumentException代码更易读。

九、 Index 初始化器

这个主要是用在Dictionary上,至于有什么用,我目前没感觉到有一点用处,谁能知道很好的使用场景,欢迎补充:

1
2
3
4
5
6
7
8
9
10
11
12
var names = new Dictionary<int, string>
{
[1] = "Jack",
[2] = "Alex",
[3] = "Eric",
[4] = "Jo"
};

foreach (var item in names)
{
Console.WriteLine($"{item.Key} = {item.Value}");
}

十、using 静态类的方法可以使用 static using

这个功能在我看来,同样是很没有用的功能,也为去掉前缀有的时候我们不知道这个是来自哪里的,而且如果有一个同名方法不知道具体用哪个,当然经证实是使用类本身的覆盖,但是容易搞混不是吗?

1
2
3
4
5
6
7
8
9
10
11
12
using System;
using static System.Math;
namespace CSharp6NewFeatures
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(Log10(5)+PI);
}
}
}

总结

上面一到八我认为都是比较有用的新特性,后面的几个我觉得用处不大,当然如果找到合适的使用场景应该有用,欢迎大家补充。

最后,祝大家编程愉快。

微软程序员最好的时代来了

每过一段时间就有人跳出来说微软不行了,.NET不行了,然后就去舔Java, 但是一直让我觉得比较奇怪的是,几年以后那些人还在用.NET,而且继续喷着.NET, 舔着JAVA, 在我看来,这些人和那些天天喷自己的公司,却依然在那个公司,天天喷中国,却依然在中国的那些人是一样的。

语言只是工具

因为我不是非常熟习JAVA, 所以我不知道JAVA擅长做什么,但是我觉得.NET能做的事,基本上JAVA应该都能做,就像我认为JAVA能做的事.NET基本也都能做一样。但是奇怪的是我经常看到的是.NET人员喷.NET, 很少听到JAVA人员喷.NET, 不过我估计很多JAVA人员应该也忙着去喷JAVA吧。这个其实和语言没关系,只是和人的心理有关系,因为人总觉得“碗里的没锅里的好”。

语言之争已经持续很多年了,其实这个大家都知道没什么意义,网上不是流传一个语言的鄙视链吗?我觉得大家把他当个玩笑罢了,千万别当真,因为不管怎么鄙视,我们毕竟要吃饭的。

本来我也觉得写这篇文章没什么意义,但是看到那些喷.NET文章,基本上句句说的都没理,但是却可能对初入行的人带来很大的误解,就像骗子很容易骗小孩,因为小孩涉世未深。

我做开发已经10多年了,学过很多语言,但是真正用来吃饭的最主要的语言其实是Visual Basic和.NET, 当然还有”汉语”. 当初也学过Java, JSP, SSH. 但是最后选择了.NET,为什么,一是因为.NET技术真的入门很快,当年入门快的好处之一就是你立马就有一定的生产力,也就是你能很快开始干活,这样就有公司愿意给你付薪水了,也就意味着你可以更早从父母那里断奶了,另外一个原因就是当时面试的要.NET比要JAVA的多呀。

C# 语言

我学习过C, Java,Visual Basic, Ruby,Node.js等,但是我还是觉得C#是生产力非常高的一门语言,比如一些非常优秀的语言特性,你刚刚才能从其它语言里看到一点点,比如自动属性,LINQ, Lamda表达式,Action等,另外C#对多线程的封装让我们在多线程编程时极其方便,比如TPL. 还有令大家头疼的异步回调的问题,C#用非常优雅的Async, Await来解决,我们看到ES7里面已经开始实现类似的东西,是不是借鉴了C#呢?

多语言混用

对一个稍微复杂一点的程序来说,我们为什么一定要只使用一个语言呢?比如我们前端可以使用ASP.NET MVC, 后端可以使用Java 甚至是别的任何一个语言,服务我们可以使用WCF, 搜索我们可以使用Solar等等,我们甚至是WEB层,业务层都是用.NET, 而数据库可以使用MySQL或者MongoDB.

.NET或者JAVA只是系统的一部分

我们知道一个WEB程序,除了后端以外,很多其它的东西比如HTML, CSS,JavaScript,数据库这些不管你做Java还是.NET都是一样需要的。也就是前端技术都是相同,另外HTTP协议,TCP/IP这些也不分语言吧。不管你是JAVA还是.NET, AngularJS, ReactJS, HTML5, Bootstrap, Bower, Grunt, Gulp这些东西对你都是一样的吧?

微软技术能做什么

我没有做过JAVA程序,但是我使用微软技术10多年了,我就说一说微软技术能做什么。

桌面程序

Visual Basic

当年我毕业没多久,就加入一家马来西亚在中国的软件公司,这个公司主要是做门票系统,当时我们选择了Visual Basic, 做过VB的人都知道VB是多么的强大,除了极其方便的可见即所得的Form外,而且有几乎一切你想要的组件,另外我们都知道Windows是桌面系统中当之无愧的的霸主, 我不知道JAVA在这方面的优势是什么,如果有些人说要跨平台,但是我做了这么多年的企业软件,没有几个企业软件是需要跨平台的。当时在这个公司里做了售票系统,还有闸机系统,POS系统,我估计很多人不知道闸机系统原来用Visual Basic也可以做。广州,北京最大的游乐园都是我们当年做的,而当年整个乐园的信息系统都是使用的微软的技术。

WPF

微软推出的WPF, 我觉得是对桌面开发程序的极大的进步,几乎所有的桌面程序的展示都可以使用WPF来呈现,而通过WPF可以让我们非常方便快速的做出非常炫的桌面程序,WPF的XAML方式给了我们非常方便的写桌面窗体。 同时模板,动画等在WPF都是极其容易实现,另外MVVM在WPF的应用里非常盛行,WPF可是比Angular早出现了很多年。

UWP

不久前,微软推出了Windows 10, 这使一套系统可以运行在桌面,平板以及手机上,而我们可以使用XAML,甚至是JavaScript来开发一套程序就可以运行在多个终端,这个对个人用户可能没有什么,但是对企业用户太重要了,具体的细节我觉得大家可以去看一下MSDN或者Channel9

WEB

ASP.NET WEB Forms

我觉得很多人对微软的误解可能主要是这一部分,当年的ASP.NET WEB Forms由于是快速拖控件,导致界面生成很多难以认识的代码,大量的ViewState等,但是这并不能说ASP.NET WEB Forms不好,第一我们可以使用ASP.NET WEB Forms快速完成一些小型的WEB程序,加上有大量的第三方控件,是开发一些常规的程序快如闪电。第二我们可以尽量使用客户端的控件比如Repeater等,现在仍然有很多网站都是ASP.NET WEB Forms做的,难道这些网站都没有用户?!

ASP.NET WEB MVC

由于很多人对ASP.NET WEB Forms充满抱怨,而且WEB开发技术不断地革新,微软也顺应潮流及时的推出了ASP.NET MVC, 使用MVC可以写出非常清爽的代码,我们团队从ASP.NET MVC1.0 beta就开始使用,由于我也学习过Ruby On Rails,基本上这两个框架非常相似,极其方便的路由管理,View, Controller, Model的分层,使我们可以很好的使用强类型类开发,我们已经使用ASP.NET MVC成果提交过很多项目,还从来没有遇到过问题是出自.NET本身的。

WEB 服务

WCF

使用统一的模型,让我们开发WEB服务极其方便,大部分情况下我们只需要定义一个接口,配置相关的Binding和EndPoint就可以了,可以很方便的使用Http, TCP, Https,可以非常方便的使用各种安全策略,而这些就只需要简简单单的几行配置而已,WCF极大的简化了WEB服务的开发。

ASP.NET WEB API

除了WCF, 微软又推出了ASP.NET WEB API, 使得我们开发轻量级的WEB 服务极其容易,使用ASP.NET WEB API 我们可以非常容易实现服务的Restful. 而使用OWEN我们可以以任何方式来部署我们的API。

Azure 微软云

微软的云非常的强大,使用过微软云的都知道,微软云几乎可以满足我们一切对程序的要求,我们可以使用Azure web apps很方便的创建一个WEB, Webjob, 我们可以使用Azure SQL, 使用Azure storage, 可以极其方便的使用云的Queue, Bus等等,而且云集成了很多优秀的第三方程序,比如我们可以使用Redis作为Cache. 另外微软的Cloud Services让我们可以极其方便的管理我们的部署。这让我们很多程序员从一个程序员可以快速成为可以运维企业整套系统的人,而你只需要学习一些Azure的管理知识,比如自动扩展等等,而这些在云里面都可以快速简单的配置。我们几乎一半客户的系统都运行在Azure里面,而且我们可以一键部署,如果有问题,我们甚至可以使用Visual Studio来直接Debug云里面的网站。

移动开发

由于Mono,现在可以使用.NET来开发移动的应用程序,使用Xamarin可以使用C#开发出和原生性能一模一样的iOS和Android程序,我也熟悉Objective-C, 而且用Objective-C开发过程序,虽然说Objective-C 在某些方面很优秀,但是开发效率实在不敢恭维,不然Apple也不会推出Swift来革自己的命,另外当你既需要iOS,又需要Android的时候,你至少需要一个熟悉Objective-C的,又需要一个熟悉Java. 而同样地东西需要做两遍。而使用Xamarin做企业级APP,大量的业务逻辑可以共享,更别说可以直接使用效率非常的C#语言了,我们已经成功提交了好几个基于Xamarin的程序。

另外,Xamarin推出的Xamarin.Forms可以使用XAML及C#用一套代码来开发iOS, Android以及Windows Phone的程序,而且性能和原生的一模一样,我们也顺利开发过基于Xamarin.Forms的程序。

游戏开发

现在已经有很多程序基于Unity开发,而你可以选择C#来作为基于Unity的游戏的主要语言。可以搜索一下,已经有大量的上架游戏是使用.NET开发的。

小结

当然,以上只是我使用的微软相关技术,目前来说,除了客户明确选择其它语言以外,还真的没有多少是Java能做而.NET做不了的。

桌面,WEB, 移动开发 都可以使用.NET,简单点说,就是你都可以使用一门语言,那就是C#来开发

开发工具

Visual Studio

做.NET开发,配套的开发工具是Visual Studio, 我觉得Visual Studio是最好的IDE之一,你几乎可以使用做任何语言的开发,这个使用过的人都非常清楚,虽然说Vim是编辑器之神,而Emacs是神的编辑器,但是显示世界能有多少个神?

Resharper

Resharper是每个.NET程序员的必备工具之一,基本上可以让我们的开发效率提高三分之一,设个谁用过谁知道。

领域驱动设计

如果我没有记错的话,虽然Eric Evans较早写了领域驱动设计一书,但真正推动领域驱动设计的是有很多做.NET开发的,比如Greg Young, 我们可以看看NServicebus. 可以看看 NServicebus

我们已经使用领域驱动设计提交了一个非常大型的项目,这个项目是一个世界500强的主要系统。而这个系统就是使用的.NET C#,使用了CQRS, NServicebus, ASP.NET MVC, ASP.NET WEB API,SQL Server等等,系统已经运行了好几年了,还没有发现什么问题。

有兴趣的可以关注一下我的领域驱动系列。

长尾理论

很多人说大型的一些系统都没有用.NET, 比如BAT, 比如新浪微博,他们是不是一点都没有用.NET我不知道。我想说的第一那些系统都比较庞大,使用JAVA或者PHP很多时候是基于历史的选择,另外这些系统就那么几个,而且并不是所有的软件都是电商,都是微博?

我在这里想说的长尾,就是第一中小企业几乎占据所有的企业的80%, 而这些企业需要各种各样的系统,而这些企业不论是国内还是国外,都是Windows占大部分。虽然我也非常喜欢苹果的产品,但是企业是需要赚钱的。 也就是说80%的企业都不会像BAT那么大,那么至少这80%的系统使用.NET开发没有任何问题,再加上.NET有着很高的开发效率,我们有什么理由不选择呢?

使用.NET应该是企业或者客户项目的第一选择

如果只做WEB系统,或者只做电商之类,那么使用其它任何语言都没问题,但是一旦做企业系统,往往.NET是一开始非常安全的选择,为什么? 因为很多企业使用的系统是Windows, 使用的办公软件是Office, 使用的服务器是Windows Server,使用的是AD, 使用的邮件系统是Exchang Server, 我不知道你们使用JAVA和PHP和这些系统集成时是否方便,但是使用.NET是非常方便的。目前来看,.NET几乎可以满足企业应用的所有的现有的需求以及潜在的需求。

关于开源,关于免费

现在.NET很多东西都开源了,.NET CORE 和 ASP.NET VNext已经可以跑在Mac和Linux上了,我相信会越来越多的.NET程序将来会跑在Linux服务器上,另外大部分程序根本就用不了那么多服务器,如果我们真的需要那么多服务,证明公司已经很有钱了,还买不起几个Windows?而且如果真的需要那么多服务器,我们可以使用Microsoft Azure, 买几个Windows总比要请几个Linux运维工程师要便宜很多吧。

最后,没有人限制你只会.NET

没有谁能限制.NET程序员学习其他的语言,.NET程序员可以学习Java, Ruby, Node.js, 可以学习Event Driven, Message Queue, Solar, 学习MongoDB, Redis, 学习分布式缓存,学习任何其它语言需要学习的东西。

关于薪水

我不相信一个人学两个月JAVA, 不学习其它东西就可以立马成为一个优秀的程序员,就可以拿到很高的薪水,因为在我看来,要成为一个优秀的.NET程序员,需要学习大量的知识,我相信JAVA程序员也是一样。如果说.NET程序员年薪几百万我没见过,但是把.NET学好可以拿到相对不错的薪水还是没有问题的。

我们就是使用.NET的技术,如果你觉得你.NET技术还可以而没有地方发挥的,欢迎联系我 wangdeshui@outlook.com

  

领域驱动设计系列(六):CQRS

CQRS是Command Query Responsibility Seperation(命令查询职责分离)的缩写。 世上很多事情都比较复杂,但是我们只要进行一些简单的分类后,那么事情就简单了很多,比如我们把人分为男人和女人,也可以把人分为大人和小孩,还比如,我们说国内和国外,城市和农村。经过一些类似这样的划分,我们的对不同的类就有不同的关注。 这样我们就会有妇女儿童医院专门让女人生孩子,而不会建一个医院让男女都生孩子。

##CRUD

CRUD (Create, Read, Update, Delete) 增查改删,我们很多系统都是对数据的增查改删。过去我们很多系统比较简单,基本上增加的数据就是你要查询的数据,所以很多时候其实一个简单的Excel就能搞定。 而且增删改查也足够的简单,所以我们很多系统分层后在数据层Repository里仍是对单表的增删改查,这样对不少的系统都符合。

但是,系统规模稍微大一点,我们都知道我们的数据库里的数据模型很难和我们业务层需要的模型一致。 于是我们引入了Domain Model, Repository里就会做Domain Model的来回转换

同时我们在UI层要的数据,往往又和具体的Domain不同,这个时候我们又要定义一个ViewModel. 而这些ViewModel又是组合不同的DomainModel得来。

传统的代码里的问题:

  • 领域里有很多分页和排序,尤其是Repository里
  • 查询的方法里暴露了很多不应该有的领域模型的属性,因为需要组装DTO
  • 如果使用ORM,预加载了很多数据以提高性能,但是占用大量内存,而且需要维护这些数据。
  • 加载组合庞大的数据,比如页面是需要一个名字,我们也会把整个User数据取出来。

重要的原来把数据混在一起,复杂的查询相当难以优化。 尤其是数据库出现大量的Join 系统性能极速下降。

最重要的是我们把读写都放在了一起,显得责任不够清晰,代码也更复杂了一些,比如读数据是不太关心事物的,读数据是不需要验证的,只有写的时候才需要做数据校验,这也比较符合SRP(单一职责),但是用CRUD的思维是我们全都混在了一起。

CQRS

我们仔细看CRUD, 其实可以更简单的分为读(R)和写(CUD), 我们想想大部分情况都是,一个方法要么是执行一个Command完成一个动作,要么就是查询返回数据。 比如我们回答问题的人不应该去修改问题。

当我们读写分离后,我们对应的代码也会分离。

数据存储

写的一端需要保证事物,所以一般数据存储为第三范式,
读的一端一般都是反范式可以避免Join操作,这样我们只需要把数据存储为第一范式

扩展

大部分的系统里写数据要远远少于读数据,并且一般都是每次修改很少的一部分数据,所以在写这端扩展都不是特别紧迫,读数据基本都远大于写数据的次数, 所以扩展就更重要。 我们很难建立同一个Model 既能给写数据和读数据公用而且能够保证性能都比较好的。

查询端

查询端由于只是读数据,那么所有的方法应该都是返回数据,而且返回的数据就是界面直接需要的DTO, 这样可以减少传统的方法中把DomainModel映射为ViewModel或者DTO. 同时可以减少传统的领域里的一些混乱。

写端

由于把读分离出去,所以我们就只关注写,那么我们写这一段需要保证事物,数据输入的验证,另外一般写这一端都不需要及时的看到结果,所以大部分都需要一个void方法就可以,那么让我们系统异步就更加方便。这样使系统的扩展性大大增强。

代码更容易集中处理

当我在一些系统中使用CQRS后,很多地方代码大大简化,比如我所有的写操作都是一个Command, 那么我定义一个UICommand, 让所有的Command集成这个,那么我可以在这个UICommand里做一些通用的处理,比如Validation

同时我只需要定义一个CommandBus, 然后把对应的CommandBus分发到对应的Handler里(我前面几篇有实例代码),那么代码的耦合度大大降低。

代码分工协作更容易

由于读这一端直接读数据,而且对数据库没有任何操作,那么我们可以根据UI定义对应的DTO, 那么开发的时候我们可以用Mock数据,至于数据怎么存的,那么我们随后只要添加一层Thin Data Layer即可,实际上当我们使用CQRS后,很多时候我们把数据保存的时候都直接保存为Denormalize的,那么从数据里直接查询单表的数据就可以拿到页面需要的数据,大大提升读取数据的性能,同时代码也会极其的简化,开发读这一段代码的开发人员甚至都不需要对业务有太多了解。

实现

简单的实现

使用的Event后

使用了Event Source 和Service bus后

  
 DDD

.Net WEB 程序员需要掌握的技能

最近公司里有新入职的员工,根据个人的理解和经验,我来列一下一个.Net WEB程序员需要掌握的知识

基础部分

  • C# 基础语法
  • OOP的概念,面向对象的理解
    • 继承
    • 封装
    • 多态
  • ASP.NET MVC (Web Form 用的越来越少,如果你不熟悉,可以不看)
  • JavaScript
    • 基础语法
    • 如何在HTML里使用JavaScript
    • 常用Dom 操作
  • SQL Server
    • 常用T-SQL 增删改查
    • SQL Managment studio 常用操作
  • jQuery 常用API
  • HTML
    • HTML 所有标签的语义,什么时候用什么标签
  • CSS
    • 常用CSS的知识,如何在HTML使用
    • 常用的布局
  • Bootstrap
  • C# 代码规范
  • CSS 代码规范
  • JavScript代码规范
  • Git的基本使用
    • 常用命令使用
    • 获取代码
    • 解决冲突
    • 提交代码
    • 新建分支,合并分支
  • Github
    • 帐号设置
    • SSH 配置
  • 三层设计
    • UI
    • Service
    • Repository
    • 事务的代码实现

提高部分

  • 单元测试

    • Nunit
    • Moq
    • 单元测试的基本知识 Arrange, Action, Assert
  • 依赖注入

    • 面向接口编程的理解
    • Castle Windsor
      • 与ASP.NET MVC 的集成
      • Installer
      • 不同的生命周期
  • Entity Framework Migration

  • JavaScript

    • 闭包
    • JavaScript 的面向对象
      • prototype
      • JavaScript的动态特性
  • AngularJS

  • .NET

    • Async
    • TPL
    • WCF 基本使用
  • Debug

    • VS 常用Debug 功能
    • Firebug / Chrome Developer
    • Fiddler
  • 常用的快捷键

    • windows
    • Visual studio
    • Resharper
    • Chrome

进阶部分

  • 常用设计模式
  • 常用设计原则的理解
    • S.O.L.I.D
    • DRY
  • Repsiotory Pattern
  • Unit Of Work
  • HTTP 协议
  • 前端常用的性能优化
  • .Net 常用性能优化的方法
  • RequireJS
  • Async.js
  • MVC 模式
  • MVVM 模式
  • SignalR
  • PowerShell

深入部分

  • AOP
  • 领域驱动设计DDD
    • CQRS
    • NServicebus
    • Event Sourcing
  • Event Driven
  • MSMQ/RabbitMQ
  • Load Balance
  • Memcache
  • NoSQL
    • MongoDB
    • Redis
  • Load Balance
    • Cache 问题
    • Session 问题

敏捷

  • SCRUM
  • TDD
  • BDD
  • 使用一个敏捷管理工具 JIRA Scrum/Kanban, Trello, Target Process, Pivotal Tracker, 国产的WorkTile应该也不错
  • XP
  • 持续集成 TeamCity
  • Grunt

扩展部分

  • Node.js 服务器端

  • Socket.io

  • Mobile APP

    • Hybrid APP Phonegap
    • Native APP
      • iOS Objective C /Swift
      • iPhone/iPad development
      • Xamarin Cross-platform development
  • 买一台Mac

    • 熟悉Mac 常用操作
    • 熟悉Shell
  • 学一门动态语言,比如Ruby

  • WPF 熟悉一下最新的Windows Desktop开发

推荐书籍

  

领域驱动设计系列(五):事件驱动之异步事件

前言

上一篇讲了事件,以及为什么要使用事件,主要是为了解耦,但是有同学就问了,同步如果订阅事件的人太多,比如13亿人都关心上头条的事,那么RaiseEvent得等13亿人都处理完,那得多久呀,从此再也不敢发事件了。
举个例子,你在网上下单,下完单要通知库房,甚至要通知供应商补货,如果都是同步的话,消费者还不等急死呀,实际上你在电商网站上下个单, 一般你很快就能到订单页面,那个页面告诉你:“兄弟,订单已经创建成功,订单号是xxxxx-xxxxx-xxxx-xxxx,你的订单已经提交到库房” 等。然后你就很快了的下另一单了。好吧,
提问的同学,说好的妹子呢?

实现思路

发出事件

1
2
3
4
5
6
7
8
9
10
11
12
  public void Head()
{
var NewsPaper = new NewsPaper("南都娱乐");
NewsPaper.WriteToHeader("汪峰");

RaiseEvent(new HeadedEvent {Name = "汪峰"});
}

private void RaiseEvent(HeadedEvent headedEvent)
{
EventBus.Publish<HeadedEvent>(new HeadedEvent { Name = "汪峰" });
}

所以我们只需在代码里RaiseEvent就可以了。

订阅事件

其实很简单,因为我们要实现的是同步的事件,我们只需要找到所有处理这个事件的实现类,然后调用所有就可以了。

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
public interface IEventHandler<TEvent> where TEvent : Event
{
void Handle(TEvent e);
}

public class HeadedEvent:Event
{
public string Name { get; set; }
}

public class GuoJiZhangMotherEventHandler : IEventHandler<HeadedEvent>
{
public void Handle(HeadedEvent e)
{
Console.WriteLine(e.Name+", Are you kidding me?");
}
}

public class PiMingEventHandler:IEventHandler<HeadedEvent>
{
public void Handle(HeadedEvent e)
{
Console.WriteLine(e.Name+", Guo Ji Zhang is your last wife?");
}
}

我们可以看到正真的事件协调者是EventBus, 之前的代码如下是同步的。

1
2
3
4
5
6
7
8
9
10
11
public class EventBus
{
public static void Publish<T>(T concreteEvent) where T: Event
{
var handlers = _container.ResolveAll<IEventHandler<T>>();
foreach (var handle in handlers)
{
handle.Handle(concreteEvent);
}
}
}

为了提高性能,我们可以先来第一步改进

1
2
3
4
5
6
7
public void Publish<T>(T @event) where T : Event
{
var handlers = _eventHandlerFactory.GetHandlers<T>();

handlers.AsParallel().ForAll((h)=> h.Handle(@event));

}

我们可以看到,现在并行处理可以大大加快速度,但是有两个问题,第一个问题就是没有处理异常,所以让我们加上异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 public void Publish<T>(T @event) where T : Event
{
var handlers = _eventHandlerFactory.GetHandlers<T>();

handlers.AsParallel().ForAll((h)=> HandleEvent<T>(h,@event));

}

private void HandleEvent<T>(IEventHandler<T> handle, T @event) where T : Event
{
try
{
handle.Handle(@event);

}
catch (Exception e)
{

// Log the exception, as the caller don't care this
}
}
}

第二个问题,就是我们虽然用了并行加快了速度,但是还没有正真实现异步,整个程序还是等所有Handler处理完才返回。

1
2
3
4
5
6
7
public void Publish<T>(T @event) where T : Event
{
var handlers = _eventHandlerFactory.GetHandlers<T>();

handlers.Select(h => Task.Factory.StartNew(() => HandleEvent<T>(h, @event)));

}

这段代码执行完,尽然发现Handler没有执行,好吧,原因是IQueryable的延迟执行,所以我们需要调用一下ToList

1
2
3
4
5
6
7
public void Publish<T>(T @event) where T : Event
{
var handlers = _eventHandlerFactory.GetHandlers<T>();

handlers.Select(h => Task.Factory.StartNew(() => HandleEvent<T>(h, @event))).ToArray();

}

好了,我们就这样轻易的实现了一个AsyncEventBus, 是不是感谢.Net的强大?

总结

这里还只是一个系统内部的Async, 如果涉及到系统之间的交互,这个就不行了,而且如果异步处理有错误,我们就会有信息丢失,所以需要更健壮的异步事件处理系统,这个后面再讲,但是一般的系统我们只需要把出错的时间记录下来,然后再看要不要处理就可以。

另外,异步要处理的东西很多,比如处理完毕后,如何通知用户,还是让用户刷新? 我个人建议,一般情况下都不要用异步,只有在真的需要的时候再用。

  
 DDD

领域驱动设计系列 (四):事件驱动下

前言

上一篇说到为什么要使用事件驱动,但是只有概念是不够的,我们要代码呀!记得脸书的老总说过: “Talk is cheap, Show me the code!”

实现思路

发出事件

事件顾名思义就是一件事情发生了,比如我要上头条,这不是一个事件,这事一个Command, HeadCommand, 而我上头条了这就是一个事件HeadedEvent,事件就是一件事情已经发生了。 好,先来一个伪代码

1
2
3
4
5
6
7
public void Head()
{
var NewsPaper = new NewsPaper("南都娱乐");
NewsPaper.WriteToHeader("汪峰");

RaiseEvent(new HeadedEvent {Name = "汪峰"});
}

所以我们只需在代码里RaiseEvent就可以了。

那么如何订阅事件

其实很简单,因为我们要实现的是同步的事件,我们只需要找到所有处理这个事件的实现类,然后调用所有就可以了。

1
2
3
4
5
6
7
8
9
public interface IEventHandler<TEvent> where TEvent : Event
{
void Handle(TEvent e);
}

public class HeadedEvent:Event
{
public string Name { get; set; }
}

如果国际章的妈妈关注这个Event, 我们就实现一个GuoJiZhangMotherEventHandler

1
2
3
4
5
6
7
public class GuoJiZhangMotherEventHandler : IEventHandler<HeadedEvent>
{
public void Handle(HeadedEvent e)
{
Console.WriteLine(e.Name+", Are you kidding me?");
}
}

如果我等屁民也关心这个事件的话,我们只需要再实现一个 PiMingEventHandler

1
2
3
4
5
6
7
public class PiMingEventHandler:IEventHandler<HeadedEvent>
{
public void Handle(HeadedEvent e)
{
Console.WriteLine(e.Name+", Guo Ji Zhang is your last wife?");
}
}

看,我们可以任意增加关注事件的代码,不用修改原来的代码吧,说好的OCP没骗你吧? 那么问题来了,发出事件的人和接受事件的人怎么联系上的?在现实世界中,我们都是订阅报纸来看头条知道的,但是代码里我们就需要一个协调者了。如是我们就需要一个EventBus, 直接上代码吧

1
2
3
4
5
6
7
8
9
10
11
12
public void Head()
{
var NewsPaper = new NewsPaper("南都娱乐");
NewsPaper.WriteToHeader("汪峰");

RaiseEvent(new HeadedEvent {Name = "汪峰"});
}

private void RaiseEvent(HeadedEvent headedEvent)
{
EventBus.Publish<HeadedEvent>(new HeadedEvent { Name = "汪峰" });
}

EventBus找出所有Handle这个事件的实现类,调用对应的Handle方法,我们可以通过Castle或者任何注入框架轻易的实现

1
2
3
4
5
6
7
8
9
10
11
public class EventBus
{
public static void Publish<T>(T concreteEvent) where T: Event
{
var handlers = _container.ResolveAll<IEventHandler<T>>();
foreach (var handle in handlers)
{
handle.Handle(concreteEvent);
}
}
}

好了,哥只负责帮汪老师上头条,上完我发出了事件通知,谁关注谁自己处理去,我的代码也不用改。

我代码实现完了,如果各位还不知道如何实现一个同步的事件驱动架构,那拜托你们找个漂亮的妹子来问我。事件驱动架构我就只能帮你到这里了。

  
 DDD

领域驱动设计系列(三):事件驱动上

前言

今天讲一下事件驱动,这个不是领域驱动设计里的事件源(Event Source), 这个以后再讲,今天主要讲一下如何用事件来解耦,主要的原因是我们有个项目有个功能我觉得用事件的方式比较好,正好写篇博客,就不用专门给他们讲了。

解耦

说到解耦,我们很熟悉分层设计,比如上层依赖于抽象,不依赖于具体的实现。比如一个类使用另一个类,我们使用接口而不直接使用实现类。

1
2
3
4
5
public EquipmentService(IEmailService emailService, IEquipmentRepository equipmentRepository)
{
_emailService = emailService;
_equipmentRepository = equipmentRepository;
}

为何用事件?

SRP (单一职责)

比如我们一个会议室预定系统,我们的一个设备坏了。我们需要通知预定这个会议室的所有人。于是我们需要发邮件。

伪代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class EquipmentService
{
private readonly IEmailService _emailService;
private readonly IEquipmentRepository _equipmentRepository;

public EquipmentService(IEmailService emailService, IEquipmentRepository equipmentRepository)
{
_emailService = emailService;
_equipmentRepository = equipmentRepository;
}

public void SetEquipmentBroken(string Id)
{
var equipment = _equipmentRepository.GetById(Id);
equipment.DeActive();

_emailService.SendEmail();
}
}

但是,问题来了,如果后来我们要说,如果设备坏了,我们要更改可用库存的数量,这时候我们是不是要在这里修改代码而引入IInventoryService? 后来如果经理说设备坏了你们尽然不告诉我,你们要闹哪样?这个时候我们是不是要修改代码引入ISMSService.Info(Manager)? 即使我们不考虑OCP原则,不考虑单一职责,我们程序员也会哭,我就DeActive一个设备,你要我做这么多事,我哪里清楚所有的功能?我就骂过程序员,你做这个功能呢为什么没考虑全!!!漏掉了这么重要的功能。

而问题,程序员从来没考虑全过,因此我就想办法如何解决这个程序员不仔细的问题。

事件驱动

因为我熟悉iOS的开发,我就想到了iOS的Notification Center. 那我我DeActive一个设备,我就只DeActive这个设备,很SRP是不是? 但是别的地方如何拿到通知? 于是事件就自然的付出水面了。如果设备被DeActive了,程序就只需要喊一声,老子把设备DeActive了,你们要闹哪样你们自己看着办,代码如下。

1
2
3
4
5
6
7
8
public void SetEquipmentBroken(string Id)
{
var equipment = _equipmentRepository.GetById(Id);
equipment.DeActive();

EventBus.Publish(new EquipmentDeActivedEvent {Id = equipment.Id});

}

这样,通知会议室预定者的模块去通知,给老板发短信的模块就通知老板就OK了。

总结

这里我们先将事件驱动,下一篇展示如何实现同步的事件,以后转换为异步那也很容易,让多个接受者处理这个事件,接受者可以是动态的哦,以后老板娘也想知道的话,代码也不用改的亲,好,我先去写实现代码去!

  
 DDD

领域驱动设计系列(二):领域Model?

前言

领域驱动设计里有很多东西,我们可以应用在各种各样的开发模式里,所以接下来说的一些东西,我们可以部分使用。

说道领域驱动的领域,大家肯定就要开始说Bounded Context,聚合,聚合根,容易让大家搞糊涂。 我觉得先抛开这些概念,后面再来说如何设计聚合,先简单来说。

模型

过去,我们在多层设计里定义了很多Model, 数据库的Model(DB Entity), 然后为了不依赖数据库,我们有设计了业务的Domain Model, 同时我们又设计了ViewModel, 这样一般也没什么问题,职责也很清晰。但是有几个问题

  1. 我们要做很多的模型转换,转入转出。当然我们可以用AutoMapper来但是AutoMapper的性能实在难以恭维,大家可以在网上搜索AutoMapper performance.
  2. 领域模型成了一个单纯的DTO了。

领域模型

首先我们要看领域,就是我们尽量把业务聚合到一个领域里,比如我们要做一个功能,可以看到用户每一次的登录日志,那个这个登录日志其实就是属于用户这个领域里。

其次我们看模型,原来我们的模型都是只有属性,也就是贫血模型,贫血的意思就是没有行为,像木乃伊一样,但是实际上领域是我们要完成业务的最主要的地方,我们希望领域能够自制,也就是领域自己管理自己。

示例

比如有一个Employee, 他的状态有Active, Pending, DeActive, 业务上是Pending只能改为Active.

1
2
3
4
5
6
7
public class Employee : Entity
{
public Name Name { get; set; }

public EmployeeStatus EmployeeStatus { get; set; }

}c

如果是贫血的Employee模型,我们往往代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class EmployeeService : IEmployeeService
{
private readonly IUnitOfWorkFactory _unitOfWorkFactory;
private readonly IEmployeeRepository _employeeRepository;

public EmployeeService(IUnitOfWorkFactory unitOfWorkFactory, IEmployeeRepository employeeRepository)
{
_unitOfWorkFactory = unitOfWorkFactory;
_employeeRepository = employeeRepository;
}

public void ChangeStatus(EmployeeStatus status, Guid employeeId)
{
using (var unitOfWork = _unitOfWorkFactory.GetCurrentUnitOfWork())
{
var employee = _employeeRepository.GetById(employeeId);
employee.EmployeeStatus = status;

unitOfWork.Commit();
}
}
}

但是上面的代码的问题就是领域没有自治,本来修改我的状态是我的事,你能不能修改,外面随意修改我的状态是很危险的,比如Pending状态只能改为Active状态。 所以如果不是贫血的模型,我们代码就会这样,让领域自己来管理

1
2
3
4
5
6

public class Employee : Entity
{

public UserId UserId { get; private set; }
public EmployeeStatus EmployeeStatus { get; private set; }
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
    public void ChangeStatus(EmployeeStatus status)
{
if (this.EmployeeStatus == EmployeeStatus.Pending && status != EmployeeStatus.Active)
{
throw new Exception("Only can Active when status is pending");
}

this.EmployeeStatus = status;
}

}

public class EmployeeService : IEmployeeService
{
private readonly IUnitOfWorkFactory _unitOfWorkFactory;
private readonly IEmployeeRepository _employeeRepository;

public EmployeeService(IUnitOfWorkFactory unitOfWorkFactory, IEmployeeRepository employeeRepository)
{
_unitOfWorkFactory = unitOfWorkFactory;
_employeeRepository = employeeRepository;
}

public void ChangeStatus(EmployeeStatus status, Guid employeeId)
{
using (var unitOfWork = _unitOfWorkFactory.GetCurrentUnitOfWork())
{
var employee = _employeeRepository.GetById(employeeId);
employee.ChangeStatus(status);

unitOfWork.Commit();
}
}
}

因此可以看出,我们把业务代码尽量写在领域里让领域自治。

后记

其实领域驱动设计最难的就是设计领域(Domain), 也就是后面会说到的AggregateRoot 聚合,但是我想我们先让领域不再贫血,这样在传统的多层设计,数据驱动等架构都可以使用这种模式。

  
 DDD

领域驱动设计系列(一):为何要领域驱动设计?

前言

领域驱动设计最近貌似开始火起来了,越来越多的人开始认识到领域设计的重要性,从我做过的项目来看,似乎欧洲已经有很多的公司开始实施领域驱动设计了,我看领域驱动设计也有些时间了,但是网上不管是文章还是代码,都显得太过“高大上”,一谈领域驱动设计,一大堆的概念一股脑的给你上上来,搞的有点晕头转向,而我想在一些中小项目实施领域驱动也遇到了不小的障碍,大家对很多东西都处于一种恐惧的状态,而且在正真开始实施时,也遇到一些疑问,所以也想和大家交流学习,因此开始在此写写对领域驱动的理解,后面会有一些轻量的演进代码。

为何要领域驱动设计?

简化数据存储

领域驱动设计有很多原因,谈到我为啥要在公司推行领域驱动设计,说起来还是很好玩的,因为原来基于数据驱动的开发方式,也就是传统的多层开发架构,大家定义了一堆DAL来操作数据, 在.Net大家一般有两种使用方式,一种是用ORM像Entity Framework, 另一种想使用Dapper这样轻量级的Mapping工具,这些都要把关系型数据转换为对象。结果导致以下几种结果。

  • 没有正确的使用ORM, 导致数据加载过多,导致系统性能很差。
  • 为了解决性能问题,就不加载一些导航属性,但是确保DB Entity返回上层,这样对象的一些属性为空,上层使用这个数据时根本不知道什么时间这个属性是有值的,这个是很丑陋的是不是?
  • 如是又开始使用一些轻量级的数据方法,比如使用Dapper然后自己写SQL语句,这本来是很不错的方式,但是大部分人的SQL能力实在不敢恭维,大部分写出来的SQL语句,甚至比EnityFramework生成的语句还差。

所以,我就想我们做项目,大部分处理的应该是因为,如何让程序员从数据存储,模型转换的大泥潭里解放出来,领域驱动设计就进入了我的实现,当然但从数据这个角度还不足以选择领域驱动设计,用一个NoSQL数据库是不是就解决了? 但是NoSQL也有一些问题,比如MongoDB如何更优雅的保证事务以及数据的一致性等。

更多了解上下文

我们很多软件的问题,大家都知道是需求的问题,也就是客户的需求我们很难理解准确,导致程序员更加关注”HOW” 而忽略了”WHAT”, 最终做了几个礼拜甚至更长时间,结果客户会说:”What?! I told you”, 但是客户告诉我的,我们理解是不一样的。比如客户说:“ Great job, I love you!” 这个Love肯定不是男女之间的Love, 我们拿到的是一个客户的需求,他的上下文是什么? 比如说:“这个球打的好”, 如果是在打篮球,肯定说的事篮球,如果是在打乒乓球肯定说的是乒乓球。 而领域驱动设计里我们可以让业务人员更多的参与系统,更早的参与系统。

统一语言(Ubiquitous Language)

业务人员和我们使用一样的语言,我们的程序比如让业务尽量集中在领域里,比如在传统的数据驱动里,如果说Jack爱Rose, 我们一般会这么写

1
UserService.Love(Jack, Rose)

但是我们业务人员很奇怪谁Love谁? 为什么要UserService?, 如果我们写成下面这样

1
Jack.Love(Rose)

还有如果我们用

1
Company.hire(employee)

来代替

1
companyservice.hire(company,employee)

这样我们就更容易让业务人员参与进来,而且代码可以更易于表示真实的业务场景。

  
 DDD