## 4.4.1 功能与路由设计
在开发博客之前,我们首先需要明确博客要实现哪些功能。由于本教程面向初学者,所以只实现了博客最基本的功能,其余的功能(如归档、标签、分页等等)读者可自行实现。
功能及路由设计如下:
1. 注册
1. 注册页:`GET /signup`
2. 注册(包含上传头像):`POST /signup`
2. 登录
1. 登录页:`GET /signin`
2. 登录:`POST /signin`
3. 登出:`GET /signout`
4. 查看文章
1. 主页:`GET /posts`
2. 个人主页:`GET /posts?author=xxx`
3. 查看一篇文章(包含留言):`GET /posts/:postId`
5. 发表文章
1. 发表文章页:`GET /posts/create`
2. 发表文章:`POST /posts`
6. 修改文章
1. 修改文章页:`GET /posts/:postId/edit`
2. 修改文章:`POST /posts/:postId/edit`
7. 删除文章:`GET /posts/:postId/remove`
8. 留言
1. 创建留言:`POST /posts/:postId/comment`
2. 删除留言:`GET /posts/:postId/comment/:commentId/remove`
由于我们博客页面是后端渲染的,所以只通过简单的 `<a>(GET)` 和 `<form>(POST)` 与后端进行交互,如果使用 jQuery 或者其他前端框架(如 angular、vue、react 等等)可通过 Ajax 与后端交互,则 api 的设计应尽量遵循 restful 风格。
#### restful
restful 是一种 api 的设计风格,提出了一组 api 的设计原则和约束条件。
如上面删除文章的路由设计:
```
GET /posts/:postId/remove
```
restful 风格的设计:
```
DELETE /post/:postId
```
可以看出,restful 风格的 api 更直观且优雅。
更多阅读:
1. http://www.ruanyifeng.com/blog/2011/09/restful
2. http://www.ruanyifeng.com/blog/2014/05/restful_api.html
3. http://developer.51cto.com/art/200908/141825.htm
4. http://blog.jobbole.com/41233/
## 4.4.2 会话
由于 HTTP 协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识具体的用户,这个机制就是会话(Session)。关于 Session 的讲解网上有许多资料,这里不再赘述。参考:
1. http://justsee.iteye.com/blog/1570652
2. https://www.zhihu.com/question/19786827
#### cookie 与 session 的区别
1. cookie 存储在浏览器(有大小限制),session 存储在服务端(没有大小限制)
2. 通常 session 的实现是基于 cookie 的,即 session id 存储于 cookie 中
我们通过引入 express-session 中间件实现对会话的支持:
```
app.use(session(options))
```
session 中间件会在 req 上添加 session 对象,即 req.session 初始值为 `{}`,当我们登录后设置 `req.session.user = 用户信息`,返回浏览器的头信息中会带上 `set-cookie` 将 session id 写到浏览器 cookie 中,那么该用户下次请求时,通过带上来的 cookie 中的 session id 我们就可以查找到该用户,并将用户信息保存到 `req.session.user`。
## 4.4.3 页面通知
我们还需要这样一个功能:当我们操作成功时需要显示一个成功的通知,如登录成功跳转到主页时,需要显示一个 `登陆成功` 的通知;当我们操作失败时需要显示一个失败的通知,如注册时用户名被占用了,需要显示一个 `用户名已占用` 的通知。通知只显示一次,刷新后消失,我们可以通过 connect-flash 中间件实现这个功能。
[connect-flash](https://www.npmjs.com/package/connect-flash) 是基于 session 实现的,它的原理很简单:设置初始值 `req.session.flash={}`,通过 `req.flash(name, value)` 设置这个对象下的字段和值,通过 `req.flash(name)` 获取这个对象下的值,同时删除这个字段。
#### express-session、connect-mongo 和 connect-flash 的区别与联系
1. `express-session`: 会话(session)支持中间件
2. `connect-mongo`: 将 session 存储于 mongodb,需结合 express-session 使用,我们也可以将 session 存储于 redis,如 [connect-redis](https://www.npmjs.com/package/connect-redis)
3. `connect-flash`: 基于 session 实现的用于通知功能的中间件,需结合 express-session 使用
## 4.4.4 权限控制
不管是论坛还是博客网站,我们没有登录的话只能浏览,登陆后才能发帖或写文章,即使登录了你也不能修改或删除其他人的文章,这就是权限控制。我们也来给博客添加权限控制,如何实现页面的权限控制呢?我们可以把用户状态的检查封装成一个中间件,在每个需要权限控制的路由加载该中间件,即可实现页面的权限控制。在 myblog 下新建 middlewares 文件夹,在该目录下新建 check.js,添加如下代码:
**middlewares/check.js**
```
module.exports = {
checkLogin: function checkLogin(req, res, next) {
if (!req.session.user) {
req.flash('error', '未登录');
return res.redirect('/signin');
}
next();
},
checkNotLogin: function checkNotLogin(req, res, next) {
if (req.session.user) {
req.flash('error', '已登录');
return res.redirect('back');//返回之前的页面
}
next();
}
};
```
可以看出:
1. `checkLogin`: 当用户信息(`req.session.user`)不存在,即认为用户没有登录,则跳转到登录页,同时显示 `未登录` 的通知,用于需要用户登录才能操作的页面及接口
2. `checkNotLogin`: 当用户信息(`req.session.user`)存在,即认为用户已经登录,则跳转到之前的页面,同时显示 `已登录` 的通知,如登录、注册页面及登录、注册的接口
最终我们创建以下路由文件:
**routes/index.js**
```
module.exports = function (app) {
app.get('/', function (req, res) {
res.redirect('/posts');
});
app.use('/signup', require('./signup'));
app.use('/signin', require('./signin'));
app.use('/signout', require('./signout'));
app.use('/posts', require('./posts'));
};
```
**routes/posts.js**
```
var express = require('express');
var router = express.Router();
var checkLogin = require('../middlewares/check').checkLogin;
// GET /posts 所有用户或者特定用户的文章页
// eg: GET /posts?author=xxx
router.get('/', function(req, res, next) {
res.send(req.flash());
});
// POST /posts 发表一篇文章
router.post('/', checkLogin, function(req, res, next) {
res.send(req.flash());
});
// GET /posts/create 发表文章页
router.get('/create', checkLogin, function(req, res, next) {
res.send(req.flash());
});
// GET /posts/:postId 单独一篇的文章页
router.get('/:postId', function(req, res, next) {
res.send(req.flash());
});
// GET /posts/:postId/edit 更新文章页
router.get('/:postId/edit', checkLogin, function(req, res, next) {
res.send(req.flash());
});
// POST /posts/:postId/edit 更新一篇文章
router.post('/:postId/edit', checkLogin, function(req, res, next) {
res.send(req.flash());
});
// GET /posts/:postId/remove 删除一篇文章
router.get('/:postId/remove', checkLogin, function(req, res, next) {
res.send(req.flash());
});
// POST /posts/:postId/comment 创建一条留言
router.post('/:postId/comment', checkLogin, function(req, res, next) {
res.send(req.flash());
});
// GET /posts/:postId/comment/:commentId/remove 删除一条留言
router.get('/:postId/comment/:commentId/remove', checkLogin, function(req, res, next) {
res.send(req.flash());
});
module.exports = router;
```
**routes/signin.js**
```
var express = require('express');
var router = express.Router();
var checkNotLogin = require('../middlewares/check').checkNotLogin;
// GET /signin 登录页
router.get('/', checkNotLogin, function(req, res, next) {
res.send(req.flash());
});
// POST /signin 用户登录
router.post('/', checkNotLogin, function(req, res, next) {
res.send(req.flash());
});
module.exports = router;
```
**routes/signup.js**
```
var express = require('express');
var router = express.Router();
var checkNotLogin = require('../middlewares/check').checkNotLogin;
// GET /signup 注册页
router.get('/', checkNotLogin, function(req, res, next) {
res.send(req.flash());
});
// POST /signup 用户注册
router.post('/', checkNotLogin, function(req, res, next) {
res.send(req.flash());
});
module.exports = router;
```
**routes/signout.js**
```
var express = require('express');
var router = express.Router();
var checkLogin = require('../middlewares/check').checkLogin;
// GET /signout 登出
router.get('/', checkLogin, function(req, res, next) {
res.send(req.flash());
});
module.exports = router;
```
最后, 修改 index.js 如下:
**index.js**
```
var path = require('path');
var express = require('express');
var session = require('express-session');
var MongoStore = require('connect-mongo')(session);
var flash = require('connect-flash');
var config = require('config-lite');
var routes = require('./routes');
var pkg = require('./package');
var app = express();
// 设置模板目录
app.set('views', path.join(__dirname, 'views'));
// 设置模板引擎为 ejs
app.set('view engine', 'ejs');
// 设置静态文件目录
app.use(express.static(path.join(__dirname, 'public')));
// session 中间件
app.use(session({
name: config.session.key,// 设置 cookie 中保存 session id 的字段名称
secret: config.session.secret,// 通过设置 secret 来计算 hash 值并放在 cookie 中,使产生的 signedCookie 防篡改
cookie: {
maxAge: config.session.maxAge// 过期时间,过期后 cookie 中的 session id 自动删除
},
store: new MongoStore({// 将 session 存储到 mongodb
url: config.mongodb// mongodb 地址
})
}));
// flash 中间价,用来显示通知
app.use(flash());
// 路由
routes(app);
// 监听端口,启动程序
app.listen(config.port, function () {
console.log(`${pkg.name} listening on port ${config.port}`);
});
```
> 注意:中间件的加载顺序很重要。如上面设置静态文件目录的中间件应该放到 routes(app) 之前加载,这样静态文件的请求就不会落到业务逻辑的路由里;flash 中间件应该放到 session 中间件之后加载,因为 flash 是基于 session 的。
运行 `supervisor --harmony index` 启动博客,访问以下地址查看效果:
1. http://localhost:3000/posts
2. http://localhost:3000/signout
3. http://localhost:3000/signup
- 使用 Express + MongoDB 搭建多人博客
- 1.1 Node.js 的安装与使用
- 1.2 MongoDB 的安装与使用
- 2.1 require
- 2.2 exports 和 module.exports
- 2.3 Promise
- 2.4 环境变量
- 2.5 package.json
- 2.6 npm 使用注意事项
- 3.1 初始化一个 Express 项目
- 3.2 路由
- 3.3 模板引擎
- 3.4 Express 浅析
- 4.1 开发环境
- 4.2 准备工作
- 4.3 配置文件
- 4.4 功能设计
- 4.5 页面设计
- 4.6 连接数据库
- 4.7 注册
- 4.8 登出与登录
- 4.9 文章
- 4.10 留言
- 4.11 404 页面
- 4.12 错误页面
- 4.13 日志
- 4.14 测试
- 4.15 部署