暂无图片
暂无图片
1
暂无图片
暂无图片
暂无图片

Express与MongoDB 搭建多人博客

yBmZlQzJ 2024-01-10
137

Table of Contents

前言

第 1 章 第1章 一个简单的博客

第 2 章 第2章 使用 Markdown

第 3 章 第3章 增加文件上传功能

第 4 章 第4章 实现用户页面和文章页面

第 5 章 第5章 增加编辑与删除功能

第 6 章 第6章 实现留言功能

第 7 章 第7章 实现分页功能

第 8 章 第8章 增加存档页面

第 9 章 第9章 增加标签和标签页面

第 10 章 第10章 增加pv统计和留言统计

第 11 章 第11章 增加文章检索功能

第 12 章 第12章 增加友情链接

第 13 章 第13章 增加404页面

第 14 章 第14章 增加头像

第 15 章 第15章 增加转载功能和转载统计

第 16 章 第16章 增加日志功能

第 17 章 番外篇之——使用 _id 查询

第 18 章 番外篇之——使用 Async

第 19 章 番外篇之——使用 Disqus

第 20 章 番外篇之——使用 generic pool

第 21 章 番外篇之——使用 Handlebars

第 22 章 番外篇之——使用 KindEditor

第 23 章 番外篇之——使用 Mongoose

第 24 章 番外篇之——使用 Passport

第 25 章 番外篇之——部署到 Heroku

前言

express 是 Node.js 上最流行的 Web 开发框架,正如他的名字一样,使用它我们可以快速的开发一个 Web 应用。

致谢
内容撰写:https://github.com/island205/HelloSea.js/

更新日期

更新内容

2015-05-29

Express与MongoDB 搭建多人博客

1

第1章 一个简单的博客

学习环境

Node.js : 0.10.32

Express : 4.10.2

MongoDB : 2.6.1

快速开始

安装 Express

express 是 Node.js 上最流行的 Web 开发框架,正如他的名字一样,使用它我们可以快速的开发一个 Web 应用。我们用 express 来搭建我们的博客,打开命令行,输入:

$ npm install -g express-generator

安装 express 命令行工具,使用它我们可以初始化一个 express 项目。

新建一个工程

在命令行中输入:

$ express -e blog
$ cd blog && npm install

初始化一个 express 项目并安装所需模块,如下图所示:

1.1.jpg

然后运行:

$ DEBUG=blog node ./bin/www

启动项目,此时命令行中会显示 blog Express server listening on port 3000 +0ms,在浏览器里访问 localhost:3000,如下图所示:

1.2.jpg

至此,我们用 express 初始化了一个工程项目,并指定使用 ejs 模板引擎,下一节我们讲解工程的内部结构。

工程结构

我们回头看看生成的工程目录里面都有什么,打开我们的 blog 文件夹,里面如图所示:

1.3.jpg

app.js:启动文件,或者说入口文件 package.json:存储着工程的信息及模块依赖,当在 dependencies 中添加依赖的模块时,运行 npm install,npm 会检查当前目录下的 package.json,并自动安装所有指定的模块 node_modules:存放 package.json 中安装的模块,当你在 package.json 添加依赖的模块并安装后,存放在这个文件夹下 public:存放 image、css、js 等文件 routes:存放路由文件 views:存放视图文件或者说模版文件 bin:存放可执行文件

打开app.js,让我们看看里面究竟有什么:

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
 
var routes = require('./routes/index');
var users = require('./routes/users');
 
var app = express();
 
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
 
// uncomment after placing your favicon in /public
//app.use(favicon(__dirname + '/public/favicon.ico'));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
 
app.use('/', routes);
app.use('/users', users);
 
// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
 
// error handlers
 
// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: err
});
});
}
 
// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: {}
});
});
 
 
module.exports = app;

这里我们通过require()加载了express、path 等模块,以及 routes 文件夹下的index. js和 users.js 路由文件。 下面来讲解每行代码的含义。

(1) var app = express():生成一个express实例 app。 (2)app.set('views', path.join(__dirname, 'views’)):设置 views 文件夹为存放视图文件的目录, 即存放模板文件的地方,__dirname 为全局变量,存储当前正在执行的脚本所在的目录。 (3)app.set('view engine', 'ejs’):设置视图模板引擎为 ejs。 (4)app.use(favicon(__dirname + '/public/favicon.ico’)):设置/public/favicon.ico为favicon图标。 (5)app.use(logger('dev’)):加载日志中间件。 (6)app.use(bodyParser.json()):加载解析json的中间件。 (7)app.use(bodyParser.urlencoded({ extended: false })):加载解析urlencoded请求体的中间件。 (8)app.use(cookieParser()):加载解析cookie的中间件。 (9)app.use(express.static(path.join(__dirname, 'public'))):设置public文件夹为存放静态文件的目录。 (10)app.use('/', routes);和app.use('/users', users):路由控制器。 (11)

app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});

捕获404错误,并转发到错误处理器。 (12)

if (app.get('env') === 'development') {
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: err
});
});
}

开发环境下的错误处理器,将错误信息渲染error模版并显示到浏览器中。 (13)

app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: {}
});
});

生产环境下的错误处理器,将错误信息渲染error模版并显示到浏览器中。 (14)module.exports = app :导出app实例供其他模块调用。

我们再看 bin/www 文件:

#!/usr/bin/env node
var debug = require('debug')('blog');
var app = require('../app');
 
app.set('port', process.env.PORT || 3000);
 
var server = app.listen(app.get('port'), function() {
debug('Express server listening on port ' + server.address().port);
});

(1)#!/usr/bin/env node:表明是 node 可执行文件。 (2)var debug = require('debug')('blog’):引入debug模块,打印调试日志。 (3)var app = require('../app’):引入我们上面导出的app实例。 (4)app.set('port', process.env.PORT || 3000):设置端口号。 (5)

var server = app.listen(app.get('port'), function() {
debug('Express server listening on port ' + server.address().port);
});

启动工程并监听3000端口,成功后打印 Express server listening on port 3000。

我们再看 routes/index.js 文件:

var express = require('express');
var router = express.Router();
 
/* GET home page. */
router.get('/', function(req, res) {
res.render('index', { title: 'Express' });
});
 
module.exports = router;

生成一个路由实例用来捕获访问主页的GET请求,导出这个路由并在app.js中通过app.use('/', routes); 加载。这样,当访问主页时,就会调用res.render('index', { title: 'Express' });渲染views/index.ejs模版并显示到浏览器中。

我们再看看 views/index.ejs 文件:

<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<h1><%= title %></h1>
<p>Welcome to <%= title %></p>
</body>
</html>

在渲染模板时我们传入了一个变量 title 值为 express 字符串,模板引擎会将所有 <%= title %> 替换为 express ,然后将渲染后生成的html显示到浏览器中,如上图所示。

在这一小节我们学习了如何创建一个工程并启动它,了解了工程的大体结构和运作流程,下一小节我们将学习 express 的基本使用及路由控制。

路由控制

工作原理

routes/index.js 中有以下代码:

router.get('/', function(req, res){
res.render('index', { title: 'Express' });
});

这段代码的意思是当访问主页时,调用 ejs 模板引擎,来渲染 index.ejs 模版文件(即将 title 变量全部替换为字符串 Express),生成静态页面并显示在浏览器中。

我们来作一些修改,以上代码实现了路由的功能,我们当然可以不要 routes/index.js 文件,把实现路由功能的代码都放在 app.js 里,但随着时间的推移 app.js 会变得臃肿难以维护,这也违背了代码模块化的思想,所以我们把实现路由功能的代码都放在 routes/index.js 里。官方给出的写法是在 app.js 中实现了简单的路由分配,然后再去 index.js 中找到对应的路由函数,最终实现路由功能。我们不妨把路由控制器和实现路由功能的函数都放到 index.js 里,app.js 中只有一个总的路由接口。

最终将 app.js 修改为:

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
 
var routes = require('./routes/index');
 
var app = express();
 
app.set('port', process.env.PORT || 3000);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
 
//app.use(favicon(__dirname + '/public/favicon.ico'));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
 
routes(app);
 
app.listen(app.get('port'), function() {
console.log('Express server listening on port ' + app.get('port'));
});

修改 index.js 如下:

module.exports = function(app) {
app.get('/', function (req, res) {
res.render('index', { title: 'Express' });
});
};

现在,再运行你的 app,你会发现主页毫无二致。这里我们在 routes/index.js 中通过 module.exports 导出了一个函数接口,在 app.js 中通过 require 加载了 index.js 然后通过 routes(app) 调用了 index.js 导出的函数。

路由规则

express 封装了多种 http 请求方式,我们主要只使用 get 和 post 两种,即 app.get() 和 app.post() 。

app.get() 和 app.post() 的第一个参数都为请求的路径,第二个参数为处理请求的回调函数,回调函数有两个参数分别是 req 和 res,代表请求信息和响应信息 。路径请求及对应的获取路径有以下几种形式:

req.query

// GET /search?q=tobi+ferret
req.query.q
// => "tobi ferret"
 
// GET /shoes?order=desc&shoe[color]=blue&shoe[type]=converse
req.query.order
// => "desc"
 
req.query.shoe.color
// => "blue"
 
req.query.shoe.type
// => "converse"

req.body

// POST user[name]=tobi&user[email]=tobi@learnboost.com
req.body.user.name
// => "tobi"
 
req.body.user.email
// => "tobi@learnboost.com"
 
// POST { "name": "tobi" }
req.body.name
// => "tobi"

req.params

// GET /user/tj
req.params.name
// => "tj"
 
// GET /file/javascripts/jquery.js
req.params[0]
// => "javascripts/jquery.js"

req.param(name)

// ?name=tobi
req.param('name')
// => "tobi"
 
// POST name=tobi
req.param('name')
// => "tobi"
 
// /user/tobi for /user/:name
req.param('name')
// => "tobi"

不难看出:

  • req.query: 处理 get 请求,获取 get 请求参数
  • req.params: 处理 /:xxx 形式的 get 或 post 请求,获取请求参数
  • req.body: 处理 post 请求,获取 post 请求体
  • req.param(): 处理 get 和 post 请求,但查找优先级由高到低为 req.params→req.body→req.query

路径规则还支持正则表达式,更多请查阅 Express 官方文档

添加路由规则

当我们访问 localhost:3000 时,会显示:

1.4.jpg

当我们访问 localhost:3000/nswbmw 这种不存在的页面时就会显示:

1.5.jpg

这是因为不存在 /nswbmw 的路由规则,而且它也不是一个 public 目录下的文件,所以 express 返回了 404 Not Found 的错误。下面我们来添加这条路由规则,使得当访问 localhost:3000/nswbmw 时,页面显示 hello,world!

注意:以下修改仅用于测试,看到效果后再把代码还原回来。

修改 index.js,在 app.get('/') 函数后添加一条路由规则:

app.get('/nswbmw', function (req, res) {
res.send('hello,world!');
});

重启之后,访问 localhost:3000/nswbmw 页面显示如下:

1.6.jpg

很简单吧?这一节我们学习了基本的路由规则及如何添加一条路由规则,下一节我们将学习模板引擎的知识。

模版引擎

什么是模板引擎

模板引擎(Template Engine)是一个将页面模板和要显示的数据结合起来生成 HTML 页面的工具。 如果说上面讲到的 express 中的路由控制方法相当于 MVC 中的控制器的话,那模板引擎就相当于 MVC 中的视图。

模板引擎的功能是将页面模板和要显示的数据结合起来生成 HTML 页面。它既可以运 行在服务器端又可以运行在客户端,大多数时候它都在服务器端直接被解析为 HTML,解析完成后再传输给客户端,因此客户端甚至无法判断页面是否是模板引擎生成的。有时候模板引擎也可以运行在客户端,即浏览器中,典型的代表就是 XSLT,它以 XML 为输入,在客户端生成 HTML 页面。但是由于浏览器兼容性问题,XSLT 并不是很流行。目前的主流还是由服务器运行模板引擎。 在 MVC 架构中,模板引擎包含在服务器端。控制器得到用户请求后,从模型获取数据,调用模板引擎。模板引擎以数据和页面模板为输入,生成 HTML 页面,然后返回给控制器,由控制器交回客户端。 ——《Node.js开发指南》

什么是 ejs ?

ejs 是模板引擎的一种,也是我们这个教程中使用的模板引擎,因为它使用起来十分简单,而且与 express 集成良好。

使用模板引擎

前面我们通过以下两行代码设置了模板文件的存储位置和使用的模板引擎:

app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');

注意:我们通过 express -e blog 只是初始化了一个使用 ejs 模板引擎的工程而已,比如 node_modules 下添加了 ejs 模块,views 文件夹下有 index.ejs 。并不是说强制该工程只能使用 ejs 不能使用其他的模板引擎比如 jade,真正指定使用哪个模板引擎的是 app.set('view engine', 'ejs'); 。

在 routes/index.js 中通过调用 res.render() 渲染模版,并将其产生的页面直接返回给客户端。它接受两个参数,第一个是模板的名称,即 views 目录下的模板文件名,扩展名 .ejs 可选。第二个参数是传递给模板的数据对象,用于模板翻译。

打开 views/index.ejs ,内容如下:

index.ejs

<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<h1><%= title %></h1>
<p>Welcome to <%= title %></p>
</body>
</html>

当我们 res.render('index', { title: 'Express' }); 时,模板引擎会把 <%= title %> 替换成 Express,然后把替换后的页面显示给用户。

渲染后生成的页面代码为:

<!DOCTYPE html>
<html>
<head>
<title>Express</title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<h1>Express</h1>
<p>Welcome to Express</p>
</body>
</html>

注意:我们通过 app.use(express.static(path.join(__dirname, 'public'))) 设置了静态文件目录为 public 文件夹,所以上面代码中的 href='/stylesheets/style.css' 就相当于 href='public/stylesheets/style.css' 。

ejs 的标签系统非常简单,它只有以下三种标签:

  • <% code %>:JavaScript 代码。
  • <%= code %>:显示替换过 HTML 特殊字符的内容。
  • <%- code %>:显示原始 HTML 内容。

注意: <%= code %> 和 <%- code %> 的区别,当变量 code 为普通字符串时,两者没有区别。当 code 比如为

hello

这种字符串时,<%= code %> 会原样输出

hello

,而 <%- code %> 则会显示 H1 大的 hello 字符串。

我们可以在 <% %> 内使用 JavaScript 代码。下面是 ejs 的官方示例:

The Data

supplies: ['mop', 'broom', 'duster']

The Template

<ul>
<% for(var i=0; i<supplies.length; i++) {%>
<li><%= supplies[i] %></li>
<% } %>
</ul>

The Result

<ul>
<li>mop</li>
<li>broom</li>
<li>duster</li>
</ul>

我们可以用上述三种标签实现页面模板系统能实现的任何内容。

页面布局

这里我们不使用layout进行页面布局,而是使用更为简单灵活的include。include 的简单使用如下:

index.ejs

<%- include a %>
hello,world!
<%- include b %>

a.ejs

this is a.ejs

b.ejs

this is b.ejs

最终 index.ejs 会显示:

this is a.ejs
hello,world!
this is b.ejs

这一节我们学习了模版引擎的相关知识,下一节我们正式开始学习如何从头开始搭建一个多人博客。

搭建多人博客

功能分析

搭建一个简单的具有多人注册、登录、发表文章、登出功能的博客。

设计目标

未登录:主页左侧导航显示 home、login、register,右侧显示已发表的文章、发表日期及作者。 登陆后:主页左侧导航显示 home、post、logout,右侧显示已发表的文章、发表日期及作者。 用户登录、注册、发表成功以及登出后都返回到主页。

未登录:

主页:

1.7.jpg

登录页:

1.8.jpg

注册页:

1.9.jpg

登录后:

主页:

1.10.jpg

发表页:

1.11.jpg

注意:没有登出页,当点击 LOGOUT 后,退出登陆并返回到主页。

路由规划

我们已经把设计的构想图贴出来了,接下来的任务就是完成路由规划了。路由规划,或者说控制器规划是整个网站的骨架部分,因为它处于整个架构的枢纽位置,相当于各个接口之间的粘合剂,所以应该优先考虑。

根据构思的设计图,我们作以下路由规划:

/ :首页
/login :用户登录
/reg :用户注册
/post :发表文章
/logout :登出

我们要求 /login 和 /reg 只能是未登录的用户访问,而 /post 和 /logout 只能是已登录的用户访问。左侧导航列表则针对已登录和未登录的用户显示不同的内容。

修改 index.js 如下:

module.exports = function(app) {
app.get('/', function (req, res) {
res.render('index', { title: '主页' });
});
app.get('/reg', function (req, res) {
res.render('reg', { title: '注册' });
});
app.post('/reg', function (req, res) {
});
app.get('/login', function (req, res) {
res.render('login', { title: '登录' });
});
app.post('/login', function (req, res) {
});
app.get('/post', function (req, res) {
res.render('post', { title: '发表' });
});
app.post('/post', function (req, res) {
});
app.get('/logout', function (req, res) {
});
};

如何针对已登录和未登录的用户显示不同的内容呢?或者说如何判断用户是否已经登陆了呢?进一步说如何记住用户的登录状态呢?我们通过引入会话(session)机制记录用户登录状态,还要访问数据库来保存和读取用户信息。下一节我们将学习如何使用数据库。

使用数据库

MongoDB简介

MongoDB 是一个基于分布式文件存储的 NoSQL(非关系型数据库)的一种,由 C++ 语言编写,旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。MongoDB 支持的数据结构非常松散,是类似 json 的 bjson 格式,因此可以存储比较复杂的数据类型。MongoDB 最大的特点是他支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。

MongoDB 没有关系型数据库中行和表的概念,不过有类似的文档和集合的概念。文档是 MongoDB 最基本的单位,每个文档都会以唯一的 _id 标识,文档的属性为 key/value 的键值对形式,文档内可以嵌套另一个文档,因此可以存储比较复杂的数据类型。集合是许多文档的总和,一个数据库可以有多个集合,一个集合可以有多个文档。

下面是一个 MongoDB 文档的示例:

{
"_id" : ObjectId( "4f7fe8432b4a1077a7c551e8" ),
"name" : "nswbmw",
"age" : 22,
"email" : [ "xxx@126.com", "xxx@gmail.com" ],
"family" : {
"mother" : { ... },
"father" : { ... },
"sister : {
"name" : "miaomiao",
"age" : 27,
"email" : "xxx@163.com",
"family" : {
"mother" : { ... },
"father" : { ... },
"brother : { ... },
"husband" : { ... },
"son" : { ... }
}
}
}
}

更多有关 MongoDB 的知识请参阅 《mongodb权威指南》或查阅:http://www.mongodb.org/

安装MongoDB

安装 MongoDB 很简单,去官网下载对应系统的 MongoDB 压缩包即可。解压后将文件夹重命名为 mongodb,并在 mongodb 文件夹里新建 blog 文件夹作为我们博客内容的存储目录。进入到 bin 目录下:运行:

mongod --dbpath ../blog/

以上命令的意思是:设置 blog 文件夹作为我们工程的存储目录并启动数据库。

连接MongoDB

数据库虽然安装并启动成功了,但我们需要连接数据库后才能使用数据库。怎么才能在 Node.js 中使用 MongoDB 呢?我们使用官方提供的 node-mongodb-native 驱动模块,打开 package.json,在 dependencies 中添加一行:

"mongodb": "1.4.15"

然后运行 npm install 更新依赖的模块,稍等片刻后 mongodb 模块就下载并安装完成了。

接下来在工程的根目录中创建 settings.js 文件,用于保存该博客工程的配置信息,比如数据库的连接信息。我们将数据库命名为 blog,因为数据库服务器在本地,所以 settings.js 文件的内容如下:

module.exports = {
cookieSecret: 'myblog',
db: 'blog',
host: 'localhost',
port: 27017
};

其中 db 是数据库的名称,host 是数据库的地址,port是数据库的端口号,cookieSecret 用于 Cookie 加密与数据库无关,我们留作后用。

接下来在根目录下新建 models 文件夹,并在 models 文件夹下新建 db.js ,添加如下代码:

var settings = require('../settings'),
Db = require('mongodb').Db,
Connection = require('mongodb').Connection,
Server = require('mongodb').Server;
module.exports = new Db(settings.db, new Server(settings.host, settings.port),
{safe: true});

其中通过 new Db(settings.db, new Server(settings.host, settings.port), {safe: true}); 设置数据库名、数据库地址和数据库端口创建了一个数据库连接实例,并通过 module.exports 导出该实例。这样,我们就可以通过 require 这个文件来对数据库进行读写了。

打开 app.js,在 var routes = require('./routes/index'); 下添加:

var settings = require('./settings');

会话支持

会话是一种持久的网络协议,用于完成服务器和客户端之间的一些交互行为。会话是一个比连接粒度更大的概念, 一次会话可能包含多次连接,每次连接都被认为是会话的一次操作。在网络应用开发中,有必要实现会话以帮助用户交互。例如网上购物的场景,用户浏览了多个页面,购买了一些物品,这些请求在多次连接中完成。许多应用层网络协议都是由会话支持的,如 FTP、Telnet 等,而 HTTP 协议是无状态的,本身不支持会话,因此在没有额外手段的帮助下,前面场景中服务器不知道用户购买了什么。

为了在无状态的 HTTP 协议之上实现会话,Cookie 诞生了。Cookie 是一些存储在客户端的信息,每次连接的时候由浏览器向服务器递交,服务器也向浏览器发起存储 Cookie 的请求,依靠这样的手段服务器可以识别客户端。我们通常意义上的 HTTP 会话功能就是这样实现的。具体来说,浏览器首次向服务器发起请求时,服务器生成一个唯一标识符并发送给客户端浏览器,浏览器将这个唯一标识符存储在 Cookie 中,以后每次再发起请求,客户端浏览器都会向服务器传送这个唯一标识符,服务器通过这个唯一标识符来识别用户。 对于开发者来说,我们无须关心浏览器端的存储,需要关注的仅仅是如何通过这个唯一标识符来识别用户。很多服务端脚本语言都有会话功能,如 PHP,把每个唯一标识符存储到文件中。

——《Node.js开发指南》

express 也提供了会话中间件,默认情况下是把用户信息存储在内存中,但我们既然已经有了 MongoDB,不妨把会话信息存储在数据库中,便于持久维护。为了使用这一功能,我们需要借助 express-session 和 connect-mongo 这两个第三方中间件,在 package.json 中添加:

"express-session": "1.9.1",
"connect-mongo": "0.4.1"

运行npm install安装模块,打开app.js,添加以下代码:

var session = require('express-session');
var MongoStore = require('connect-mongo')(session);
 
app.use(session({
secret: settings.cookieSecret,
key: settings.db,//cookie name
cookie: {maxAge: 1000 * 60 * 60 * 24 * 30},//30 days
store: new MongoStore({
db: settings.db,
host: settings.host,
port: settings.port
})
}));

使用 express-session 和 connect-mongo 模块实现了将会化信息存储到mongoldb中。secret 用来防止篡改 cookie,key 的值为 cookie 的名字,通过设置 cookie 的 maxAge 值设定 cookie 的生存期,这里我们设置 cookie 的生存期为 30 天,设置它的 store 参数为 MongoStore 实例,把会话信息存储到数据库中,以避免丢失。在后面的小节中,我们可以通过 req.session 获取当前用户的会话对象,获取用户的相关信息。

注册和登陆

我们已经准备好了数据库访问和会话的相关信息,接下来我们完成用户注册和登录功能。

页面设计

首先我们来完成主页、登录页和注册页的页面设计。

修改 views/index.ejs 如下:

<%- include header %>
这是主页
<%- include footer %>

在 views 文件夹下新建 header.ejs,添加如下代码:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Blog</title>
<link rel="stylesheet" href="/stylesheets/style.css">
</head>
<body>
 
<header>
<h1><%= title %></h1>
</header>
 
<nav>
<span><a title="主页" href="/">home</a></span>
<span><a title="登录" href="/login">login</a></span>
<span><a title="注册" href="/reg">register</a></span>
</nav>
 
<article>

新建 footer.ejs,添加如下代码:

</article>
</body>
</html>

修改 public/stylesheets/style.css 如下:

/* inspired by http://yihui.name/cn/ */
*{padding:0;margin:0;}
body{width:600px;margin:2em auto;padding:0 2em;font-size:14px;font-family:"Microsoft YaHei";}
p{line-height:24px;margin:1em 0;}
header{padding:.5em 0;border-bottom:1px solid #cccccc;}
nav{float:left;font-family:"Microsoft YaHei";font-size:1.1em;text-transform:uppercase;margin-left:-12em;width:9em;text-align:right;}
nav a{display:block;text-decoration:none;padding:.7em 1em;color:#000000;}
nav a:hover{background-color:#ff0000;color:#f9f9f9;-webkit-transition:color .2s linear;}
article{font-size:16px;padding-top:.5em;}
article a{color:#dd0000;text-decoration:none;}
article a:hover{color:#333333;text-decoration:underline;}
.info{font-size:14px;}

运行 app ,主页显示如下:

1.12.jpg

接下来在 views 文件夹下新建 login.ejs,内容如下:

<%- include header %>
<form method="post">
用户名:<input type="text" name="name"/><br />
密码: <input type="password" name="password"/><br />
<input type="submit" value="登录"/>
</form>
<%- include footer %>

登录页面显示如下:

1.13.jpg

在 views 文件夹下新建 reg.ejs,内容如下:

<%- include header %>
<form method="post">
用户名: <input type="text" name="name"/><br />
密码: <input type="password" name="password"/><br />
确认密码:<input type="password" name="password-repeat"/><br />
邮箱: <input type="email" name="email"/><br />
<input type="submit" value="注册"/>
</form>
<%- include footer %>

注册页面显示如下:

1.14.jpg

至此,未登录时的主页、注册页、登录页都已经完成。

现在,启动我们的博客看看吧。

注意:每次我们更新代码后,都需要手动停止并重启应用,使用 supervisor 模块可以解决这个问题,每当我们保存修改的文件时,supervisor 都会自动帮我们重启应用。通过:

$ npm install -g supervisor

安装 supervisor 。使用 supervisor 命令启动 app.js:

$ supervisor app

页面通知

接下来我们实现用户的注册和登陆,在这之前我们需要引入 flash 模块来实现页面通知(即成功与错误信息的显示)的功能。

什么是 flash?

我们所说的 flash 即 connect-flash 模块https://github.com/jaredhanson/connect-flash,flash 是一个在 session 中用于存储信息的特定区域。信息写入 flash ,下一次显示完毕后即被清除。典型的应用是结合重定向的功能,确保信息是提供给下一个被渲染的页面。

在 package.json 添加一行代码:

"connect-flash": "0.1.1"

然后 npm install 安装 connect-flash 模块。修改 app.js ,在 var settings = require('./settings'); 后添加:

var flash = require('connect-flash');

在 app.set('view engine', 'ejs'); 后添加:

app.use(flash());

现在我们就可以使用 flash 功能了。

注册响应

前面我们已经完成了注册页,当然现在点击注册是没有效果的,因为我们还没有实现处理 POST 请求的功能,下面就来实现它。

在 models 文件夹下新建 user.js,添加如下代码:

var mongodb = require('./db');
 
function User(user) {
this.name = user.name;
this.password = user.password;
this.email = user.email;
};
 
module.exports = User;
 
//存储用户信息
User.prototype.save = function(callback) {
//要存入数据库的用户文档
var user = {
name: this.name,
password: this.password,
email: this.email
};
//打开数据库
mongodb.open(function (err, db) {
if (err) {
return callback(err);//错误,返回 err 信息
}
//读取 users 集合
db.collection('users', function (err, collection) {
if (err) {
mongodb.close();
return callback(err);//错误,返回 err 信息
}
//将用户数据插入 users 集合
collection.insert(user, {
safe: true
}, function (err, user) {
mongodb.close();
if (err) {
return callback(err);//错误,返回 err 信息
}
callback(null, user[0]);//成功!err 为 null,并返回存储后的用户文档
});
});
});
};
 
//读取用户信息
User.get = function(name, callback) {
//打开数据库
mongodb.open(function (err, db) {
if (err) {
return callback(err);//错误,返回 err 信息
}
//读取 users 集合
db.collection('users', function (err, collection) {
if (err) {
mongodb.close();
return callback(err);//错误,返回 err 信息
}
//查找用户名(name键)值为 name 一个文档
collection.findOne({
name: name
}, function (err, user) {
mongodb.close();
if (err) {
return callback(err);//失败!返回 err 信息
}
callback(null, user);//成功!返回查询的用户信息
});
});
});
};

我们通过 User.prototype.save 实现了用户信息的存储,通过 User.get 实现了用户信息的读取。

打开 index.js ,在最前面添加如下代码:

var crypto = require('crypto'),
User = require('../models/user.js');

通过 require() 引入 crypto 模块和 user.js 用户模型文件,crypto 是 Node.js 的一个核心模块,我们用它生成散列值来加密密码。

修改 index.js 中 app.post('/reg') 如下:

app.post('/reg', function (req, res) {
var name = req.body.name,
password = req.body.password,
password_re = req.body['password-repeat'];
//检验用户两次输入的密码是否一致
if (password_re != password) {
req.flash('error', '两次输入的密码不一致!');
return res.redirect('/reg');//返回注册页
}
//生成密码的 md5 值
var md5 = crypto.createHash('md5'),
password = md5.update(req.body.password).digest('hex');
var newUser = new User({
name: name,
password: password,
email: req.body.email
});
//检查用户名是否已经存在
User.get(newUser.name, function (err, user) {
if (err) {
req.flash('error', err);
return res.redirect('/');
}
if (user) {
req.flash('error', '用户已存在!');
return res.redirect('/reg');//返回注册页
}
//如果不存在则新增用户
newUser.save(function (err, user) {
if (err) {
req.flash('error', err);
return res.redirect('/reg');//注册失败返回主册页
}
req.session.user = user;//用户信息存入 session
req.flash('success', '注册成功!');
res.redirect('/');//注册成功后返回主页
});
});
});

注意:我们把用户信息存储在了 session 里,以后就可以通过 req.session.user 读取用户信息。

  • req.body: 就是 POST 请求信息解析过后的对象,例如我们要访问 POST 来的表单内的 name="password" 域的值,只需访问 req.body['password'] 或 req.body.password 即可。
  • res.redirect: 重定向功能,实现了页面的跳转,更多关于 res.redirect 的信息请查阅:http://expressjs.com/api.html#res.redirect 。
  • User:在前面的代码中,我们直接使用了 User 对象。User 是一个描述数据的对象,即 MVC 架构中的模型。前面我们使用了许多视图和控制器,这是第一次接触到模型。与视图和控制器不同,模型是真正与数据打交道的工具,没有模型,网站就只是一个外壳,不能发挥真实的作用,因此它是框架中最根本的部分。 现在,启动应用,在浏览器输入 localhost:3000 注册试试吧!注册成功后显示如下:

1.15.jpg

这样我们并不知道是否注册成功,我们查看数据库中是否存入了用户的信息,打开一个命令行切换到 mongodb/bin/ (保证数据库已打开的前提下),输入:

1.16.jpg

可以看到,用户信息已经成功存入数据库。

接下来我们实现当注册成功返回主页时,左侧导航显示 HOME 、POST 、LOGOUT ,右侧显示 注册成功! 字样,即添加 flash 的页面通知功能。

修改 header.ejs,将

修改如下:

<nav>
<span><a title="主页" href="/">home</a></span>
<% if (user) { %>
<span><a title="发表" href="/post">post</a></span>
<span><a title="登出" href="/logout">logout</a></span>
<% } else { %>
<span><a title="登录" href="/login">login</a></span>
<span><a title="注册" href="/reg">register</a></span>
<% } %>
</nav>

<

article> 后添加如下代码:

<% if (success) { %>
<div><%= success %></div>
<% } %>
<% if (error) { %>
<div><%= error %> </div>
<% } %>
修改 index.js ,将 app.get('/') 修改如下:
 
app.get('/', function (req, res) {
res.render('index', {
title: '主页',
user: req.session.user,
success: req.flash('success').toString(),
error: req.flash('error').toString()
});
});

将 app.get('reg') 修改如下:

app.get('/reg', function (req, res) {
res.render('reg', {
title: '注册',
user: req.session.user,
success: req.flash('success').toString(),
error: req.flash('error').toString()
});
});

现在运行我们的博客,注册成功后显示如下:

1.17.jpg

我们通过对 session 的使用实现了对用户状态的检测,再根据不同的用户状态显示不同的导航信息。 简单解释一下流程:用户在注册成功后,把用户信息存入 session ,页面跳转到主页显示 注册成功! 的字样。同时把 session 中的用户信息赋给变量 user ,在渲染 index.ejs 文件时通过检测 user 判断用户是否在线,根据用户状态的不同显示不同的导航信息。

success: req.flash('success').toString() 的意思是将成功的信息赋值给变量 success, error: req.flash('error').toString() 的意思是将错误的信息赋值给变量 error ,然后我们在渲染 ejs 模版文件时传递这两个变量来进行检测并显示通知。

登录与登出响应

现在我们来实现用户登录的功能。

打开 index.js ,将 app.post('/login') 修改如下:

app.post('/login', function (req, res) {
//生成密码的 md5 值
var md5 = crypto.createHash('md5'),
password = md5.update(req.body.password).digest('hex');
//检查用户是否存在
User.get(req.body.name, function (err, user) {
if (!user) {
req.flash('error', '用户不存在!');
return res.redirect('/login');//用户不存在则跳转到登录页
}
//检查密码是否一致
if (user.password != password) {
req.flash('error', '密码错误!');
return res.redirect('/login');//密码错误则跳转到登录页
}
//用户名密码都匹配后,将用户信息存入 session
req.session.user = user;
req.flash('success', '登陆成功!');
res.redirect('/');//登陆成功后跳转到主页
});
});

将 app.get('/login') 修改如下:

app.get('/login', function (req, res) {
res.render('login', {
title: '登录',
user: req.session.user,
success: req.flash('success').toString(),
error: req.flash('error').toString()});
});

(这样就不会出现 'user is not defined' 的错误了)

接下来我们实现登出响应。修改 app.get('/logout') 如下:

app.get('/logout', function (req, res) {
req.session.user = null;
req.flash('success', '登出成功!');
res.redirect('/');//登出成功后跳转到主页
});

注意:通过把 req.session.user 赋值 null 丢掉 session 中用户的信息,实现用户的退出。

登录后页面显示如下:

1.18.jpg

登出后页面显示如下:

1.19.jpg

至此,我们实现了用户注册与登陆的功能,并且根据用户登录状态显示不同的导航。

页面权限控制

我们虽然已经完成了用户注册与登陆的功能,但并不能阻止比如已经登陆的用户访问 localhost:3000/reg 页面,读者可亲自尝试下。为此,我们需要为页面设置访问权限。即注册和登陆页面应该阻止已登陆的用户访问,登出及后面我们将要实现的发表页只对已登录的用户开放。如何实现页面权限的控制呢?我们可以把用户登录状态的检查放到路由中间件中,在每个路径前增加路由中间件,即可实现页面权限控制。我们添加 checkNotLogin 和 checkLogin 函数来实现这个功能。

function checkLogin(req, res, next) {
if (!req.session.user) {
req.flash('error', '未登录!');
res.redirect('/login');
}
next();
}
 
function checkNotLogin(req, res, next) {
if (req.session.user) {
req.flash('error', '已登录!');
res.redirect('back');//返回之前的页面
}
next();
}

checkNotLogin 和 checkLogin 用来检测是否登陆,并通过 next() 转移控制权,检测到未登录则跳转到登录页,检测到已登录则跳转到前一个页面。

最终 index.js 代码如下:

var crypto = require('crypto'),
User = require('../models/user.js');
 
module.exports = function(app) {
app.get('/', function (req, res) {
res.render('index', {
title: '主页',
user: req.session.user,
success: req.flash('success').toString(),
error: req.flash('error').toString()
});
});
 
app.get('/reg', checkNotLogin);
app.get('/reg', function (req, res) {
res.render('reg', {
title: '注册',
user: req.session.user,
success: req.flash('success').toString(),
error: req.flash('error').toString()
});
});
 
app.post('/reg', checkNotLogin);
app.post('/reg', function (req, res) {
var name = req.body.name,
password = req.body.password,
password_re = req.body['password-repeat'];
if (password_re != password) {
req.flash('error', '两次输入的密码不一致!');
return res.redirect('/reg');
}
var md5 = crypto.createHash('md5'),
password = md5.update(req.body.password).digest('hex');
var newUser = new User({
name: name,
password: password,
email: req.body.email
});
User.get(newUser.name, function (err, user) {
if (err) {
req.flash('error', err);
return res.redirect('/');
}
if (user) {
req.flash('error', '用户已存在!');
return res.redirect('/reg');
}
newUser.save(function (err, user) {
if (err) {
req.flash('error', err);
return res.redirect('/reg');
}
req.session.user = user;
req.flash('success', '注册成功!');
res.redirect('/');
});
});
});
 
app.get('/login', checkNotLogin);
app.get('/login', function (req, res) {
res.render('login', {
title: '登录',
user: req.session.user,
success: req.flash('success').toString(),
error: req.flash('error').toString()
});
});
 
app.post('/login', checkNotLogin);
app.post('/login', function (req, res) {
var md5 = crypto.createHash('md5'),
password = md5.update(req.body.password).digest('hex');
User.get(req.body.name, 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('/');
});
});
 
app.get('/post', checkLogin);
app.get('/post', function (req, res) {
res.render('post', {
title: '发表',
user: req.session.user,
success: req.flash('success').toString(),
error: req.flash('error').toString()
});
});
 
app.post('/post', checkLogin);
app.post('/post', function (req, res) {
});
 
app.get('/logout', checkLogin);
app.get('/logout', function (req, res) {
req.session.user = null;
req.flash('success', '登出成功!');
res.redirect('/');
});
 
function checkLogin(req, res, next) {
if (!req.session.user) {
req.flash('error', '未登录!');
res.redirect('/login');
}
next();
}
 
function checkNotLogin(req, res, next) {
if (req.session.user) {
req.flash('error', '已登录!');
res.redirect('back');
}
next();
}
};

注意:为了维护用户状态和 flash 的通知功能,我们给每个 ejs 模版文件传入了以下三个值:

user: req.session.user,
success: req.flash('success').toString(),
error: req.flash('error').toString()

发表文章

现在我们的博客已经具备了用户注册、登陆、页面权限控制的功能,接下来我们完成博客最核心的部分——发表文章。在这一节,我们将会实现发表文章的功能,完成整个博客的设计。

页面设计

我们先来完成发表页的页面设计。在 views 文件夹下新建 post.ejs ,添加如下代码:

<%- include header %>
<form method="post">
标题:<br />
<input type="text" name="title" /><br />
正文:<br />
<textarea name="post" rows="20" cols="100"></textarea><br />
<input type="submit" value="发表" />
</form>
<%- include footer %>

文章模型

仿照用户模型,我们将文章模型命名为 Post 对象,它拥有与 User 相似的接口,分别是 Post.get 和 Post.prototype.save 。Post.get 的功能是从数据库中获取文章,可以按指定用户获取,也可以获取全部的内容。Post.prototype.save 是 Post 对象原型的方法,用来将文章保存到数据库。 在 models 文件夹下新建 post.js ,添加如下代码:

var mongodb = require('./db');
 
function Post(name, title, post) {
this.name = name;
this.title = title;
this.post = post;
}
 
module.exports = Post;
 
//存储一篇文章及其相关信息
Post.prototype.save = function(callback) {
var date = new Date();
//存储各种时间格式,方便以后扩展
var time = {
date: date,
year : date.getFullYear(),
month : date.getFullYear() + "-" + (date.getMonth() + 1),
day : date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate(),
minute : date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate() + " " +
date.getHours() + ":" + (date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes())
}
//要存入数据库的文档
var post = {
name: this.name,
time: time,
title: this.title,
post: this.post
};
//打开数据库
mongodb.open(function (err, db) {
if (err) {
return callback(err);
}
//读取 posts 集合
db.collection('posts', function (err, collection) {
if (err) {
mongodb.close();
return callback(err);
}
//将文档插入 posts 集合
collection.insert(post, {
safe: true
}, function (err) {
mongodb.close();
if (err) {
return callback(err);//失败!返回 err
}
callback(null);//返回 err 为 null
});
});
});
};
 
//读取文章及其相关信息
Post.get = function(name, 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);
}
var query = {};
if (name) {
query.name = name;
}
//根据 query 对象查询文章
collection.find(query).sort({
time: -1
}).toArray(function (err, docs) {
mongodb.close();
if (err) {
return callback(err);//失败!返回 err
}
callback(null, docs);//成功!以数组形式返回查询的结果
});
});
});
};

发表响应

接下来我们给发表文章注册响应,打开 index.js ,在 User = require('../models/user.js') 后添加一行代码:

,Post = require('../models/post.js');

修改 app.post('/post') 如下:

app.post('/post', checkLogin);
app.post('/post', function (req, res) {
var currentUser = req.session.user,
post = new Post(currentUser.name, req.body.title, req.body.post);
post.save(function (err) {
if (err) {
req.flash('error', err);
return res.redirect('/');
}
req.flash('success', '发布成功!');
res.redirect('/');//发表成功跳转到主页
});
});

最后,我们修改 index.ejs ,让主页右侧显示发表过的文章及其相关信息。

打开 index.ejs ,修改如下:

<%- include header %>
<% posts.forEach(function (post, index) { %>
<p><h2><a href="#"><%= post.title %></a></h2></p>
<p class="info">
作者:<a href="#"><%= post.name %></a> |
日期:<%= post.time.minute %>
</p>
<p><%- post.post %></p>
<% }) %>
<%- include footer %>

打开 index.js ,修改 app.get('/') 如下:

app.get('/', function (req, res) {
Post.get(null, function (err, posts) {
if (err) {
posts = [];
}
res.render('index', {
title: '主页',
user: req.session.user,
posts: posts,
success: req.flash('success').toString(),
error: req.flash('error').toString()
});
});
});

至此,我们的博客就建成了。

启动我们的博客,发表一篇博文,如图所示:

1.20.jpg

此时,查看一下数据库,如图所示:

1.21.jpg

Tips:Robomongo 是一个基于 Shell 的跨平台开源 MongoDB 管理工具。嵌入了 JavaScript 引擎和 MongoDB mongo 。只要你会使用 mongo shell ,你就会使用 Robomongo,它提供语法高亮、自动完成、差别视图等。

下载安装 Robomongo后,运行我们的博客,注册一个用户并发表几篇文章,初次打开 Robomongo ,点击 Create 创建一个名为 blog (名字自定)的数据库链接(默认监听 localhost:27017),点击 Connect 就连接到数据库了。如图所示:

1.22.jpg

2

第2章 使用 Markdown

现在我们来给博客添加支持 markdown 发表文章的功能。 假如你不还熟悉 markdown,请转到:http://wowubuntu.com/markdown/

打开 package.json ,添加一行代码:

"markdown": “0.5.0"

使用 npm install 安装 markdown 模块。

打开 post.js,在 mongodb = require('./db') 后添加一行代码:

markdown = require('markdown').markdown;

在 Post.get 函数里的 callback(null, docs); 前添加以下代码:

//解析 markdown 为 html
docs.forEach(function (doc) {
doc.post = markdown.toHTML(doc.post);
});

现在我们就可以使用 markdown 发表文章了。

注意:每当我们给博客添加新功能后,都要清空数据库(即删除 mongodb/blog 文件夹里所有文件)再启动我们的博客。以后每一章都是如此,后面便不再赘述。

运行我们的博客,如图所示:

发表前

2.1.jpg

发表后

2.2.jpg

3

第3章 增加文件上传功能

一个完整的博客怎么能缺少图片呢?现在,我们来给博客添加文件上传功能,这样我们就可以使用 markdown 来链接图片了。

我们使用express的第三方中间件 multer 实现文件上传功能。打开 package.json,在 dependencies 中添加:

"multer": “0.1.6"

并 npm install 安装模块。然后修改 app.js,添加:

var multer = require('multer');
 
app.use(multer({
dest: './public/images',
rename: function (fieldname, filename) {
return filename;
}
}));

其中,dest 是上传的文件所在的目录,rename 函数用来修改上传后的文件名,这里设置为保持原来的文件名。

修改 header.ejs ,在archive
下一行添加:

<span><a title="上传" href="/upload">upload</a></span>

然后打开 index.js ,在 app.get('/logout') 函数后添加如下代码:

app.get('/upload', checkLogin);
app.get('/upload', function (req, res) {
res.render('upload', {
title: '文件上传',
user: req.session.user,
success: req.flash('success').toString(),
error: req.flash('error').toString()
});
});

注意:我们设置 app.get('/upload', checkLogin); 限制只有登陆的用户才能上传文件。

接下来,我们在 views 文件夹下新建 upload.ejs ,添加如下代码:

<%- include header %>
<form method='post' action='/upload' enctype='multipart/form-data' >
<input type="file" name='file1'/><br>
<input type="file" name='file2'/><br>
<input type="file" name='file3'/><br>
<input type="file" name='file4'/><br>
<input type="file" name='file5'/><br>
<input type="submit" />
</form>
<%- include footer %>

现在我们就可以访问文件上传页面了。这里我们限制一次最多可以上传 5 个文件。清空数据库,重新注册登录后,上传文件页面如下图:

3.1.jpg

我们现在只是有了一个可以上传文件的表单而已,并不能上传文件,接下来我们添加对上传文件的支持。

在 app.get('/upload') 后添加如下代码:

app.post('/upload', checkLogin);
app.post('/upload', function (req, res) {
req.flash('success', '文件上传成功!');
res.redirect('/upload');
});

注意:假如从桌面启动的博客上传图片时失败,尝试在命令行中从博客根目录下启动。

现在,我们给博客添加了文件上传功能。假如我们上传了一张名为 lufei.jpg 的图片,示例:

发表前

3.2.jpg

发表后

3.3.jpg

4

第4章 实现用户页面和文章页面

现在,我们来给博客添加用户页面和文章页面。

所谓用户页面就是当点击某个用户名链接时,跳转到:域名/u/用户名 ,并列出该用户的所有文章。 同理,文章页面就是当点击某篇文章标题时,跳转到:域名/u/用户名/时间/文章名 ,进入到该文章的页面(也许还有该文章的评论等)。

在开始之前我们需要做一个工作,打开 post.js ,将 Post.get 修改为 Post.getAll ,同时将 index.js 中 Post.get 修改为 Post.getAll 。在 post.js 最后添加如下代码:

//获取一篇文章
Post.getOne = function(name, day, title, 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);
}
//根据用户名、发表日期及文章名进行查询
collection.findOne({
"name": name,
"time.day": day,
"title": title
}, function (err, doc) {
mongodb.close();
if (err) {
return callback(err);
}
//解析 markdown 为 html
doc.post = markdown.toHTML(doc.post);
callback(null, doc);//返回查询的一篇文章
});
});
});
};

简单解释一下:

  • Post.getAll :获取一个人的所有文章(传入参数 name)或获取所有人的文章(不传入参数)。
  • Post.getOne :根据用户名、发表日期及文章名精确获取一篇文章。 下面我们来实现用户页面和文章页面。

打开 index.js ,在 app.post('/upload') 后添加如下代码:

app.get('/u/:name', function (req, res) {
//检查用户是否存在
User.get(req.params.name, function (err, user) {
if (!user) {
req.flash('error', '用户不存在!');
return res.redirect('/');//用户不存在则跳转到主页
}
//查询并返回该用户的所有文章
Post.getAll(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()
});
});
});
});

这里我们添加了一条路由规则 app.get('/u/:name'),用来处理访问用户页的请求,然后从数据库取得该用户的数据并渲染 user.ejs 模版,生成页面并显示给用户。

接下来我们添加文章页面的路由规则。 在 app.get('/u/:name') 后添加如下代码:

