创建定制的npm脚本

1
2
3
4
5
6
7
npm install --save-dev babel-cli babel-preset-es2015
npm i --save-dev uglify-es

// package.json
"babel": "./node_modules/.bin/babel browser.js -d build/",
"uglify": "./node_modules/.bin/uglifyjs build/browser.js -o build/browser.min.js",
"build": "npm run babel && npm run uglify"

把Gulp添加到项目中

1
2
3
npm i -D gulp-sourcemaps gulp-babel babel-preset-es2015
npm i -D gulp-concat react react-dom babel-preset-react
npm i -D gulp-watch // 监听变化

配置和运行Webpack

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
npm install -S react react-dom
npm install -D webpack babel-loader babel-core
npm install -D babel-preset-es2015 babel-preset-react
npm install -S webpack-dev-server@1.14.1

// webpack.config.js
const path = require('path');
const webpack = require('webpack');

module.exports = {
entry: './app/index.js', // 输入文件
output: { // 输出文件
path: __dirname,
filename: 'dist/bundle.js'
},
module: {
loaders: [{
test: /.jsx?$/, // 匹配所有的JSX文件
loader: 'babel-loader',
exclude: /node_modules/,
query: {
presets: ['es2015', 'react'] // 使用Babel ES2015和React插件
}
}]
}
}

创建Express 路由

1
2
npm i -S redis
redis-server
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
// 创建到Redios的长连接
const redis = require('redis');
const bcrypt = require('bcrypt');
const db = redis.createClient();

// 返回列表 key 中指定区间内的元素,区间以偏移量 start 和 stop 指定。 key start stop
db.lrange('entries', from, to, (err, items) => {

});

// 将一个或多个值 value 插入到列表 key 的表头 (rpush 表尾)
db.lpush(
'entries',
entryJSON,
(err) => {
if (err) return cb(err);
cb();
}
);

// 返回列表 key 的长度。
db.llen('entries', cb);

// 为键 key 储存的数字值加上一。
// 如果键 key 不存在, 那么它的值会先被初始化为 0 , 然后再执行 INCR 命令。
// 如果键 key 储存的值不能被解释为数字, 那么 INCR 命令将返回一个错误。
db.incr('user:ids', (err, id) => {
});

// 将字符串值 value 关联到 key 。
db.set(`user:id:${this.name}`, id, (err) => {
if (err) return cb(err);
});

// 同时将多个 field-value (域-值)对设置到哈希表 key 中。 HMSET key field value [field value …]
db.hmset(`user:${id}`, this, (err) => {
cb(err);
});

// 取得由名称索引的ID
db.get(`user:id:${name}`, cb);

// 获取普通对象哈希
db.hgetall(`user:id:${name}`, (err, user) => {
...
})

关系型数据库

PostgreSQL

创建一个名为articles的库

createdb articles

删掉已有数据库中的全部数据

dropdb articles

在node中与Postgres交互
npm install pg --save

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const pg = require("pg");
const db = new pg.Client({
database: "articles",
user: "postgres",
password: "123456",
port: 5432
});
db.connect((err, client) => {
db.query(
`
CREATE TABLE IF NOT EXISTS snippets (
id SERIAL,
PRIMARY KEY(id),
body text
);
`,
(err, result) => {
if (err) throw err;
console.log(`Created table "snippets"`);
db.end();
}
);

});

knex

Knex是一个轻便的SQL抽象包,他被称为查询构建器。
npm install knex --save
npm install sqlite3 --save

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
// app.js
const db = require('./db');

db().then(() => {
console.log('db ready');

db.Article.create({
title: 'my article',
content: 'article content'
}).then(() => {
db.Article.all().then(articles => {
console.log(articles);

process.exit();
});
});
})
.catch(err => { throw err });


// db.js
const knex = require('knex');

const db = knex({
client: 'sqlite3',
connection: {
filename: 'tldr.sqlite'
},
useNullAsDefault: true
});

module.exports = () => {
return db.schema.createTableIfNotExists('articles', table => {
table.increments('id').primary();
table.string('title');
table.text('content');
});
};

module.exports.Article = {
all() {
return db('articles').orderBy('title');
},

find(id) {
return db('articles').where({ id }).first();
},

create(data) {
return db('articles').insert(data);
},

delete(id) {
return db('articles').del().where({ id });
}
};

如果切换成postgreSQL
npm install pg --save

修改knex的配置就能切换新的数据库

1
2
3
4
5
6
7
8
9
10
const db = knex({
client: 'pg',
connection: {
database: "articles",
user: "postgres",
password: "123456",
port: 5432
},

});

非关系型数据库

