1.首先用webstorm7.0.2创建一个Node.js的工程,注意Template engine类型选择EJS。
2.创建完工程之后,开始安装MongoDB数据库。
(1)下载MongoDB 下载地址http://www.mongodb.org/,根据自己机子的系统,选择相应的版本。
(2)设置MongoDB目录 比如,解压到F盘下面的MongDB文件夹,路径为F:\MongDB。这个目录路径是任意的。
(3)设置数据文件路径 在F:\MongoDB里面新建logs和data文件夹。
(4)配置MongoDB服务端
打开cmd窗口:输入下面命令:
> f:
> cd F:\MongoDB\bin
> mongod --dbpath "F:\MongoDB\data" --logpath "F:\MongoDB\logs\mongodb.log" --install
F:\mongodb\bin\mongo
MongoDB shell version: 1.8.1
connecting to: test
>
这个表示成功,如果有错误,会有提示信息。
如果有问题,可以查看日志 F:\MongoDB\logs下面的mongodb.log
这样配置之后,在管理工具->服务里就会多一个Mongo DB服务,这个是自动开始的。以后每次开机,MongoDB服务都是开启的,不用再配置了。
3. 建立微博网站
(1)功能分析
微博应该以用户为中心,因此需要有用户的注册和登录功能,微博网站最核心的功能是信息的发表,这个功能设计许多方面,包括数据库访问、前端显示。该微博不够完整,只是出于演示目的,信息的评论,转发,圈点用户等功能都没有。如果大家感兴趣,可以自己加。
(2)路由规划
路由规划,或者说控制器规划是整个网站的骨架部分,因为它处于整个架构的枢纽位置,相当于各个接口之间的粘合剂,所以应该优先考虑。
根据功能设计,我们把路由按照以下方案规划。
/:首页
/u/[user]:用户的主页
/post:发表信息
/reg:用户注册
/login:用户登录
/logout:用户登出
以上页面还可以根据用户状态细分。发表信息以及用户登出页面必须是已登录用户才能操作的功能,而用户注册和用户登入所面向的对象必须是未登入的用户。首页和用户主页则针对已登入和未登入的用户显示不同的内容。
其中 /post、/login 和 /reg 由于要接受表单信息,因此使用 app.post 注册路由。/login和 /reg 还要显示用户注册时要填写的表单,所以要以 app.get 注册。
具体app.js代码如下:
/**
* Module dependencies.
*/
var express = require('express');
var http = require('http');
var routes = require('./routes');
var settings = require('./settings');
var MongoStore = require('connect-mongo')(express);
var partials = require('express-partials');
var flash = require('connect-flash');
var sessionStore = new MongoStore({
db : settings.db
}, function() {
console.log('connect mongodb success...');
});
var app = express();
app.configure(function(){
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(partials());
app.use(flash());
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(express.cookieParser());
app.use(express.session({
secret : settings.cookie_secret,
cookie : {
maxAge : 60000 * 20 //20 minutes
},
store : sessionStore
}));
app.use(app.router);
app.use(express.static(__dirname + '/public'));
});
app.configure('development', function(){
app.use(express.errorHandler());
});
app.get('/', routes.index);
app.get('/u/:user', routes.user);
app.post('/post', routes.post);
app.get('/reg', routes.reg);
app.post('/reg', routes.doReg);
app.get('/login', routes.login);
app.post('/login', routes.doLogin);
app.get('/logout', routes.logout);
http.createServer(app).listen(app.get('port'), function(){
console.log('Express server listening on port ' + app.get('port'));
});
connect-mongo、connect-flash、express-partials,mongodb这四个模块需要安装,不然运行app.js的时候 ,会报找不到模块。在Webostorm安装模块,选择File -> Settings,会弹出Settings对话框。
在Settings对话框里的JavaScript的Node.js and NPM, 点击 Install按钮,又会弹出一个Available Packages对话框,在放大镜那块输入你要安装的模块名称,找到后选中你要安装的模块,点击Install Package,就开始安装模块。当然,你也可以通过node.js的命令安装。
(3)界面设计
我们在开发网站的时候必须时刻意识到网站是为用户开发的,因而用户界面是非常重要的。现在我们就用 Bootstrap 开始设计我们的界面。从http://twitter.github.com/bootstrap下载bootstrap.zip。将 img 目录复制到工程 public 目录下,将 bootstrap.css、bootstrap-responsive.css 复制到 public/stylesheets 中,将 bootstrap.js 复制到 public/javascripts 目录中,然后从http://jquery.com/下载一份最新版的 jquery.js 也放入 public/javascripts 目录中。
接下来,修改 views/layout.ejs:
<!DOCTYPE html>
<html>
<head>
<title><%= title %> - Microblog</title>
<link rel='stylesheet' href='/stylesheets/bootstrap.css' />
<style type="text/css">
body {
padding-top: 60px;
padding-bottom: 40px;
}
</style>
<link href="/stylesheets/bootstrap-responsive.css" rel="stylesheet">
</head>
<body>
<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</a>
<a class="brand" href="/">Microblog</a>
<div class="nav-collapse">
<ul class="nav">
<li class="active"><a href="/">首页</a></li>
<% if (!user) { %>
<li><a href="/login">登录</a></li>
<li><a href="/reg">注册</a></li>
<% } else { %>
<li><a href='javascript:;'>欢迎您,<%= user.name%></a></li>
<li><a href="/logout">退出</a></li>
<% } %>
</ul>
</div>
</div>
</div>
</div>
<div id="container" class="container">
<% if (success) { %>
<div class="alert alert-success">
<%= success %>
</div>
<% } %>
<% if (error) { %>
<div class="alert alert-error">
<%= error %>
</div>
<% } %>
<%- body %>
<hr />
<footer>
<p><a href="javascript:;">Meteoric</a> 2012</p>
</footer>
</div>
</body>
<script src="/javascripts/jquery.js"></script>
<script src="/javascripts/bootstrap.js"></script>
</html>
上面代码是使用 Bootstrap部件实现的一个简单页面框架,整个页面分为顶部工具栏、正文和页脚三部分,其中正文和页脚包含在名为 container 的 div 标签中。
最后我们设计首页,修改 views/index.ejs:
<% if (!user) { %>
<div class="hero-unit">
<h1>欢迎来到 Microblog</h1>
<p>Microblog 是一个基于 Node.js 的微博系統。</p>
<p>
<a class="btn btn-primary btn-large" href="/login">登录</a>
<a class="btn btn-large" href="/reg">立即注册</a>
</p>
</div>
<% } else { %>
<%- partial('say') %>
<% } %>
<%- partial('posts', {posts:posts}) %>
4.用户注册和登录
要实现用户会话的功能,包括用户注册和登录状态的维护。为了实现这些功能,我们需要引入会话机制来记录用户状态,还要访问数据库来保存和读取用户信息。现在就让我们从数据库开始。
打开工程目录中的 package.json,修改如下:
接下来在工程的目录中创建 settings.js 文件,这个文件用于保存数据库的连接信息。我们将用到的数据库命名为 microblog,数据库服务器在本地,因此Settings.js文件的内容如下:
module.exports = {
cookie_secret : 'secret_meteoric',
db: 'microblog',
host: 'localhost',
};
接下来在 models 子目录中创建 db.js,内容是:
var settings = require('../settings');
var Db = require('mongodb').Db;
var Connection = require('mongodb').Connection;
var Server = require('mongodb').Server;
module.exports = new Db(settings.db, new Server(settings.host, Connection.DEFAULT_PORT, {}));
(1)会话支持
在完成用户注册和登录功能之前,我们需要先了解会话的概念。会话是一种持久的网络协议,用于完成服务器和客户端之间的一些交互行为。会话是一个比连接粒度更大的概念,一次会话可能包含多次连接,每次连接都被认为是会话的一次操作。在网络应用开发中,有必要实现会话以帮助用户交互。例如网上购物的场景,用户浏览了多个页面,购买了一些物品,这些请求在多次连接中完成。许多应用层网络协议都是由会话支持的,如 FTP、Telnet 等,而 HTTP 协议是无状态的,本身不支持会话,因此在没有额外手段的帮助下,前面场景中服务器不知道用户购买了什么。
Express 也提供了会话中间件,默认情况下是把用户信息存储在内存中,但我们既然已经有了 MongoDB,不妨把会话信息存储在数据库中,便于持久维护。为了使用这一功
能,我们首先要获得一个叫做 connect-mongo 的模块,在 package.json 中添加一行代码:
{
"name": "microblog",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node app.js"
},
"dependencies": {
"express": "3.4.6",
"ejs": "*",
"connect-mongo": ">= 0.1.7",
"mongodb": ">= 0.9.9"
}
}
app.js的一段代码如下:
app.configure(function(){
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(partials());
app.use(flash());
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(express.cookieParser());
app.use(express.session({
secret : settings.cookie_secret,
cookie : {
maxAge : 60000 * 20 //20 minutes
},
store : sessionStore
}));
app.use(app.router);
app.use(express.static(__dirname + '/public'));
});
其中 express.cookieParser() 是 Cookie 解析的中间件。express.session() 则提供会话支持,设置它的 store 参数为 MongoStore 实例,把会话信息存储到数据库中,
以避免丢失。可以通过 req.session 获取当前用户的会话对象,以维护用户相关的信息。
5. 注册和登入
我们已经准备好了数据库访问和会话存储的相关信息,接下来开始实现网站的第一个功能,用户注册和登入。
(1)注册页面
首先来设计用户注册页面的表单,创建 views/reg.ejs 文件,内容是:
<form class="form-horizontal" method="post">
<fieldset>
<legend>用户注册</legend>
<div class="control-group">
<label class="control-label" for="username">用户名</label>
<div class="controls">
<input type="text" class="input-xlarge" id="username" name="username">
<p class="help-block">你的帐号的名称,用于登录和显示</p>
</div>
</div>
<div class="control-group">
<label class="control-label" for="password">密码</label>
<div class="controls">
<input type="password" class="input-xlarge" id="password" name="password">
</div>
</div>
<div class="control-group">
<label class="control-label" for="password-repeat">重复密码</label>
<div class="controls">
<input type="password" class="input-xlarge" id="password-repeat" name="password-repeat">
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">注册</button>
</div>
</fieldset>
</form>
这个表单中有3个输入单元,分别是 username、password 和 password-repeat。表单的请求方法是 POST,将会发送到相同的路径下。
(2)注册响应
现在实现 POST 请求发送后的功能,在 routes/index.js 中添加 /reg 的 POST 响应函数:
exports.post = function(req, res) {
var currentUser = req.session.user;
var post = new Post(currentUser.name, req.body.post);
post.save(function(err) {
if (err) {
req.flash('error', err);
return res.redirect('/');
}
req.flash('success', '发表成功');
res.redirect('/u/' + currentUser.name);
});
};
exports.reg = function(req, res) {
res.render('reg', {
title: '用户注册',
user : req.session.user,
success : req.flash('success').toString(),
error : req.flash('error').toString()
});
};
exports.doReg = function(req, res) {
//检查密码
if (req.body['password-repeat'] != req.body['password']) {
req.flash('error', '两次输入的密码不一致');
return res.redirect('/reg');
}
//生成md5的密码
var md5 = crypto.createHash('md5');
var password = md5.update(req.body.password).digest('base64');
var newUser = new User({
name: req.body.username,
password: password,
});
//检查用户名是否已经存在
User.get(newUser.name, function(err, user) {
if (user)
err = 'Username already exists.';
if (err) {
req.flash('error', err);
return res.redirect('/reg');
}
//如果不存在則新增用戶
newUser.save(function(err) {
if (err) {
req.flash('error', err);
return res.redirect('/reg');
}
req.session.user = newUser;
req.flash('success', '注册成功');
res.redirect('/');
});
});
};
这段代码用到了一些新的东西,我们一一说明。
req.body 就是 POST 请求信息解析过后的对象,例如我们要访问用户传递的password 域的值,只需访问 req.body['password'] 即可。
req.flash 是 Express 提供的一个奇妙的工具,通过它保存的变量只会在用户当前和下一次的请求中被访问,之后会被清除,通过它我们可以很方便地实现页面的通知
和错误信息显示功能。
res.redirect 是重定向功能,通过它会向用户返回一个 303 See Other 状态,通知浏览器转向相应页面。
crypto 是 Node.js 的一个核心模块,功能是加密并生成各种散列,使用它之前首先要声明 var crypto = require('crypto')。我们代码中使用它计算了密码的散列值。
User 是我们设计的用户对象,在后面我们会详细介绍,这里先假设它的接口都是可用的,使用前需要通过 var User = require('../models/user.js') 引用。
User.get 的功能是通过用户名获取已知用户,在这里我们判断用户名是否已经存在。User.save 可以将用户对象的修改写入数据库。
通过 req.session.user = newUser 向会话对象写入了当前用户的信息,在后面我们会通过它判断用户是否已经登录。
(3)用户模型
在前面的代码中,我们直接使用了 User 对象。User 是一个描述数据的对象,即 MVC架构中的模型。前面我们使用了许多视图和控制器,这是第一次接触到模型。与视图和控制器不同,模型是真正与数据打交道的工具,没有模型,网站就只是一个外壳,不能发挥真实的作用,因此它是框架中最根本的部分。现在就让我们来实现 User 模型吧。
在 models 目录中创建 user.js 的文件,内容如下:
var mongodb = require('./db');
function User(user) {
this.name = user.name;
this.password = user.password;
};
module.exports = User;
User.prototype.save = function save(callback) {
// 存入 Mongodb 的文檔
var user = {
name: this.name,
password: this.password,
};
mongodb.open(function(err, db) {
if (err) {
return callback(err);
}
// 讀取 users 集合
db.collection('users', function(err, collection) {
if (err) {
mongodb.close();
return callback(err);
}
// 爲 name 屬性添加索引
collection.ensureIndex('name', {unique: true});
// 寫入 user 文檔
collection.insert(user, {safe: true}, function(err, user) {
mongodb.close();
callback(err, user);
});
});
});
};
User.get = function get(username, callback) {
mongodb.open(function(err, db) {
if (err) {
return callback(err);
}
// 讀取 users 集合
db.collection('users', function(err, collection) {
if (err) {
mongodb.close();
return callback(err);
}
// 查找 name 屬性爲 username 的文檔
collection.findOne({name: username}, function(err, doc) {
mongodb.close();
if (doc) {
// 封裝文檔爲 User 對象
var user = new User(doc);
callback(err, user);
} else {
callback(err, null);
}
});
});
});
};
以上代码实现了两个接口,User.prototype.save 和 User.get,前者是对象实例的方法,用于将用户对象的数据保存到数据库中,后者是对象构造函数的方法,用于从数据
库中查找指定的用户。
(4)登入和登出
登入和登出仅仅是 req.session.user 变量的标记,非常简单。把下面的代码加到 routes/index.js 中:
exports.login = function(req, res) {
res.render('login', {
title: '用户登录',
user : req.session.user,
success : req.flash('success').toString(),
error : req.flash('error').toString()
});
};
exports.doLogin = function(req, res) {
//生成口令的散列值
var md5 = crypto.createHash('md5');
var password = md5.update(req.body.password).digest('base64');
User.get(req.body.username, function(err, user) {
if (!user) {
req.flash('error', '用户不存在');
return res.redirect('/login');
}
if (user.password != password) {
req.flash('error', '密码错误');
return res.redirect('/login');
}
req.session.user = user;
req.flash('success', '登录成功');
res.redirect('/');
});
};
exports.logout = function(req, res) {
req.session.user = null;
req.flash('success', '登出成功');
res.redirect('/');
};
但这会不会有安全性问题呢?不会的,因为这个变量只有服务端才能访问到,只要不是黑客攻破了整个服务器,无法从外部改动。
最后我们创建 views/login.ejs,内容如下:
<form class="form-horizontal" method="post">
<fieldset>
<legend>用户登录</legend>
<div class="control-group">
<label class="control-label" for="username">用户名</label>
<div class="controls">
<input type="text" class="input-xlarge" id="username" name="username">
</div>
</div>
<div class="control-group">
<label class="control-label" for="password">密码</label>
<div class="controls">
<input type="password" class="input-xlarge" id="password" name="password">
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">登录</button>
</div>
</fieldset>
</form>
6.发表微博
现在网站已经具备了用户注册、登入、页面权限控制的功能,这些功能为网站最核心的部分——发表微博做好了准备。
(1)微博模型
现在让我们从模型开始设计。仿照用户模型,将微博模型命名为 Post 对象,它拥有与User 相似的接口,分别是 Post.get 和 Post.prototype.save。Post.get 的功能是从
数据库中获取微博,可以按指定用户获取,也可以获取全部的内容。Post.prototype.save是 Post 对象实例的方法,用于将对象的变动保存到数据库。
创建 models/post.js,写入以下内容:
var mongodb = require('./db');
function Post(username, post, time) {
this.user = username;
this.post = post;
if (time) {
this.time = time;
} else {
this.time = new Date();
}
};
module.exports = Post;
Post.prototype.save = function save(callback) {
// 存入 Mongodb 的文檔
var post = {
user: this.user,
post: this.post,
time: this.time,
};
mongodb.open(function(err, db) {
if (err) {
return callback(err);
}
// 讀取 posts 集合
db.collection('posts', function(err, collection) {
if (err) {
mongodb.close();
return callback(err);
}
// 爲 user 屬性添加索引
collection.ensureIndex('user');
// 寫入 post 文檔
collection.insert(post, {safe: true}, function(err, post) {
mongodb.close();
callback(err, post);
});
});
});
};
Post.get = function get(username, callback) {
mongodb.open(function(err, db) {
if (err) {
return callback(err);
}
// 讀取 posts 集合
db.collection('posts', function(err, collection) {
if (err) {
mongodb.close();
return callback(err);
}
// 查找 user 屬性爲 username 的文檔,如果 username 是 null 則匹配全部
var query = {};
if (username) {
query.user = username;
}
collection.find(query).sort({time: -1}).toArray(function(err, docs) {
mongodb.close();
if (err) {
callback(err, null);
}
// 封裝 posts 爲 Post 對象
var posts = [];
docs.forEach(function(doc, index) {
var post = new Post(doc.user, doc.post, doc.time);
posts.push(post);
});
callback(null, posts);
});
});
});
};
var mongodb = require('./db');
function Post(username, post, time) {
this.user = username;
this.post = post;
if (time) {
this.time = time;
} else {
this.time = new Date();
}
};
module.exports = Post;
Post.prototype.save = function save(callback) {
// 存入 Mongodb 的文檔
var post = {
user: this.user,
post: this.post,
time: this.time,
};
mongodb.open(function(err, db) {
if (err) {
return callback(err);
}
// 讀取 posts 集合
db.collection('posts', function(err, collection) {
if (err) {
mongodb.close();
return callback(err);
}
// 爲 user 屬性添加索引
collection.ensureIndex('user');
// 寫入 post 文檔
collection.insert(post, {safe: true}, function(err, post) {
mongodb.close();
callback(err, post);
});
});
});
};
Post.get = function get(username, callback) {
mongodb.open(function(err, db) {
if (err) {
return callback(err);
}
// 讀取 posts 集合
db.collection('posts', function(err, collection) {
if (err) {
mongodb.close();
return callback(err);
}
// 查找 user 屬性爲 username 的文檔,如果 username 是 null 則匹配全部
var query = {};
if (username) {
query.user = username;
}
collection.find(query).sort({time: -1}).toArray(function(err, docs) {
mongodb.close();
if (err) {
callback(err, null);
}
// 封裝 posts 爲 Post 對象
var posts = [];
docs.forEach(function(doc, index) {
var post = new Post(doc.user, doc.post, doc.time);
posts.push(post);
});
callback(null, posts);
});
});
});
};
约定通过 POST 方法访问 /post 以发表微博,现在让我们来实现这个控制器。在 routes/index.js 中添加下面的代码:
exports.post = function(req, res) {
var currentUser = req.session.user;
var post = new Post(currentUser.name, req.body.post);
post.save(function(err) {
if (err) {
req.flash('error', err);
return res.redirect('/');
}
req.flash('success', '发表成功');
res.redirect('/u/' + currentUser.name);
});
};
这段代码通过 req.session.user 获取当前用户信息,从 req.body.post 获取用户发表的内容,建立 Post 对象,调用 save() 方法存储信息,最后将用户重定向到用户页面。
(2)用户页面
用户页面的功能是展示用户发表的所有内容,在routes/index.js中加入以下代码:
exports.user = function(req, res) {
User.get(req.params.user, function(err, user) {
if (!user) {
req.flash('error', '用户不存在');
return res.redirect('/');
}
Post.get(user.name, function(err, posts) {
if (err) {
req.flash('error', err);
return res.redirect('/');
}
res.render('user', {
title: user.name,
posts: posts,
user : req.session.user,
success : req.flash('success').toString(),
error : req.flash('error').toString()
});
});
});
};
它的功能是首先检查用户是否存在,如果存在则从数据库中获取该用户的微博,最后通过 posts 属性传递给 user 视图。views/user.ejs 的内容如下:
<% if (user) { %>
<%- partial('say') %>
<% } %>
<%- partial('posts', {posts:posts}) %>
根据 DRY 原则,我们把重复用到的部分都提取出来,分别放入 say.ejs 和 posts.ejs。say.ejs的功能是显示一个发表微博的表单,它的内容如下:
<form method="post" action="/post" class="well form-inline center" style="text-align:center;">
<input type="text" class="span8" name="post">
<button type="submit" class="btn btn-success"><i class="icon-comment icon-white"></i> 发言</button>
</form>
posts.ejs 的目的是按照行列显示传入的 posts 的所有内容:
<% posts.forEach(function(post, index) {
if (index % 3 == 0) { %>
<div class="row">
<%} %>
<div class="span4">
<h2><a href="/u/<%= post.user %>"><%= post.user %></a> 说</h2>
<p><small><%= post.time %></small></p>
<p><%= post.post %></p>
</div>
<% if (index % 3 == 2) { %>
</div><!-- end row -->
<% } %>
<%}) %>
<% if (posts.length % 3 != 0) { %>
</div><!-- end row -->
<% } %>
(3)首页
最后一步是实现首页的内容。我们计划在首页显示所有用户发表的微博,按时间从新到旧的顺序。
在 routes/index.js 中添加下面代码:
exports.index = function(req, res){
Post.get(null, function(err, posts) {
if (err) {
posts = [];
}
res.render('index', {
title: '首页',
posts : posts,
user : req.session.user,
success : req.flash('success').toString(),
error : req.flash('error').toString()
});
});
};
它的功能是读取所有用户的微博,传递给页面 posts 属性。接下来修改首页的模板index.ejs:
<% if (!user) { %>
<div class="hero-unit">
<h1>欢迎来到 Microblog</h1>
<p>Microblog 是一个基于 Node.js 的微博系統。</p>
<p>
<a class="btn btn-primary btn-large" href="/login">登录</a>
<a class="btn btn-large" href="/reg">立即注册</a>
</p>
</div>
<% } else { %>
<%- partial('say') %>
<% } %>
<%- partial('posts', {posts:posts}) %>
整个代码编写完成之后,先运行app.js,然后在浏览器(支持html5)输入:http://localhost:3000/
就会打开下面的效果。
具体的代码,见参考链接:http://download.csdn.net/detail/noted2011/6938761
需要说明的是,由于《Node.js开发指南》里面的用的是Express2.x,而现在新的Express用的是3.X,有些接口函数和用法有所改变,不一样。
参考文档:
1. 《Node.js开发指南》
2. http://www.cnblogs.com/qq4004229/archive/2011/11/11/2245599.html
3. http://www.cnblogs.com/meteoric_cry/archive/2012/07/27/2604890.html