基本

fs

异步读文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var fs = require('fs');
// 当读取二进制文件时,不传入文件编码时,回调函数的data参数将返回一个Buffer对象。在Node.js中,Buffer对象就是一个包含零个或任意个字节的数组(注意和Array不同)。
fs.readFile('sample.txt', 'utf-8', function (err, data) {
if (err) {
console.log(err);
} else {
console.log(data);
}
});

// Buffer -> String
var text = data.toString('utf-8');
console.log(text);
// String -> Buffer
var buf = Buffer.from(text, 'utf-8');
console.log(buf);

同步读文件

1
2
3
4
5
6
7
8
9
10
11
var fs = require('fs');

var data = fs.readFileSync('sample.txt', 'utf-8');
console.log(data);

try {
var data = fs.readFileSync('sample.txt', 'utf-8');
console.log(data);
} catch (err) {
// 出错了
}

写文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var fs = require('fs');

var data = 'Hello, Node.js';
fs.writeFile('output.txt', data, function (err) {
if (err) {
console.log(err);
} else {
console.log('ok.');
}
});
// 和readFile()类似,writeFile()也有一个同步方法,叫
// writeFileSync():

var data = 'Hello, Node.js';
fs.writeFileSync('output.txt', data);

stream

从文件流读取文本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var fs = require('fs');

// 打开一个流:
var rs = fs.createReadStream('sample.txt', 'utf-8');
rs.on('data', function (chunk) {
console.log('DATA:')
console.log(chunk);
});
rs.on('end', function () {
console.log('END');
});
rs.on('error', function (err) {
console.log('ERROR: ' + err);
});

流写入

以流的形式写入文件,只需要不断调用write()方法,最后以end()结束:

1
2
3
4
5
6
7
8
9
10
11
var fs = require('fs');

var ws1 = fs.createWriteStream('output1.txt', 'utf-8');
ws1.write('使用Stream写入文本数据...\n');
ws1.write('END.');
ws1.end();

var ws2 = fs.createWriteStream('output2.txt');
ws2.write(new Buffer('使用Stream写入二进制数据...\n', 'utf-8'));
ws2.write(new Buffer('END.', 'utf-8'));
ws2.end();

所有可以读取数据的流都继承自stream.Readable,所有可以写入的流都继承自stream.Writable。

pipe

1
2
3
4
5
6
var fs = require('fs');

var rs = fs.createReadStream('sample.txt');
var ws = fs.createWriteStream('copied.txt');

rs.pipe(ws);

http

request对象封装了HTTP请求,我们调用request对象的属性和方法就可以拿到所有HTTP请求的信息;
response对象封装了HTTP响应,我们操作response对象的方法,就可以把HTTP响应返回给浏览器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 导入http模块:
var http = require('http');
// 创建http server,并传入回调函数:
var server = http.createServer(function (request, response) {
// 回调函数接收request和response对象,
// 获得HTTP请求的method和url:
console.log(request.method + ': ' + request.url);
// 将HTTP响应200写入response, 同时设置Content-Type: text/html:
response.writeHead(200, {'Content-Type': 'text/html'});
// 将HTTP响应的HTML内容写入response:
response.end('<h1>Hello world!</h1>');
});
// 让服务器监听8080端口:
server.listen(8080);
console.log('Server is running at http://127.0.0.1:8080/');

解析url

解析URL需要用到Node.js提供的url模块,它使用起来非常简单,通过parse()将一个字符串解析为一个Url对象:

1
2
var url = require('url');
console.log(url.parse('http://user:pass@host.com:8080/path/to/file?query=string#hash'));

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
Url {
protocol: 'http:',
slashes: true,
auth: 'user:pass',
host: 'host.com:8080',
port: '8080',
hostname: 'host.com',
hash: '#hash',
search: '?query=string',
query: 'query=string',
pathname: '/path/to/file',
path: '/path/to/file?query=string',
href: 'http://user:pass@host.com:8080/path/to/file?query=string#hash' }