mongoDB

1
mongo//启动服务

连接MongoDB

1
2
3
4
5
6
7
8
9
const {
MongoClient
} = require('mongodb');

MongoClient.connect('mongodb://localhost:27017/articles')
.then(db => {
console.log('Client ready');
db.close();
}, console.error);

collection API

大部分数据库交互都是通过collection API完成的

  1. collection.drop()—移除整个数据集;

  2. collection.update(query)—更新跟查询匹配的文档;

  3. collection.count(query)—对跟查询匹配的文档计数。

    为满足操作一个或多个文档的需求, find insert和 delete等操作有几种变体。比如:

  4. collection. insertOne(doc)—插入单个文档;

  5. collection.insertMany([doc,doc2])—插入多个文档;

  6. collection.findone(query)—找到一个跟查询匹配的文档;

ObjectID

MongoDB的标识是二进制JSON(BSON)格式的。文档上的_id是一个JavaScript对象,其内部封装了BSON格式的ObjectID。

ObjectID表面上看起来可能像字符串一样,但实际上是对象。比较方法:

1
2
3
article1._id.equals(article2._id);
String(article1._id) === String(article2._id);
assert.deepEqual(article1._id, article2._id)

传给mongodb驱动的标识必须是BSON格式的ObjecID。ObjectID构造器可以将字符串转换成ObjectID:

1
2
3
const { ObjectID } = require('mongodb');
const stringID = "555f6b455123a3b00e1c3c18";
const bsonID = new ObjectID(stringID);`2

redis

redis-server启动redis

npm i redis --save

处理键/值对

1
2
3
4
5
6
7
8
9
10
11
12
13
const redis = require('redis');

const db = redis.createClient();
db.on('connect', () => console.log('Redis client connected to server'));
db.on('ready', () => console.log('Redis server is ready'));
db.set('color', 'red', err => {
if (err) throw err;
})
db.get('color', (err, value) => {
if (err) throw err;
console.log('Got:', value);
})
db.on('error', err => console.error('Redis error', err));

如果写入的键已经存在了,那么原来的值会被覆盖掉。如果读取的键不存在,则会得到值null,而不会被当作错误。

处理键

exists可以检查某个键是否存在,它能接受任何数据类型:

1
2
3
4
db.exists('users', (err, doesExist) => {
if (err) throw err;
console.log('users exists:', doesExist);
})

编码与数据类型

在Redis服务器中,键和值都是二进制对象,跟传给客户端时所用的编码没有关系。所有有效的JavaS字符串都是有效的键或值。默认情况下,再写入时会将键和值强制转换成字符串。

使用散列表

散列表是键/值对的数据集。

1
2
3
4
5
6
7
8
9
10
11
12
db.hmset('camping', {
shelter: '2-person tent',
cooking: 'campstove'
}, redis.print);
db.hget('camping', 'cooking', (err, value) => {
if (err) throw err;
console.log('Will be cooking with:', value);
});
db.hkeys('camping', (err, keys) => {
if (err) throw err;
keys.forEach(key => console.log(` ${key}`));
})

使用列表

列表是包含字符串值的有序数据集,可以存在同一值的多个副本。

1
2
3
4
5
6
db.lpush('tasks', 'Paint the bikeshed red.', redis.paint);
db.lpush('tasks', 'Paint the bikeshed green.', redis.paint);
db.lrange('tasks', 0, -1, (err, items) => {
if (err) throw err;
items.forEach(item => console.log(`${item}`));
});

使用集合

集合是无序数据集,其中不允许有重复值。集合是一种高性能的数据结构,检查成员、添加成员和移除记录都可以在O(1)时间内完成,所以其非常适合对性能要求比较高的任务:

1
2
3
4
5
6
7
db.sadd('admins', 'Alice', redis.print);
db.sadd('admins', 'Bob', redis.print);
db.sadd('admins', 'Alice', redis.print); //操作失败
db.smembers('admins', (err, members) => {
if (err) throw err;
console.log(members);
})

提升性能

npm install hiredis --save

嵌入式数据库

LevelDB

npm install level --save

键值读写

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

const db = level('./app.db', {
valueEncoding: 'json'
})

const key = 'user';
const value = {
name: 'Alice'
};

db.put(key, value, err => {
if (err) throw err;
db.get(key, (err, result) => {
if (err) throw err;
console.log('got value', result);
db.del(key, err => {
if (err) throw err;
console.log('value was deleted');
})
})
})

可插拔的后台

npm install --save levelup memdown

1
2
3
4
5
6
7
8
const level = require('levelup');
const memdown = require('memdown');

const db = level('./level-articles.db', { // 这里随便写,因为memdown根本不用硬盘
keyEncoding: 'json',
valueEncoding: 'json',
db: memdown // 这里的参数db设为memdown
})

游览器存储

1
2
3
4
5
6
localStorage.setItem(key, value) //存储键值对
localStorage.getItem(key) //获取指定键对应的值
localStorage.removeItem(key) //移除指定键对应的值
localStorage.clear() //移除所有键值对
localStorage.key(index) //获取指定索引处的值
localStorage.length //键总数

键和值只能是字符串。如果提供的值不是字符串,会被强制转换成字符串。这种转换用的是.toString,不会产生JSON字符串。所以对象的序列化结果就是[object object]。

访问Web存储中的数据是同步操作。

用localStorage缓存记忆memoize它的返回结果

1
2
3
4
5
6
7
8
9
10
function memoizedExpensiveOperation(data) {
const key = `/memoized/${JSON.stringify(data)}`;
const memoizedResult = localStorage.getItem(key);
if (memoizedResult != null) return memoizedResult;
// 完成高成本工作
const result = expensiveWork(data);
// 将结果保存到localStorage中,以后就不用再计算了
localStorage.setItem(key, result);
return result;
}

localForage

localForage的接口基本上跟Web存储一模一样,只不过是异步非阻塞方式的。

1
2
3
4
5
6
7
8
9
localforage.setItem(key, value, callback) //存储键值对
localforage.getItem(key, callback) //获取指定键对应的值
localforage.removeItem(key, callback) //移除指定键对应的值
localforage.clear(callback) //移除所有键值对
localforage.key(index, callback) //获取指定索引处的值
localforage.length //键总数

localforage.keys(callback) //获取所有的键
localforage.iterate(iterator, callback) //循环遍历键值对

localForage API有promise和回调两种方式。

1
2
3
4
5
6
const value = localStorage.getItem(key);
console.log(value);

localforage.getItem(key, (err, value) => {
console.log(value);
});

localForage会在底层使用当前游览器环境中最好的存储机制。如果有IndexedDB,就用IndexedDB。否则就试着用WebSQL,甚至用Web存储。这些存储的优先级是可以配置的,甚至可以禁止使用某种存储:
localforage.setDriver([localforage.INDEXEDDB,localforage.WEBSQL])

单元测试

assert

assert模块是Node中大多数单元测试的基础。

代办事项列表模型:

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
// todo.js
class Todo {
constructor() {
this.todos = [];
}

add(item) {
if (!item) throw new Error('Todo.prototype.add requires an item');
this.todos.push(item);
}

deleteAll() {
this.todos = [];
}

get length() {
return this.todos.length;
}

doAsync(cb) {
setTimeout(cb, 2000, true);
}
}

module.exports = Todo;
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
// test.js
const assert = require('assert');
const Todo = require('./todo');
const todo = new Todo();
let testCompleted = 0;

function deleteTest() {
todo.add('Delete Me');
assert.equal(todo.length, 1, '1 item should exist'); // 结果必须等于1
todo.deleteAll();
assert.equal(todo.length, 0, 'No items should exist'); // 结果必须等于0
testCompleted++;
}

function addTest() {
todo.deleteAll();
todo.add('Added');
assert.notEqual(todo.length, 0, '1 item should exist'); // 结果必须不等于0
testCompleted++;
}

function doAsyncTest(cb) {
todo.doAsync(value => {
assert.ok(value, 'Callback should be passed true'); // 判断结果值是否为true
testCompleted++;
cb();
})
}

function throwsTest(cb) {
assert.throws(todo.add, /Todo.prototype.add requires an item/); // 检查程序抛出的错误消息是否正确
testCompleted++;
}

deleteTest();
addTest();

throwsTest();
doAsyncTest(() => {
console.log(`Completed ${testCompleted} tests`); //表们完成数
});

Mocha

Mocha是个流行的测试框架,默认是BDD风格的,但也可以用在TDD风格的测试中。

全局变量检测:如果想金庸全局检测泄露,在运行mocha命令时加上--ignored-leaks选项。此外,如果想指明要用的几个全局变量,可以把它们放在--globals选项后面,用逗号分开。

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

Mocha挂钩

beforeEach(),afterEach(),before(),after()

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 memdb = require('..');
const assert = require('assert');

describe('memdb', () => { //描述memdb功能
describe('memdb', () => {
beforeEach((done) => { //在每个测试用例之前都要清理数据库,保持测试的无状态性
memdb.clear(done);
})
})

describe('synchronous .saveSync(doc)', () => { //描述.saveSync()方法的功能
it('should save the document', () => { //描述期望值
const pet = {
name: 'Tobi'
};
memdb.saveSync(pet);
const ret = memdb.first({
name: 'Tobi'
});
console.log(ret == pet);
assert(ret == pet); //确保找到了pet
})
})

describe('asyncronous .save(doc)', () => {
it('should save the document', (done) => {
const pet = {
name: 'Tobi'
};
memdb.save(pet, () => { //保存文档
const ret = memdb.first({
name: 'Tobi'
});
console.log(ret == pet);
assert(ret == pet); //断言文档正确保存了
done(); //告诉Mocha这个测试用例做完了
})
})
})

describe('.first(obj)', () => {
it('should return the first matching doc', () => {
const tobi = {
name: 'Tobi'
};
const loki = {
name: 'Loki'
};
memdb.saveSync(tobi);
memdb.saveSync(loki);
let ret = memdb.first({
name: 'Tobi'
});
assert(JSON.stringify(ret) == JSON.stringify(tobi));
ret = memdb.first({
name: 'Loki'
});
assert(JSON.stringify(ret) == JSON.stringify(loki));
})
it('should return null when no doc matches', () => {
const ret = memdb.first({
name: 'Manny'
});
assert(ret == null);
});
});
});

Vows

一个测试套件中包含一或多个批次。你可以把批次当作一组相互关联的情境,或者要测试的概念领域。批次和上下文是并行运行的。上下文中可能包含主题、一或多个誓约,以及/或者一或多个相关联的情景(内部情境也是并行运行的)。主题是跟情境相关的测试逻辑。誓约是对主题结果的测试。

1
2
3
"scripts": {
"test": "vows test/*.js"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const vows = require('vows');
const assert = require('assert');
const Todo = require('./../todo');

vows.describe('Todo').addBatch({ // 批次
'when adding an item': { // 情境
topic: () => { // 主题
const todo = new Todo();
todo.add('Feed my cat');
return todo;
},
'it should exist in my todos': (err, todo) => { // 誓约
assert.equal(todo.length, 1);
}
}
}).export(module);

Should.js

断言库,用BDD的风格表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
npm i --save-dev should

const tips = require('..');
const should = require('should');
const tax = 0.12;
const tip = 0.15;
const prices = [10, 20];

const pricesWithTipAndTax = tips.addPercentageToEach(prices, tip + tax);
pricesWithTipAndTax[0].should.equal(12.7);
pricesWithTipAndTax[1].should.equal(25.4);

const totalAmount = tips.sum(pricesWithTipAndTax).toFixed(2);
totalAmount.should.equal('38.10');

const totalAmountAsCurrency = tips.dollarFormat(totalAmount);
totalAmountAsCurrency.should.equal('$38.10');

const tipAsPercent = tips.percentFormat(tip);
tipAsPercent.should.equal('15%');

Sinon.js的探测器和存根

探测器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const sinon = require('sinon');
const Database = require('./db');
const fs = require('fs');
const database = new Database('./sample.json');

const fsWriteFileSpy = sinon.spy(fs, 'writeFile'); //替换fs方法
const saveDone = sinon.spy();

database.insert('name', 'Charles Dickens');
database.save(saveDone);

sinon.assert.calledOnce(fsWriteFileSpy); //断言writeFile只调用了一次

fs.writeFile.restore(); //恢复原来的方法

存根

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const sinon = require('sinon');
const Database = require('./db');
const fs = require('fs');
const database = new Database('./sample.json');

const stub = sinon.stub(fs, 'writeFile', (file, data, cb) => { //用自己的函数代替writeFile
cb();
})
const saveDone = sinon.spy();

database.insert('name', 'Charles Dickens');
database.save(saveDone);

sinon.assert.calledOnce(stub); //断言writeFIle被调用了
sinon.assert.calledOnce(saveDone); //断言database.save的回调运行了

fs.writeFile.restore();

功能测试

无头测试

基于游览器的测试

Selenium

部署

Heroku

  1. 注册
  2. 安装Heroku CLI
  3. heroku login
1
2
3
4
5
6
7
8
9
10
mkdir heroku-example
npm i -g express-generator
express
npm i
npm start//确保一切正常
git init
git add .
git commit -m 'Initial commit' // 修改重复这两步
heroku create
git push heroku master // 修改重复这两步

docker

  1. 安装Docker
  2. 创建一个Node程序
  3. 在项目中添加文件Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
FROM node:argon

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

COPY package.json /usr/src/app/
RUN npm install

COPY . /usr/src/app

EXPOSE 3000
CMD ["npm", "start"]
  1. docker build .

编写命令行程序