二.创建版本库

1.什么是版本库


 版本库又名仓库,英文名repository,你可以简单理解成一个目录,这个目录里面的所有文件都可以被Git管理起来,每个文件的修改、删除,Git都能跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻可以“还原”。

所以,创建一个版本库非常简单,首先,选择一个合适的地方,创建一个空目录:

1
2
3
4
$ mkdir learngit
$ cd learngit
$ pwd
/home/shouliang/learngit

pwd命令用于显示当前目录。在环境中这个仓库位于/home/shouliang/learngit。

通过git init命令把这个目录变成Git可以管理的仓库:

1
2
$ git init
Initialized empty Git repository in /home/shouliang/learngit/.git/

  瞬间Git就把仓库建好了,而且告诉你是一个空的仓库(empty Git repository),细心的读者可以发现当前目录下多了一个.git的目录,这个目录是Git来跟踪管理版本库的,没事千万不要手动修改这个目录里面的文件,不然改乱了,就把Git仓库给破坏了。

  如果你没有看到.git目录,那是因为这个目录默认是隐藏的,用ls -ah命令就可以看见。

2.添加文件


我们了解下版本控制系统,其实只能跟踪文本文件的改动,比如TXT文件,网页,所有的程序代码等等,Git也不例外。版本控制系统可以告诉你每次的改动,比如在第5行加了一个单词“Linux”,在第8行删了一个单词“Windows”。而图片、视频这些二进制文件,虽然也能由版本控制系统理,但没法跟踪文件的变化,只能把二进制文件每次改动串起来,也就是只知道图片从100KB改成了120KB,但到底改了啥,版本控制系统不知道,也没法知道。

为了简明起见,我们创建一个readme.txt作为练习:

1
2
3
4
echo "Git is a version control system." > readme.txt
// 输入这句话保存到创建的readme.txt文件中
echo " Git is free software." >> readme.txt
// 输入此内容追加到readme.txt中

一定要放到learngit目录下(子目录也行),因为这是一个Git仓库,放到其他地方Git再厉害也找不到这个文件。

用命令git add告诉Git,把文件添加到仓库:

1
$ git add readme.txt

git add 实际上是个脚本命令,没有任何显示,说明添加成功。

3.提交文件


用命令git commit告诉Git,把文件提交到仓库:

1
2
3
4
$ git commit -m "wrote a readme file"
[master (root-commit) cb926e7] wrote a readme file
1 file changed, 2 insertions(+)
create mode 100644 readme.txt

  简单解释一下git commit命令,-m后面输入的是本次提交的说明,可以输入任意内容,当然最好是有意义的,这样你就能从历史记录里方便地找到改动记录。

  git commit命令执行成功后会告诉你,1个文件被改动(我们新添加的readme.txt文件),插入了两行内容(readme.txt有两行内容)。

  为什么Git添加文件需要add,commit一共两步呢?因为commit可以一次提交很多文件,所以你可以多次add不同的文件,比如:

1
2
3
$ git add file1.txt
$ git add file2.txt file3.txt
$ git commit -m "add 3 files."

4.小结


此节知识点我们所学习的内容:

初始化一个Git仓库,使用git init命令。

添加文件到Git仓库,分两步:

  • 第一步,使用命令git add ,注意,可反复多次使用,添加多个文件;
  • 第二步,使用命令git commit,完成。  

一.简介

1.Git的诞生


同生活中的许多伟大事物一样,Git 诞生于一个极富纷争大举创新的年代。

  Linux 内核开源项目有着为数众广的参与者。 绝大多数的 Linux 内核维护工作都花在了提交补丁和保存归档的繁琐事务上(1991-2002年间)。 到 2002 年,整个项目组开始启用一个专有的分布式版本控制系统 BitKeeper 来管理和维护代码。

  到了 2005 年,开发 BitKeeper 的商业公司同 Linux 内核开源社区的合作关系结束,他们收回了 Linux 内核社区免费使用 BitKeeper 的权力。 这就迫使 Linux 开源社区(特别是 Linux 的缔造者 Linux Torvalds)基于使用 BitKcheper 时的经验教训,开发出自己的版本系统。 他们对新的系统制订了若干目标:

速度
简单的设计
对非线性开发模式的强力支持(允许成千上万个并行开发的分支)
完全分布式
  有能力高效管理类似 Linux 内核一样的超大规模项目(速度和数据量)

  自诞生于 2005 年以来,Git 日臻成熟完善,在高度易用的同时,仍然保留着初期设定的目标。 它的速度飞快,极其适合管理大项目,有着令人难以置信的非线性分支管理系统。

  Git迅速成为最流行的分布式版本控制系统,尤其是2008年,GitHub网站上线了,它为开源项目免费提供Git存储,无数开源项目开始迁移至GitHub,包括jQuery,PHP,Ruby等等。

2.Git介绍


Git是一款免费、开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目。

  Git是一个开源的分布式版本控制系统,用以有效、高速的处理从很小到非常大的项目版本管理。Git 是 Linus Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软件。

  最原始的版本控制是纯手工的版本控制:修改文件,保存文件副本。有时候偷懒省事,保存副本时命名比较随意,时间长了就不知道哪个是新的,哪个是老的了,即使知道新旧,可能也不知道每个版本是什么内容,相对上一版作了什么修改了,当几个版本过去后,很可能就是下面的样子了:
  