http 服务器

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
var
fs = require('fs'),
url = require('url'),
path = require('path'),
http = require('http');

// 从命令行参数获取root目录,默认是当前目录:
var root = path.resolve(process.argv[2] || '.');
console.log('Static root dir: ' + root);

// 创建服务器:
var server = http.createServer(function (request, response) {
// 获得URL的path,类似 '/css/bootstrap.css':
var pathname = url.parse(request.url).pathname;
// 获得对应的本地文件路径,类似 '/srv/www/css/bootstrap.css':
var filepath = path.join(root, pathname);
// 获取文件状态:
fs.stat(filepath, function (err, stats) {
if (!err && stats.isFile()) {
// 没有出错并且文件存在:
console.log('200 ' + request.url);
// 发送200响应:
response.writeHead(200);
// 将文件流导向response:
fs.createReadStream(filepath).pipe(response);
} else {
// 出错了或者文件不存在:
console.log('404 ' + request.url);
// 发送404响应:
response.writeHead(404);
response.end('404 Not Found');
}
});
});
server.listen(8080);
console.log('Server is running at http://127.0.0.1:8080/');

没有必要手动读取文件内容。由于response对象本身是一个Writable Stream,直接用pipe()方法就实现了自动读取文件内容并输出到HTTP响应。

node .\file_server.js ./public 是相对目录

node .\file_server.js /public 就是D:\\public

crypto

  1. crypto.createCipheriv(params, password, iv);
    该方法的第一个参数是在加密数据时所使用的算法,比如 ‘ase-256-cbc’等等,第二个参数用于指定加密时所使用的密码,该参数值必须为一个二进制格式的字符串或一个Buffer对象。 第三个参数指定加密时所使用的初始向量,参数值也必须为一个二进制格式的字符串或一个Buffer对象。
    该方法返回的也是一个被创建的cipher对象。
  2. cipher.update(data, [input_encoding], [output_encoding])
    其中第一个参数为必选项(其他的参数为可选项), 该参数值是一个Buffer对象或一个字符串,用于指定需要加密的数据。第二个参数用于指定被加密的数据所使用的编码格式,可指定参数值为 ‘utf-8’, ‘ascii’ 或 ‘binary’. 如果不使用第二个参数的话,那么第一个参数必须为一个Buffer对象。
    第三个参数用于指定输出加密数据时使用的编码格式,可指定的参数值为 ‘hex’, ‘binary’ 或 ‘base64’ 等。如果不使用第三个参数的话,该方法就返回了一个存放了加密数据的Buffer对象。
  3. cipher.final([output_encoding]);
    该方法使用一个可选参数,该参数值为一个字符串,用于指定在输出加密数据的编码格式,可指定参数值为 ‘hex’, ‘binary’, 及 ‘base64’. 如果使用了
    该参数,那么final方法返回字符串格式的加密数据,如果不使用该参数,那么该方法就返回一个Buffer对象。
  4. crypto.createDecipheriv(params, password, iv);
    同createCipheriv,用于解密

MD5和SHA1

MD5是一种常用的哈希算法,用于给任意数据一个“签名”。这个签名通常用一个十六进制的字符串表示:

1
2
3
4
5
6
7
const crypto = require('crypto');
const hash = crypto.createHash('md5');

// 可任意多次调用update():
hash.update('Hello, world!');
hash.update('Hello, nodejs!');
console.log(hash.digest('hex')); // 7e1977739c748beac0c0fd14fd26a544

update()方法默认字符串编码为UTF-8,也可以传入Buffer。

如果要计算SHA1,只需要把'md5'改成’sha1',就可以得到SHA1的结果1f32b9c9932c02227819a4151feed43e131aca40

还可以使用更安全的sha256sha512

Hmac

Hmac算法也是一种哈希算法,它可以利用MD5或SHA1等哈希算法。不同的是,Hmac还需要一个密钥:

1
2
3
4
5
6
7
const crypto = require('crypto');
const hmac = crypto.createHmac('sha256', 'secret-key');

