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 生成文件说明
- app.js:express的主配置文件
- package.json:存储着工程的信息及模块依赖,当在 dependencies 中添加依赖的模块时,运行
npm install
,npm 会检查当前目录下的 package.json,并自动安装所有指定的模块 - node_modules:存放 package.json 中安装的模块,当你在 package.json 添加依赖的模块并安装后,存放在这个文件夹下
- public:存放 image、css、js 等文件
- routes:存放路由文件
- views:存放视图文件或者说模版文件
- bin:可执行文件,可以从此启动服务器
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
- / :首页
- /user/login :用户登录
- /user/reg :用户注册
- /user/logout :登出
- /article/post :发表文章
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-session
和 connect-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 页面 为此,我们需要为页面设置访问权限。即注册和登陆页面应该阻止已登陆的用户访问 登出及后面我们将要实现的发表页只对已登录的用户开放 如何实现页面权限的控制呢?我们可以把用户登录状态的检查放到路由中间件中,在每个路径前增加路由中间件,即可实现页面权限控制 我们添加checkNotLogin
和 checkLogin
函数来实现这个功能
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
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 发布结果
珠峰培训node培训之MongoDB+Express+Angular+Node+Bootstrap+Linux开发的爬虫