app.get('/u/:name/:day/:title', function (req, res) {
Post.getOne(req.params.name, req.params.day, req.params.title, function (err, post) {
if (err) {
req.flash('error', err);
return res.redirect('/');
}
res.render('article', {
title: req.params.title,
post: post,
user: req.session.user,
success: req.flash('success').toString(),
error: req.flash('error').toString()
});
});
});

最后,我们创建用户页面和文章页面的模版文件。

在 views 文件夹下新建 user.ejs,添加如下代码,同时也将 index.ejs 也修改成如下代码:

<%- include header %>
<% posts.forEach(function (post, index) { %>
<p><h2><a href="/u/<%= post.name %>/<%= post.time.day %>/<%= post.title %>"><%= post.title %></a></h2></p>
<p class="info">
作者:<a href="/u/<%= post.name %>"><%= post.name %></a> |
日期:<%= post.time.minute %>
</p>
<p><%- post.post %></p>
<% }) %>
<%- include footer %>

在 views 文件夹下新建 article.ejs ,添加如下代码:

<%- include header %>
<p class="info">
作者:<a href="/u/<%= post.name %>"><%= post.name %></a> |
日期:<%= post.time.minute %>
</p>
<p><%- post.post %></p>
<%- include footer %>

然后将index.ejs中的href更改如下:

<%- include header %>
<% posts.forEach(function (post, index) { %>
<p><h2><a href="u/<%= post.name %>/<%= post.time.day %>/<%= post.title %>"><%= post.title %></a></h2></p>
<p class="info">
作者:<a href="u/<%= post.name %>"><%= post.name %></a> |
日期:<%= post.time.minute %>
</p>
<p><%- post.post %></p>
<% }) %>
<%- include footer %>

现在,我们给博客添加了用户页面和文章页面。示例:

用户页面

4.1.jpg

文章页面

4.2.jpg

5

第5章 增加编辑与删除功能

现在,我们来给博客添加编辑文章与删除文章的功能。

我们设定:当一个用户在线时,只允许他在自己发表的文章页进行编辑或删除,编辑时,只能编辑文章内容,不能编辑文章标题。

打开 style.css ,添加如下样式:

.edit{margin:3px;padding:2px 5px;border-radius:3px;background-color:#f3f3f3;color:#333;font-size:13px;}
.edit:hover{text-decoration:none;background-color:#f00;color:#fff;-webkit-transition:color .2s linear;}

打开 article.ejs ,将代码修改成如下:

<%- include header %>
<p>
<span><a class="edit" href="/edit/<%= post.name %>/<%= post.time.day %>/<%= post.title %>">编辑</a></span>
<span><a class="edit" href="/remove/<%= post.name %>/<%= post.time.day %>/<%= post.title %>">删除</a></span>
</p>
<p class="info">
作者:<a href="/u/<%= post.name %>"><%= post.name %></a> |
日期:<%= post.time.minute %>
</p>
<p><%- post.post %></p>
<%- include footer %>

至此,我们只是在文章页面添加了编辑和删除文章的链接。接下来,我们注册这两个链接的点击事件的响应。

打开 post.js ,在最后添加如下代码:

//返回原始发表的内容(markdown 格式)
Post.edit = function(name, day, title, 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);
}
//根据用户名、发表日期及文章名进行查询
collection.findOne({
"name": name,
"time.day": day,
"title": title
}, function (err, doc) {
mongodb.close();
if (err) {
return callback(err);
}
callback(null, doc);//返回查询的一篇文章(markdown 格式)
});
});
});
};

打开 index.js ,在 app.get('/u/:name/:day/:title') 后添加如下代码:

app.get('/edit/:name/:day/:title', checkLogin);
app.get('/edit/:name/:day/:title', function (req, res) {
var currentUser = req.session.user;
Post.edit(currentUser.name, req.params.day, req.params.title, function (err, post) {
if (err) {
req.flash('error', err);
return res.redirect('back');
}
res.render('edit', {
title: '编辑',
post: post,
user: req.session.user,
success: req.flash('success').toString(),
error: req.flash('error').toString()
});
});
});

在 views 下新建 edit.ejs ,添加如下代码:

<%- include header %>
<form method="post">
标题:<br />
<input type="text" name="title" value="<%= post.title %>" disabled="disabled" /><br />
正文:<br />
<textarea name="post" rows="20" cols="100"><%= post.post %></textarea><br />
<input type="submit" value="保存修改" />
</form>
<%- include footer %>

现在,运行我们的博客看看吧。在文章页面,当我们点击 编辑 后就会跳转到该文章对应的编辑页面了。接下来我们实现将修改后的文章提交到数据库。

打开 post.js ,在最后添加如下代码:

//更新一篇文章及其相关信息
Post.update = function(name, day, title, post, 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);
}
//更新文章内容
collection.update({
"name": name,
"time.day": day,
"title": title
}, {
$set: {post: post}
}, function (err) {
mongodb.close();
if (err) {
return callback(err);
}
callback(null);
});
});
});
};

打开 index.js ,在 app.get('/edit/:name/:day/:title') 后添加如下代码:

app.post('/edit/:name/:day/:title', checkLogin);
app.post('/edit/:name/:day/:title', function (req, res) {
var currentUser = req.session.user;
Post.update(currentUser.name, req.params.day, req.params.title, req.body.post, function (err) {
var url = encodeURI('/u/' + req.params.name + '/' + req.params.day + '/' + req.params.title);
if (err) {
req.flash('error', err);
return res.redirect(url);//出错!返回文章页
}
req.flash('success', '修改成功!');
res.redirect(url);//成功!返回文章页
});
});

现在,我们就可以编辑并保存文章了。赶紧试试吧!

接下来,我们实现删除文章的功能。打开 post.js ,在最后添加如下代码:

//删除一篇文章
Post.remove = function(name, day, title, 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);
}
//根据用户名、日期和标题查找并删除一篇文章
collection.remove({
"name": name,
"time.day": day,
"title": title
}, {
w: 1
}, function (err) {
mongodb.close();
if (err) {
return callback(err);
}
callback(null);
});
});
});
};

打开 index.js ,在 app.post('/edit/:name/:day/:title') 后添加如下代码:

app.get('/remove/:name/:day/:title', checkLogin);
app.get('/remove/:name/:day/:title', function (req, res) {
var currentUser = req.session.user;
Post.remove(currentUser.name, req.params.day, req.params.title, function (err) {
if (err) {
req.flash('error', err);
return res.redirect('back');
}
req.flash('success', '删除成功!');
res.redirect('/');
});
});

至此我们完成了大部分的工作,接下来我们实现页面权限的控制。假如你现在注册了两个帐号 A 和 B,那么当 B 访问 A 的用户页面时,也会出现编辑和删除的选项,虽然点击后并不能编辑和删除 A 的文章。那怎么才能实现一个账号只能编辑和删除自己发表的文章呢?很简单,添加一个判断即可。打开 article.js ,将:

<span><a class="edit" href="/edit/<%= post.name %>/<%= post.time.day %>/<%= post.title %>">编辑</a></span>
<span><a class="edit" href="/remove/<%= post.name %>/<%= post.time.day %>/<%= post.title %>">删除</a></span>

修改为:

<% if (user && (user.name == post.name)) { %>
<span><a class="edit" href="/edit/<%= post.name %>/<%= post.time.day %>/<%= post.title %>">编辑</a></span>
<span><a class="edit" href="/remove/<%= post.name %>/<%= post.time.day %>/<%= post.title %>">删除</a></span>
<% } %>

以上代码的意思是:通过检测 session 中的用户名是否存在,若存在且和当前文章页面的作者名相同,则显示编辑和删除按钮,否则不显示。

现在,我们完成了给博客添加编辑文章与删除文章的功能。

6

第6章 实现留言功能

一个完整的博客怎么能缺少留言功能呢,当然我们可以用第三方社会化评论插件,既然我们有了数据库,我们不妨把留言保存到自己的数据库里。

我们设定:只有在文章页面才会显示留言板。

打开 post.js ,修改 Post.prototype.save 中要存入的文档为:

var post = {
name: this.name,
time: time,
title:this.title,
post: this.post,
comments: []
};

我们在文档里增加了 comments 键(数组),用来存储此文章上的留言(一个个对象)。为了也让留言支持 markdown 语法,我们将 Post.getOne 函数里的:

doc.post = markdown.toHTML(doc.post);

修改为:

if (doc) {
doc.post = markdown.toHTML(doc.post);
doc.comments.forEach(function (comment) {
comment.content = markdown.toHTML(comment.content);
});
}

接下来我们在 models 下新建 comment.js 文件,添加如下代码:

var mongodb = require('./db');
 
function Comment(name, day, title, comment) {
this.name = name;
this.day = day;
this.title = title;
this.comment = comment;
}
 
module.exports = Comment;
 
//存储一条留言信息
Comment.prototype.save = function(callback) {
var name = this.name,
day = this.day,
title = this.title,
comment = this.comment;
//打开数据库
mongodb.open(function (err, db) {
if (err) {
return callback(err);
}
//读取 posts 集合
db.collection('posts', function (err, collection) {
if (err) {
mongodb.close();
return callback(err);
}
//通过用户名、时间及标题查找文档,并把一条留言对象添加到该文档的 comments 数组里
collection.update({
"name": name,
"time.day": day,
"title": title
}, {
$push: {"comments": comment}
} , function (err) {
mongodb.close();
if (err) {
return callback(err);
}
callback(null);
});
});
});
};

修改 index.js ,在 Post = require('../models/post.js') 后添加一行代码:

Comment = require('../models/comment.js');

这里我们创建了 comment 的模型文件,用于存储新的留言到数据库,并在 index.js 中引入以作后用。

接下来我们创建 comment 的视图文件,在 views 文件夹下新建 comment.ejs ,添加如下代码:

<br />
<% post.comments.forEach(function (comment, index) { %>
<p><a href="<%= comment.website %>"><%= comment.name %></a>
<span class="info"> 回复于 <%= comment.time %></span></p>
<p><%- comment.content %></p>
<% }) %>
 
<form method="post">
<% if (user) { %>
姓名:<input type="text" name="name" value="<%= user.name %>" /><br />
邮箱:<input type="text" name="email" value="<%= user.email %>" /><br />
网址:<input type="text" name="website" value="/u/<%= user.name %>" /><br />
<% } else { %>
姓名:<input type="text" name="name" /><br />
邮箱:<input type="text" name="email" /><br />
网址:<input type="text" name="website" value="http://" /><br />
<% } %>
<textarea name="content" rows="5" cols="80"></textarea><br />
<input type="submit" value="留言" />
</form>

注意:这里根据用户登录状态的不同,显示不同的提示信息。还需注意的一点是,未登录的用户在留言的时候, 网址 这一项需要加上 http:// 前缀,否则在生成连接的时候会基于当前 url (本地是 localhost:3000)。

打开 article.ejs ,在 <%- include footer %> 前添加一行代码:

<%- include comment %>

这样我们就在文章页面引入了留言模块。

最后,修改 index.js ,注册留言的 POST 响应,在 app.get('/u/:name/:day/:title') 后添加如下代码:

app.post('/u/:name/:day/:title', function (req, res) {
var date = new Date(),
time = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate() + " " +
date.getHours() + ":" + (date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes());
var comment = {
name: req.body.name,
email: req.body.email,
website: req.body.website,
time: time,
content: req.body.content
};
var newComment = new Comment(req.params.name, req.params.day, req.params.title, comment);
newComment.save(function (err) {
if (err) {
req.flash('error', err);
return res.redirect('back');
}
req.flash('success', '留言成功!');
res.redirect('back');
});
});

注意:这里我们使用 res.redirect('back'); ,即留言成功后返回到该文章页。

现在,我们就给博客添加了留言的功能。

7

第7章 实现分页功能

现在我们给博客的主页和用户页面增加分页功能。

我们设定:主页和用户页面每页最多显示十篇文章。

这里我们要用到 mongodb 的 skip 和 limit 操作,具体可查阅《mongodb权威指南》。

打开 post.js ,把 Post.getAll 函数修改如下:

//一次获取十篇文章
Post.getTen = function(name, page, 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);
}
var query = {};
if (name) {
query.name = name;
}
//使用 count 返回特定查询的文档数 total
collection.count(query, function (err, total) {
//根据 query 对象查询,并跳过前 (page-1)*10 个结果,返回之后的 10 个结果
collection.find(query, {
skip: (page - 1)*10,
limit: 10
}).sort({
time: -1
}).toArray(function (err, docs) {
mongodb.close();
if (err) {
return callback(err);
}
//解析 markdown 为 html
docs.forEach(function (doc) {
doc.post = markdown.toHTML(doc.post);
});
callback(null, docs, total);
});
});
});
});
};

打开 index.js ,修改 app.get('/', function(req,res){ 如下:

app.get('/', function (req, res) {
//判断是否是第一页,并把请求的页数转换成 number 类型
var page = req.query.p ? parseInt(req.query.p) : 1;
//查询并返回第 page 页的 10 篇文章
Post.getTen(null, page, function (err, posts, total) {
if (err) {
posts = [];
}
res.render('index', {
title: '主页',
posts: posts,
page: page,
isFirstPage: (page - 1) == 0,
isLastPage: ((page - 1) * 10 + posts.length) == total,
user: req.session.user,
success: req.flash('success').toString(),
error: req.flash('error').toString()
});
});
});

注意:这里通过 req.query.p 获取的页数为字符串形式,我们需要通过 parseInt() 把它转换成数字以作后用。同时把 Post.getAll 改成了 Post.getTen 。

修改 app.get('/u/:name') 如下:

app.get('/u/:name', function (req, res) {
var page = req.query.p ? parseInt(req.query.p) : 1;
//检查用户是否存在
User.get(req.params.name, function (err, user) {
if (!user) {
req.flash('error', '用户不存在!');
return res.redirect('/');
}
//查询并返回该用户第 page 页的 10 篇文章
Post.getTen(user.name, page, function (err, posts, total) {
if (err) {
req.flash('error', err);
return res.redirect('/');
}
res.render('user', {
title: user.name,
posts: posts,
page: page,
isFirstPage: (page - 1) == 0,
isLastPage: ((page - 1) * 10 + posts.length) == total,
user: req.session.user,
success: req.flash('success').toString(),
error: req.flash('error').toString()
});
});
});
});

接下来在 views 文件夹下新建 paging.ejs ,添加如下代码:

<br />
<div>
<% if (!isFirstPage) { %>
<span class="prepage"><a title="上一页" href="?p=<%= page-1 %>">上一页</a></span>
<% } %>
 
<% if (!isLastPage) { %>
<span class="nextpage"><a title="下一页" href="?p=<%= page+1 %>">下一页</a></span>
<% } %>
</div>

这里通过 if(!isFirstPage) 判断是否为第一页,不是第一页则显示 “上一页” ;通过 if(!isLastPage) 判断是否为最后一页,不是最后一页则显示 “下一页” 。

接下来在主页和用户页引入分页。修改 index.ejs 和 user.ejs ,在 <%- include footer %> 前添加一行代码:

<%- include paging %>

在主页和用户页面引入分页模块。

最后,在 style.css 中添加以下样式:

.prepage a{float:left;text-decoration:none;padding:.5em 1em;color:#ff0000;font-weight:bold;}
.nextpage a{float:right;text-decoration:none;padding:.5em 1em;color:#ff0000;font-weight:bold;}
.prepage a:hover,.nextpage a:hover{text-decoration:none;background-color:#ff0000;color:#f9f9f9;-webkit-transition:color .2s linear;}

现在,我们实现了博客的分页功能。

8

第8章 增加存档页面

现在我们来给博客增加存档功能,当进入存档页时,按年份和日期的降序列出所有的文章。

首先,我们在主页左侧导航中添加存档页(archive)的链接,修改 header.ejs,在 home 下添加一行代码:

<span><a title="存档" href="/archive">archive</a></span>

打开 post.js ,在最后添加以下代码:

//返回所有文章存档信息
Post.getArchive = function(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);
}
//返回只包含 name、time、title 属性的文档组成的存档数组
collection.find({}, {
"name": 1,
"time": 1,
"title": 1
}).sort({
time: -1
}).toArray(function (err, docs) {
mongodb.close();
if (err) {
return callback(err);
}
callback(null, docs);
});
});
});
};