hmac.update('Hello, world!');
hmac.update('Hello, nodejs!');

console.log(hmac.digest('hex')); // 80f7e22570...

只要密钥发生了变化,那么同样的输入数据也会得到不同的签名,因此,可以把Hmac理解为用随机数“增强”的哈希算法。

RSA

  1. 生成RSA密钥对openssl genrsa -aes256 -out rsa-key.pem 2048
  2. 导出原始的私钥openssl rsa -in rsa-key.pem -outform PEM -out rsa-prv.pem
  3. 导出原始的公钥openssl rsa -in rsa-key.pem -outform PEM -pubout -out rsa-pub.pem
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
const
fs = require('fs'),
crypto = require('crypto');

// 从文件加载key:
function loadKey(file) {
// key实际上就是PEM编码的字符串:
return fs.readFileSync(file, 'utf8');
}

let
prvKey = loadKey('./rsa-prv.pem'),
pubKey = loadKey('./rsa-pub.pem'),
message = 'Hello, world!';

// 使用私钥加密:
let enc_by_prv = crypto.privateEncrypt(prvKey, Buffer.from(message, 'utf8'));
console.log('encrypted by private key: ' + enc_by_prv.toString('hex'));

// 使用公钥解密
let dec_by_pub = crypto.publicDecrypt(pubKey, enc_by_prv);
console.log('decrypted by public key: ' + dec_by_pub.toString('utf8'));

// 使用公钥加密:
let enc_by_pub = crypto.publicEncrypt(pubKey, Buffer.from(message, 'utf8'));
console.log('encrypted by public key: ' + enc_by_pub.toString('hex'));

// 使用私钥解密:
let dec_by_prv = crypto.privateDecrypt(prvKey, enc_by_pub);
console.log('decrypted by private key: ' + dec_by_prv.toString('utf8'));

Koa2

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
// 导入koa,和koa 1.x不同,在koa2中,我们导入的是一个class,因此用大写的Koa表示:
const Koa = require('koa');

// 创建一个Koa对象表示web app本身:
const app = new Koa();

// 对于任何请求,app将调用该异步函数处理请求:
app.use(async (ctx, next) => {
console.log(`${ctx.request.method} ${ctx.request.url}`); // 打印URL
await next(); // 调用下一个middleware
});

app.use(async (ctx, next) => {
const start = new Date().getTime(); // 当前时间
await next(); // 调用下一个middleware
const ms = new Date().getTime() - start; // 耗费时间
console.log(`Time: ${ms}ms`); // 打印耗费时间
});

app.use(async (ctx, next) => {
await next();
ctx.response.type = 'text/html';
ctx.response.body = '<h1>Hello, koa2!</h1>';
});

// 在端口3000监听:
app.listen(3000);
console.log('app started at port 3000...');

koa把很多async函数组成一个处理链,每个async函数都可以做一些自己的事情,然后用await next()来调用下一个async函数。我们把每个async函数称为middleware,这些middleware可以组合起来,完成很多有用的功能。

middleware的顺序很重要,也就是调用app.use()的顺序决定了middleware的顺序。

最后注意ctx对象有一些简写的方法,例如ctx.url相当于ctx.request.urlctx.type相当于ctx.response.type

koa-router

npm i -S koa-router

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
const Koa = require('koa');

// 注意require('koa-router')返回的是函数:
const router = require('koa-router')();

const app = new Koa();

// log request URL:
app.use(async (ctx, next) => {
console.log(`Process ${ctx.request.method} ${ctx.request.url}...`);
await next();
});

// add url-route:
router.get('/hello/:name', async (ctx, next) => {
var name = ctx.params.name;
ctx.response.body = `<h1>Hello, ${name}!</h1>`;
});

router.get('/', async (ctx, next) => {
ctx.response.body = '<h1>Index</h1>';
});

// add router middleware:
app.use(router.routes());

app.listen(3000);
console.log('app started at port 3000...');

处理post 请求

post请求通常会发送一个表单,或者JSON,它作为request的body发送,但无论是Node.js提供的原始request对象,还是koa提供的request对象,都不提供解析request的body的功能!