3.Git特点


分布式相比于集中式的最大区别在于开发者可以提交到本地,每个开发者通过克隆(git clone),在本地机器上拷贝一个完整的Git仓库。

  • 直接记录快照,而非差异比较 : Git 更像是把变化的文件作快照后,记录在一个微型的文件系统中。
  • 近乎所有操作都是本地执行 :在 Git 中的绝大多数操作都只需要访问本地文件和资源,不用连网。
  • 时刻保持数据完整性 :在保存到 Git 之前,所有数据都要进行内容的校验和(checksum)计算,并将此结果作为数据的唯一标识和索引。
  • 多数操作仅添加数据 :常用的 Git 操作大多仅仅是把数据添加到数据库。
    开发流程示意图:

4.集中式


CVS及SVN都是集中式的版本控制系统,而Git是分布式版本控制系统。

  集中式版本控制系统,版本库是集中存放在中央服务器的,一起工作的人需要用自己的电脑从服务器上同步更新或上传自己的修改。
  

但是,所有的版本数据都存在服务器上,用户的本地设备就只有自己以前所同步的版本,如果不连网的话,用户就看不到历史版本,也无法切换版本验证问题,或在不同分支工作。。

  而且,所有数据都保存在单一的服务器上,有很大的风险这个服务器会损坏,这样就会丢失所有的数据,当然可以定期备份。  

5.分布式


那分布式版本控制系统与集中式版本控制系统有何不同呢?

 分布式版本控制系统根本没有“中央服务器”,每个人的电脑上都是一个完整的版本库,不需要联网就可以工作。既然每个人电脑上都有一个完整的版本库,那多个人如何协作呢?比方说你和同事在各自电脑修改相同文件,这时,你们俩之间只需把各自的修改推送给对方,就可以互相看到对方的修改了。

  分布式版本控制系统的安全性要高很多,因为每个人电脑里都有完整的版本库。大家之间可以相互复制。

  分布式版本控制系统通常也有一台充当“中央服务器”的电脑,但这个服务器的作用仅仅是用来方便“交换”大家的修改,没有它大家也一样干活,只是交换修改不方便而已。

6.Git安装


最早Git是在Linux上开发的,很长一段时间内,Git也只能在Linux和Unix系统上跑。不过,慢慢地有人把它移植到了Windows上。现在,Git可以在Linux、Unix、Mac和Windows这几大平台上正常运行了。

  在Linux上安装Git

  首先,你可以试着输入git,看看系统有没有安装Git:

1
$ git

  像上面的命令,有很多Linux会友好地告诉你Git没有安装,还会告诉你如何安装Git。

  如果你碰巧用Debian或Ubuntu Linux,通过一条sudo apt-get install git就可以直接完成Git的安装,非常简单。如果想查看是否安装成功,通过git –version。

  如果是其他Linux版本,可以直接通过源码安装。先从Git官网下载源码,然后解压,依次输入:./config,make,sudo make install这几个命令安装就好了。

安装完成后,还需要最后一步设置,在命令行输入:

1
2
$ git config --global user.name "Your Name"
$ git config --global user.email "email@example.com"

因为Git是分布式版本控制系统,所以每个机器都必须自报家门:你的名字和Email地址。

注意git config命令的–global参数,用了这个参数,表示你这台机器上所有的Git仓库都会使用这个配置,当然也可以对某个仓库指定不同的用户名和Email地址。

Mocha—四.为项目开发一个BDD测试

1.辅助模块


我们进行单元测试,一般都需要组合几个工具来来使用的。下面我们开始介绍:

chai断言库
chai 断言库支持BDD 的 expect/should 语法风格 和TDD的 assert 语法风格。

superagent
在用Node做Web开发的时候,模拟HTTP请求时必不可少的。这也就引出了superagent这个模块,它是一个模拟http请求的库。它作用是简化发起请求的库。

项目的package.json 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"name": "mocha-test",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node app.js",
"test": "mocha test"
},
"devDependencies": {
"superagent": "1.4.0",
"chai": "3.4.0"
}
}

2.项目描述


首先我们创建一个app.js文件。内容如下:

1
2
3
4
5
6
7
8
9
10
var http = require('http'),
PORT = 3000;
function onRequest(request, response) {
console.log("Request received.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}
var server = http.createServer(onRequest);
server.listen(PORT);

描述:为了让项目尽可能的简单,我们没有用到任何的框架。只是创建了一个http服务器监听了3000端口。

3.项目重构


我们的app.js需要最外部暴露两个方法。所以要对我们右边的项目进行修改。

需要修改的代码:

1
2
var server = http.createServer(onRequest);
server.listen(PORT);

重构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var server = http.createServer(onRequest);
var boot = function () {
server.listen(PORT, function () {
console.info('Express server listening on port ' + PORT);
});
};
var shutdown = function () {
server.close();
};
if (require.main === module) {
boot();
} else {
console.info('Running app as a module');
exports.boot = boot;
exports.shutdown = shutdown;
}

重构描述:现在我们把启动服务和关闭服务分别进行了封装并且对外进行了暴露。

4.测试用例


现在,我们创建一个名字为 tests 的测试文件夹,并创建一个index.js的文件。测试用例前需要启动服务器,结束后关闭服务。这个时候就用到了前面暴露的 boot() 和 shutdown() 方法。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var boot = require('../app').boot,
shutdown = require('../app').shutdown,
request = require('superagent'),
expect = require('chai').expect;

describe('server', function () {
before(function () {
boot();
});
describe('index', function () {
it('should respond to GET', function (done) {
request
.get('http://localhost:3000')
.end(function (err, res) {
expect(res.status).to.equal(200);
done();
});
});
});
after(function () {
shutdown();
});
});

运行命令:mocha tests

Mocha—三.hook机制和测试技巧

1.hook机制


hook 就是在测试流程的不同时段触发,比如在整个测试流程之前,或在每个独立测试之前等。

hook也可以理解为是一些逻辑,通常表现为一个函数或者一些声明,当特定的事件触发时 hook 才执行。

提供方法有:before()、beforeEach() after() 和 afterEach()。

方法解析:

  • before():所有测试用例的统一前置动作
  • after():所有测试用例的统一后置动作
  • beforeEach():每个测试用例的前置动作
  • afterEach():每个测试用例的后置动作
    用法:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    describe('hooks', function() {
    before(function() {
    //在执行本区块的所有测试之前执行
    });

    after(function() {
    //在执行本区块的所有测试之后执行
    });

    beforeEach(function() {
    //在执行本区块的每个测试之前都执行
    });

    afterEach(function() {
    //在执行本区块的每个测试之后都执行
    });

    //测试用例

    });

2.描述hook


所有的 hook 都可以加上描述,这样可以更好地定位到测试用例中的错误。如果 hook 函数指定了名称,会在没有描述时使用函数名,例如:

1
2
3
4
5
6
7
8
9
10
11
beforeEach(function() {
//beforeEach hook
});

beforeEach(function needFun() {
//beforeEach: namedFun
});

beforeEach('some description', function() {
//beforeEach:some description
});

补充:上述示例中 注释的内容就是对 hook 的描述。

3.测试占位


测试用例占位只要添加一个没有回调的 it() 方法即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var assert = require('assert');

describe('Array', function() {

describe('#indexOf()', function() {
//同步测试
it('当值不存在时应该返回 -1', function() {
assert.equal(-1, [1,2,3].indexOf(5));
assert.equal(-1, [1,2,3].indexOf(0));
});
});

describe('Array', function() {
describe('#indexOf()', function() {
//下面是一个挂起的测试
it('当值不存在时应该返回 -1');
});
});
});

4.仅执行指定测试


仅执行指定测试的特性可以让你通过添加 .only() 来指定唯一要执行的测试套件或测试用例:

1
2
3
4
describe('Array', function(){
describe.only('#indexOf()', function(){
})
})

或一个指定的测试用例:

1
2
3
4
5
6
7
8
9
10
describe('Array', function(){
describe('#indexOf()', function(){
it.only('当值不存在时应该返回 -1', function(){

})
it('当值不存在时应该返回 -1', function(){

})
})
})

PS:注意只能出现一个 .only()

5.忽略指定测试


该特性和 .only() 非常相似,通过添加 .skip() 你可以告诉 Mocha 忽略的测试套件或者测试用例(可以有多个)。该操作使得这些操作处于挂起的状态,这比使用注释来的要好,因为你可能会忘记把注释给取消掉。

1
2
3
4
5
describe('Array', function(){
describe.skip('#indexOf()', function(){
...
})
})

或一个指定的测试用例:

1
2
3
4
5
6
7
8
9
10
11
describe('Array', function(){
describe('#indexOf()', function(){
it.skip('当值不存在时应该返回 -1', function(){

})

it('当值不存在时应该返回 -1', function(){

})
})
})

6.动态生成测试


由于mocha 可以使用 function.prototype.call 和function 表达式定义测试套件和测试用例,所以可以动态生成测试用例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var assert = require('assert');

function add() {
return Array.prototype.slice.call(arguments).reduce(function(prev, curr) {
return prev + curr;
}, 0);
}

describe('add()', function() {
var tests = [
{args: [1, 2], expected: 3},
{args: [1, 2, 3], expected: 6},
{args: [1, 2, 3, 4], expected: 10}
];

tests.forEach(function(test) {
it('correctly adds ' + test.args.length + ' args', function() {
var res = add.apply(null, test.args);
assert.equal(res, test.expected);
});
});
});

Mocha—二.mocha接口

1.BDD行为驱动开发


mocha “接口” 系统允许开发者选择自身喜爱的特定领域语言风格, mocha 提供 TDD(测试驱动开发)、BDD (行为驱动开发) 和 exports 风格的接口。

BDD是“行为驱动的开发”(Behavior-Driven Development)的简称,指的是写出优秀测试的最佳实践的总称。

BDD认为,不应该针对代码的实现细节写测试,而是要针对行为写测试。BDD测试的是行为,即软件应该怎样运行。

BDD接口提供以下方法:

  • describe():测试套件
  • it():测试用例
  • before():所有测试用例的统一前置动作
  • after():所有测试用例的统一后置动作
  • beforeEach():每个测试用例的前置动作
  • afterEach():每个测试用例的后置动作
    BDD的特征就是使用describe()和it() 这两个方法。

before()、after()、beforeEach()和afterEach() 是为测试做辅助的作用域,它们合起来组成了hook的概念。

2.方法解析


descript()

describe()方法接收两个参数:第一个参数是一个字符串,表示测试套件的名字或标题,表示将要测试什么。第二个参数是一个函数,用来实现这个测试套件。

上述中引出了一个概念:测试套件。那什么是测试套件呢?

测试套件(test suite)指的是,一组针对软件规格的某个方面的测试用例。也可以看作,对软件的某个方面的描述(describe)。结构如下:

1
2
3
describe("A suite", function() {
// ...
});

it()

要想理解it(),首先我们要知道什么是测试用例? 测试用例(test case)指的是,针对软件一个功能点的测试,是软件测试的最基本单位。一组相关的测试用例,构成一个测试套件。

测试用例由it函数构成,它与describe函数一样,接受两个参数:第一个参数是字符串,表示测试用例的标题;第二个参数是函数,用来实现这个测试用例。

BDD风格用例

1
2
3
4
5
6
7
8
9
10
11
var expect = require('chai').expect;
describe('Array', function(){
before(function(){
console.log('在测试之前运行');
});
describe('#indexOf()', function(){
it('当值不存在时应该返回 -1', function(){
expect([1,2,3].indexOf(4)).to.equal(-1);
});
});
});

3.TDD测试驱动开发


TDD风格

TDD(测试驱动开发)组织方式是使用测试集(suite)和测试(test)。

每个测试集都有 setup 和 teardown 函数。这些方法会在测试集中的测试执行前执行,它们的作用是为了避免代码重复以及最大限度使得测试之间相互独立。

TDD接口:

  • suite:类似BDD中 describe()
  • test:类似BDD中 it()
  • setup:类似BDD中 before()
  • teardown:类似BDD中 after()
  • suiteSetup:类似BDD中 beforeEach()
  • suiteTeardown:类似BDD中 afterEach()
    示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var assert = require("assert");

    suite('Array', function(){
    setup(function(){
    console.log('测试执行前执行');
    });

    suite('#indexOf()', function(){
    test('当值不存在时应该返回 -1', function(){
    assert.equal(-1, [1,2,3].indexOf(4));
    });
    });
    });

运行mocha:

mocha –ui tdd *.js (*表示的是文件名)

PS:mocha 默认是使用 bdd 的接口,所以在这里我们告诉mocha我们用的是tdd.

4.exports风格


exports类似于nodejs里的模块语法,关键字 before, after, beforeEach, 和 afterEach 是特殊保留的,值为对象时是一个测试套件,为函数时则是一个测试用例。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
before: function(){
// ...
},

'Array': {
'#indexOf()': {
'当值不存在时应该返回 -1': function(){
expect([1,2,3].indexOf(4)).to.equal(-1);
}
}
}
};

运行 mocha

mocha –ui exports *.js

5.小结


我们在前文中讲到了 mocha 提供 TDD(测试驱动开发)、BDD (行为驱动开发) 和 exports 风格的接口。其实还有 QUnit 和 require 风格的接口。

但是比较常用就是 BDD 和 TDD,mocha 默认的也是BDD,且 BDD 是 TDD 的一个专业版本,它指定了从业务需求的角度出发需要哪些单元测试。

Mocha—一.快速开始

1.什么是mocha


mocha 是一个功能丰富的javascript测试框架,可以运行在nodejs和浏览器环境,使异步测试变得简单有趣。mocha 串联运行测试,允许灵活和精确地报告结果,同时映射未捕获的异常用来纠正测试用例。

支持TDD/BDD 的 开发方式,结合 should.js/expect/chai/better-assert 断言库,能轻松构建各种风格的测试用例。

特点

  • 简单
  • 灵活
  • 有趣

    安装

    通过npm全局安装: npm install -g mocha

2.第一个测试用例


我们首先来见识一下mocha最基本的测试用例是怎么的结构,如下。

测试用例:

//模块依赖

1
var assert = require("assert");

//断言条件

1
2
3
4
5
6
7
8
describe('Array', function(){
describe('#indexOf()', function(){
it('当值不存在时应该返回 -1', function(){
assert.equal(-1, [1,2,3].indexOf(5));
assert.equal(-1, [1,2,3].indexOf(0));
});
});
});

示例解析:测试用例首先需要引用断言模块,如上文中var assert = require(‘assert’);,代码 assert.equal(-1, [1,2,3].indexOf(5)); 中使用的是assert.equal(actual, expected, [message]) 语法。作用等同于使用’==’进行相等判断。actual为实际值,expected 为期望值。message为返回的信息。

运行 Mocha:$ mocha

3.assert断言


断言(assert)指的是对代码行为的预期。一个测试用例内部,包含一个或多个断言(assert)。

断言会返回一个布尔值,表示代码行为是否符合预期。测试用例之中,只要有一个断言为false,这个测试用例就会失败,只有所有断言都为true,测试用例才会通过。

比如上节示例中的:

assert.equal(-1, [1,2,3].indexOf(5));

assert.equal(-1, [1,2,3].indexOf(0));

实际值(-1)和期望值([1,2,3].indexOf(5))是一样的,断言为true,所以这个测试用例成功了。

mocha 允许开发者使用任意的断言库,当这些断言库抛出了一个错误异常时,mocha将会捕获并进行相应处理。这意味着你可以利用如 should.js断言库、 Node.js 常规的 assert 模块或其它类似的断言代码库。以下是众所周知的适用于Node.js或浏览器的断言库:

  • should.js
  • expect.js
  • chai.js
  • better-assert
  • assert:nodejs原生模块,在前文示例中我们有应用到。

4.chai.js断言库


Chai 是一个非常灵活的断言库,它可以让你使用如下三种主要断言方式的任何一种:

assert:

这是来自老派测试驱动开发的经典的assert方式。比如:

assert.equal(variable, “value”);

expect:

这种链式的断言方式在行为驱动开发中最为常见。比如:

expect(variable).to.equal(“value”);

should:

这也是在测试驱动开发中比较常用的方式之一。举例:

variable.should.equal(“value”);

5.expect语法


expect 库应用是非常广泛的,它拥有很好的链式结构和仿自然语言的方法。通常写同一个断言会有几个方法,比如expect(response).to.be(true) 和 expect(response).equal(true)。以下列举了 expect 常用的主要方法:

  • ok :检查是否为真
  • true:检查对象是否为真
  • to.be、to:作为连接两个方法的链式方法
  • not:链接一个否定的断言,如 expect(false).not.to.be(true)
  • a/an:检查类型(也适用于数组类型)
  • include/contain:检查数组或字符串是否包含某个元素
  • below/above:检查是否大于或者小于某个限定值
    mocha支持TDD/BDD 的 开发方式,结合 should.js、expect、chai、better-assert 断言库,能轻松构建各种风格的测试用例。这里面有两个知识点,一个是断言库,另一个是 TDD/BDD 。

二.准备登陆

1.安装模板


我们要使用express框架实现一个简单的用户登陆功能,先准备一下相关资源。

在nodejs中使用express框架,它默认的是ejs和jade渲染模板,以ejs模板为例,讲述模板渲染网页模板的基础功能。

ejs模板安装方法

1
npm install ejs

1.目录下安装好了之后,如何调用呢,如下所示:

1
2
// 指定渲染模板文件的后缀名为ejs
app.set('view engine', 'ejs');

2.默认ejs模板只支持渲染以ejs为扩展名的文件,可能在使用的时候会觉得它的代码书写方式很不爽还是想用html的形式去书写,该怎么办呢,这时就得去修改模板引擎了,也就会用到express的engine函数。

3.engine注册模板引擎的函数,处理指定的后缀名文件。

1
2
3
4
// 修改模板文件的后缀名为html
app.set( 'view engine', 'html' );
// 运行ejs模块
app.engine( '.html', require( 'ejs' ).__express );

“__express”,ejs模块的一个公共属性,表示要渲染的文件扩展名。

2.静态资源


如果要在网页中加载静态文件(css、js、img),就需要另外指定一个存放静态文件的目录,当浏览器发出非HTML文件请求时,服务器端就会到这个目录下去寻找相关文件。

项目目录下添加一个存放静态文件的目录为public。

在public目录下在添加三个存放js、css、img的目录,相应取名为javascripts、stylesheets、images。

然后就可以把相关文件放到相应的目录下了。

比如,浏览器发出如下的样式表请求:

1
<link href="/stylesheets/bootstrap.min.css" rel="stylesheet" media="screen">

服务器端就到public/stylesheets/目录中寻找bootstrap.min.css文件。

有了静态目录文件,我们还得在启动文件里告诉它这个静态文件路径,需要指定一下,如下所示:

1
app.use(express.static(require('path').join(__dirname, 'public')));

PS:express.static —— 指定静态文件的查找目录。

使用use函数调用中间件指定express静态访问目录,’public’就是我们我们新建的用来存放静态文件的总目录。

3.添加视图


下面我们就来添加网页模板了,项目中我们会新建一个目录用来单独存放模板文件,这里我们就统一放到根路径上了。

下面开始新建index.html、login.html、home.html三个页面。

1.index.html页面参考内容如下:

1
2
3
4
5
6
7
8
9
10
11
<div style="height:400px;width:550px;margin:50px auto;margin-left:auto;border:solid 1px;background: rgb(246, 246, 253);">
<div style="margin-left: 35px;">
# 首页
<form action="#" role="form" style="margin-top: 90px;margin-left: 60px;">
# 欢迎进入首页!
<div style="margin-top: 145px;">
<input type="button" value="登 陆" />
</div>
</form>
</div>
</div>

2.login.html页面参考内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
<title>用户登录</title>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
...
<div style="height:300px;width:350px;margin:100px auto;margin-left:auto;border:solid 1px;background: rgb(246, 246, 253);">
<div style="width:200px;margin:auto;margin-top:50px;">
# 用户登录
<form action="#" role="form" method="post" >
<input id="username" type="text" name="username" style="margin: 20px 0px;" />
<input id="password" type="password" name="password" />
<div style="margin-top:30px;margin-left:125px;">
<input type="button" value="登 陆" />
</div>
</form>
</div>
</div>

3.home.html页面参考内容如下:

1
2
3
4
5
6
7
8
9
10
11
<div style="height:400px;width:550px;margin:50px auto;margin-left:auto;border:solid 1px;background: rgb(246, 246, 253);">
<div style="margin-left: 45px;">
# 主页
<form action="#" role="form" style="margin-top: 90px;">
# 登陆成功!
<div style="margin-top: 145px;">
<input type="button" value="退 出" />
</div>
</form>
</div>
</div>

和静态文件一样,我们也要设置views存放的目录,如下:

1
2
// 设定views变量,意为视图存放的目录
app.set('views', __dirname);

有了网页模板和指定目录,下面就可以访问它们了。

4.渲染视图


我们要如何对网页模板进行访问呢,这就要用到res对象的render函数了。

render函数,对网页模板进行渲染。

格式:res.render(view, [locals], callback);

参数view就是模板的文件名callback用来处理返回的渲染后的字符串,options、callback可省略,在渲染模板时locals可为其模板传入变量值,在模板中就可以调用所传变量了。

比如渲染我们刚刚添加的index.html页面,我们就可以在app.js中写入如下内容:

1
2
3
4
5
6
7
8
9
10
ar express = require('express');
var app = express();
var path = require('path');
app.set('views', __dirname);
app.set( 'view engine', 'html' );
app.engine( '.html', require( 'ejs' ).__express );
app.get('/', function(req, res) {
res.render('index');
});
app.listen(80);

5.url重定向


redirect基本用法

redirect方法允许网址的重定向,跳转到指定的url并且可以指定status,默认为302方式。

格式:res.redirect([status], url);

例1:使用一个完整的url跳转到一个完全不同的域名。

1
res.redirect("http://www.hubwiz.com");

例2:跳转指定页面,比如登陆页,如下:

1
res.redirect("login");

后面我们开始实现登陆功能,先试一下redirect重定向

一.基础知识

1.认识express


npm提供了大量的第三方模块,其中不乏许多Web框架,比如其中的一个轻量级的Web框架 ——— Express。

Express是一个简洁、灵活的node.js Web应用开发框架, 它提供一系列强大的功能,比如:模板解析、静态文件服务、中间件、路由控制等等,并且还可以使用插件或整合其他模块来帮助你创建各种 Web和移动设备应用,是目前最流行的基于Node.js的Web开发框架,并且支持Ejs、jade等多种模板,可以快速地搭建一个具有完整功能的网站。

好,下面开始吧!

1.NPM安装

1
npm install express

2.获取、引用

1
2
var express = require('express');
var app = express();

通过变量“app”我们就可以调用express的各种方法了。

2.创建应用


认识了Express框架,我们开始创建我们的第一个express应用。

在我们的默认项目主文件app.js添加如下内容:

1
2
3
4
5
6
var express = require('express');
var app = express();
app.get('/', function (request, response) {
response.send('Hello World!');
});
app.listen(80);

3.get请求


前面我们实现了一个简单的express应用,下面我们就开始具体讲述它的具体实现,首先我们先来学习Express的常用方法。

get方法 —— 根据请求路径来处理客户端发出的GET请求。

格式:app.get(path,function(request, response));

path为请求的路径,第二个参数为处理请求的回调函数,有两个参数分别是request和response,代表请求信息和响应信息。

如下示例:

1
2
3
4
5
6
7
8
9
10
11
12
var express = require('express');
var app = express();
app.get('/', function(request, response) {
response.send('Welcome to the homepage!');
});
app.get('/about', function(request, response) {
response.send('Welcome to the about page!');
});
app.get("*", function(request, response) {
response.send("404 error!");
});
app.listen(80);

上面示例中,指定了about页面路径、根路径和所有路径的处理方法。并且在回调函数内部,使用HTTP回应的send方法,表示向浏览器发送一个字符串。

4.中间件


1.什么是中间件?

中间件(middleware)就是处理HTTP请求的函数,用来完成各种特定的任务,比如检查用户是否登录、分析数据、以及其他在需要最终将数据发送给用户之前完成的任务。 它最大的特点就是,一个中间件处理完,可以把相应数据再传递给下一个中间件。

2.一个不进行任何操作、只传递request对象的中间件,大概是这样:

1
2
3
function Middleware(request, response, next) { 
next();
}

上面代码的next为中间件的回调函数。如果它带有参数,则代表抛出一个错误,参数为错误文本。

1
2
3
function Middleware(request, response, next) { 
next('出错了!');
}

抛出错误以后,后面的中间件将不再执行,直到发现一个错误处理函数为止。如果没有调用next方法,后面注册的函数也是不会执行的。

5.all方法


all函数的基本用法

和get函数不同app.all()函数可以匹配所有的HTTP动词,也就是说它可以过滤所有路径的请求,如果使用all函数定义中间件,那么就相当于所有请求都必须先通过此该中间件。

格式:app.all(path,function(request, response));

如下所示,我们使用all函数在请求之前设置响应头属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var express = require("express");
var app = express();
app.all("*", function(request, response, next) {
response.writeHead(200, { "Content-Type": "text/html;charset=utf-8" }); //设置响应头属性值
next();
});
app.get("/", function(request, response) {
response.end("欢迎来到首页!");
});
app.get("/about", function(request, response) {
response.end("欢迎来到about页面!");
});
app.get("*", function(request, response) {
response.end("404 - 未找到!");
});
app.listen(80);

上面代码参数中的“*”表示对所有路径有效,这个方法在给特定前缀路径或者任意路径上处理时会特别有用,不管我们请求任何路径都会事先经过all函数。

6.use方法1


use基本用法1

use是express调用中间件的方法,它返回一个函数。

格式:app.use([path], function(request, response, next){});

可选参数path默认为”/“。

1.使用中间件

1
app.use(express.static(path.join(__dirname, '/')));

如上,我们就使用use函数调用express中间件设定了静态文件目录的访问路径(这里假设为根路径)。

如何连续调用两个中间件呢,如下示例:

1
2
3
4
5
6
7
8
9
10
11
var express = require('express');
var app = express();
app.use(function(request, response, next){
console.log("method:"+request.method+" ==== "+"url:"+request.url);
next();
});
app.use(function(request, response){
response.writeHead(200, { "Content-Type": "text/html;charset=utf-8" });
response.end('示例:连续调用两个中间件');
});
app.listen(80);

回调函数的next参数,表示接受其他中间件的调用,函数体中的next(),表示将请求数据传递给下一个中间件。

上面代码先调用第一个中间件,在控制台输出一行信息,然后通过next(),调用第二个中间件,输出HTTP回应。由于第二个中间件没有调用next方法,所以req对象就不再向后传递了。

7.use方法2


use基本用法2

use方法不仅可以调用中间件,还可以根据请求的网址,返回不同的网页内容,如下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var express = require("express");
var app = express();
app.use(function(request, response, next) {
if(request.url == "/") {
response.send("Welcome to the homepage!");
}else {
next();
}
});
app.use(function(request, response, next) {
if(request.url == "/about") {
response.send("Welcome to the about page!");
}else {
next();
}
});
app.use(function(request, response) {
response.send("404 error!");
});
app.listen(80);

上面代码通过request.url属性,判断请求的网址,从而返回不同的内容。

8.回调函数


Express回调函数有两个参数,分别是request(简称req)和response(简称res),request代表客户端发来的HTTP请求,response代表发向客户端的HTTP回应,这两个参数都是对象。示例如下:

1
2
function(req, res) {
}

9.获取主机、路径名


如何使用req对象来处理客户端发来的HTTP请求。

1.req.host返回请求头里取的主机名(不包含端口号)。

2.req.path返回请求的URL的路径名。

如下示例:

1
2
3
4
5
6
7
var express = require('express');
var app = express();
app.get("*", function(req, res) {
console.log(req.path);
res.send("req.host获取主机名,req.path获取请求路径名!");
});
app.listen(80);

10.Get请求 - query


query是一个可获取客户端get请求路径参数的对象属性,包含着被解析过的请求参数对象,默认为{}。

1
2
3
4
5
6
7
var express = require('express');
var app = express();
app.get("*", function(req, res) {
console.log(req.query.参数名);
res.send("测试query属性!");
});
app.listen(80);

通过req.query获取get请求路径的对象参数值。

格式:req.query.参数名;请求路径如下示例:

例1: /search?n=Hanmeimei

1
req.query.n  // "Hanmeimei"

例2: /shoes?order=desc&shoe[color]=blue&shoe[type]=converse

1
2
3
req.query.order  // "desc"
req.query.shoe.color // "blue"
req.query.shoe.type // "converse"

11.Get请求 - param


和属性query一样,通过req.param我们也可以获取被解析过的请求参数对象的值。

格式:req.param(“参数名”);请求路径如下示例:

例1: 获取请求根路径的参数值,如/?n=Lenka,方法如下:

1
2
3
4
5
6
7
var express = require('express');
var app = express();
app.get("/", function(req, res) {
console.log(req.param("n")); //Lenka
res.send("使用req.param属性获取请求根路径的参数对象值!");
});
app.listen(80);

例2:我们也可以获取具有相应路由规则的请求对象,假设路由规则为 /user/:name/,请求路径/user/mike,如下:

1
2
3
4
app.get("/user/:name/", function(req, res) {
console.log(req.param("name")); //mike
res.send("使用req.param属性获取具有路由规则的参数对象值!");
});

PS:所谓“路由”,就是指为不同的访问路径,指定不同的处理方法。

12.Get请求 - params


和param相似,但params是一个可以解析包含着有复杂命名路由规则的请求对象的属性。

格式:req.params.参数名;

例1. 如上课时请求根路径的例子,我们就可以这样获取,如下:

1
2
3
4
5
6
7
var express = require('express');
var app = express();
app.get("/user/:name/", function(req, res) {
console.log(req.params.name); //mike
res.send("使用req.params属性获取具有路由规则的参数对象值!");
});
app.listen(80);

查看运行结果,和param属性功能是一样的,同样获取name参数值。

例2:当然我们也可以请求复杂的路由规则,如/user/:name/:id,假设请求地址为:/user/mike/123,如下:

1
2
3
4
app.get("/user/:name/:id", function(req, res) {
console.log(req.params.id); //"123"
res.send("使用req.params属性复杂路由规则的参数对象值!");
});

对于请求地址具有路由规则的路径来说,属性params比param属性是不是又强大了那么一点点呢!

13.send


send()方法向浏览器发送一个响应信息,并可以智能处理不同类型的数据。格式如下:

1
res.send([body|status], [body]);

1.当参数为一个String时,Content-Type默认设置为”text/html”。

1
res.send('Hello World'); //Hello World

2.当参数为Array或Object时,Express会返回一个JSON。

1
2
res.send({ user: 'tobi' }); //{"user":"tobi"}
res.send([1,2,3]); //[1,2,3]

3.当参数为一个Number时,并且没有上面提到的任何一条在响应体里,Express会帮你设置一个响应体,比如:200会返回字符”OK”。

1
2
3
res.send(200); // OK
res.send(404); // Not Found
res.send(500); // Internal Server Error

send方法在输出响应时会自动进行一些设置,比如HEAD信息、HTTP缓存支持等等。

二.文档的增删改查

1.简单插入文档


在数据库中,数据插入是最基本的操作,在MongoDB使用db.collection.insert(document)语句来插入文档,如下图:

document是文档数据,collection是存放文档数据的集合。

例如:所有用户的信息存放在users集合中,每个用户的信息为一个user文档,插入数据:

1
db.users.insert(user);

如果collection存在,document会添加到collection目录下, 如果collection不存在,数据库会先创建collection,然后再保存document。

2.批量插入文档


nsert语句不但可以插入单个文档,还可以一次性插入多个文档。

插入多个文档时,insert命令的参数为一个数组,数组元素为BSON格式的文档。

多个文档可以放在一个数组内,一次插入多条数据,例如:

1
db.users.insert([{name:"tommy"},{name:"xiaoming"}])

文档批量插入非常方便,但是使用批量插入时也有一些问题需要注意,因为BSON格式的限制,一次插入的数据量不能超过16M,在一个insert命令中插入多条数据时,MongoDB不保证完全成功或完全失败。

3.查询与投影


在MongoDB中,查询指向特定的文档集合,查询设定条件,指明MongoDB需要返回的文档;查询也可以包含一个投影,指定返回的字段。

如下图,在查询过程指定了一个查询条件和一个排序修饰。

在关系型数据库中,投影指的是对列的筛选,类似的,在MongoDB中,投影指的是对出现在结果集中的对象属性的筛选。

4.find()方法


MongoDB中查询检索数据时使用find命令,使用方法如下:

语法:
db.collection.find(criteria,projection);
参数:
criteria – 查询条件,文档类型,可选。

projection– 返回的字段,文档类型,可选,若需返回所有字段,则忽略此参数。

find命令两个可选参数,criteria为查询条件,projection为返回的字段,如果不传入条件数据库会返回该集合的所有文档。

如下图简单示例:

5.修改文档 - update命令


update命令可以更新指定文档的特定字段值,也可以替换整个文档,如果更新操作会增加文档大小,MongoDB将重新分配空间并重新定位。

语法:
db.collection.update(query,update,{upsert:boolean,multi:boolean});
参数:
query:查询条件,文档,和find中的查询条件写法一致。

update:修改内容,文档。

upsert(可选):如果值为true,那么当集合中没有匹配文档时,创建文档。默认false。

multi(可选):如果值为true,那么将更新全部符合条件的文档,否则仅更新一个文档,默认false。

如下示例:将users集合中所有符合条件”age>18”文档的status字段更新为”A”。

6.修改文档 - save命令


save命令可以更新或插入一个新文档,与update命令不同的是,save只能对一个文档进行操作。

语法:

1
db.collection.save();

参数:
document:新的文档;

7.删除文档 - remove命令


需要删除文档时使用remove命令,删除文档可以清理掉不需要的数据,释放存储空间,提升检索效率,但是错误的删除会是一场灾难,因此在执行数据删除操作时需要非常的谨慎!

语法:

1
2
3
4
db.collection.remove(
query,
justOne
)

参数:
query:BSON类型,删除文档的条件。

justOne:布尔类型,true:只删除一个文档,false:默认值,删除所有符合条件的文档。

下面是一个是将users集合中所有status=”D”的文档删除操作,对比一下MongoDB和传统SQL数据库删除的操作,看看有哪些不同之处。

与传统SQL比较 - 删除文档

MongoDB

传统SQL

一.简单介绍

1.什么是MongoDB


MongoDB是一个基于分布式文件存储的数据库,由C++语言编写,旨在为WEB应用提供可扩展的高性能数据存储解决方案。

MongoDB是一个高性能,开源,无模式的文档型数据库,官方给自己的定义是Key-value存储(高性能和高扩展)和传统RDBMS(丰富的查询和功能)之间的一座桥梁。

2.Document和BSON


MongoDB中保存的数据格式为BSON,如:

MongoDB中数据的基本单元称为文档(Document),它是MongoDB的核心概念,由多个键极其关联的值有序的放置在一起组成,数据库中它对应于关系型数据库的行。

数据在MongoDB中以BSON(Binary-JSON)文档的格式存储在磁盘上。

BSON(Binary Serialized Document Format)是一种类json的一种二进制形式的存储格式,简称Binary JSON,BSON和JSON一样,支持内嵌的文档对象和数组对象,但是BSON有JSON没有的一些数据类型,如Date和BinData类型。

BSON的优点是灵活性高,但它的缺点是空间利用率不是很理想,BSON有三个特点:轻量性、可遍历性、高效性。