接下来我们在 index.js 中添加 /archive 的路由规则。打开 index.js ,在 app.get('/u/:name') 前添加以下代码:

app.get('/archive', function (req, res) {
Post.getArchive(function (err, posts) {
if (err) {
req.flash('error', err);
return res.redirect('/');
}
res.render('archive', {
title: '存档',
posts: posts,
user: req.session.user,
success: req.flash('success').toString(),
error: req.flash('error').toString()
});
});
});

在 views 文件夹下新建 archive.ejs 模版文件,添加如下代码:

<%- include header %>
<ul class="archive">
<% var lastYear = 0 %>
<% posts.forEach(function (post, index) { %>
<% if (lastYear != post.time.year) { %>
<li><h3><%= post.time.year %></h3></li>
<% lastYear=post.time.year } %>
<li><time><%= post.time.day %></time></li>
<li><a href="/u/<%= post.name %>/<%= post.time.day %>/<%= post.title %>"><%= post.title %></a></li>
<% }) %>
</ul>
<%- include footer %>

这里我们通过在模版中设置一个 lastYear 变量判断是否和上次已经显示的年份相同,相同则不再显示年份,不同则显示。

最后,在 style.css 中添加如下样式:

.archive{list-style:none;line-height:28px;}
.archive h3{margin:0.5em 0;}
.archive time{float:left;font-size:14px;color:#999999;margin-right:1.2em;}

现在,我们增加了文章的存档页面。

9

第9章 增加标签和标签页面

现在我们来给博客增加标签和标签页。

我们设定:每篇文章最多有三个标签(少于三个也可以),当点击主页左侧标签页链接时,跳转到标签页并列出所有已存在标签;当点击任意一个标签链接时,跳转到该标签页并列出所有含有该标签的文章。

添加标签

首先我们来实现给文章添加标签的功能。

打开 post.ejs ,在
后添加:

标签:<br />
<input type="text" name="tag1" /><input type="text" name="tag2" /><input type="text" name="tag3" /><br />

打开 index.js ,将 app.post('/post') 内的:

var currentUser = req.session.user,
post = new Post(currentUser.name, req.body.title, req.body.post);

修改为:

var currentUser = req.session.user,
tags = [req.body.tag1, req.body.tag2, req.body.tag3],
post = new Post(currentUser.name, req.body.title, tags, req.body.post);

打开 post.js ,将:

function Post(name, title, post) {
this.name = name;
this.title= title;
this.post = post;
}

修改为:

function Post(name, title, tags, post) {
this.name = name;
this.title = title;
this.tags = tags;
this.post = post;
}

将:

var post = {
name: this.name,
time: time,
title: this.title,
post: this.post,
comments: []
};

修改为:

var post = {
name: this.name,
time: time,
title: this.title,
tags: this.tags,
post: this.post,
comments: []
};

现在我们就可以在发表文章的时候添加标签了。接下来我们修改 index.ejs 、 user.ejs 和 article.ejs 来显示文章的标签。

修改 index.ejs 、 user.ejs 和 article.ejs,将:

<p class="info">
作者:<a href="/u/<%= post.name %>"><%= post.name %></a> |
日期:<%= post.time.minute %>
</p>

修改为:

<p class="info">
作者:<a href="/u/<%= post.name %>"><%= post.name %></a> |
日期:<%= post.time.minute %> |
标签:
<% post.tags.forEach(function (tag, index) { %>
<% if (tag) { %>
<a class="tag" href="/tags/<%= tag %>"><%= tag %></a>
<% } %>
<% }) %>
</p>

最后,在 style.css 中添加如下样式:

.tag{background-color:#ff0000;border-radius:3px;font-size:14px;color:#ffffff;display:inline-block;padding:0 5px;margin-bottom:8px;}
.tag:hover{text-decoration:none;background-color:#ffffff;color:#000000;-webkit-transition:color .2s linear;}

至此,我们给博客添加了标签功能。赶紧看看效果吧!

添加标签页

接下来我们给博客增加标签页。

修改 header.ejs ,在archive
下一行添加:

<span><a title="标签" href="/tags">tags</a></span>

修改 index.js ,在 app.get('/archive') 后添加如下代码:

app.get('/tags', function (req, res) {
Post.getTags(function (err, posts) {
if (err) {
req.flash('error', err);
return res.redirect('/');
}
res.render('tags', {
title: '标签',
posts: posts,
user: req.session.user,
success: req.flash('success').toString(),
error: req.flash('error').toString()
});
});
});

打开 post.js ,在最后添加:

//返回所有标签
Post.getTags = function(callback) {
mongodb.open(function (err, db) {
if (err) {
return callback(err);
}
db.collection('posts', function (err, collection) {
if (err) {
mongodb.close();
return callback(err);
}
//distinct 用来找出给定键的所有不同值
collection.distinct("tags", function (err, docs) {
mongodb.close();
if (err) {
return callback(err);
}
callback(null, docs);
});
});
});
};

注意:这里我们用了 distinct (详见《mongodb权威指南》)返回 tags 键的所有不同值,因为有时候我们发表文章的标签是一样的,所以这样避免了获取重复的标签。

在 views 文件夹下新建 tags.ejs ,添加如下代码:

<%- include header %>
<% posts.forEach(function (tag, index) { %>
<a class="tag" href="/tags/<%= tag %>"><%= tag %></a>
<% }) %>
<%- include footer %>

至此,我们就给博客添加了标签页。

添加特定标签的页面

现在我们来添加特定标签的页面,即当点击任意一个标签链接时,跳转到该标签页并列出所有含有该标签的文章信息。

修改 post.js ,在最后添加如下代码:

//返回含有特定标签的所有文章
Post.getTag = function(tag, callback) {
mongodb.open(function (err, db) {
if (err) {
return callback(err);
}
db.collection('posts', function (err, collection) {
if (err) {
mongodb.close();
return callback(err);
}
//查询所有 tags 数组内包含 tag 的文档
//并返回只含有 name、time、title 组成的数组
collection.find({
"tags": tag
}, {
"name": 1,
"time": 1,
"title": 1
}).sort({
time: -1
}).toArray(function (err, docs) {
mongodb.close();
if (err) {
return callback(err);
}
callback(null, docs);
});
});
});
};

修改 index.js ,在 app.get('/tags') 后添加如下代码:

app.get('/tags/:tag', function (req, res) {
Post.getTag(req.params.tag, function (err, posts) {
if (err) {
req.flash('error',err);
return res.redirect('/');
}
res.render('tag', {
title: 'TAG:' + req.params.tag,
posts: posts,
user: req.session.user,
success: req.flash('success').toString(),
error: req.flash('error').toString()
});
});
});

在 views 文件夹下新建 tag.ejs ,添加如下代码:

<%- include header %>
<ul class="archive">
<% var lastYear = 0 %>
<% posts.forEach(function (post, index) { %>
<% if (lastYear != post.time.year) { %>
<li><h3><%= post.time.year %></h3></li>
<% lastYear = post.time.year } %>
<li><time><%= post.time.day %></time></li>
<li><a href="/u/<%= post.name %>/<%= post.time.day %>/<%= post.title %>"><%= post.title %></a></li>
<% }) %>
</ul>
<%- include footer %>

最后,别忘了修改 edit.ejs ,为了保持和 post.ejs 一致。将 edit.ejs 修改为:

<%- include header %>
<form method="post">
标题:<br />
<input type="text" name="title" value="<%= post.title %>" disabled="disabled" /><br />
标签:<br />
<input type="text" name="tag1" value="<%= post.tags[0] %>" disabled="disabled" />
<input type="text" name="tag2" value="<%= post.tags[1] %>" disabled="disabled" />
<input type="text" name="tag3" value="<%= post.tags[2] %>" disabled="disabled" /><br />
正文:<br />
<textarea name="post" rows="20" cols="100"><%= post.post %></textarea><br />
<input type="submit" value="保存修改" />
</form>
<%- include footer %>

注意:这里我们设定了编辑时不能编辑文章的标题和标签。

现在,我们的博客就增加了标签和标签页的功能。

10

第10章 增加pv统计和留言统计

现在我们来给每篇文章增加 pv 统计和留言统计。

我们设定:在主页、用户页和文章页均显示 pv 统计和留言统计。

修改 post.js ,将:

var post = {
name: this.name,
time: time,
title:this.title,
tags: this.tags,
post: this.post,
comments: []
};

修改为:

var post = {
name: this.name,
time: time,
title:this.title,
tags: this.tags,
post: this.post,
comments: [],
pv: 0
};

注意:我们给要存储的文档添加了 pv 键并直接赋初值为 0。

打开 post.js ,将 Post.getOne() 修改为:

//获取一篇文章
Post.getOne = function(name, day, title, 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);
}
//根据用户名、发表日期及文章名进行查询
collection.findOne({
"name": name,
"time.day": day,
"title": title
}, function (err, doc) {
if (err) {
mongodb.close();
return callback(err);
}
if (doc) {
//每访问 1 次,pv 值增加 1
collection.update({
"name": name,
"time.day": day,
"title": title
}, {
$inc: {"pv": 1}
}, function (err) {
mongodb.close();
if (err) {
return callback(err);
}
});
//解析 markdown 为 html
doc.post = markdown.toHTML(doc.post);
doc.comments.forEach(function (comment) {
comment.content = markdown.toHTML(comment.content);
});
callback(null, doc);//返回查询的一篇文章
}
});
});
});
};

更多关于 $inc 的知识请参阅《mongodb权威指南》。

增加留言统计就简单多了,直接取 comments.length 即可。修改 index.ejs 、user.ejs 及 article.ejs ,在:

<p><%- post.post %></p>

下一行添加一行代码:

<p class="info">阅读:<%= post.pv %> | 评论:<%= post.comments.length %></p>

现在,我们给博客增加了 pv 统计和留言统计。

11

第11章 增加文章检索功能

现在我们来给博客增加文章检索功能,即根据关键字模糊查询文章标题,且字母不区分大小写。 首先,我们修改 header.ejs ,在 前添加一行代码:

<span><form action="/search" method="GET"><input type="text" name="keyword" placeholder="SEARCH" class="search" /></form></span>

在 style.css 中添加一行样式:

.search{border:0;width:6em;text-align:center;font-size:1em;margin:0.5em 0;}

打开 post.js ,在最后添加如下代码:

//返回通过标题关键字查询的所有文章信息
Post.search = function(keyword, callback) {
mongodb.open(function (err, db) {
if (err) {
return callback(err);
}
db.collection('posts', function (err, collection) {
if (err) {
mongodb.close();
return callback(err);
}
var pattern = new RegExp(keyword, "i");
collection.find({
"title": pattern
}, {
"name": 1,
"time": 1,
"title": 1
}).sort({
time: -1
}).toArray(function (err, docs) {
mongodb.close();
if (err) {
return callback(err);
}
callback(null, docs);
});
});
});
};

修改 index.js ,在 app.get('/u/:name') 前添加如下代码:

app.get('/search', function (req, res) {
Post.search(req.query.keyword, function (err, posts) {
if (err) {
req.flash('error', err);
return res.redirect('/');
}
res.render('search', {
title: "SEARCH:" + req.query.keyword,
posts: posts,
user: req.session.user,
success: req.flash('success').toString(),
error: req.flash('error').toString()
});
});
});

在 views 文件夹下新建 search.ejs ,添加如下代码:

<%- include header %>
<ul class="archive">
<% var lastYear = 0 %>
<% posts.forEach(function (post, index) { %>
<% if(lastYear != post.time.year) { %>
<li><h3><%= post.time.year %></h3></li>
<% lastYear = post.time.year } %>
<li><time><%= post.time.day %></time></li>
<li><a href="/u/<%= post.name %>/<%= post.time.day %>/<%= post.title %>"><%= post.title %></a></li>
<% }) %>
</ul>
<%- include footer %>

注意:目前为止,你会发现 tag.ejs 和 search.ejs 代码完全一样,因为我们都用相同的布局。这也突出了模版的优点之一 —— 可以重复利用,但我们这里并没有把这两个文件用一个代替,因为每一个文件的名字代表了不同的意义。

现在,我们给博客添加了文章检索功能。

12

第12章 增加友情链接

现在我们来给博客添加友情链接。

打开 header.ejs ,在:

<span><a title="标签" href="/tags">tags</a></span>

下一行添加一行代码:

<span><a title="友情链接" href="/links">links</a></span>

修改 index.js ,在 app.get('/search') 前添加如下代码:

app.get('/links', function (req, res) {
res.render('links', {
title: '友情链接',
user: req.session.user,
success: req.flash('success').toString(),
error: req.flash('error').toString()
});
});

在 views 文件夹下新建 links.ejs ,添加如下代码:

<%- include header %>
<ul style="list-style:none">
<li><h3><a href="https://love.alipay.com/donate/donateSingle.htm?name=201304201216494301">支付宝公益网</a></h3>天佑四川,为雅安地震捐款</li>
<li><h3><a href="http://www.onefoundation.cn/html/cn/beneficence.html">壹基金</a></h3>壹基金雅安地震救援,刻不容缓!</li>
</ul>
<%- include footer %>

现在,我们给博客添加了友情链接。

13

第13章 增加404页面

现在我们来给博客添加 404 页面,即当访问的路径都不匹配时,跳转到 404 页面。

打开 index.js ,在:

function checkLogin(req, res, next){ ... }

前添加如下代码:

app.use(function (req, res) {
res.render("404");
});

在 views 文件夹下新建 404.ejs ,添加如下代码:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Blog</title>
</head>
<body>
<script type="text/javascript" src="http://www.qq.com/404/search_children.js" charset="utf-8"></script>
</body>
</html>

现在,我们给博客添加了 404 页面。

14

第14章 增加头像

现在我们来给博客添加用户头像,注册的用户根据注册时的邮箱获取 gravatar 头像,未注册的用户则根据留言填的邮箱获取 gravatar 头像。 什么是 gravatar ?详情请戳:http://www.gravatar.com

我们设定:在主页和用户页的文章标题右侧显示作者头像,在文章页面留言的人的头像显示在留言板左侧。

我们需要用到 Node.js 中的 crypto 模块,之前已经引入过,所以这里可以直接使用。

添加已注册用户的头像

打开 user.js ,在最上面添加一行代码:

var crypto = require('crypto');

然后将 User.prototype.save 内的:

var user = {
name: this.name,
password: this.password,
email: this.email
};

修改为:

var md5 = crypto.createHash('md5'),
email_MD5 = md5.update(this.email.toLowerCase()).digest('hex'),
head = "http://www.gravatar.com/avatar/" + email_MD5 + "?s=48";
//要存入数据库的用户信息文档
var user = {
name: this.name,
password: this.password,
email: this.email,
head: head
};

这里我们在用户文档里添加了 head 键,方便后面使用。

注意:需要把 email 转化成小写再编码。

打开 index.js ,将 app.post('/post') 中的:

post = new Post(currentUser.name, req.body.title, tags, req.body.post);

修改成:

post = new Post(currentUser.name, currentUser.head, req.body.title, tags, req.body.post);

修改 post.js ,将:

function Post(name, title, tags, post) {
this.name = name;
this.title = title;
this.tags = tags;
this.post = post;
}

修改为:

function Post(name, head, title, tags, post) {
this.name = name;
this.head = head;
this.title = title;
this.tags = tags;
this.post = post;
}

将:

var post = {
name: this.name,
time: time,
title:this.title,
tags: this.tags,
post: this.post,
comments: [],
pv: 0
};

修改为:

var post = {
name: this.name,
head: this.head,
time: time,
title:this.title,
tags: this.tags,
post: this.post,
comments: [],
pv: 0
};

最后,修改 index.ejs 和 user.ejs ,在 后添加一行代码:

<a href="/u/<%= post.name %>"><img src="<%= post.head %>" class="r_head" /></a>

至此,我们实现了给已注册的用户添加头像的功能。

添加未注册用户的头像

修改 app.post('/u/:name/:day/:title'),将:

var comment = {
name: req.body.name,
email: req.body.email,
website: req.body.website,
time: time,
content: req.body.content
};

修改为:

var md5 = crypto.createHash('md5'),
email_MD5 = md5.update(req.body.email.toLowerCase()).digest('hex'),
head = "http://www.gravatar.com/avatar/" + email_MD5 + "?s=48";
var comment = {
name: req.body.name,
head: head,
email: req.body.email,
website: req.body.website,
time: time,
content: req.body.content
};

打开 comment.ejs ,将:

<% post.comments.forEach(function (comment, index) { %>
<p><a href="<%= comment.website %>"><%= comment.name %></a>
<span class="info"> 回复于 <%= comment.time %></span></p>
<p><%- comment.content %></p>
<% }) %>

修改为:

<% post.comments.forEach(function (comment, index) { %>
<div style="padding-left:4em">
<p><img src="<%= comment.head %>" class="l_head" /><a href="<%= comment.website %>"><%= comment.name %></a>
<span class="info"> 回复于 <%= comment.time %></span></p>
<p><%- comment.content %></p>
</div>
<% }) %>

最后,在 style.css 中添加两行样式:

.l_head{float:left;margin-left:-4em;box-shadow:0px 1px 4px #888;}
.r_head{float:right;margin-top:-2.5em;box-shadow:0px 1px 4px #888;}

现在,我们给博客添加了头像的功能。

15

第15章 增加转载功能和转载统计

现在,我们来给博客添加转载文章和转载数统计的功能。

我们的设计思路是:当在线用户满足特定条件时,文章页面才会显示 转载 链接字样,当用户点击 转载 后,复制一份存储当前文章的文档,修改后以新文档的形式存入数据库,而不是单纯的添加一条指向被转载的文档的 "引用" ,这种设计是合理的,因为这样我们也可以将转载来的文章进行修改。

首先,我们来完成转载文章的功能。

打开 post.js ,将 Post.prototype.save 内的:

var post = {
name: this.name,
head: this.head,
time: time,
title:this.title,
tags: this.tags,
post: this.post,
comments: [],
pv: 0
};

修改为:

var post = {
name: this.name,
head: this.head,
time: time,
title:this.title,
tags: this.tags,
post: this.post,
comments: [],
reprint_info: {},
pv: 0
};

这里我们给文档里添加了 reprint_info 键,最多为以下形式:

{
reprint_from: {name: xxx, day: xxx, title: xxx},
reprint_to: [
{name: xxx, day: xxx, title: xxx},
{name: xxx, day: xxx, title: xxx},
...
]
}

reprint_from 表示转载来的原文章的信息,reprint_to 表示该文章被转载的信息。为了节约存储空间,我们初始设置 reprint_info 键为 {},而不是以下形式:

{
reprint_from: {},
reprint_to: []
}

这是因为大多数文章是没有经过任何转载的,所以为每个文档都添加以上形式的 reprint_info 是有点浪费的。假如某篇文章是转载来的,我们只需给 reprint_info 添加上 reprint_from 键即可,假如某篇文章被转载了,我们只需给 reprint_info 添加上 reprint_to 键即可,假如某篇文章是转载来的且又被转载了,那我们就给 reprint_info 添加上 reprint_from 和 reprint_to 键即可。

打开 post.js ,在最后添加如下代码:

//转载一篇文章
Post.reprint = function(reprint_from, reprint_to, callback) {
mongodb.open(function (err, db) {
if (err) {
return callback(err);
}
db.collection('posts', function (err, collection) {
if (err) {
mongodb.close();
return callback(err);
}
//找到被转载的文章的原文档
collection.findOne({
"name": reprint_from.name,
"time.day": reprint_from.day,
"title": reprint_from.title
}, function (err, doc) {
if (err) {
mongodb.close();
return callback(err);
}
 
var date = new Date();
var time = {
date: date,
year : date.getFullYear(),
month : date.getFullYear() + "-" + (date.getMonth() + 1),
day : date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate(),
minute : date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate() + " " +
date.getHours() + ":" + (date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes())
}
 
delete doc._id;//注意要删掉原来的 _id
 
doc.name = reprint_to.name;
doc.head = reprint_to.head;
doc.time = time;
doc.title = (doc.title.search(/[转载]/) > -1) ? doc.title : "[转载]" + doc.title;
doc.comments = [];
doc.reprint_info = {"reprint_from": reprint_from};
doc.pv = 0;
 
//更新被转载的原文档的 reprint_info 内的 reprint_to
collection.update({
"name": reprint_from.name,
"time.day": reprint_from.day,
"title": reprint_from.title
}, {
$push: {
"reprint_info.reprint_to": {
"name": doc.name,
"day": time.day,
"title": doc.title
}}
}, function (err) {
if (err) {
mongodb.close();
return callback(err);
}
});
 
//将转载生成的副本修改后存入数据库,并返回存储后的文档
collection.insert(doc, {
safe: true
}, function (err, post) {
mongodb.close();
if (err) {
return callback(err);
}
callback(err, post[0]);
});
});
});
});
};

这里我们在 Post.reprint() 内实现了被转载的原文章的更新和转载后文章的存储。

打开 index.js ,在 app.get('/remove/:name/:day/:title') 后添加如下代码:

app.get('/reprint/:name/:day/:title', checkLogin);
app.get('/reprint/:name/:day/:title', function (req, res) {
Post.edit(req.params.name, req.params.day, req.params.title, function (err, post) {
if (err) {
req.flash('error', err);
return res.redirect(back);
}
var currentUser = req.session.user,
reprint_from = {name: post.name, day: post.time.day, title: post.title},
reprint_to = {name: currentUser.name, head: currentUser.head};
Post.reprint(reprint_from, reprint_to, function (err, post) {
if (err) {
req.flash('error', err);
return res.redirect('back');
}
req.flash('success', '转载成功!');
var url = encodeURI('/u/' + post.name + '/' + post.time.day + '/' + post.title);
//跳转到转载后的文章页面
res.redirect(url);
});
});
});

至此,我们给 转载 链接注册了路由响应。

注意:我们需要通过 Post.edit() 返回一篇文章 markdown 格式的文本,而不是通过 Post.getOne 返回一篇转义后的 HTML 文本,因为我们还要将修改后的文档存入数据库,而数据库中应该存储 markdown 格式的文本。

最后,我们在文章页(article.ejs)添加转载链接。

打开 article.ejs ,在:

<% if (user && (user.name == post.name)) { %>
<span><a class="edit" href="/edit/<%= post.name %>/<%= post.time.day %>/<%= post.title %>">编辑</a></span>
<span><a class="edit" href="/remove/<%= post.name %>/<%= post.time.day %>/<%= post.title %>">删除</a></span>
<% } %>

后添加如下代码:

<% var flag = 1 %>
<% if (user && (user.name != post.name)) { %>
<% if ((post.reprint_info.reprint_from != undefined) && (user.name == post.reprint_info.reprint_from.name)) { %>
<% flag = 0 %>
<% } %>
<% if ((post.reprint_info.reprint_to != undefined)) { %>
<% post.reprint_info.reprint_to.forEach(function (reprint_to, index) { %>
<% if (user.name == reprint_to.name) { %>
<% flag = 0 %>
<% } %>
<% }) %>
<% } %>
<% } else { %>
<% flag = 0 %>
<% } %>
<% if (flag) { %>
<span><a class="edit" href="/reprint/<%= post.name %>/<%= post.time.day %>/<%= post.title %>">转载</a></span>
<% } %>

以上代码的意思是:我们设置一个 flag 标志,如果用户是游客,或者是该文章的目前作者,或者是该文章的上一级作者,或者已经转载过该文章,都会将 flag 设置为 0 ,即不显示 转载 链接,即不能转载该文章。最后判断 flag 为 1 时才会显示 转载 链接,即才可以转载这篇文章。

最后,我们需要添加一个 原文链接 来指向被转载的文章。 打开 index.ejs 、user.ejs 、article.ejs,在第一个

<

p class="info"> 里最后添加如下代码:

<% if (post.reprint_info.reprint_from) { %>
<br><a href="/u/<%= post.reprint_info.reprint_from.name %>/<%= post.reprint_info.reprint_from.day %>/<%= post.reprint_info.reprint_from.title %>">原文链接</a>
<% } %>

现在我们就给博客添加了转载功能。

接下来我们添加转载统计。 添加转载统计就简单多了,我们只需使用 reprint_info.reprint_to.length 即可。

打开 index.ejs 、user.ejs 、article.ejs ,将:

<p class="info">阅读:<%= post.pv %> | 评论:<%= post.comments.length %></p>

修改为:

<p class="info">
阅读:<%= post.pv %> |
评论:<%= post.comments.length %> |
转载:
<% if (post.reprint_info.reprint_to) { %>
<%= post.reprint_info.reprint_to.length %>
<% } else { %>
<%= 0 %>
<% } %>
</p>

现在我们就给博客添加了转载统计的功能。但工作还没有完成,假如我们要删除一篇转载来的文章时,还要将被转载的原文章所在文档的 reprint_to 删除遗留的转载信息。

打开 post.js ,将 Post.remove 修改为:

//删除一篇文章
Post.remove = function(name, day, title, 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);
}
//查询要删除的文档
collection.findOne({
"name": name,
"time.day": day,
"title": title
}, function (err, doc) {
if (err) {
mongodb.close();
return callback(err);
}
//如果有 reprint_from,即该文章是转载来的,先保存下来 reprint_from
var reprint_from = "";
if (doc.reprint_info.reprint_from) {
reprint_from = doc.reprint_info.reprint_from;
}
if (reprint_from != "") {
//更新原文章所在文档的 reprint_to
collection.update({
"name": reprint_from.name,
"time.day": reprint_from.day,
"title": reprint_from.title
}, {
$pull: {
"reprint_info.reprint_to": {
"name": name,
"day": day,
"title": title
}}
}, function (err) {
if (err) {
mongodb.close();
return callback(err);
}
});
}
 
//删除转载来的文章所在的文档
collection.remove({
"name": name,
"time.day": day,
"title": title
}, {
w: 1
}, function (err) {
mongodb.close();
if (err) {
return callback(err);
}
callback(null);
});
});
});
});
};

注意:我们使用了 $pull 来删除数组中的特定项,关于 $pull 的详细使用请查阅 《MongoDB 权威指南》。

至此,我们给博客添加了转载功能和转载统计。

16

第16章 增加日志功能

现在我们来给博客增加日志,实现访问日志(access.log)和错误日志(error.log)功能。

前面我们介绍过,使用 Express 自带的 logger 中间件实现了终端日志的输出:

app.use(logger('dev'));

那我们想把日志保存为日志文件该怎么办呢?很简单,我们只需在以上代码的下一行添加:

app.use(logger({stream: accessLog}));

并在 var app = express(); 之前添加如下代码即可:

var fs = require('fs');
var accessLog = fs.createWriteStream('access.log', {flags: 'a'});
var errorLog = fs.createWriteStream('error.log', {flags: 'a'});

这样,我们每一次访问的请求信息,不仅显示在了命令行中,也都保存在了工程根目录下的 access.log 文件里。但 Express 并没有提供记录错误日志的功能,所以我们需自己写一个简单的中间件,在 app.use(express.static(path.join(__dirname, 'public'))); 下一行添加如下代码 :

app.use(function (err, req, res, next) {
var meta = '[' + new Date() + '] ' + req.url + '\n';
errorLog.write(meta + err.stack + '\n');
next();
});

这样,当有错误发生时,就将错误信息保存到了根目录下的 error.log 文件夹里。

至此,我们就给博客添加了日志的功能。

17

番外篇之——使用 _id 查询

我们知道,MongoDB 会自动为每个文档添加一个特殊的 _id 键,这个 _id 键的值是经过特殊计算的长度为 24 的字符串的 ObjectId 对象(详见《MongoDB 权威指南》),因此保证了每个文档的 _id 都是独一无二的。那我们可不可以使用 _id 键查询一个独一无二的文档呢?当然可以,这也是设计 _id 的原因所在。

注意:使用 name 、day 、title 查询一篇文章有个小 bug ,即不能在同一天发表相同标题的文章,或者说发表了相同标题的文章后只能返回最近发表的那篇文章。使用 _id 就可以很好的避免这个 bug 。

下面我们举例使用 _id 代替使用 name 、day 、title 来查询一篇文章,即将:

app.get('/u/:name/:day/:title')

修改为以下形式:

app.get('/p/:_id')

打开 post.js ,在最上面添加:

var ObjectID = require('mongodb').ObjectID;

将:

Post.getOne = function(name, day, title, callback) {

修改为:

Post.getOne = function(_id, callback) {

并将 Post.getOne() 内两处的:

"name": name,
"time.day": day,
"title": title

都修改为:

"_id": new ObjectID(_id)

打开 index.js ,将 app.get('/u/:name/:day/:title') 修改如下:

app.get('/p/:_id', function (req, res) {
Post.getOne(req.params._id, function (err, post) {
if (err) {
req.flash('error', err);
return res.redirect('/');
}
res.render('article', {
title: post.title,
post: post,
user: req.session.user,
success: req.flash('success').toString(),
error: req.flash('error').toString()
});
});
});

注意:我们将文章页面的路由修改为 app.get('/p/:_id') 而不是 app.get('/u/:_id') 是为了防止和上面的用户页面的路由 app.get('/u/:name') 冲突,况且,p 也代表 post ,表示发表的文章的意思。

打开 index.ejs ,将:

<p><h2><a href="/u/<%= post.name %>/<%= post.time.day %>/<%= post.title %>"><%= post.title %></a></h2>

修改为:

<p><h2><a href="/p/<%= post._id %>"><%= post.title %></a></h2>

现在,运行你的博客并发表一篇文章,从主页点击标题进入该文章页面,就变成了以下的 url 形式:

http://localhost:3000/p/52553dcd5bb408ec11000002

注意:MongoDB 数据库中是以以下形式存储 _id 的:

"_id" : ObjectId("52553dcd5bb408ec11000002")

我们可以直接使用 post._id 从数据库中获取 _id 的值(24 位长字符串),但在查询的时候,要把 _id 字符串包装成 MongoDB 特有的 ObjectId 类型。

读者可依此类推,自行将剩余的工作完成。

18

番外篇之——使用 Async

Async 是一个流行的异步编程类库,提供了直接而强大的 JavaScript 异步功能。虽然是为 Node.js 设计的,但是它也可以直接在浏览器中使用。

Async 提供了大约 20 个函数,包括常用的 map, reduce, filter, forEach 等等,也有常用的异步流程控制函数,包括 parallel, series, waterfall 等等。所有这些函数都是假设你遵循了 Node.js 的约定:在异步函数的最后提供一个回调函数作为参数。

Async 包括三部分:

  1. 流程控制:简化十种常见流程的处理
  2. 集合处理:如何使用异步操作处理集合中的数据
  3. 工具类:几个常用的工具类 这里我们不会讲解 Async 的使用,读者可去以下链接学习 Async 的相关知识:

Async : https://github.com/caolan/async stackoverflow : http://stackoverflow.com/ Async详解之一:流程控制 : http://freewind.me/blog/20120515/917.html Async详解之二:工具类 : http://freewind.me/blog/20120517/931.html Async详解之三:集合操作 : http://freewind.me/blog/20120518/932.html Nodejs异步流程控制Async : http://blog.fens.me/nodejs-async/

我们在操作数据库的时候经常会这样写,以 Post.getOne 为例:

Post.getOne = function(name, day, title, callback) {
mongodb.open(function (err, db) {
if (err) { ... }
db.collection('posts', function (err, collection) {
if (err) { ... }
collection.findOne({ ... }, function (err, doc) {
if (err) { ... }
collection.update({ ... }, function (err) {
mongodb.close();
callback( ... );
});
});
});
});
};

这就是典型的深度嵌套回调,代码看起来并不美观。下面我们使用 Async 解决这个问题。

首先,在 package.json 中添加对 Async 的依赖:

"async": "*"

并 npm install 安装 Async 包。

在使用 Async 之前,我们先学习下 async.waterfall 的基本用法。

waterfall(tasks, [callback]) :多个函数依次执行,且前一个的输出为后一个的输入,即每一个函数产生的值,都将传给下一个函数。如果中途出错,后面的函数将不会被执行。错误信息以及之前产生的结果,将传给 waterfall 最终的 callback,一个简单的例子:

var async = require('async');
 
async.waterfall([
function(callback){
callback(null, 'one', 'two');
},
function(arg1, arg2, callback){
console.log('arg1 => ' + arg1);
console.log('arg2 => ' + arg2);
callback(null, 'three');
},
function(arg3, callback){
console.log('arg3 => ' + arg3);
callback(null, 'done');
}
], function (err, result) {
console.log('err => ' + err);
console.log('result => ' + result);
});

运行结果为:

arg1 => one
arg2 => two
arg3 => three
err => null
result => done

将 callback(null, 'three'); 修改为:

callback('error occurred !', 'three');

运行结果为:

arg1 => one
arg2 => two
err => error occurred !
result => three

我们以修改 user.js 为例,将 user.js 修改如下:

var mongodb = require('./db');
var crypto = require('crypto');
var async = require('async');
 
function User(user) {
this.name = user.name;
this.password = user.password;
this.email = user.email;
};
 
module.exports = User;
 
User.prototype.save = function(callback) {
var md5 = crypto.createHash('md5'),
email_MD5 = md5.update(this.email.toLowerCase()).digest('hex'),
head = "http://www.gravatar.com/avatar/" + email_MD5 + "?s=48";
var user = {
name: this.name,
password: this.password,
email: this.email,
head: head
};
async.waterfall([
function (cb) {
mongodb.open(function (err, db) {
cb(err, db);
});
},
function (db, cb) {
db.collection('users', function (err, collection) {
cb(err, collection);
});
},
function (collection, cb) {
collection.insert(user, {
safe: true
}, function (err, user) {
cb(err, user);
});
}
], function (err, user) {
mongodb.close();
callback(err, user[0]);
});
};
 
User.get = function(name, callback) {
async.waterfall([
function (cb) {
mongodb.open(function (err, db) {
cb(err, db);
});
},
function (db, cb) {
db.collection('users', function (err, collection) {
cb(err, collection);
});
},
function (collection, cb) {
collection.findOne({
name: name
}, function (err, user) {
cb(err, user);
});
}
], function (err, user) {
mongodb.close();
callback(err, user);
});
};

关于 Async 的使用详见 https://github.com/caolan/async ,读者可自行完成剩余的修改工作。

19

番外篇之——使用 Disqus

前面我们搭建的博客使用了自建的留言系统,支持 Markdown ,并且将留言存到了数据库中。现在我们来使用 Disqus 代替原来的留言系统。

什么是 Disqus?

Disqus 是一个第三方社会化评论系统,主要为网站主提供评论托管服务。CNN、NBC、Fox News、Engadget、Time 等知名网站均使用了 Disqus 提供的社会化评论系统。WordPress、Blogger、Tumblr 等第三方博客平台均提供了 Disqus 第三方评论插件。目前,第三方社会化评论系统在美国,基本是主流网站的标配。

Disqus 的主要目标是通过提供功能强大的第三评论系统,将当前不同网站的相对孤立、隔绝的评论系统,连接成具有社会化特性的大网。通过 Disqus 评论系统所具备的评论回复通知、评论分享和热文分享等社会化功能,网站主可以有效的提高网站用户的活跃度和流量。用户使用 Disqus,在不同网站上评论,无需重复注册账号,只需使用 Disqus 账号或者第三方平台账号,即可方便的进行评论,且所有评论都会存储、保存在 Disqus 账号后台,方便随时查看、回顾。而且,当有用户回复自己的评论时,可以选择使用邮箱接收相关信息,保证所有评论的后续行为都可以随时掌握。与此同时,Disqus 将社交交友功能也很好的融入到了评论系统中,当用户在某一网站上看到有与自己类似观点的评论时,可对该评论的评论者进行关注,关注后,该评论者以后的所有评论都会显示在自己的账号后台。

为什么使用 Disqus?

  • 相比较使用自建的留言系统,使用 Disqus 有以下几点优势:
  • 支持评论嵌套
  • 支持使用 Disqus 或第三方账号评论
  • 简单安全。不用存储到自己的数据库,安全性也得到提高
  • 方便并且强大的评论管理功能
  • 集成良好,自适应,简洁优美
  • 等等

注册 Disqus

https://disqus.com/profile/signup/

使用 Disqus

使用 Disqus 非常简单!

第一步:登陆后进入到 http://disqus.com/dashboard/ 页面,点击左侧的 +add 按钮创建一个站点,填写好信息后点击 Finish registration 完成创建。

第二步:此时进入到了 Choose your platform 页面。这里根据我们的实际情况点击第一个 Universal Code 按钮。

第三步:此时进入到了 Disqus 安装说明页。这里有详细的说明步骤,我们这里只需复制第一个代码块中的代码。然后打开 comment.ejs ,删除所有代码并粘贴刚才复制的代码,保存即可。

现在运行我们的博客,发表篇文章试试吧,如下图所示:

5.1.jpeg

读者可自行删除有关存储评论的代码,这里不再赘述。

参考文献

Disqus 百度百科 : http://baike.baidu.com/view/5941866.htm

20

番外篇之——使用 generic pool

目前为止,我们都是这样处理请求的,比如:当用户访问某个文章页的时候,系统会创建一个数据库连接,通过该连接到数据库中查找并返回该文章的数据,然后关闭该连接。但是当我们的博客访问量巨大的时候,频繁的创建和销毁连接会产生非常大的系统开销。这个时候,我们就需要引入数据库连接池了。

什么是连接池(connection pool)呢?维基百科中是这样定义的:

connection pool is a cache of database connections maintained so that the connections can be reused when future requests to the database are required.

说白了就是,我们一开始就创建一沓数据库连接,并保持长连不断开。当我们需要访问数据库的时候,就去那一沓连接(俗称连接池)中拿来一个用,用完(对数据库增删改查完)后再把这条连接释放到连接池中(依然不断开)。这样我们只在一开始创建一沓数据库连接时会有一些开销,而这种开销总比频繁的创建和销毁连接小得多。

在 Node.js 中,我们可以使用 generic-pool 这个模块帮助我们创建和管理数据库连接池。

首先,在 package.json 中添加对 generic-pool 的依赖:

"generic-pool": "*"

并 npm install 安装 generic-pool 模块。

打开 db.js ,将:

module.exports = new Db(settings.db, new Server(settings.host, settings.port), {safe: true});

修改为:

module.exports = function() {
return new Db(settings.db, new Server(settings.host, settings.port), {safe: true, poolSize: 1});
}

这里我们导出一个函数,每次调用该函数则创建一个数据库连接。

打开 post.js ,将:

var mongodb = require('./db'),
markdown = require('markdown').markdown;

修改为:

var Db = require('./db');
var markdown = require('markdown').markdown;
var poolModule = require('generic-pool');
var pool = poolModule.Pool({
name : 'mongoPool',
create : function(callback) {
var mongodb = Db();
mongodb.open(function (err, db) {
callback(err, db);
})
},
destroy : function(mongodb) {
mongodb.close();
},
max : 100,
min : 5,
idleTimeoutMillis : 30000,
log : true
});

以上就创建了一个 mongodb 连接池,其中 name 指明该连接池的名字,create 指明创建一条数据库连接的方法,并返回创建的连接,destroy 指明如何销毁连接,max 指明连接池中最大连接数,min 指明连接池中最小连接数,idleTimeoutMillis 指明不活跃连接销毁的毫秒数,这里为 30000 即当一条连接 30 秒处于不活跃状态(即没有被使用过)时则销毁该连接。log 指明是否打印连接池日志,这里我们选择打印。

如何使用连接池呢?很简单。只需将所有:

mongodb.open(function (err, db) {
...
mongodb.close();
});

修改为:

pool.acquire(function (err, mongodb) {
...
pool.release(mongodb);
});

这里我们使用 pool.acquire 去连接池中获取一条可用连接,使用完毕后通过 pool.release 释放该连接,而不是 close 掉。

读者可自行完成剩余的修改工作。

21

番外篇之——使用 Handlebars

前面我们在 Express 中使用的 EJS 模板引擎进行渲染视图和页面的展示。当模版文件代码比较多且逻辑复杂时,代码就变得非常难看了,满眼的 <% 和 %>。下面我们尝试使用 Handlebars 这个模版引擎替换 EJS ,代码会变得整洁许多。

Handlebars 是 JavaScript 一个语义模板库,通过对 view 和 data 的分离来快速构建 Web 模板。它采用 "Logic-less template"(无逻辑模版)的思路,在加载时被预编译,而不是到了客户端执行到代码时再去编译,这样可以保证模板加载和运行的速度。Handlebars 兼容 Mustache,你可以在 Handlebars 中导入 Mustache 模板。

Handlebars 的语法也非常简单易学。这里我们不会讲解 Handlebars 的语法,官网( http://handlebarsjs.com/ )的文档非常全面。

我们使用 express-handlebars 这个第三方包添加对 Handlebars 的支持。

注意:也许你会非常自觉的认为应该使用 npm install handlebars 安装 Handlebars 然后开始大刀阔斧地修改代码。但在这里我们不使用官方提供的 Handlebars 包,Express 默认支持的模板引擎中不包含 Handlebars ,虽然我们可以通过 consolidate.js + handlebars 实现,但仍然有一个缺点是不支持从一个模版文件加载另一个模版文件,而在 EJS 中可以使用 <%- include someTemplate %> 轻松实现。express-handlebars 包弥补了该缺点,所以我们使用 express-handlebars 来完成代码的修改。

首先,打开 package.json ,删除 ejs 并添加对 express-handlebars 的依赖:

"express-handlebars": "*"

并 npm install 安装 express-handlebars 包。

打开 app.js ,添加一行:

var exphbs = require('express-handlebars');

然后将:

app.set('view engine', 'ejs');

修改为:

app.engine('hbs', exphbs({
layoutsDir: 'views',
defaultLayout: 'layout',
extname: '.hbs'
}));
app.set('view engine', 'hbs');

这里我们注册模板引擎处理后缀名为 hbs 的文件,然后通过 app.set('view engine', 'hbs'); 设置模板引擎。以上参数的意思是:

  • layoutsDir: 'views': 设置布局模版文件的目录为 views 文件夹
  • defaultLayout: 'layout': 设置默认的页面布局模版为 layout.hbs 文件,跟 Express 2.x 中的 layout.ejs 作用类似。
  • extname: '.hbs': 模版文件使用的后缀名,这个 .hbs 是我们自定的,我们当然可以使用 .html 和 .handlebars 等作为后缀,只需把以上的 hbs 全部替换即可。

我们以修改主页为例,学习如何使用 Handlebars 。为了测试修改后能否正常显示文章及其相关信息,在开始之前,我们先注册几个用户并发表几篇文章,然后进行一些互相转载、访问和留言等工作,而不是清空数据库。

然后打开 views 文件夹,删除 header.ejs 和 footer.ejs ,新建 layout.hbs ,添加如下代码:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Blog</title>
<link rel="stylesheet" href="/stylesheets/style.css">
</head>
<body>
 
<header>
<h1>{{title}}</h1>
</header>
 
<nav>
<span><a title="主页" href="/">home</a></span>
<span><a title="存档" href="/archive">archive</a></span>
<span><a title="标签" href="/tags">tags</a></span>
<span><a title="友情链接" href="/links">links</a></span>
{{#if user}}
<span><a title="上传" href="/upload">upload</a></span>
<span><a title="发表" href="/post">post</a></span>
<span><a title="登出" href="/logout">logout</a></span>
{{else}}
<span><a title="登录" href="/login">login</a></span>
<span><a title="注册" href="/reg">register</a></span>
{{/if}}
<span><form action="/search" method="GET"><input type="text" name="keyword" placeholder="SEARCH" class="search" /></form></span>
</nav>
 
<article>
 
{{#if success}}
<div>{{success}}</div>
{{/if}}
{{#if error}}
<div>{{error}}</div>
{{/if}}
 
{{{body}}}
 
</article>
</body>
</html>

这里我们定义了一个默认的页面布局模版(layout.hbs)。其余所有的模版都将 "继承" 该模版,即替换掉 {{{body}}} 部分。

删除 index.ejs ,新建 index.hbs ,添加如下代码:

{{#each posts}}
<p><h2><a href="/u/{{name}}/{{time.day}}/{{title}}">{{title}}</a></h2>
<a href="/u/{{name}}"><img src="{{head}}" class="r_head" /></a></p>
<p class="info">
作者:<a href="/u/{{name}}">{{name}}</a> |
日期:{{time.minute}} |
标签:
{{#each tags}}
{{#if this}}
<a class="tag" href="/tags/{{this}}">{{this}}</a>
{{/if}}
{{/each}}
{{#if reprint_info.reprint_from}}
<br><a href="/u/{{reprint_info.reprint_from.name}}/{{reprint_info.reprint_from.day}}/{{reprint_info.reprint_from.title}}">原文链接</a>
{{/if}}
</p>
<p>{{{post}}}</p>
<p class="info">
阅读:{{pv}} |
评论:{{comments.length}} |
转载:
{{#if reprint_info.reprint_to}}
{{reprint_info.reprint_to.length}}
{{else}}
0
{{/if}}
</p>
{{/each}}

这样就可以了,现在运行你的博客试试吧。

当我们渲染 index.hbs (res.render('index', { ... });)时,index.hbs 会替换 layout.hbs 中的 {{{body}}} 部分,然后渲染视图。需要注意的是,我们在 {{#each}} ... {{/each}} 中使用了 this ,这里的 this 指向当前上下文,即代表遍历的每一项。

注意:Handlebars 中的 {{{htmlContext}}},相当于 EJS 中的 <%- htmlContext %> ,{{textContext}} 相当于 <%= textContext %> 。

在 ejs 中,我们可以随意使用 JavaScript 表达式,如 <% if (1 + 1 === 2) { %> ... <% } %> ,但在 Handlebars 中我们却不能这样写 {{#if (1 + 1 === 2)}} ... {{/if}} ,那么该如何修改 archive.ejs 呢?archive.ejs 代码如下:

<%- include header %>
<ul class="archive">
<% var lastYear = 0 %>
<% posts.forEach(function (post, index) { %>
<% if (lastYear != post.time.year) { %>
<li><h3><%= post.time.year %></h3></li>
<% lastYear = post.time.year } %>
<li><time><%= post.time.day %></time></li>
<li><a href="/u/<%= post.name %>/<%= post.time.day %>/<%= post.title %>"><%= post.title %></a></li>
<% }) %>
</ul>
<%- include footer %>

我们通过定义了一个 lastYear 变量实现了判断并只显示一次年份的功能。在 Handlebars 中,我们可以通过 registerHelper 实现以上功能,关于 registerHelper 的使用详见 http://handlebarsjs.com/block_helpers.html。在 express-handlebars 中使用 registerHelper 也很简单,具体如下。

打开 index.js ,将 app.get('/archive') 修改如下:

app.get('/archive', function (req, res) {
Post.getArchive(function (err, posts) {
if (err) {
req.flash('error', err);
return res.redirect('/');
}
res.render('archive', {
title: '存档',
posts: posts,
user: req.session.user,
success: req.flash('success').toString(),
error: req.flash('error').toString(),
helpers: {
showYear: function(index, options) {
if ((index == 0) || (posts[index].time.year != posts[index - 1].time.year)) {
return options.fn(this);
}
}
}
});
});
});

删除 archive.ejs ,新建 archive.hbs ,添加如下代码:

<ul class="archive">
{{#each posts}}
{{#showYear @index}}
<li><h3>{{this.time.year}}</h3></li>
{{/showYear}}
<li><time>{{this.time.day}}</time></li>
<li><a href="/u/{{this.post.name}}/{{this.time.day}}/{{this.title}}">{{this.title}}</a></li>
{{/each}}
</ul>

假如你了解如何使用 Handlebars 中的 registerHelper ,那么上面的代码就很容易理解了。其中,{{#each}} ... {{/each}} 内的 @index 表示当前遍历的索引。

最后,还需提醒的一点是:我们每次渲染一个视图文件时,都会结合 layout.hbs 然后渲染,有时候我们并不需要 layout.hbs ,比如 404 页面,需设置为:

res.render('404', {
layout: false
});

通过设置 layout: false 就取消了自动加载 layout.hbs 页面布局模版。

至此,我们通过采用 layout 的方式实现了视图文件的加载及渲染,express-handlebars 还提供了另一种类似于 EJS 中 include 的加载方式——使用 partial ,前面的修改中我们并没有添加分页模版(paging.hbs),要想引入分页模版使用 {{> paging}} 即可。详细使用见 https://github.com/ericf/express-handlebars。

读者可自行完成剩余的修改工作。

22

番外篇之——使用 KindEditor

前面我们搭建的博客使用了 Markdown 来写文章,假如普通用户使用的话不懂什么是 Markdown ,加之 Markdown 的表现力还并不是很丰富。这个时候,我们就需要一款强大的编辑器了,我们不妨试试 KindEditor。

什么是 KindEditor

KindEditor 是一套开源的在线 HTML 编辑器,主要用于让用户在网站上获得所见即所得编辑效果,开发人员可以用 KindEditor 把传统的多行文本输入框(textarea)替换为可视化的富文本输入框。KindEditor 使用 JavaScript 编写,可以无缝地与 Java、.NET、PHP、ASP 等程序集成,比较适合在 CMS、商城、论坛、博客、Wiki、电子邮件等互联网应用上使用。

主要特点

快速:体积小,加载速度快 开源:开放源代码,高水平,高品质 底层:内置自定义 DOM 类库,精确操作 DOM 扩展:基于插件的设计,所有功能都是插件,可根据需求增减功能 风格:修改编辑器风格非常容易,只需修改一个 CSS 文件 兼容:支持大部分主流浏览器,比如 IE、Firefox、Safari、Chrome、Opera

使用 KindEditor

到官网 http://www.kindsoft.net/ 下载最新的 KindEditor 压缩包,解压后将文件夹重命名为 kindEditor 并放到 public 文件夹下。

注意:可以根据自己需求删除文件夹或文件,我们删除以下文件夹:

  • asp - ASP程序
  • asp.net - ASP.NET程序
  • php - PHP程序
  • jsp - JSP程序
  • examples - 演示文件

首先,我们来将多行文本输入框(textarea)替换为 kindEditor 编辑器。打开 header.ejs ,在:

<link rel="stylesheet" href="/stylesheets/style.css">

下一行添加如下代码:

<script charset="utf-8" src="/KindEditor/kindeditor-min.js"></script>
<script charset="utf-8" src="/KindEditor/lang/zh_CN.js"></script>
<script>
var editor;
KindEditor.ready(function(K) {
editor = K.create('textarea', {
allowImageUpload : false,
items : [
'fontname', 'fontsize', '|', 'forecolor', 'hilitecolor', 'bold', 'italic',
'underline', 'removeformat', '|', 'justifyleft', 'justifycenter', 'justifyright',
'insertorderedlist', 'insertunorderedlist', '|', 'emoticons', 'image', 'link']
});
});
</script>

注意:这里我们通过 create 创建了一个编辑器,第一个参数为 CSS 选择器,设置为 textarea ,则发表、编辑及留言的 textarea 都会变为编辑器。假如我们只想让发表和编辑时使用编辑器,留言时不使用编辑器,则只需将 textarea 修改为 textarea[name="post"] 即可。第二个参数可以设置编辑器的编辑选项,这里我们通过自定义 items 配置编辑器的工具栏,其中可用 "/" 表示换行,"|" 表示分隔符。,并设置 allowImageUpload : false 取消编辑器的图片上传按钮。详细的编辑器配置请查阅 http://www.kindsoft.net/docs/option.html。

以上是简单的(simple)编辑器样式,我们也可以使用 KindEditor 默认的(default)编辑器样式,将以上 KindEditor.ready 替换为以下代码即可(这里我们不做修改):

var editor;
KindEditor.ready(function(K) {
editor = K.create('#kindeditor');
});

最后,删除有关转换 Markdown 的代码。打开 post.js ,删除:

markdown = require('markdown').maparkdown

删除 Post.getTen 内的:

docs.forEach(function (doc) {
doc.post = markdown.toHTML(doc.post);
});

删除 Post.getOne 内的:

doc.post = markdown.toHTML(doc.post);
doc.comments.forEach(function (comment) {
comment.content = markdown.toHTML(comment.content);
});

现在,运行你的博客试试吧。

发表前

6.1.jpg

发表后

6.2.jpg

注意:添加图片地址时,引用站外的图片要用绝对地址,引用站内的图片则用相对地址,如:/images/lufei.jpg 。

更多关于 KindEditor 的使用详见官方文档。

参考文献

  • KindEditor : http://www.kindsoft.net/
  • 可视化HTML编辑器 KindEditor : http://www.oschina.net/p/kindeditor/

23

番外篇之——使用 Mongoose

Mongoose 是 MongoDB 数据库的模型工具,为 Node.js 设计,工作于异步环境下,基于 node-mongodb-native。

与使用 node-mongodb-native 相比,使用 Mongoose 可以简化不少代码。这里我们不会讲解 Mongoose 的使用,读者可去以下链接学习 Mongoose 的相关知识:

首先,在 package.json 中添加对 mongoose 的依赖:

"mongoose": "*"

并 npm install 安装 mongoose 包。

注意:完全使用 mongoose 的话可以删除 mongodb 模块,但我们这里只是局部使用 mongoose ,所以暂时保留。

修改 user.js 如下:

var crypto = require('crypto');
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/blog');
 
var userSchema = new mongoose.Schema({
name: String,
password: String,
email: String,
head: String
}, {
collection: 'users'
});
 
var userModel = mongoose.model('User', userSchema);
 
function User(user) {
this.name = user.name;
this.password = user.password;
this.email = user.email;
};
 
User.prototype.save = function(callback) {
var md5 = crypto.createHash('md5'),
email_MD5 = md5.update(this.email.toLowerCase()).digest('hex'),
head = "http://www.gravatar.com/avatar/" + email_MD5 + "?s=48";
var user = {
name: this.name,
password: this.password,
email: this.email,
head: head
};
 
var newUser = new userModel(user);
 
newUser.save(function (err, user) {
if (err) {
return callback(err);
}
callback(null, user);
});
};
 
User.get = function(name, callback) {
userModel.findOne({name: name}, function (err, user) {
if (err) {
return callback(err);
}
callback(null, user);
});
};
 
module.exports = User;

注意:Mongoose 会自动为每一个文档添加一个 __v 即 versionKey (版本锁),如下所示:

> db.users.find()
{ "name" : "nswbmw", "password" : "d41d8cd98f00b204e9800998ecf8427e", "email" :
"gxqzk@126.com", "head" : "http://www.gravatar.com/avatar/11c35a5b58d99d2c8a9501
65b795917d?s=48", "_id" : ObjectId("527ae6e8d38086540a000001"), "__v" : 0 }

关于 versionKey 的使用详见: http://mongoosejs.com/docs/guide.html#versionKey 。

读者可自行完成剩余的修改工作。

24

番外篇之——使用 Passport

前面我们自己写了一个简单的登陆认证系统,即用户在登陆时,通过输入事先注册的用户名和密码,服务器确认用户的身份后,从而获得操作权限。这也是最传统的登陆认证方式。

随着互联网的不断开放与发展,又出现了一种新的登陆认证方式——第三方登陆认证,即我们常说的oAuth/oAuth2.0。

什么是 oAuth?

OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是OAUTH的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAUTH是安全的。

什么是 Passport?

Passport是一个基于Node.js的认证中间件。极其灵活并且模块化,Passport可以很容易地跟任意基于Express的Web应用结合使用。 现在我们来修改代码,使得我们的博客既支持本地登陆又支持使用 GitHub 账户登录。

首先,登录 GitHub ,点击右上角的 Account settings ,然后点击左侧的 Applications ,然后点击右上角的 Register new application 创建一个 GitHub 应用。

创建成功后如下图所示:

7.1.jpeg

稍后我们将会用到 Client ID 、Client Secret 和 Authorization callback URL。

打开 package.json ,添加 passport 和 passport-github 模块:

"passport": "*",
"passport-github": "*"

并 npm install 安装这两个模块。

至此,准备工作都已完成,接下来我们修改代码支持使用 GitHub 账户登录。

首先,添加使用 GitHub 登陆的链接。打开 login.ejs,在:

<%- include footer %>

上一行添加如下代码:

<a href="/login/github">使用 GitHub 登录</a>

然后打开 app.js ,在 var app = express(); 下添加如下代码:

var passport = require('passport')
, GithubStrategy = require('passport-github').Strategy;
在 app.use(app.router); 上添加一行代码:
 
app.use(passport.initialize());//初始化 Passport

在 if ('development' == app.get('env')) 上添加如下代码:

passport.use(new GithubStrategy({
clientID: "xxx",
clientSecret: "xxx",
callbackURL: "xxx"
}, function(accessToken, refreshToken, profile, done) {
done(null, profile);
}));

注意:将 clientID、clientSecret 和 callbackURL 分别替换为刚才创建 GitHub 应用得到的信息。

以上代码的意思是:我们定义了一个 Passport 策略,并尝试从 GitHub 获得授权,从 GitHub 登陆并授权成功后以跳转到 callbackURL 并以 JSON 形式返回用户的一些相关信息,并将这些信息存储在 req.user 中。

打开 index.js ,在上方添加一行代码:

var passport = require('passport');

并在 app.get('/login') 后添加如下代码:

app.get("/login/github", passport.authenticate("github", {session: false}));
app.get("/login/github/callback", passport.authenticate("github", {
session: false,
failureRedirect: '/login',
successFlash: '登陆成功!'
}), function (req, res) {
req.session.user = {name: req.user.username, head: "https://gravatar.com/avatar/" + req.user._json.gravatar_id + "?s=48"};
res.redirect('/');
});

这里我们可以直接使用 Express 的 session 功能,所以禁掉 Passport 的 session 功能,前面提到过 Passport 默认会将取得的用户信息存储在 req.user 中而不是 req.session.user,为了保持兼容,所以我们提取并序列化有用的数据保存到 req.session.user 中。

至此,我们的博客也支持 GitHub 登录了,是不是很简单?目前还存在三个问题:

  1. GitHub 用户名和本地数据库用户名重名的问题。
  2. 不能访问使用 GitHub 账户登录的用户的用户页。
  3. 无法从 GitHub 获得用户的邮箱。

第一个问题的简单粗暴的解决方法是当用户以 GitHub 账户登录时,把获取的用户名到本地数据库查一下,若存在则禁止登录,若不存在则允许登陆。

第二个问题修改一下代码即可解决,删除 index.js 中 app.get('/u/:name') 内的那层判断数据库中是否存在该用户名的函数即可。

第三个问题暂时无法解决,因为 GitHub 返回的信息中并不包含有效的用户邮箱。

25

番外篇之——部署到 Heroku

使用 MongoHQ

在把我们的博客部署到 Heroku 之前,我们首先学习下如何使用 MongoHQ 。MongoHQ 是一个提供 MongoDB 存储服务的云平台,使用起来非常简单,提供了在线查询和修改数据库的功能。MongoHQ 的免费套餐提供了 512MB 的存储空间。

注册

https://bridge.mongohq.com/signup

创建一个数据库

注册后,选择一个 Free 的数据库类型,并给数据库起一个名字,点击 Create Database 创建数据库。此时跳转到如下界面:

8.1.jpg

如图所示,我们可以在命令行中连接远程数据库,也可以通过 Mongo URL 使用数据库。接下来,我们修改博客的代码,使用 MongoHQ 提供的云端数据库取代使用本地数据库。

首先,我们需要给数据库添加一个用户。点击左侧的 Admin ,然后点击 Users 进入用户管理页面。在 username 和 password 处分别填写用户名和密码:

db.addUser('username','password')

点击 Add user 添加用户。

修改 settings.js 为:

module.exports = {
cookieSecret: 'myblog',
url: 'your_Mongo_URI'
};

将 your_Mongo_URI 替换为你自己创建的数据库的 URL ,将 和 分别替换为刚才添加的用户的名字和密码。

打开 app.js ,将 app.use(express.session(...)); 修改为:

app.use(express.session({
secret: settings.cookieSecret,
cookie: {maxAge: 1000 * 60 * 60 * 24 * 30},//30 days
url: settings.url
}));

删除 db.js ,打开 post.js 、 user.js 和 comment.js ,均作以下修改:

  • 将 mongodb = require('./db') 修改为 mongodb = require('mongodb').Db
  • 添加 var settings = require('../settings');
  • 将所有 mongodb.open(function (err, db) { 修改为 mongodb.connect(settings.url, function (err, db) {
  • 将所有 mongodb.close(); 修改为 db.close(); 现在,无需启动你的本地数据库,运行你的博客试试吧~

注意:Heroku 也提供了 MongoHQ 的 Add-ons ,但需要填写信用卡信息,所以我们这里直接使用外链的 MongoHQ 。

部署到 Heroku

Heroku 是一个主流的 PaaS 提供商,在开发人员中广受欢迎。这个服务围绕着基于 Git 的工作流设计,假如你熟悉 Git ,那部署就十分简单。这个服务原本是为托管 Ruby 应用程序而设计的,但 Heroku 之后加入了对 Node.js 、Clojure 、Scala 、Python 和 Java 等语言的支持。Heroku 的基础服务是免费的。

下面我们使用 Heroku 部署我们的博客。

注册

https://www.heroku.com/

创建一个应用

注册成功后,就进入了控制面板页面,如图所示:

8.2.jpg

点击 Create a new app ,填写独一无二的应用名称后,点击 creat app 即创建成功,然后点击 Finish up 。

此时跳转到控制面板页,并且可以看到我们创建的应用了。我们通过 应用名称.herokuapp.com 即可访问我们的应用主页。如图所示:

8.3.jpg

安装 Heroku Toolbelt

Heroku 官方提供了 Heroku Toolbelt 工具更方便地部署和管理应用。它包含三个部分:

  • Heroku client :创建和管理 Heroku 应用的命令行工具
  • Foreman :一个在本地运行你的 app 的不错的选择
  • Git :分布式版本控制工具,用来把应用推送到 Heroku Heroku Toolbelt 下载地址:https://toolbelt.heroku.com/ 。

注意:假如你的电脑上已经安装了 Git ,那么在安装的时候选择 Custom Installation 并去掉安装 Git 的选项,否则选择 Full Installation 。

安装成功后,打开 Git Bash ,输入 heroku login ,然后输入在 Heroku 注册的帐号和密码进行登录。Git 会检测是否有 SSH 密钥,如果有,则使用此密钥并上传,如果没有,则创建一个密钥并上传。

Tips:SSH 密钥通常用于授予用户访问服务器的权限。可将它们用于某些配置中,以便无需密码即可访问服务器。许多 PaaS 提供商都使用了此功能。

Procfile

在工程的根目录下新建一个 Procfile 文件,添加如下内容:

web: node app.js

Procfile 文件告诉了服务器该使用什么命令启动一个 web 服务,这里我们通过 node app.js 执行 Node 脚本。为什么这里声明了一个 web 类型呢?官方解释为:

The name “web” is important here. It declares that this process type will be attached to the HTTP routing stack of Heroku, and receive web traffic when deployed.

上传应用

打开 Git Bash ,输入:

$ git init
$ git add .
$ git commit -m "init"
$ git remote add heroku git@heroku.com:yourAppName.git

注意:将 yourAppName 修改为你自己的应用名。

在 push 到 heroku 服务器之前,我们还需要做一个工作。由于我国某些政策的原因,我们需到 ~/.ssh/ 目录下,新建一个 config 文件,内容如下:

Host heroku.com
User yourName
Hostname 107.21.95.3
PreferredAuthentications publickey
IdentityFile ~/.ssh/id_rsa
port 22

然后回到 Git Bash ,输入:

$ git push heroku master

稍等片刻即上传成功。现在你就可以访问 http://yourAppName.herokuapp.com/ 了,如图所示:

8.4.jpg

注意:假如出现了 Application Error,可能是没有启动应用,到应用面板页勾选 web node app.js ,然后点击 Apply Changes 启动应用。

jk_book.png

jk_weixin.png

更多信息请访问 book_view.png

http://wiki.jikexueyuan.com/project/express-mongodb-setup-blog/

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论