需要引入另一个middleware来解析原始request请求,然后,把解析后的参数,绑定到ctx.request.body

npm i -S koa-bodyparser

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const bodyParser = require('koa-bodyparser');
app.use(bodyParser());
router.get('/', async (ctx, next) => {
ctx.response.body = `<h1>Index</h1>
<form action="/signin" method="post">
<p>Name: <input name="name" value="koa"></p>
<p>Password: <input name="password" type="password"></p>
<p><input type="submit" value="Submit"></p>
</form>`;
});

router.post('/signin', async (ctx, next) => {
var
name = ctx.request.body.name || '',
password = ctx.request.body.password || '';
console.log(`signin with name: ${name}, password: ${password}`);
if (name === 'koa' && password === '12345') {
ctx.response.body = `<h1>Welcome, ${name}!</h1>`;
} else {
ctx.response.body = `<h1>Login failed!</h1>
<p><a href="/">Try again</a></p>`;
}
});

Nunjucks

在app.js中编写以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const nunjucks = require('nunjucks');

function createEnv(path, opts) {
var
autoescape = opts.autoescape === undefined ? true : opts.autoescape,
noCache = opts.noCache || false,
watch = opts.watch || false,
throwOnUndefined = opts.throwOnUndefined || false,
env = new nunjucks.Environment(
new nunjucks.FileSystemLoader('views', {
noCache: noCache,
watch: watch,
}), {
autoescape: autoescape,
throwOnUndefined: throwOnUndefined
});
if (opts.filters) {
for (var f in opts.filters) {
env.addFilter(f, opts.filters[f]);
}
}
return env;
}

var env = createEnv('views', {
watch: true,
filters: {
hex: function (n) {
return '0x' + n.toString(16);
}
}
});

用下面代码来进行渲染

1
2
var s = env.render('hello.html', { name: '小明' });
console.log(s);

这样就避免了输出恶意脚本。

此外,可以使用Nunjucks提供的功能强大的tag,编写条件判断、循环等功能。Nunjucks官网

koa mvc

编写middleware

编写一个处理静态文件的middleware。编写middleware实际上一点也不复杂。我们先创建一个static-files.js的文件,编写一个能处理静态文件的middleware:

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
const path = require('path');
const mime = require('mime');
const fs = require('mz/fs');//使用了一个mz的包,并通过require('mz/fs');导入。mz提供的API和Node.js的fs模块完全相同,但fs模块使用回调,而mz封装了fs对应的函数,并改为Promise。这样,我们就可以非常简单的用await调用mz的函数,而不需要任何回调。

// url: 类似 '/static/'
// dir: 类似 __dirname + '/static'
function staticFiles(url, dir) {
return async (ctx, next) => {
let rpath = ctx.request.path;
// 判断是否以指定的url开头:
if (rpath.startsWith(url)) {
// 获取文件完整路径:
let fp = path.join(dir, rpath.substring(url.length));
// 判断文件是否存在:
if (await fs.exists(fp)) {
// 查找文件的mime:
ctx.response.type = mime.lookup(rpath);
// 读取文件内容并赋值给response.body:
ctx.response.body = await fs.readFile(fp);
} else {
// 文件不存在:
ctx.response.status = 404;
}
} else {
// 不是指定前缀的URL,继续处理下一个middleware:
await next();
}
};
}

module.exports = staticFiles;

app.js中加入代码

1
2
let staticFiles = require('./static-files');
app.use(staticFiles('/static/', __dirname + '/static'));

集成Nunjucks

集成Nunjucks实际上也是编写一个middleware,这个middleware的作用是给ctx对象绑定一个render(view, model)的方法,这样,后面的Controller就可以调用这个方法来渲染模板了。

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
const nunjucks = require('nunjucks');

