nodeJS

目录[-]

  • 学习环境

  • 快速开始

  • 安装Express

  • 新建一个工程

  • 工程结构

  • 路由控制

  • 工作原理

  • 路由规则

  • 添加路由规则

  • 模版引擎

  • 什么是模板引擎

  • 使用模板引擎

  • 页面布局

  • 搭建多人博客

  • 功能分析

  • 设计目标

  • 路由规划

  • 使用数据库

  • MongoDB简介

  • 安装MongoDB

  • 连接MongoDB

  • 会话支持

  • 注册和登陆

  • 页面设计

  • 页面通知

  • 注册响应

  • 登录与登出响应

  • 页面权限控制

  • 发表文章

  • 页面设计

  • 文章模型

  • 发表响应

学习环境

node.js: 0.10.7+

快速开始

安装Express

npm install -g express

我们需要用全局模式安装 express,因为只有这样我们才能在命令行中使用它。目前 express 最新版本为 express 3.2.4。

新建一个工程

今后的学习把 D:\blog 文件夹作为我们的工程目录。windows 下打开 cmd 切换到 D 盘,输入 express -e ejs blog (注意 express 3.* 中安装 ejs 不再是 -t 而是 -e,可以输入 express -h 查看),然后输入 cd blog&npm install 安装所需模块,如下图所示:

安装完成后输入 node app,此时命令行中会显示 Express server listening on port 3000,在浏览器里输入 localhost:3000,如下所示:

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

工程结构

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

app.js:启动文件。 

package.json:存储着工程的信息及所需的依赖模块,当在 dependencies 中添加依赖时,运行npm install,会检查当前目录下的 package.json,并自动安装所有指定的依赖模块。 

node_modules:存放 package.json 中安装的模块,当你在 package.json 添加依赖的模块并安装后,默认存放在这个文件夹下 

public:存放 image、css、js 等文件 

routes:存放路由文件 

views:存放模版文件

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

/**
 * Module dependencies.
 */

var express = require('express')
  , routes = require('./routes')
  , user = require('./routes/user')
  , http = require('http')
  , path = require('path');

var app = express();

// all environments
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));

// development only
if ('development' == app.get('env')) {
  app.use(express.errorHandler());
}

app.get('/', routes.index);
app.get('/users', user.list);

http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

在 node.js 中模块分为核心模块和文件模块两种,核心模块是通过 require('xxxx') 导入的,文件模块是以 require('/xxxx') 或 require('./xxxx')、require('../xxxx') 形式导入的;核心模块是用c/c++编译的二进制模块,而文件模块是后缀为.js、.json、.node 的文件,在 node.js 中一个文件/文件夹也可以称之为一个模块。更多关于模块及模块加载顺序的信息请查阅官网:http://nodejs.org/api/all.html#all_modules 

这里导入了 express、http、path 核心模块,routes 文件夹下的 index.js 和 user.js 文件模块。

因为 express 框架是依赖 connect 框架(Node的一个中间件框架)创建而成的,可查阅 connect 文档:http://www.senchalabs.org/connect/和 express 官方文档:http://expressjs.com/api.html了解更多内容。 

app.set(name, value):设置 name 的值为 value 

app.set('port', process.env.PORT || 3000):设置端口为 process.env.PORT 或 3000 

app.set('views', __dirname + '/views'):设置 views 文件夹为视图文件的目录,存放模板文件,__dirname 为全局变量,存储着当前正在执行脚本所在的目录名。 

app.set('view engine', 'ejs'):设置视图模版引擎为 ejs

app.use([path], function):使用中间件 function,可选参数path默认为”/” 

app.use(express.favicon()):connect 内建的中间件,使用默认的 favicon 图标,如果想使用自己的图标,需改为 app.use(express.favicon(__dirname + '/public/images/favicon.ico')); 这里我们把自定义的 favicon.ico 放到了 public/images 文件夹下。 

app.use(express.logger('dev')):connect 内建的中间件,在开发环境下使用,在终端显示简单的不同颜色的日志,比如在启动 app.js 后访问 localhost:3000,终端会输出:

Express server listening on port 3000
GET / 200 21ms - 206b
GET /stylesheets/style.css 304 4ms

数字200显示为绿色,304显示为蓝色。假如你去掉这一行代码,不管你怎么刷新网页,终端都只有一行 Express server listening on port 3000。 

app.use(express.bodyParser()):connect 内建的中间件,用来解析请求体,支持 application/json, 

application/x-www-form-urlencoded, 和 multipart/form-data。 

app.use(express.methodOverride()):connect 内建的中间件,可以协助处理 POST 请求,伪装 PUT、DELETE 和其他 HTTP 方法。 

app.use(app.router):设置应用的路由(可选),详细请参考:http://stackoverflow.com/questions/12695591/node-js-express-js-how-does-app-router-work 

app.use(express.static(path.join(__dirname, 'public'))):connect 内建的中间件,设置根目录下的 public 文件夹为静态文件服务器,存放 image、css、js 文件于此。 

if ('development' == app.get('env')) {app.use(express.errorHandler());}:开发环境下的错误处理,输出错误信息。

app.get('/', routes.index):路由控制器,如果用户访问” / “路径,则由 routes.index 来控制,routes/index.js 内容如下:

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

通过 exports.index 导出 index 函数接口,app.get('/', routes.index) 相当于:

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

res.render('index', { title: 'Express' }):调用 ejs 模板引擎解析 views/index.ejs(我们之前通过 app.set('views', __dirname + '/views') 设置了模版文件默认存储在 views 下),并传入一个对象作为参数,这个对象只有一个属性 title: 'Express',即用字符串 Express 替换 views/index.ejs 中所有 title 变量,后面我们将会了解更多关于模板引的内容。

http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

这段代码的意思是创建服务器并监听3000端口,成功后在命令行中显示 Express server listening on port 3000,然后我们就可以通过在浏览器输入 localhost:3000 来访问了。

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

路由控制

工作原理

上面提到过 app.js 中 app.get('/', routes.index) 可以用以下代码取代:

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

这段代码的意思是当访问主页时,调用 ejs 模板引擎,传入 title 变量的值为字符串 Express,来渲染 index.ejs 模版文件,生成静态页面并显示在浏览器里。

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

打开 app.js,删除 , user = require('./routes/user') (我们这里用不到 routes/user.js,同时删除这个文件)和

app.get('/', routes.index);
app.get('/users', user.list);

在 app.js 最后添加:

routes(app);

修改 index.js 如下:

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

现在,再运行你的 app,你会发现主页毫无二致。这两种写法的区别就好比: 

你的朋友结婚了,你收到请帖要赴宴。到了酒店门口被总管给拦住了。官方的写法是总管看了看请帖然后给你指了朋友团的地方,然后你过去坐下。咱的写法是总管看了看请帖简单确认了下你是被邀请的人,然后你进去自己找到朋友团的地方坐下。

路由规则

express 封装了多种 http 请求方式,但我们这里主要只使用 get 和 post 两种。 

get 和 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][email protected]  
req.body.user.name  
// => "tobi"  

req.body.user.email  
// => "[email protected]"  

// 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 请求 

req.params 处理 /:xxx 形式的 get 请求 

req.body 处理 post 请求 

req.param(name) 可以处理 get 和 post 请求,但查找优先级由高到低为 req.params→req.body→req.query 

路径规则还支持正则表达式,更多请查阅:http://expressjs.com/api.html

添加路由规则

当我们访问 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 ?

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

使用模板引擎

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

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

在 routes/index.js 中通过调用 res.render 渲染模版,并将其产生的页面直接返回给客户端。它接受两个参数,第一个是模板的名称,即 views 目录下的模板文件名,扩展名 .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>

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

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

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

注意: <%= code %> 和 <%- code %> 的区别,当变量 code 为字符串时,两者没有区别;当 code 为比如<h1>hello</h1> 时,<%= code %> 会原样输出 <h1>hello</h1>,而<%- code %> 则会输出H1大的 hello。

EJS 的官方示例:

The Data

{ title:    'Cleaning Supplies',
  supplies: ['mop', 'broom', 'duster'] }

The Template

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

The Result

  • mop

  • broom

  • duster

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

页面布局

Express 3.* 中我们不再使用 layout.ejs 进行页面布局,转而使用 include 来替代。include 的简单使用如下:

a.ejs

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

b.ejs

this is b

c.ejs

this is c

最终 a.ejs 会显示:

this is b
hello,world!
this is c

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

搭建多人博客

功能分析

作为入门教程,我们要搭建的博客具有简单的允许多人注册、登录、发表文章、登出的功能。

设计目标

未登录:主页左侧导航显示 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){
  });
};

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

使用数据库

MongoDB简介

MongoDB 是一个对象数据库,它没有表、行等概念,也没有固定的模式和结构,所有的数据以文档的形式存储。所谓文档就是一个关联数组式的对象,它的内部由属性组成,一个属性对应的值可能是一个数、字符串、日期、数组,甚至是一个嵌套的文档。

——《Node.js开发指南》

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

{ 
  "_id" : ObjectId( "4f7fe8432b4a1077a7c551e8" ),   
  "name" : "nswbmw",  
  "age" : 22,
  "email" : [ "[email protected]", "[email protected]" ],  
  "family" : {  
    "mother" : { ... },  
    "father" : { ... },  
    "sister : { ... },  
    "address" : "earth"  
  }
}

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

安装MongoDB

安装 mongodb 很简单,去官网(http://www.mongodb.org/downloads)下载最新版的 mongodb,解压到D盘并把文件夹重命名为 mongodb,并在 mongodb 文件夹下新建 blog 文件夹作为我们的存储目录。打开 cmd,切换到 d:\mongodb\bin 目录下,在命令行中输入 mongod -dbpath “d:\mongodb\blog” 设置 blog 文件夹作为我们工程的存储目录,这样数据库就成功启动了。为了方便以后使用数据库,我们在桌面上新建 “启动mongodb.bat” ,并写入 d:\mongodb\bin\mongod.exe -dbpath d:\mongodb\blog ,这样我们以后只需运行桌面上的 “启动mongodb.bat” 就可启动数据库了。

连接MongoDB

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

{
  "name": "blog",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "3.2.4",
    "ejs": "*",
    "mongodb": "*"
  }
}

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

接下来在工程的目录中创建 settings.js 文件,这个文件用于保存数据库的连接信息。我们将数据库命名为 blog,数据库服务器在本地,因此 settings.js 文件的内容如下:

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

其中 db 是数据库的名称,host 是数据库的地址。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, Connection.DEFAULT_PORT, {}));

我们先不用弄清楚代码为什么这么写,我们现在只需知道以上代码通过 module.exports 输出了创建的数据库连接,在后面的小节中我们会用到这个模块。

会话支持

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

为了在无状态的 HTTP 协议之上实现会话,Cookie 诞生了。Cookie 是一些存储在客户端的信息,每次连接的时候由浏览器向服务器递交,服务器也向浏览器发起存储 Cookie 的请求,依靠这样的手段服务器可以识别客户端。我们通常意义上的 HTTP 会话功能就是这样实现的。具体来说,浏览器首次向服务器发起请求时,服务器生成一个唯一标识符并发送给客户端浏览器,浏览器将这个唯一标识符存储在 Cookie 中,以后每次再发起请求,客户端

浏览器都会向服务器传送这个唯一标识符,服务器通过这个唯一标识符来识别用户。 对于开发者来说,我们无须关心浏览器端的存储,需要关注的仅仅是如何通过这个唯一标识符来识别用户。很多服务端脚本语言都有会话功能,如 PHP,把每个唯一标识符存储到文件中。Express 也提供了会话中间件,默认情况下是把用户信息存储在内存中,但我们既然已经有了 MongoDB,不妨把会话信息存储在数据库中,便于持久维护。

——《Node.js开发指南》

为了使用这一功能,我们首先要获取一个叫做 connect-mongo 的模块,在 package.json 中添加一行代码:

{
  "name": "blog",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "3.2.4",
    "ejs": "*",
    "mongodb": "*",
    "connect-mongo": "*"
  }
}

运行 npm install 安装模块。然后打开 app.js,在 , path = require('path') 后添加以下代码:

, MongoStore = require('connect-mongo')(express)
, settings = require('./settings');

在 app.use(express.methodOverride()); 后添加:

app.use(express.cookieParser());
app.use(express.session({
  secret: settings.cookieSecret,
  key: settings.db,
  cookie: {maxAge: 1000 * 60 * 60 * 24 * 30},//30 days
  store: new MongoStore({
    db: settings.db
  })
}));

其中 express.cookieParser() 是 Cookie 解析的中间件。express.session() 则提供会话支持,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>

在 views 下新建 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.js ,主页显示如下:

接下来在 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 %>

注册页面显示如下:

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

页面通知

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

什么是 flash?

req.flash 是 Express 提供的一个奇妙的工具,通过它保存的变量只会在用户当前和下一次的请求中被访问( req.flash 是存放在session里的),之后会被清除,通过它我们可以很方便地实现页面的通知和错误信息显示功能。

——《Node.js开发指南》

在 package.json 添加一行代码:

{
  "name": "blog",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "3.2.4",
    "ejs": "*",
    "mongodb": "*",
    "connect-mongo": "*",
    "connect-flash": "*"
  }
}

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

, 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);
    }
    //读取 users 集合
    db.collection('users', function(err, collection){
      if(err){
        mongodb.close();
        return callback(err);
      }
      //将用户数据插入 users 集合
      collection.insert(user,{safe: true}, function(err, user){
        mongodb.close();
        callback(err, user);//成功!返回插入的用户信息
      });
    });
  });
};

User.get = function(name, callback){//读取用户信息
  //打开数据库
  mongodb.open(function(err, db){
    if(err){
      return callback(err);
    }
    //读取 users 集合
    db.collection('users', function(err, collection){
      if(err){
        mongodb.close();
        return callback(err);
      }
      //查找用户名 name 值为 name文档
      collection.findOne({
        name: name
      },function(err, doc){
        mongodb.close();
        if(doc){
          var user = new User(doc);
          callback(err, user);//成功!返回查询的用户信息
        } else {
          callback(err, null);//失败!返回null
        }
      });
    });
  });
};

我们暂时先不去研究这些代码,我们只需知道通过 User.prototype.save 实现了用户信息的存储,通过 User.get 实现了用户信息的读取。

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

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

通过 require 引入 user.js 用户模型和 crypto 模块,crypto 是 Node.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');
  }
  //生成密码的散列值
  var md5 = crypto.createHash('md5'),
      password = md5.update(req.body.password).digest('hex');
  var newUser = new User({
      name: req.body.name,
      password: password,
      email: req.body.email
  });
  //检查用户名是否已经存在 
  User.get(newUser.name, function(err, user){
    if(user){
      err = '用户已存在!';
    }
    if(err){
      req.flash('error', err);
      return res.redirect('/reg');
    }
    //如果不存在则新增用户
    newUser.save(function(err){
      if(err){
        req.flash('error',err);
        return res.redirect('/reg');
      }
      req.session.user = newUser;//用户信息存入session
      req.flash('success','注册成功!');
      res.redirect('/');
    });
  });
});

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

  • req.body: 就是 POST 请求信息解析过后的对象,例如我们要访问用户传递的 name=“password” 域的值,只需访问 req.body['password'] 或 req.body.password 即可。

  • res.redirect: 重定向功能,实现了页面的跳转,更多关于 res.redirect 的信息请查阅:http://expressjs.com/api.html#res.redirect

  • User:在前面的代码中,我们直接使用了 User 对象。User 是一个描述数据的对象,即 MVC 

    架构中的模型。前面我们使用了许多视图和控制器,这是第一次接触到模型。与视图和控制器不同,模型是真正与数据打交道的工具,没有模型,网站就只是一个外壳,不能发挥真实的作用,因此它是框架中最根本的部分。

在桌面新建 “启动app.bat” 并写入 :

supervisor d:\blog\app
//node d:\blog\app

以后我们可以通过打开 “启动app.bat” 来启动我们的博客,而不必每次都要在命令行中启动。

现在先后运行你的 “启动mongodb.bat” 和 “启动app.bat” ,打开浏览器输入 localhost:3000/reg 注册试试吧!注册成功后显示如下:

我们查看数据库中是否存入了用户的信息,运行 d:\mongodb\bin\mongo ,输入:

用户信息已经成功存入数据库。但这还不是我们想要的效果,我们想要的效果是当注册成功返回主页时,左侧导航显示 HOME 、POST 、LOGOUT ,右侧显示 “注册成功!”字样。下面我们来实现它。

修改 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()
  });
});

修改 header.ejs,将<nav></nav>修改如下:

<nav>
<span><a title="主页" href="/">home</a></span>
<% if(locals.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 (locals.success) { %>
  <div ><%= locals.success %></div>
<% } %>
<% if (locals.error) { %>
  <div><%= locals.error %> </div>
<% } %>

现在注册成功后显示如下:

我们通过对 session 的使用实现了对用户状态的检测,再根据不同的用户状态显示不同的导航信息。 

简单解释一下流程:用户在注册成功后,把用户信息存入 session ,同时页面跳转到主页显示 “注册成功!” 。然后把 session 中的用户信息赋给全局变量 res.locals.user ,在渲染 ejs 文件时通过检测 locals.user 判断用户是否在线,根据用户状态的不同显示不同的导航信息。

登录与登出响应

现在我们来实现登陆的响应。 

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

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');
    }
    //用户名密码都匹配后,将用户信息存入 session
    req.session.user = user;
    req.flash('success','登陆成功!');
    res.redirect('/');
  });
});

接下来我们实现登出响应。修改 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 函数。 

最终 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: req.body.name,
        password: password,
        email: req.body.email
    });
    //检查用户名是否已经存在 
    User.get(newUser.name, function(err, user){
      if(user){
        err = '用户已存在!';
      }
      if(err){
        req.flash('error', err);
        return res.redirect('/reg');
      }
      //如果不存在则新增用户
      newUser.save(function(err){
        if(err){
          req.flash('error',err);
          return res.redirect('/reg');
        }
        req.session.user = newUser;//用户信息存入session
        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');
      }
      //用户名密码都匹配后,将用户信息存入 session
      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','未登录!'); 
    return res.redirect('/login');
  }
  next();
}

function checkNotLogin(req,res,next){
  if(req.session.user){
    req.flash('error','已登录!'); 
    return res.redirect('/');
  }
  next();
}

注意:为了维护用户状态和 flash 通知功能的使用,我们在 app.get('/reg)' 和 app.get('/login) 和 app.get('/post') 里添加了以下代码:

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()
  }
  //要存入数据库的文档
  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,post) {
        mongodb.close();
        callback(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) {
          callback(err, null);//失败!返回 null
        }
        callback(null, docs);//成功!以数组形式返回查询的结果
      });
    });
  });
};

发表响应

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

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

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

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()
    });
  });
});

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


你可能感兴趣的:(nodeJS)