基本 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
crypto.createCipheriv(params, password, iv); 该方法的第一个参数是在加密数据时所使用的算法,比如 ‘ase-256-cbc’等等,第二个参数用于指定加密时所使用的密码,该参数值必须为一个二进制格式的字符串或一个Buffer对象。 第三个参数指定加密时所使用的初始向量,参数值也必须为一个二进制格式的字符串或一个Buffer对象。 该方法返回的也是一个被创建的cipher对象。
cipher.update(data, [input_encoding], [output_encoding]) 其中第一个参数为必选项(其他的参数为可选项), 该参数值是一个Buffer对象或一个字符串,用于指定需要加密的数据。第二个参数用于指定被加密的数据所使用的编码格式,可指定参数值为 ‘utf-8’, ‘ascii’ 或 ‘binary’. 如果不使用第二个参数的话,那么第一个参数必须为一个Buffer对象。 第三个参数用于指定输出加密数据时使用的编码格式,可指定的参数值为 ‘hex’, ‘binary’ 或 ‘base64’ 等。如果不使用第三个参数的话,该方法就返回了一个存放了加密数据的Buffer对象。
cipher.final([output_encoding]); 该方法使用一个可选参数,该参数值为一个字符串,用于指定在输出加密数据的编码格式,可指定参数值为 ‘hex’, ‘binary’, 及 ‘base64’. 如果使用了 该参数,那么final方法返回字符串格式的加密数据,如果不使用该参数,那么该方法就返回一个Buffer对象。
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
。
还可以使用更安全的sha256
和sha512
。
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
生成RSA密钥对openssl genrsa -aes256 -out rsa-key.pem 2048
导出原始的私钥openssl rsa -in rsa-key.pem -outform PEM -out rsa-prv.pem
导出原始的公钥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 const Koa = require ('koa' );const app = new Koa();app.use(async (ctx, next) => { console .log(`${ctx.request.method} ${ctx.request.url} ` ); await next(); }); app.use(async (ctx, next) => { const start = new Date ().getTime(); await next(); 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>' ; }); 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.url
,ctx.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;
创建一个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 }
定义模型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必须遵守一套规范:
统一主键,名称必须是id,类型必须是STRING(50);
主键可以自己指定,也可以由框架自动生成(如果为null或undefined);
所有字段默认为NOT NULL,除非显式指定;
统一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对象。