function createEnv(path, opts) {
var
autoescape = opts.autoescape === undefined ? true : opts.autoescape,
noCache = opts.noCache || false,
watch = opts.watch || false,
throwOnUndefined = opts.throwOnUndefined || false,
env = new nunjucks.Environment(
new nunjucks.FileSystemLoader(path || 'views', {
noCache: noCache,
watch: watch,
}), {
autoescape: autoescape,
throwOnUndefined: throwOnUndefined
});
if (opts.filters) {
for (var f in opts.filters) {
env.addFilter(f, opts.filters[f]);
}
}
return env;
}

function templating(path, opts) {
// 创建Nunjucks的env对象:
var env = createEnv(path, opts);
return async (ctx, next) => {
// 给ctx绑定render函数:
ctx.render = function (view, model) {
// 把render后的内容赋值给response.body:
ctx.response.body = env.render(view, Object.assign({}, ctx.state || {}, model || {}));
// 设置Content-Type:
ctx.response.type = 'text/html';
};
// 继续处理请求:
await next();
};
}

module.exports = templating;

app.js里的middleware的顺序

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
// 第一个middleware是记录URL以及页面执行时间:
app.use(async (ctx, next) => {
console.log(`Process ${ctx.request.method} ${ctx.request.url}...`);
var
start = new Date().getTime(),
execTime;
await next();
execTime = new Date().getTime() - start;
ctx.response.set('X-Response-Time', `${execTime}ms`);
});


// 第二个middleware处理静态文件:
if (! isProduction) {
let staticFiles = require('./static-files');
app.use(staticFiles('/static/', __dirname + '/static'));
}

// 第三个middleware解析POST请求:
app.use(bodyParser());

// 第四个middleware负责给ctx加上render()来使用Nunjucks:
app.use(templating('view', {
noCache: !isProduction,
watch: !isProduction
}));

// 最后一个middleware处理URL路由:
app.use(controller());

mysql

cd C:\Program Files\MySQL\MySQL Server 8.0\bin

mysql -u root -p

输入exit退出MySQL命令行模式。

目前使用最广泛的MySQL Node.js驱动程序是开源的mysql,可以直接使用npm安装。

ORM

Object-Relational Mapping,把关系数据库的表结构映射到对象上。

我们选择Node的ORM框架Sequelize来操作数据库。这样,我们读写的都是JavaScript对象,Sequelize帮我们把对象变成数据库中的行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use mysql; //选择mysql数据库

create user 'www'@'%' identified by 'www'; //创建用户(貌似必须跟赋权语句分开)

grant all privileges on test.* to 'www'@'%'; //授予权限

flush privileges; //刷新权限

//创建了pets表
create table pets (
id varchar(50) not null,
name varchar(100) not null,
gender bool not null,
birth varchar(10) not null,
createdAt bigint not null,
updatedAt bigint not null,
version bigint not null,
primary key (id)
) engine=innodb;

添加如下依赖,注意mysql是驱动,我们不直接使用,但是sequelize会用。

1
2
3
4
"dependencies": {
"sequelize": "3.24.1",
"mysql": "2.11.1"
}

config.js实际上是一个简单的配置文件:

1
2
3
4
5
6
7
8
9
var config = {
database: 'test', // 使用哪个数据库
username: 'www', // 用户名
password: 'www', // 口令
host: 'localhost', // 主机名
port: 3306 // 端口号,MySQL默认3306
};

module.exports = config;
  1. 创建一个sequelize
1
2
3
4
5
6
7
8
9
10
11
const Sequelize = require('sequelize');
const config = require('./config');

