简介
- 所有网站和Web应用程序(除了最简单的)都需要某种持久化方式,即某种比易失性内存更持久的数据存储方式,这样当遇到服务器宕机、断电、升级和迁移等情况时数据才能保存下来。
文件系统持久化
实现持久化的一种方式是将数据存到扁平文件中(“扁平”的意思是文件没有内在结构,只是一串字节)。Node通过
fs
(文件系统)模块实现文件系统持久化。文件系统持久化有一些不足之处,特别是它的扩展性不好。
当需要不止一台服务器以满足流量的需求时,除非所有服务器都能访问一个共享的文件系统,否则就会遇到文件系统持久化的问题。
此外,因为扁平文件没有内在结构,定位、排序和过滤数据就变成了应用程序的负担; 出于这些原因,应该用数据库而不是文件系统来做数据排序。
排序二进制文件是个例外,比如图片、音频文件或视频。尽管很多数据库可以处理这类数据,但极少能达到文件系统那种效率。
如果确实需要存储二进制数据,且主机不能访问共享的文件系统(一般是这样);应该考虑将二进制文件存在数据库中(一般要做些配置,以免数据库被拖垮),或者基于云的存储服务。
云持久化
数据库持久化
- 两种最流行的NoSQL数据库是文档数据库和键-值数据库。
- 文档数据库善于存储对象,这使得它们非常适合Node和JavaScript。
- 键-值数据库如其名所示,极其简单,对于数据模式可以轻松映射到键-值对的程序来说是很好的选择。
关于性能
- 关系型数据库传统上依赖于它们严格的数据结构和几十年的优化研究而取得高性能。
- 另一方面,NoSQL数据库像Node一样,接受了互联网分布式的本性, 专注于用并发来扩展性能(关系型数据库也支持并发,但一般只用于最有需要的应用程序)。
设置MongoDB
设置MongoDB实例的困难之处会随操作系统而变化。为了避开各种问题,可以选择免费的MongoDB托管服务MongoLab。
到个人主页。在数据库下面,点击“新建”,然后进入新建数据库的页面。首先要选的是云提供商。对于免费(沙盒)账号而言,选什么无关紧要,不过应该找一个离你近的数据中心。选择“单节点(开发)”和沙盒。可以选择自己要用的MongoDB版本。最后,选择数据库名称,然后点击“新建MongoDB部署”。
Mongoose
JavaScript的优势之一是它的对象模型极其灵活。如果想给一个对象添加属性或方法,尽管去做,并且不用担心要修改类。
可惜,那种随心所欲的灵活性可能会对数据库产生负面影响,因为它们会变得零碎和难以调优。
Mongoose试图确立平衡,它引入了模式和模型(联合的,模式和模型类似于传统面向对象编程中的类)。模式很灵活,但仍为数据库提供了一些必要的结构。
在开始之前,我们要先把Mongoose模块装上:
npm install --save mongoose
- 然后将数据库凭证添加到credentials.js文件里:
mongo: {
development: {
connectionString: 'your_dev_connection_string',
},
production: {
connectionString: 'your_production_connection_string',
},
},
- 注意,这里存了两组凭证:一个用于开发,一个用于生产。可以现在设置两个数据库,或者将两个指向同一个数据库(等正式启用的时候,可以转换成使用两个单独的数据库)。
使用Mongoose连接数据库
- 先从创建数据库的连接开始:
var mongoose = require('mongoose');
var opts = {
server: {
socketOptions: { keepAlive: 1 }
}
};
switch(app.get('env')){
case 'development':
mongoose.connect(credentials.mongo.development.connectionString, opts);
break;
case 'production':
mongoose.connect(credentials.mongo.production.connectionString, opts);
break;
default:
throw new Error('Unknown execution environment: ' + app.get('env'));
}
-
opts
对象是可选的,但我们想指定keepAlive
选项,以防止长期运行的应用程序(比如网站)出现数据库连接错误。
创建模式和模型
- 接下来为草地鹨旅行社创建一个度假包数据库。先从定义模式和模型开始。创建文件models/vacation.js:
var mongoose = require('mongoose');
var vacationSchema = mongoose.Schema({
name: String,
slug: String,
category: String,
sku: String,
description: String,
priceInCents: Number,
tags: [String],
inSeason: Boolean,
available: Boolean,
requiresWaiver: Boolean,
maximumGuests: Number,
notes: String,
packagesSold: Number,
});
vacationSchema.methods.getDisplayPrice = function(){
return '$' + (this.priceInCents / 100).toFixed(2);
};
var Vacation = mongoose.model('Vacation', vacationSchema);
module.exports = Vacation;
由于浮点数的特质,在JavaScript中涉及金融计算时要谨慎。以美分为单位存储价格有帮助,但并不能根除这个问题。在下一版的JavaScript (ES6)中会有个适合做金融计算的decimal类型。
输出了Mongoose创建的Vacation模型对象。要在程序中使用这个模型,我们可以像下面这样引入它:
var Vacation = require('./models/vacation.js');
添加初始数据
Vacation.find(function(err, vacations){
if(vacations.length) return;
new Vacation({
name: 'Hood River Day Trip',
slug: 'hood-river-day-trip',
category: 'Day Trip',
sku: 'HR199',
description: 'Spend a day sailing on the Columbia and ' +
'enjoying craft beers in Hood River!',
priceInCents: 9995,
tags: ['day trip', 'hood river', 'sailing', 'windsurfing', 'breweries'],
inSeason: true,
maximumGuests: 16,
available: true,
packagesSold: 0,
}).save();
new Vacation({
name: 'Oregon Coast Getaway',
slug: 'oregon-coast-getaway',
category: 'Weekend Getaway',
sku: 'OC39',
description: 'Enjoy the ocean air and quaint coastal towns!',
priceInCents: 269995,
tags: ['weekend getaway', 'oregon coast', 'beachcombing'],
inSeason: false,
maximumGuests: 8,
available: true,
packagesSold: 0,
}).save();
new Vacation({
name: 'Rock Climbing in Bend',
slug: 'rock-climbing-in-bend',
category: 'Adventure',
sku: 'B99',
description: 'Experience the thrill of climbing in the high desert.',
priceInCents: 289995,
tags: ['weekend getaway', 'bend', 'high desert', 'rock climbing'],
inSeason: true,
requiresWaiver: true,
maximumGuests: 4,
available: false,
packagesSold: 0,
notes: 'The tour guide is currently recovering from a skiing accident.',
}).save();
});
- 这里用到了两个Mongoose方法。
find
和save
获取数据
- 给产品页创建个视图,views/vacations.handlebars:
<h1>Vacations</h1>
{{#each vacations}}
<div class="vacation">
<h3>{{name}}</h3>
<p>{{description}}</p>
{{#if inSeason}}
<span class="price">{{price}}</span>
<a href="/cart/add?sku={{sku}}" class="btn btn-default">Buy Now!</a>
{{else}}
<span class="outOfSeason">We're sorry, this vacation is currently not in season.
{{! The "notify me when this vacation is in season"
page will be our next task. }}
<a href="/notify-me-when-in-season?sku={{sku}}">Notify me when this vacation is in season.</a>
{{/if}}
</div>
{{/each}}
- 创建路由处理器把它全串起来:
app.get('/vacations', function(req, res){
Vacation.find({ available: true }, function(err, vacations){
var context = {
vacations: vacations.map(function(vacation){
return {
sku: vacation.sku,
name: vacation.name,
description: vacation.description,
price: vacation.getDisplayPrice(),
inSeason: vacation.inSeason,
}
})
};
res.render('vacations', context);
});
});
- 不要将未映射的数据库对象直接传给视图。视图会得到一堆它可能不需要的属性,并且可能是以它不能兼容的格式。
添加数据
- 首先要创建模式和模型(models/vacationInSeasonListener.js):
var mongoose = require('mongoose');
var vacationInSeasonListenerSchema = mongoose.Schema({
email: String,
skus: [String],
});
var VacationInSeasonListener = mongoose.model('VacationInSeasonListener',
vacationInSeasonListenerSchema);
module.exports = VacationInSeasonListener;
- 然后创建视图,views/notify-me-when-in-season.handlebars:
<div class="formContainer">
<form class="form-horizontal newsletterForm" role="form"
action="/notify-me-when-in-season" method="POST">
<input type="hidden" name="sku" value="{{sku}}">
<div class="form-group">
<label for="fieldEmail" class="col-sm-2 control-label">Email</label>
<div class="col-sm-4">
<input type="email" class="form-control" required id="fieldName" name="email">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-4">
<button type="submit" class="btn btn-default">Submit</button>
</div>
</div>
</form>
</div>
- 最后是路由处理器:
var VacationInSeasonListener = require('./models/vacationInSeasonListener.js');
app.get('/notify-me-when-in-season', function(req, res){
res.render('notify-me-when-in-season', { sku: req.query.sku });
});
app.post('/notify-me-when-in-season', function(req, res){
VacationInSeasonListener.update(
{ email: req.body.email },
{ $push: { skus: req.body.sku } },
{ upsert: true },
function(err){
if(err) {
console.error(err.stack);
req.session.flash = {
type: 'danger',
intro: 'Ooops!',
message: 'There was an error processing your request.',
};
return res.redirect(303, '/vacations');
}
req.session.flash = {
type: 'success',
intro: 'Thank you!',
message: 'You will be notified when this vacation is in season.',
};
return res.redirect(303, '/vacations');
}
);
});
- Mongoose方便的upsert(“更新”和“插入”的混成词), 我们能在VacationInSeasonListener还不存在的时候更新其中的记录。
用MongoDB存储会话数据
- 用session-mongoose包提供MongoDB会话存储。
npm install --save session-mongoose
:
var MongoSessionStore = require('session-mongoose')(require('connect'));
var sessionStore = new MongoSessionStore({ url: credentials.mongo.connectionString });
app.use(require('cookie-parser')(credentials.cookieSecret));
app.use(require('express-session')({ store: sessionStore }));
- 想要用不同的币种显示度假产品的价格。此外,我们还希望网站记住用户偏好的币种; 先要在度假产品页面底部添加一个币种选择器:
<hr>
<p>Currency:
<a href="/set-currency/USD" class="currency {{currencyUSD}}">USD</a> |
<a href="/set-currency/GBP" class="currency {{currencyGBP}}">GBP</a> |
<a href="/set-currency/BTC" class="currency {{currencyBTC}}">BTC</a>
</p>
然后是一点CSS:
最后我们会添加路由处理器来设定币种,并修改/vacations的路由处理器来用当前币种显示价格
app.get('/set-currency/:currency', function(req,res){
req.session.currency = req.params.currency;
return res.redirect(303, '/vacations');
});
function convertFromUSD(value, currency){
switch(currency){
case 'USD': return value * 1;
case 'GBP': return value * 0.6;
case 'BTC': return value * 0.0023707918444761;
default: return NaN;
}
}
app.get('/vacations', function(req, res){
Vacation.find({ available: true }, function(err, vacations){
var currency = req.session.currency || 'USD';
var context = {
currency: currency,
vacations: vacations.map(function(vacation){
return {
sku: vacation.sku,
name: vacation.name,
description: vacation.description,
inSeason: vacation.inSeason,
price: convertFromUSD(vacation.priceInCents/100, currency),
qty: vacation.qty,
}
})
};
switch(currency){
case 'USD': context.currencyUSD = 'selected'; break;
case 'GBP': context.currencyGBP = 'selected'; break;
case 'BTC': context.currencyBTC = 'selected'; break;
}
res.render('vacations', context);
});
});
- MongoDB不一定是会话存储的最佳选择,它有点杀鸡用牛刀的意味。另外一个流行又易用的会话持久化方案是用Redis。请参阅connect-redis包来了解如何设置使用Redis做会话存储。