Express 是一个简洁而灵活的 node.js Web应用框架, 提供了一系列强大特性帮助你建立各类 Web 应用,和丰富的 HTTP 工具。javascript
使用 Express 能够快速地搭建一个完整功能的网站,它有一套健壮的特性,可用于开发单页、多页和混合Web应用。php
此文介绍如何使用Express搭建多人博客。css
做者: nswbmw 项目地址: N-bloghtml
Node.js: 0.10.32java
Express: 4.10.2node
MongoDB: 2.6.1jquery
express 是 Node.js 上最流行的 Web 开发框架,正如他的名字同样,使用它咱们能够快速的开发一个 Web 应用。咱们用 express 来搭建咱们的博客,打开命令行,输入:git
$ npm install -g express-generator
安装 express 命令行工具,使用它咱们能够初始化一个 express 项目。github
在命令行中输入:web
$ express -e blog $ cd blog && npm install
初始化一个 express 项目并安装所需模块,以下图所示:
而后运行:
$ DEBUG=blog node ./bin/www(windows 下:DEBUG=blog:* npm start )
(上面的代码报错的话,能够这样运行启动项目:npm start) 启动项目,此时命令行中会显示 blog Express server listening on port 3000 +0ms ,在浏览器里访问 localhost:3000
,以下图所示:
至此,咱们用 express 初始化了一个工程项目,并指定使用 ejs 模板引擎,下一节咱们讲解工程的内部结构。
咱们回头看看生成的工程目录里面都有什么,打开咱们的 blog 文件夹,里面如图所示:
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,表明请求信息和响应信息 。路径请求及对应的获取路径有如下几种形式:
// 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"
// 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"
// GET /user/tj req.params.name // => "tj" // GET /file/javascripts/jquery.js req.params[0] // => "javascripts/jquery.js"
// ?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 时,会显示:
当咱们访问 localhost:3000/nswbmw 这种不存在的页面时就会显示:
这是由于不存在 /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 页面显示以下:
很简单吧?这一节咱们学习了基本的路由规则及如何添加一条路由规则,下一节咱们将学习模板引擎的知识。
模板引擎(Template Engine)是一个将页面模板和要显示的数据结合起来生成 HTML 页面的工具。若是说上面讲到的 express 中的路由控制方法至关于 MVC 中的控制器的话,那模板引擎就至关于 MVC 中的视图。
模板引擎的功能是将页面模板和要显示的数据结合起来生成 HTML 页面。它既能够运 行在服务器端又能够运行在客户端,大多数时候它都在服务器端直接被解析为 HTML,解析完成后再传输给客户端,所以客户端甚至没法判断页面是不是模板引擎生成的。有时候模板引擎也能够运行在客户端,即浏览器中,典型的表明就是 XSLT,它以 XML 为输入,在客户端生成 HTML 页面。可是因为浏览器兼容性问题,XSLT 并非很流行。目前的主流仍是由服务器运行模板引擎。
在 MVC 架构中,模板引擎包含在服务器端。控制器获得用户请求后,从模型获取数据,调用模板引擎。模板引擎以数据和页面模板为输入,生成 HTML 页面,而后返回给控制器,由控制器交回客户端。
——《Node.js开发指南》
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 ,内容以下:
<!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 %>
和 <%- code %>
的区别,当变量 code 为普通字符串时,二者没有区别。当 code 好比为 <h1>hello</h1>
这种字符串时, <%= code %>
会原样输出 <h1>hello</h1>
,而 <%- code %>
则会显示 H1 大的 hello 字符串。
咱们能够在 <% %>
内使用 JavaScript 代码。下面是 ejs 的官方示例:
supplies: ['mop', 'broom', 'duster']
<ul> <% for(var i=0; i<supplies.length; i++) {%> <li><%= supplies[i] %></li> <% } %> </ul>
<ul> <li>mop</li> <li>broom</li> <li>duster</li> </ul>
咱们能够用上述三种标签实现页面模板系统能实现的任何内容。
这里咱们不使用layout进行页面布局,而是使用更为简单灵活的include。include 的简单使用以下:
<%- include a %> hello,world! <%- include b %>
this is a.ejs
this is b.ejs
最终 index.ejs 会显示:
this is a.ejs hello,world! this is b.ejs
这一节咱们学习了模版引擎的相关知识,下一节咱们正式开始学习如何从头开始搭建一个多人博客。
搭建一个简单的具备多人注册、登陆、发表文章、登出功能的博客。
未登陆:主页左侧导航显示 home、login、register,右侧显示已发表的文章、发表日期及做者。
登录后:主页左侧导航显示 home、post、logout,右侧显示已发表的文章、发表日期及做者。
用户登陆、注册、发表成功以及登出后都返回到主页。
主页:
登陆页:
注册页:
主页:
发表页:
注意:没有登出页,当点击 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 是一个基于分布式文件存储的 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 文件夹里新建 blog 文件夹做为咱们博客内容的存储目录。进入到 bin 目录下:运行:
./mongod --dbpath ../blog/
以上命令的意思是:设置 blog 文件夹做为咱们工程的存储目录并启动数据库。
数据库虽然安装并启动成功了,但咱们须要链接数据库后才能使用数据库。怎么才能在 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"
注意:如报"error setting ttl index on collection : sessions"错误,把"mongodb"&"connect-mongo"版本号更到最新。
运行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 ,主页显示以下:
接下来在 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 %>
登陆页面显示以下:
在 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 %>
注册页面显示以下:
至此,未登陆时的主页、注册页、登陆页都已经完成。
如今,启动咱们的博客看看吧。
注意:每次咱们更新代码后,都须要手动中止并重启应用,使用 supervisor 模块能够解决这个问题,每当咱们保存修改的文件时,supervisor 都会自动帮咱们重启应用。经过:
$ npm install -g supervisor
安装 supervisor 。使用 supervisor 命令启动 app.js:
$ supervisor app.js
接下来咱们实现用户的注册和登录,在这以前咱们须要引入 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 = newUser;//用户信息存入 session req.flash('success', '注册成功!'); res.redirect('/');//注册成功后返回主页 }); }); });
注意:咱们把用户信息存储在了 session 里,之后就能够经过 req.session.user 读取用户信息。
如今,启动应用,在浏览器输入 localhost:3000 注册试试吧!注册成功后显示以下:
这样咱们并不知道是否注册成功,咱们查看数据库中是否存入了用户的信息,打开一个命令行切换到 mongodb/bin/ (保证数据库已打开的前提下),输入:
能够看到,用户信息已经成功存入数据库。
接下来咱们实现当注册成功返回主页时,左侧导航显示 HOME 、POST 、LOGOUT ,右侧显示 注册成功! 字样,即添加 flash 的页面通知功能。
修改 header.ejs,将 <nav></nav>
修改以下:
<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() }); });
如今运行咱们的博客,注册成功后显示以下:
咱们经过对 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 中用户的信息,实现用户的退出。
登陆后页面显示以下:
登出后页面显示以下:
至此,咱们实现了用户注册与登录的功能,而且根据用户登陆状态显示不一样的导航。
咱们虽然已经完成了用户注册与登录的功能,但并不能阻止好比已经登录的用户访问 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() }); }); });
至此,咱们的博客就建成了。
启动咱们的博客,发表一篇博文,如图所示:
此时,查看一下数据库,如图所示:
Tips:Robomongo 是一个基于 Shell 的跨平台开源 MongoDB 管理工具。嵌入了 JavaScript 引擎和 MongoDB mongo 。只要你会使用 mongo shell ,你就会使用 Robomongo,它提供语法高亮、自动完成、差异视图等。
下载安装 Robomongo后,运行咱们的博客,注册一个用户并发表几篇文章,初次打开 Robomongo ,点击 Create 建立一个名为 blog (名字自定)的数据库连接(默认监听 localhost:27017),点击 Connect 就链接到数据库了。如图所示: