1. express生成器生成生成项目 #

1.1 安装 Express生成器 #

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

$ npm install -g express-generator

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

1.2 生成一个项目 #

在命令行中输入:

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

执行结果中会显示生成的文件并提示后续的命令

E:\>express -e zhufengpeixunblog

create : zhufengpeixunblog
create : zhufengpeixunblog/package.json
create : zhufengpeixunblog/app.js
create : zhufengpeixunblog/public
create : zhufengpeixunblog/routes
create : zhufengpeixunblog/routes/index.js
create : zhufengpeixunblog/routes/users.js
create : zhufengpeixunblog/public/images
create : zhufengpeixunblog/public/javascripts
create : zhufengpeixunblog/views
create : zhufengpeixunblog/views/index.ejs
create : zhufengpeixunblog/views/error.ejs
create : zhufengpeixunblog/public/stylesheets
create : zhufengpeixunblog/public/stylesheets/style.css
create : zhufengpeixunblog/bin
create : zhufengpeixunblog/bin/www

install dependencies:
 > cd zhufengpeixunblog && npm install

run the app:
 > SET DEBUG=zhufengpeixunblog:* & npm start

进入生成的目录并安装依赖

$ E:\>cd zhufengpeixunblog && npm install

debug@2.2.0 node_modules\debug
└── ms@0.7.1

morgan@1.6.1 node_modules\morgan
├── on-headers@1.0.1
├── basic-auth@1.0.3
├── depd@1.0.1
└── on-finished@2.3.0 (ee-first@1.1.1)

cookie-parser@1.3.5 node_modules\cookie-parser
├── cookie@0.1.3
└── cookie-signature@1.0.6

serve-favicon@2.3.0 node_modules\serve-favicon
├── ms@0.7.1
├── etag@1.7.0
├── fresh@0.3.0
└── parseurl@1.3.0

body-parser@1.13.3 node_modules\body-parser
├── bytes@2.1.0
├── content-type@1.0.1
├── depd@1.0.1
├── on-finished@2.3.0 (ee-first@1.1.1)
├── qs@4.0.0
├── iconv-lite@0.4.11
├── http-errors@1.3.1 (statuses@1.2.1, inherits@2.0.1)
├── type-is@1.6.9 (media-typer@0.3.0, mime-types@2.1.7)
└── raw-body@2.1.4 (unpipe@1.0.0, iconv-lite@0.4.12)

express@4.13.3 node_modules\express
├── escape-html@1.0.2
├── array-flatten@1.1.1
├── path-to-regexp@0.1.7
├── merge-descriptors@1.0.0
├── content-type@1.0.1
├── methods@1.1.1
├── utils-merge@1.0.0
├── content-disposition@0.5.0
├── range-parser@1.0.3
├── serve-static@1.10.0
├── vary@1.0.1
├── depd@1.0.1
├── cookie@0.1.3
├── cookie-signature@1.0.6
├── fresh@0.3.0
├── etag@1.7.0
├── on-finished@2.3.0 (ee-first@1.1.1)
├── parseurl@1.3.0
├── qs@4.0.0
├── finalhandler@0.4.0 (unpipe@1.0.0)
├── accepts@1.2.13 (negotiator@0.5.3, mime-types@2.1.7)
├── type-is@1.6.9 (media-typer@0.3.0, mime-types@2.1.7)
├── proxy-addr@1.0.8 (forwarded@0.1.0, ipaddr.js@1.0.1)

设置环境变量并启动服务器,在命令行中执行下列命令

$ SET DEBUG=zhufengpeixunblog:* & npm start

执行完成后会显示

zhufengpeixunblog:server Listening on port 3000 +0ms

在浏览器里访问 http://localhost:3000 就可以显示欢迎页面

Express
Welcome to Express

刚才我们就用express生成器生成了一个使用ejs模板的示例工程。

1.3 debug #

debug模块

var colors = require('colors');
colors.setTheme({
    name: 'cyan',
    cost: 'green'
});
module.exports = function(name){
   return function(msg){
     var start = Date.now();
     //判断环境变量中的DEUBG的值是是否匹配当前日志记录器的名称
     var env = process.env.DEBUG;
     env = env.replace('*','.*')
     if(new RegExp(env).test(name)){
         console.log(name.name,msg,('+'+(Date.now()-start)+'ms').cost);
     }
   }
}

使用debug模块

var debug = require('./debug');
//生成一个日志记录器 名字叫warn'
// set DEBUG=logger:*
var logger_warn = debug('logger:warn');
logger_warn('warn');

//生成一个日志记录器 名字叫error
var logger_error = debug('logger:error');
logger_error('error');

2. 项目文件分析 #

2.1 生成文件说明 #

2.2 app.js #

var express = require('express'); 加载node_modules下的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(); 生成一个express实例 app

// 设置 views 文件夹为存放视图文件的目录, 即存放模板文件的地方,
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs'); 设置视图模板引擎为 ejs。

//设置/public/favicon.ico为favicon图标
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev')); 加载日志中间件。
app.use(bodyParser.json()); 加载解析json的中间件。
//加载解析urlencoded请求体的中间件。
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser()); 加载解析cookie的中间件。
/设置public文件夹为存放静态文件的目录。
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', routes); 根目录的路由
app.use('/users', users); 用户路由

// 捕获404错误,并转发到错误处理器。
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
});
  });
}

//生产环境下的错误处理
// 不向用户暴露堆栈信息
app.use(function(err, req, res, next) {
  res.status(err.status || 500);
  res.render('error', {
message: err.message,
error: {}
  });
});


module.exports = app;  导出app供 bin/www 使用

2.3 bin/www #

!/usr/bin/env node  表明是 node 可执行文件。

//引入我们上面app.js导出的app实例
var app = require('../app');
//引入打印调试日志的debug模块,并设置名称。
var debug = require('debug')('zhufengpeixunblog:server');
var http = require('http');

//从环境变量中获取端口号并存放到express中
var port = normalizePort(process.env.PORT || '3000');
//设置端口号
app.set('port', port);

//创建http服务器
var server = http.createServer(app);

//在所有的网络接口中监听提供的端口
server.listen(port);
//监听错误事件
server.on('error', onError);
//启动工程并监听3000端口,成功后打印 。
server.on('listening', onListening);
//把一个端口处理成一个数字或字符串或者false
function normalizePort(val) {
 //先试图转成10进制数字
  var port = parseInt(val, 10);

  if (isNaN(port)) {
// 转不成数字就当作命名管道来处理
return val;
  }

  if (port >= 0) {
// port number 如果端口大于0就返回端口
return port;
  }
//不是命名管道,也不是正常端口就返回false
  return false;
}

//服务器的错误事件监听器
function onError(error) {
 //如果系统调用不是监听则抛出错误
  if (error.syscall !== 'listen') {
throw error;
  }
  var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;

  //更友好的处理特定的监听错误
  switch (error.code) { 错误编码
case 'EACCES': 没有权限
  //没有权限绑定指定的端口
  console.error(bind + ' requires elevated privileges');
  process.exit(1); 异常退出
  break;
case 'EADDRINUSE': 端口被占用
  console.error(bind + ' is already in use');
  process.exit(1);异常退出
  break;
default:
//其它的情况抛出错误并中止进程
  throw error;
  }
}

//服务器的监听端口成功事件回调
function onListening() {
//取得服务器的地址
  var addr = server.address();
//监听地址如果是字符串返回命名管道名,如果是数字返回端口
  var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
//记录日志
  debug('Listening on ' + bind);
}

2.4 routes/index.js #

//导入express模块
var express = require('express');
//生成一个路由实例
var router = express.Router();

//当用户访问根目录也就是 / 的时候执行此回调
router.get('/', function(req, res, next) {
//渲染views/index.ejs模版并显示到浏览器中
  res.render('index', { title: 'Express' });
});
//导出这个路由并在app.js中通过app.use('/', routes); 加载
module.exports = router;

2.5 views/index.ejs #

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

<!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>

3. 功能与设计 #

3.1 功能分析 #

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

3.2 设计目标 #

3.3 未登录 #

3.4 登陆后 #

3.4 发表文章 #

4. 配置路由 #

4.1 路由规划 #

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

注意实现路由的时候为了避免出错,新增加的所有的路径、文件名约定都不要加s

4.2 增加路由 #

routes/user.js 中添加下列代码

/**
 * 用户注册
 */
router.get('/reg', function (req, res) {
res.render('user/reg', {title: '注册'});
});

/**
 * 当填写用户注册信息提交时的处理
 */
router.post('/reg', function (req, res) {
});

/**
 * 显示用户登录表单
 */
router.get('/login', function (req, res) {
res.render('user/login', {title: '登录'});
});

/**
 * 当填写用户登录信息提交时的处理
 */
router.post('/login', function (req, res) {
});

router.get('/logout', function (req, res) {
});

routes/articles.js 中添加下列代码

router.get('/add', function (req, res) {
res.render('article/add', { title: '发表文章' });
});

router.post('/add', function (req, res) {

});

4.3 修改app.js #

app.js中添加以下代码

var article = require('./routes/article');
app.use('/article', article);

4.4 增加模板 #

views下增加以下文件

4.4.1 views/article/add.ejs #

<%= title %>

4.4.2 views/user/login.ejs #

<%= title %>

4.4.3 views/user/reg.ejs #

<%= title %>

4.5 显示效果 #

4.5.1 用户注册 #

http://localhost:3000/user/reg

4.5.2 用户登陆 #

http://localhost:3000/user/login

4.5.3 发表文章 #

http://localhost:3000/article/add

5. 初始化bower #

5.1 安装bower #

$ npm install bower -g

5.2 初始化bower #

bower init

E:\zhufengpeixunblog>bower init
? name: zhufengpeixunblog
? version: 0.0.0
? description:
? main file:
? what types of modules does this package expose?
? keywords:
? authors: zhangrenyang-t510 <zhang_renyang@>
? license: MIT
? homepage:
? set currently installed components as dependencies? Yes
? add commonly ignored files to ignore list? Yes

{
  name: 'zhufengpeixunblog',
  version: '0.0.0',
  authors: [
'zhangrenyang-t510 <zhang_renyang@>'
  ],
  license: 'MIT',
  ignore: [
'**/.*',
'node_modules',
'bower_components',
'test',
'tests'
  ]
}

? Looks good? Yes

执行完此命令后会生成一个 `bower.json` 文件。

5.3 创建配置文件 .bowerrc #

里面内容如下

{"directory":"./public/lib"}

这表示以后bower安装的模块都安装在./public/lib下面

5.4 安装bootstrap #

$ bower install bootstrap --save

大家可以看到,不但安装了bootstrap,也安装了依赖的jquery

E:\zhufengpeixunblog>bower install bootstrap --save
bower bootstrap#~3.3.5 install bootstrap#3.3.5
bower jquery#>= 1.9.1  install jquery#2.1.4
bootstrap#3.3.5 public\lib\bootstrap
└── jquery#2.1.4

jquery#2.1.4 public\lib\jquery

6. 完善页面 #

6.1 创建include文件夹 #

在views下创建include文件夹

6.2 创建 header.ejs #

在include下添加包含的头部

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>珠峰博客</title>
<link href="/lib/bootstrap/dist/css/bootstrap.css" rel="stylesheet">
</head>
<body>
<!--最外面的容器nav标签,并添加nav-bar样式类,表示这里面属于导航条 -->
<nav class="navbar navbar-default" role="navigation">
<!--第二步:增加header-->
<div class="navbar-header">
<!--最左侧的,默认不可见 按钮标签里嵌套了三个span的icon。
然后给与navbar-toggle样式类和属性collapse(收起),
点击的时候目标为data-target。当窗口缩小到一定程度,右侧的效果显现。-->
<button type="button" class="navbar-toggle"
data-toggle="collapse" data-target="#menus">
<span class="sr-only">珠峰博客</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="/" class="navbar-brand">珠峰博客</a>
</div>
<!-- 放置其它的导航条 -->
<div class="collapse navbar-collapse" id="menus">
<ul class="nav navbar-nav">
<li class="active"><a href="/user/reg">注册</a></li>
<li><a href="/user/login">登录</a></li>
<li><a href="/article/add">发表文章</a></li>
<li><a href="/user/logout">登出</a></li>
</ul>
</div>
</nav>

6.3 创建 footer.ejs #

在include下添加包含的尾部

<script src="/lib/jquery/dist/jquery.js"></script>
<script src="/lib/bootstrap/dist/js/bootstrap.js"></script>
</body>
</html>

6.4 修改首页 #

views/index.ejs

<% include include/header.ejs%>
<div class="container">
 主页
</div>
<% include include/footer.ejs%>

效果如下

6.5 修改用户注册页面 #

views/user/reg.ejs

   <% include ../include/header.ejs%>
<div class="container">
<form action="/user/reg" method="post"  role="form"
class="form-horizontal">
<div class="form-group">
<label for="username" class="col-sm-2 control-label">用户名</label>
<div class="input-group col-sm-10">
<div class="input-group-addon">
<span class="glyphicon glyphicon-user"></span> </div>
<input type="text" class="form-control"
id="username" name="username" placeholder="用户名"/>
</div>

</div>
<div class="form-group">
<label for="email" class="col-sm-2 control-label">邮箱</label>
<div class="input-group col-sm-10">
<div class="input-group-addon">
<span class="glyphicon glyphicon-envelope"></span></div>
<input type="email" class="form-control"
name="email" id="email" placeholder="邮箱"/>
</div>
</div>
<div class="form-group">
<label for="password" class="col-sm-2 control-label">密码</label>
<div class="input-group col-sm-10">
<div class="input-group-addon">
<span class="glyphicon glyphicon-lock"></span></div>
<input type="password" class="form-control"
name="password" id="password" placeholder="密码"/>
</div>
</div>
<div class="form-group">
<label for="repassword" class="col-sm-2 control-label">确认密码</label>
<div class="input-group col-sm-10">
<div class="input-group-addon">
<span class="glyphicon glyphicon-lock"></span></div>
<input type="password" class="form-control"
name="repassword" id="repassword" placeholder="确认密码"/>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-default">提交</button>
<button type="reset" class="btn btn-default">重置</button>
</div>
</div>

</form>
</div>
<% include ../include/footer.ejs%>

效果如下

6.6 修改用户登录界面 #

views/user/login.ejs

   <% include ../include/header.ejs%>
<div class="container">
<form action="/user/login" method="post"
 role="form" class="form-horizontal">
<div class="form-group">
<label for="username" class="col-sm-2 control-label">用户名</label>
<div class="input-group col-sm-10">
<div class="input-group-addon">
<span class="glyphicon glyphicon-user"></span> </div>
<input type="text" class="form-control"
 id="username" name="username" placeholder="用户名"/>
</div>

</div>
<div class="form-group">
<label for="password" class="col-sm-2 control-label">
密码</label>
<div class="input-group col-sm-10">
<div class="input-group-addon">
<span class="glyphicon glyphicon-lock"></span></div>
<input type="password" class="form-control"
name="password" id="password" placeholder="密码"/>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-default">提交</button>
<button type="reset" class="btn btn-default">重置</button>
</div>
</div>

</form>
</div>
<% include ../include/footer.ejs%>

效果如下

6.7 修改发表文章页面 #

views/article/add.ejs

<% include ../include/header.ejs%>
   <div class="container">
   <form action="/article/add" method="post"
role="form" class="form-horizontal">
   <div class="form-group">
   <label for="title" class="col-sm-2 control-label">标题</label>
   <div class="col-sm-10">
   <input type="text" value="" class="form-control"
 id="title" name="title" placeholder="标题"/>
   </div>
   </div>
   <div class="form-group">
   <label for="content" class="col-sm-2 control-label">正文</label>
   <div class="col-sm-10">
   <textarea class="form-control"   id="" cols="30" rows="10"
id="content" name="content" placeholder="请输入内容"></textarea>
   </div>
   </div>
   <div class="form-group">
   <div class="col-sm-offset-2 col-sm-10">
   <button type="submit" class="btn btn-default">提交</button>
   <button type="reset" class="btn btn-default">重置</button>
   </div>
   </div>
   </form>
   </div>
   <% include ../include/footer.ejs%>

效果如下

7. 连接数据库 #

7.1 安装数据库支持 #

安装mongodb模块到node_modules下面并把此配置添加到package.json文件中

$ npm install mongoose --save

7.2 创建配置文件 #

在工程根目录下创建 settings.js 文件,内容如下

module.exports = {
cookieSecret:'zhufengkey', 用于 Cookie 加密与数据库无关
url:"mongodb://123.57.143.189:27017/zhufengblog"
}

7.3 创建db文件夹 #

在db文件夹下创建文件index.js,此文件存放着所有的模型 此文件负责向外暴露模型,因为Model赋给了global作为属性,那就意味着只需要引入一次就可以在程序任何地方都可以调用了

var mongoose = require('mongoose'),
Schema = mongoose.Schema,
models = require('./models');
var settings = require('../settings');
mongoose.connect(settings.url);
mongoose.model('User', new Schema({
  username:{type:String,required:true},用户名
  password:{type:String,required:true},密码
  email:{type:String,required:true},邮箱
  avatar:{type:String,required:true}头像
}));
mongoose.model('Article', new Schema( {
  user:{type:ObjectId,ref:'User'}, 用户
  title: String, 标题
  content: String, 内容
  createAt:{type: Date, default: Date.now()} 创建时间
}));
global.Model = function (modelName) {
 return mongoose.model(modelName);
}

7.4 在app.js中引入此模块 #

  require('./db'); //导入db模块

8. 会话支持 #

8.1 安装会话支持模块 #

使用 express-sessionconnect-mongo 模块实现了将会话信息存储到mongodb中。

$ npm install express-session --save
$ npm install connect-mongo --save

8.2 修改app.js #

var settings = require('./settings');
var session = require('express-session');
var MongoStore = require('connect-mongo')(session);
app.use(session({
  secret: settings.cookieSecret,//secret 用来防止篡改 cookie
  key: settings.db,//key 的值为 cookie 的名字
  //设定 cookie 的生存期,这里我们设置 cookie 的生存期为 30 天
  cookie: {maxAge: 1000 * 60 * 60 * 24 * 30},
  resave:true,
  saveUninitialized:true,
 //设置它的 store 参数为 MongoStore 实例,把会话信息存储到数据库中,
 //以避免重启服务器时会话丢失
  store: new MongoStore({
    db: settings.db,
    host: settings.host,
    port: settings.port,
  })
}));

添加完了以后我们就可以路由中通过request.session来操作会话对象了

9. 用户注册登陆 #

9.1 增加工具方法 #

routes/users.js中增加md5加密的工具方法

function md5(val){
  return require('crypto').createHash('md5').update(val).digest('hex');
}

9.2 用户注册路由 #

注册表单的form中action="/user/reg",所以我们要实现用户注册的路由 修改 routes/users.js

router.post('/reg', function (req, res) {
  //就是 POST 请求信息解析过后的对象
  var user = req.body;//
  if(user.password != user.repassword){
      req.flash('error','两次输入的密码不一致');
      return res.redirect('/user/reg');
  }
  //由于repassword不需要保存,所以可以删除
  delete user.repassword;
  //对密码进行md5加密
  user.password = md5(user.password);
 //得到用户的头像
  user.avatar = "https://secure.gravatar.com/avatar/"
+md5(user.email)+"?s=48";
  new Model('User')(user).save(function(err,user){
    if(err){
      req.flash('error',err);
      return res.redirect('/user/reg');
  }
  //用户信息存入 session
  req.session.user = user;
  //注册成功后返回主页
  res.redirect('/');
  });
});

9.3 用户登陆路由 #

router.post('/login', function (req, res) {
  var user = req.body;
  user.password = md5(user.password);
  Model('User').findOne(user,function(err,user){
     if(err){
       req.flash('error',err);
       return res.redirect('/user/login');
     }
    req.session.user = user;//用户信息存入 session
    res.redirect('/');//注册成功后返回主页
  });
});

9.4 用户退出路由 #

router.get('/logout', function (req, res) {
  req.session.user = null;//用户信息存入 session
  res.redirect('/');//注册成功后返回主页
});

10. 发表文章路由 #

发表文章表单的form中action="/article/add",所以我们要实现发表文章路由 修改 routes/article.js 中的 post('/add')

router.post('/add', function (req, res) {
  req.body.user = req.session.user._id;
  new Model('Article')(req.body).save(function(err,article){
  if(err){
     return res.redirect('/article/add');
   }
 //发表文章成功后返回主页
  res.redirect('/');
});
});

11. 页面消息通知 #

我们需要引入 flash 模块来实现页面通知(即成功与错误信息的显示)的功能。

11.1 什么是 flash? #

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

11.2 安装模块 #

$ npm install connect-flash --save

11.3 导入模块 #

在app.js中添加调用此模块

var flash = require('connect-flash');
app.use(flash());

11.4 发表文章成功提示 #

在发表文章的路由里放置flash提示信息

router.post('/add', function (req, res) {
  req.body.user = req.session.user._id;
  new Model('Article')(req.body).save(function(err,article){
    if(err){
       req.flash('error', '更新文章失败!'); //放置失败信息
       return res.redirect('/article/add');
    }
    req.flash('success', '更新文章成功!');  //放置成功信息
    res.redirect('/');//发表文章成功后返回主页
  });
});

11.5 增加显示提示的区域 #

修改 views/include/header.ejs 在最底部增加以下区域

<div class="container text-center">
<% if (success) { %>
<div class="alert alert-success" role="alert"><%= success %></div>
<% } %>
<% if (error) { %>
<div class="alert alert-danger" role="alert"><%= error %></div>
<% } %>
</div>

11.6 设置模板默认值 #

在app.js 中增加以下中间件