var sequelize = new Sequelize(config.database, config.username, config.password, {
host: config.host,
dialect: 'mysql',
pool: {
max: 5,
min: 0,
idle: 30000
}
  1. 定义模型Pet,告诉Sequelize如何映射数据库表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var Pet = sequelize.define('pet', {
id: {
type: Sequelize.STRING(50),
primaryKey: true
},
name: Sequelize.STRING(100),
gender: Sequelize.BOOLEAN,
birth: Sequelize.STRING(10),
createdAt: Sequelize.BIGINT,
updatedAt: Sequelize.BIGINT,
version: Sequelize.BIGINT
}, {
timestamps: false
});

用sequelize.define()定义Model时,传入名称pet,默认的表名就是pets。第二个参数指定列名和数据类型,如果是主键,需要更详细地指定。第三个参数是额外的配置,我们传入{ timestamps: false }是为了关闭Sequelize的自动添加timestamp的功能。所有的ORM框架都有一种很不好的风气,总是自作聪明地加上所谓“自动化”的功能,但是会让人感到完全摸不着头脑。

添加

接下来,我们就可以往数据库中塞一些数据了。我们可以用Promise的方式写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var now = Date.now();

Pet.create({
id: 'g-' + now,
name: 'Gaffey',
gender: false,
birth: '2007-07-07',
createdAt: now,
updatedAt: now,
version: 0
}).then(function (p) {
console.log('created.' + JSON.stringify(p));
}).catch(function (err) {
console.log('failed: ' + err);
});

或者使用await方式写

1
2
3
4
5
6
7
8
9
10
11
12
(async () => {
var dog = await Pet.create({
id: 'd-' + now,
name: 'Odie',
gender: false,
birth: '2008-08-08',
createdAt: now,
updatedAt: now,
version: 0
});
console.log('created: ' + JSON.stringify(dog));
})();

查询

1
2
3
4
5
6
7
8
9
10
11
(async () => {
var pets = await Pet.findAll({
where: {
name: 'Gaffey'
}
});
console.log(`find ${pets.length} pets:`);
for (let p of pets) {
console.log(JSON.stringify(p));
}
})();

更新

1
2
3
4
5
6
7
(async () => {
var p = await queryFromSomewhere();
p.gender = true;
p.updatedAt = Date.now();
p.version ++;
await p.save();
})();

删除

1
2
3
4
(async () => {
var p = await queryFromSomewhere();
await p.destroy();
})();

出现问题>Client does not support authentication protocol requested by server
解决

1
2
3
4
mysql -u root -p
use test; //使用test表
ALTER USER 'www'@'%' IDENTIFIED WITH mysql_native_password BY 'Dcba19970527';
FLUSH PRIVILEGES;

Model

我们首先要定义的就是Model存放的文件夹必须在models内,并且以Model名字命名,例如:Pet.js,User.js等等。

其次,每个Model必须遵守一套规范:

  1. 统一主键,名称必须是id,类型必须是STRING(50);
  2. 主键可以自己指定,也可以由框架自动生成(如果为null或undefined);
  3. 所有字段默认为NOT NULL,除非显式指定;
  4. 统一timestamp机制,每个Model必须有createdAt、updatedAt和version,分别记录创建时间、修改时间和版本号。其中,createdAt和updatedAt以BIGINT存储时间戳,最大的好处是无需处理时区,排序方便。version每次修改时自增。

所以,我们不要直接使用Sequelize的API,而是通过db.js间接地定义Model。例如,User.js应该定义如下:

1
2
3
4
5
6
7
8
9
10
11
const db = require('../db');

module.exports = db.defineModel('users', {
email: {
type: db.STRING(100),
unique: true
},
passwd: db.STRING(100),
name: db.STRING(100),
gender: db.BOOLEAN
});

这样,User就具有email、passwd、name和gender这4个业务字段。id、createdAt、updatedAt和version应该自动加上,而不是每个Model都去重复定义。

db.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
const Sequelize = require('sequelize');

console.log('init sequelize...');

var sequelize = new Sequelize('dbname', 'username', 'password', {
host: 'localhost',
dialect: 'mysql',
pool: {
max: 5,
min: 0,
idle: 10000
}
});

const ID_TYPE = Sequelize.STRING(50);

function defineModel(name, attributes) {
var attrs = {};
for (let key in attributes) {
let value = attributes[key];
if (typeof value === 'object' && value['type']) {
value.allowNull = value.allowNull || false;
attrs[key] = value;
} else {
attrs[key] = {
type: value,
allowNull: false
};
}
}
attrs.id = {
type: ID_TYPE,
primaryKey: true
};
attrs.createdAt = {
type: Sequelize.BIGINT,
allowNull: false
};
attrs.updatedAt = {
type: Sequelize.BIGINT,
allowNull: false
};
attrs.version = {
type: Sequelize.BIGINT,
allowNull: false
};
return sequelize.define(name, attrs, {
tableName: name,
timestamps: false,
hooks: {
beforeValidate: function (obj) {
let now = Date.now();
if (obj.isNewRecord) {
if (!obj.id) {
obj.id = generateId();
}
obj.createdAt = now;
obj.updatedAt = now;
obj.version = 0;
} else {
obj.updatedAt = Date.now();
obj.version++;
}
}
}
});
}

接下来,我们把简单的config.js拆成3个配置文件:

  • config-default.js:存储默认的配置;
  • config-override.js:存储特定的配置;
  • config-test.js:存储用于测试的配置。

例如,默认的config-default.js可以配置如下:

1
2
3
4
5
6
7
8
9
10
var config = {
dialect: 'mysql',
database: 'nodejs',
username: 'www',
password: 'www',
host: 'localhost',
port: 3306
};

module.exports = config;

config-override.js可应用实际配置:

1
2
3
4
5
6
7
8
var config = {
database: 'production',
username: 'www',
password: 'secret-password',
host: '192.168.1.199'
};

module.exports = config;

config-test.js可应用测试环境的配置:

1
2
3
4
5
var config = {
database: 'test'
};

module.exports = config;

读取配置的时候,我们用config.js实现不同环境读取不同的配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const defaultConfig = './config-default.js';
// 可设定为绝对路径,如 /opt/product/config-override.js
const overrideConfig = './config-override.js';
const testConfig = './config-test.js';

const fs = require('fs');

var config = null;

if (process.env.NODE_ENV === 'test') {
console.log(`Load ${testConfig}...`);
config = require(testConfig);
} else {
console.log(`Load ${defaultConfig}...`);
config = require(defaultConfig);
try {
if (fs.statSync(overrideConfig).isFile()) {
console.log(`Load ${overrideConfig}...`);
config = Object.assign(config, require(overrideConfig));
}
} catch (err) {
console.log(`Cannot load ${overrideConfig}.`);
}
}

module.exports = config;

使用Model

要使用Model,就需要引入对应的Model文件,例如:User.js。一旦Model多了起来,如何引用也是一件麻烦事。

自动化永远比手工做效率高,而且更可靠。我们写一个model.js,自动扫描并导入所有Model:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const fs = require('fs');
const db = require('./db');

let files = fs.readdirSync(__dirname + '/models');

let js_files = files.filter((f)=>{
return f.endsWith('.js');
}, files);

module.exports = {};

for (let f of js_files) {
console.log(`import model from file ${f}...`);
let name = f.substring(0, f.length - 3);
module.exports[name] = require(__dirname + '/models/' + f);
}

module.exports.sync = () => {
db.sync();
};

这样,需要用的时候,写起来就像这样:

1
2
3
4
5
6
7
const model = require('./model');

let
Pet = model.Pet,
User = model.User;

var pet = await Pet.create({ ... });

注意到我们其实不需要创建表的SQL,因为Sequelize提供了一个sync()方法,可以自动创建数据库。这个功能在开发和生产环境中没有什么用,但是在测试环境中非常有用。测试时,我们可以用sync()方法自动创建出表结构,而不是自己维护SQL脚本。这样,可以随时修改Model的定义,并立刻运行测试。开发环境下,首次使用sync()也可以自动创建出表结构,避免了手动运行SQL的问题。

init-db.js的代码非常简单:

1
2
3
4
5
const model = require('./model.js');
model.sync();

console.log('init db ok.');
process.exit(0);

mocha

vscode/launch.json

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
{
"version": "0.2.0",
"configurations": [

{
"name": "Run",
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/hello.js",
"stopOnEntry": false,
"args": [],
"cwd": "${workspaceRoot}",
"preLaunchTask": null,
"runtimeExecutable": null,
"runtimeArgs": [
"--nolazy"
],
"env": {
"NODE_ENV": "development"
},
"externalConsole": false,
"sourceMaps": false,
"outDir": null
},
{
"name": "Test",
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
"stopOnEntry": false,
"args": [],
"cwd": "${workspaceRoot}",
"preLaunchTask": null,
"runtimeExecutable": null,
"runtimeArgs": [
"--nolazy"
],
"env": {
"NODE_ENV": "test"
},
"externalConsole": false,
"sourceMaps": false,
"outDir": null
}
]
}

hello-test.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const assert = require('assert');
const sum = require('../hello');

describe('#hello.js', () => {
describe('#sum()', () => {
before(function () {
console.log('before:');
});

after(function () {
console.log('after.');
});

beforeEach(function () {
console.log(' beforeEach:');
});

afterEach(function () {
console.log(' afterEach.');
});

it('sum() should return 0', () => {
assert.strictEqual(sum(), 0);
});

it('sum(1) should return 1', () => {
assert.strictEqual(sum(1), 1);
});

it('sum(1, 2) should return 3', () => {
assert.strictEqual(sum(1, 2), 3);
});

it('sum(1, 2, 3) should return 6', () => {
assert.strictEqual(sum(1, 2, 3), 6);
});
});
});

describe(name, fn) 定义一组测试

it(name, fn) 定义一项测试

可以看到每个test执行前后会分别执行beforeEach()和afterEach(),以及一组test执行前后会分别执行before()和after()

mocha http 测试

app.js只负责创建app实例,并不监听端口:

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

const app = new Koa();

app.use(async (ctx, next) => {
const start = new Date().getTime();
await next();
const ms = new Date().getTime() - start;
console.log(`${ctx.request.method} ${ctx.request.url}: ${ms}ms`);
ctx.response.set('X-Response-Time', `${ms}ms`);
});

app.use(async (ctx, next) => {
var name = ctx.request.query.name || 'world';
ctx.response.type = 'text/html';
ctx.response.body = `<h1>Hello, ${name}!</h1>`;
});

module.exports = app;

start.js负责真正启动应用:

1
2
3
4
const app = require('./app');

app.listen(3000);
console.log('app started at port 3000...');

这样做的目的是便于后面的测试。

紧接着,我们在test目录下创建app-test.js,来测试这个koa应用。

在测试前,我们在package.json中添加devDependencies,除了mocha外,我们还需要一个简单而强大的测试模块supertest

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
// app-test.js

const
request = require('supertest'),
app = require('../app');

describe('#test koa app', () => {

let server = app.listen(9900); // 让app实例监听在9900端口上,并且获得返回的server实例。

describe('#test server', () => {

it('#test GET /', async () => {
let res = await request(server) // 就可以构造一个GET请求,发送给koa的应用,然后获得响应。
.get('/')
.expect('Content-Type', /text\/html/) // 利用supertest提供的expect()更方便地断言响应的HTTP代码、返回内容和HTTP头。断言HTTP头时可用使用正则表达式
.expect(200, '<h1>Hello, world!</h1>');
});

it('#test GET /path?name=Bob', async () => {
let res = await request(server)
.get('/path?name=Bob')
.expect('Content-Type', /text\/html/)
.expect(200, '<h1>Hello, Bob!</h1>');
});
});
});

REST

Representational State Transfer

依赖:

1
2
3
4
5
"dependencies": {
"koa": "2.0.0",
"koa-bodyparser": "3.2.0",
"koa-router": "7.0.0"
}
1
2
3
4
5
6
7
8
9
10
11
12
const app = new Koa();

const controller = require('./controller');

// parse request body:
app.use(bodyParser());

// add controller:
app.use(controller());

app.listen(3000);
console.log('app started at port 3000...');

注意到app.use(bodyParser());这个语句,它给koa安装了一个解析HTTP请求body的处理函数。如果HTTP请求是JSON数据,我们就可以通过ctx.request.body直接访问解析后的JavaScript对象。