创建定制的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
连接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完成的
collection.drop()—移除整个数据集;
collection.update(query)—更新跟查询匹配的文档;
collection.count(query)—对跟查询匹配的文档计数。
为满足操作一个或多个文档的需求, find insert和 delete等操作有几种变体。比如:
collection. insertOne(doc)—插入单个文档;
collection.insertMany([doc,doc2])—插入多个文档;
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
- 注册
- 安装Heroku CLI
- 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
- 安装Docker
- 创建一个Node程序
- 在项目中添加文件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"]
|
docker build .
编写命令行程序