app.use(function(req,res,next){
  res.locals.user = req.session.user;
  res.locals.success = req.flash('success').toString();
  res.locals.error = req.flash('error').toString();
  next();
});

12. 控制导航 #

用户是否登陆看到的页面应该是不一样的,所以应该根据用户登陆状态来控制导航菜单的显示状态 修改 views/include/header.ejs

<ul class="nav navbar-nav">
  <%
  if(!user){%>
    <li class="active"><a href="/user/reg">注册</a></li>
    <li><a href="/user/login">登录</a></li>
  <%}else{
  %>
    <li><a href="/article/add">发表文章</a></li>
    <li><a href="/user/logout">登出</a></li>
  <%}%>
</ul>

用户登陆后显示发表文章和登出按钮,用户登陆前显示注册和登录按钮。

13 页面权限控制 #

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

13.1 添加中间件 #

添加middleware文件夹 在middleware下添加index.js文件 middleware/index.js

exports.checkLogin = function(req, res, next) {
 if (!req.session.user) {
   req.flash('error', '未登录!');
   return res.redirect('/user/login');
 }
 next();
}

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

13.2 在路由中添加中间件 #

修改 routes/users.js

router.get('/reg',middleware.checkNotLogin, function (req, res) {
  res.render('user/reg', {title: '注册'});
});

修改routes/article.js

router.get('/add',middleware.checkLogin, function (req, res) {
  res.render('article/add', { title: '发表文章' });
});

凡是不能登陆的中间加入 checkNotLogin 凡是需要登陆的中间加入 checkLogin

14 显示文章列表 #

14.1 修改首页模板 #

views/index.ejs

   <% include include/header.ejs%>
<div class="container">
 <ul class="media-list">
  <%
  articles.forEach(function(article){
  %>
  <li class="media">
   <div class="media-left">
<a href="#">
 <img class="media-object" src="<%=article.user.avatar%>" alt="">
</a>
   </div>
   <div class="media-body">
<h4 class="media-heading"><a href="/article/detail/<%=article._id%>">
<%=article.title%></a></h4>
<p class="media-left"><%- article.content%></p>
   </div>
   <div class="media-bottom">
作者:<%=article.user.username%>
发表时间:<%=article.createAt.toLocaleString()%>
   </div>
  </li>
  <%
  });
  %>
 </ul>
</div>
<% include include/footer.ejs%>

14.2 修改路由 #

routes/index.js

router.get('/', function(req, res, next) {
  Model('Article').find({}).populate('user').exec(function(err,articles){
     res.render('index', {title: '主页',articles:articles});
  });
});

15 支持markdown #

15.1 安装markdown #

$ npm install markdown -save

15.2 修改路由 #

routes/index.js

markdown = require('markdown').markdown;

router.get('/', function(req, res, next) {
  Model('Article').find({}).populate('user').exec(function(err,articles){
     articles.forEach(function (article) {
         article.content = markdown.toHTML(article.content);
    });
    res.render('index', {title: '主页',articles:articles});
  });
});

16 文章详情页 #

16.1 修改首页 #

views/index.ejs

<h4 class="media-heading">
<a href="/article/detail/<%=article._id%>"><%=article.title%></a></h4>

16.2 修改路由 #

routes/article.js

router.get('/detail/:_id', function (req, res) {
 Model('Article').findOne({_id:req.params._id},function(err,article){
   article.content = markdown.toHTML(article.content);
   res.render('article/detail',{title:'查看文章',article:article});
 });
});

16.3 增加详情页 #

views/article/detail.ejs

<% include ../include/header.ejs%>
<div class="container">
 <div class="panel panel-default">
   <div class="panel-heading">
      <%=article.title%>
   </div>
   <div class="panel-body">
      <%-article.content%>
   </div>
   <div class="panel-footer">
      <a href="/article/edit/<%=article._id%>"
         class="btn btn-warning">编辑</a>
       <a href="/article/delete/<%=article._id%>"
         class="btn btn-danger">删除</a>
    </div>
 </div>
</div>
<% include ../include/footer.ejs%>

17 删除文章 #

17.1 修改详情页 #

views/article/detail.ejs

 <a href="/article/delete/<%=article._id%>"
   class="btn btn-danger">删除</a>

17.2 修改路由 #

routes/article.js

router.get('/delete/:_id', function (req, res) {
  Model('Article').remove({_id:req.params._id},function(err,result){
    if(err){
      req.flash('error',err);
      res.redirect('back');
    }
    req.flash('success', '删除文章成功!');
    res.redirect('/');//注册成功后返回主页
  });
});

18 编辑文章 #

18.1 修改详情页 #

views/article/detail.ejs

<a href="/article/edit/<%=article._id%>" class="btn btn-warning">编辑</a>

18.2 修改添加文章界面 #

views/article/add.ejs

<form action="/article/add" method="post"  role="form"
class="form-horizontal" enctype="multipart/form-data">
 <input type="hidden" value="<%=article._id%>" name="_id"/>
 <div class="form-group">
 <label for="title" class="col-sm-2 control-label">标题</label>
 <div class="col-sm-10">
 <input type="text" value="<%=article.title%>" class="form-control"
id="title" name="title" placeholder="标题"/>
 </div>
 </div>
 <div class="form-group">
 <label for="content" class="col-sm-2 control-label">正文</label>
 <div class="col-sm-10">
 <textarea class="form-control"   id="" cols="30" rows="10" id="content"
name="content" placeholder="请输入内容" ><%=article.content%></textarea>
 </div>
 </div>
 <div class="form-group">
 <label for="img" class="col-sm-2 control-label">图片</label>
 <div class="col-sm-10">
 <%
 if(article.img){
 %>
  <img src="<%=article.img%>" style="width:100px;height:100px" alt=""/>
 <%
 }
  %>
 <input type="file" class="form-control"  name="img" id="img"/>
 </div>
 </div>
 <div class="form-group">
 <div class="col-sm-offset-2 col-sm-10">
 <button type="submit" class="btn btn-default">提交</button>
 <button type="reset" class="btn btn-default">重置</button>
 </div>
 </div>
</form>

18.3 修改路由 #

routes/article.js

router.post('/add',middleware.checkLogin,upload.single('img'),
 function (req, res) {
  var _id = req.body._id;
  if(_id){
    var set = {title:req.body.title,content:req.body.content};
    Model('Article').update({_id:_id},{$set:set},function(err,result){
      if(err){
        req.flash('error',err);
        return res.redirect('back');
    }
    req.flash('success', '更新文章成功!');
    res.redirect('/');//注册成功后返回主页
    });
  }else{
    req.body.user = req.session.user._id;
    new Model('Article')(req.body).save(function(err,article){
    if(err){
      req.flash('error',err);
      return res.redirect('/article/add');
    }
    req.flash('success', '发表文章成功!');
    res.redirect('/');//注册成功后返回主页
  });
  }
});

router.get('/edit/:_id', function (req, res) {
  Model('Article').findOne({_id:req.params._id},function(err,article){
    res.render('article/add',{title:'编辑文章',article:article});
  });
});

19. 搜索和分页 #

19.1 在导航栏增加搜索框 #

views/include/header.ejs

<form  class="navbar-form navbar-right" role="search" method="get"
action="/article/list/1/2">
  <div class="form-group">
    <label for="keyword">关键字</label>
    <input type="text" name="keyword" id="keyword"
    class="form-control" value="<%=keyword%>"/>
  </div>
  <button type="submit" class="btn  btn-default" value="search"
   name="searchBtn">搜索</button>
</form>

19.2 在首页增加分页条 #

views/index.ejs

 <ul class="pagination">
  <%
  for(var i=1;i<=totalPage;i++){
  %>
  <li><a href="/article/list/<%=i%>/<%=pageSize%>?keyword=<%=keyword%>">
       <%=i%></a></li>
  <%
  }
  %>
 </ul>

19.3 首页导航重定向到文章列表页 #

routes/index.js

router.get('/', function(req, res, next) {
  res.redirect('/article/list/1/2');
});

19.4 增加文章列表导航 #

routes/article.js

router.get('/list/:pageNum/:pageSize',function(req, res, next) {
  var pageNum = req.params.pageNum&&req.params.pageNum>0?
parseInt(req.params.pageNum):1;
  var pageSize =req.params.pageSize&&req.params.pageSize>0?
parseInt(req.params.pageSize):2;
  var query = {};
  var searchBtn = req.query.searchBtn;
  var keyword = req.query.keyword;
  if(searchBtn){
    req.session.keyword = keyword;
  }
  if(req.session.keyword){
    query['title'] = new RegExp(req.session.keyword,"i");
  }

  Model('Article').count(query,function(err,count){
    Model('Article').find(query).sort({createAt:-1})
      .skip((pageNum-1)*pageSize)
     .limit(pageSize).populate('user').exec(function(err,articles){
         articles.forEach(function (article) {
            article.content = markdown.toHTML(article.content);
         });
         res.render('index',{
             title:'主页',
             pageNum:pageNum,
             pageSize:pageSize,
             keyword:req.session.keyword,
             totalPage:Math.ceil(count/pageSize),
             articles:articles
         });
       });
  });
});

20. 实现评论功能 #

20.1 修改模板 #

views/article/detail.ejs

<div class="panel panel-default">
  <div class="panel-heading">
  评论列表
  </div>
  <div class="panel-body"  style="height:300px;overflow-y: scroll">
  <ul class="media-list">
      <%
      article.comments.forEach(function(comment){
      %>
         <li class="media">
            <div class="media-left">
              <a href="#">
                  <img class="media-object"
                    src="<%=comment.user.avatar%>" alt="">
              </a>
            </div>
            <div class="media-body">
               <p class="media-left"><%- comment.content%></p>
            </div>
            <div class="media-bottom">
                <%=comment.user.username%>
                 <%=comment.createAt.toLocaleString()%>
            </div>
         </li>
      <%
      });
      %>
  </ul>
  </div>

</div>

<div class="panel panel-default">
  <form action="/article/comment" method="post">
    <input type="hidden" value="<%=article._id%>" name="_id"/>
    <div class="panel-body">
      <textarea class="form-control"   id="" cols="30" rows="10"
       id="content" name="content" placeholder="请输入评论" ></textarea>
    </div>
    <div class="panel-footer">
       <button type="submit" class="btn btn-default">提交</button>
    </div>
    </form>
</div>

20.2 修改模型 #

db/models.js

 comments: [{user:{type:ObjectId,ref:'User'},content:String,
   createAt:{type: Date, default: Date.now}}],

20.3 修改路由 #

routes/article.js

router.post('/comment',middleware.checkLogin, function (req, res) {
   var user = req.session.user;
   Model('Article').update({_id:req.body._id},
     {$push:{comments:{user:user._id,content:req.body.content}}},
     function(err,result){
         if(err){
             req.flash('error',err);
             return res.redirect('back');
          }
         req.flash('success', '评论成功!');
        res.redirect('back');
   });
});

21 显示PV和评论 #

21.1 安装async #

$  npm install async --save

21.2 修改模型 #

db/models.js

pv: {type:Number,default:0},

21.3 修改路由 #

routes/article.js

var async = require('async');
router.get('/detail/:_id', function (req, res) {
    async.parallel([function (callback) {
        Model('Article').findOne({_id: req.params._id})
            .populate('user').populate('comments.user')
            .exec(function (err, article) {
                article.content = markdown.toHTML(article.content);
                callback(err, article);
            });
    }, function (callback) {
        Model('Article').update({_id: req.params._id}
        ,{$inc: {pv: 1}}, callback);
    }], function (err, result) {
        if (err) {
            req.flash('error', err);
            res.redirect('back');
        }
        res.render('article/detail',
       {title: '查看文章', article: result[0]});
    });
});

21.4 修改模板 #

views/index.ejs

阅读:<%= article.pv %>|
评论:<%= article.comments.length%>

22. 美化404中间件 #

22.1 修改404中间件 #

app.js

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  res.render("404");
});

22.2 增加404页面模板 #

views/404.ejs

<% include include/header.ejs%>
<div class="container">
  <img src="" alt="404"/>
</div>
<% include include/footer.ejs%>

23 打印日志 #

现在给博客增加日志,实现访问日志(access.log)和错误日志(error.log)功能 把日志保存为日志文件 app.js

//正常日志
var accessLog = fs.createWriteStream('access.log', {flags: 'a'});
app.use(logger('dev',{stream: accessLog}));

//错误日志
var errorLog = fs.createWriteStream('error.log', {flags: 'a'});
app.use(function (err, req, res, next) {
  var meta = '[' + new Date() + '] ' + req.url + '\n';
  errorLog.write(meta + err.stack + '\n');
  next();
});

24 发布heroKu #

https://www.heroku.com/

24.1 注册 #

24.2 验证邮箱 #

可用qq,不能126 163等

24.3 查看邮箱点击激活链接 #

24.4 需要设置密码 #

24.5 验证成功 #

24.6 进入控制面板 #

点击右上角的创建app按钮

24.7 输入app名称 #

24.8 将此APP关联到github上 #

要授权github登陆

24.9 点击布署分支 #

24.10 发布结果 #

演示地址 项目源码