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/create
  6. 修改文章
    1. 修改文章页:GET /posts/:postId/edit
    2. 修改文章:POST /posts/:postId/edit
  7. 删除文章:GET /posts/:postId/remove
  8. 留言
    1. 创建留言:POST /comments
    2. 删除留言:GET /comments/:commentId/remove

由于我们博客页面是后端渲染的,所以只通过简单的 <a>(GET)<form>(POST) 与后端进行交互,如果使用 jQuery 或者其他前端框架(如 Angular、Vue、React 等等)可通过 Ajax 与后端交互,则 api 的设计应尽量遵循 Restful 风格。

Restful

Restful 是一种 api 的设计风格,提出了一组 api 的设计原则和约束条件。

如上面删除文章的路由设计:

  1. GET /posts/:postId/remove

Restful 风格的设计:

  1. DELETE /posts/: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)。

  1. cookie 存储在浏览器(有大小限制),session 存储在服务端(没有大小限制)
  2. 通常 session 的实现是基于 cookie 的,session id 存储于 cookie 中
  3. session 更安全,cookie 可以直接在浏览器查看甚至编辑

更多 session 的资料,参考:https://www.zhihu.com/question/19786827

我们通过引入 express-session 中间件实现对会话的支持:

  1. 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 是基于 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
  3. connect-flash: 基于 session 实现的用于通知功能的中间件,需结合 express-session 使用

4.4.4 权限控制

不管是论坛还是博客网站,我们没有登录的话只能浏览,登陆后才能发帖或写文章,即使登录了你也不能修改或删除其他人的文章,这就是权限控制。我们也来给博客添加权限控制,如何实现页面的权限控制呢?我们可以把用户状态的检查封装成一个中间件,在每个需要权限控制的路由加载该中间件,即可实现页面的权限控制。在 myblog 下新建 middlewares 目录,在该目录下新建 check.js,添加如下代码:

middlewares/check.js

  1. module.exports = {
  2. checkLogin: function checkLogin (req, res, next) {
  3. if (!req.session.user) {
  4. req.flash('error', '未登录')
  5. return res.redirect('/signin')
  6. }
  7. next()
  8. },
  9. checkNotLogin: function checkNotLogin (req, res, next) {
  10. if (req.session.user) {
  11. req.flash('error', '已登录')
  12. return res.redirect('back')// 返回之前的页面
  13. }
  14. next()
  15. }
  16. }

可以看出:

  1. checkLogin: 当用户信息(req.session.user)不存在,即认为用户没有登录,则跳转到登录页,同时显示 未登录 的通知,用于需要用户登录才能操作的页面
  2. checkNotLogin: 当用户信息(req.session.user)存在,即认为用户已经登录,则跳转到之前的页面,同时显示 已登录 的通知,如已登录用户就禁止访问登录、注册页面

最终我们创建以下路由文件:

routes/index.js

  1. module.exports = function (app) {
  2. app.get('/', function (req, res) {
  3. res.redirect('/posts')
  4. })
  5. app.use('/signup', require('./signup'))
  6. app.use('/signin', require('./signin'))
  7. app.use('/signout', require('./signout'))
  8. app.use('/posts', require('./posts'))
  9. app.use('/comments', require('./comments'))
  10. }

routes/posts.js

  1. const express = require('express')
  2. const router = express.Router()
  3. const checkLogin = require('../middlewares/check').checkLogin
  4. // GET /posts 所有用户或者特定用户的文章页
  5. // eg: GET /posts?author=xxx
  6. router.get('/', function (req, res, next) {
  7. res.send('主页')
  8. })
  9. // POST /posts/create 发表一篇文章
  10. router.post('/create', checkLogin, function (req, res, next) {
  11. res.send('发表文章')
  12. })
  13. // GET /posts/create 发表文章页
  14. router.get('/create', checkLogin, function (req, res, next) {
  15. res.send('发表文章页')
  16. })
  17. // GET /posts/:postId 单独一篇的文章页
  18. router.get('/:postId', function (req, res, next) {
  19. res.send('文章详情页')
  20. })
  21. // GET /posts/:postId/edit 更新文章页
  22. router.get('/:postId/edit', checkLogin, function (req, res, next) {
  23. res.send('更新文章页')
  24. })
  25. // POST /posts/:postId/edit 更新一篇文章
  26. router.post('/:postId/edit', checkLogin, function (req, res, next) {
  27. res.send('更新文章')
  28. })
  29. // GET /posts/:postId/remove 删除一篇文章
  30. router.get('/:postId/remove', checkLogin, function (req, res, next) {
  31. res.send('删除文章')
  32. })
  33. module.exports = router

routes/comments.js

  1. const express = require('express')
  2. const router = express.Router()
  3. const checkLogin = require('../middlewares/check').checkLogin
  4. // POST /comments 创建一条留言
  5. router.post('/', checkLogin, function (req, res, next) {
  6. res.send('创建留言')
  7. })
  8. // GET /comments/:commentId/remove 删除一条留言
  9. router.get('/:commentId/remove', checkLogin, function (req, res, next) {
  10. res.send('删除留言')
  11. })
  12. module.exports = router

routes/signin.js

  1. const express = require('express')
  2. const router = express.Router()
  3. const checkNotLogin = require('../middlewares/check').checkNotLogin
  4. // GET /signin 登录页
  5. router.get('/', checkNotLogin, function (req, res, next) {
  6. res.send('登录页')
  7. })
  8. // POST /signin 用户登录
  9. router.post('/', checkNotLogin, function (req, res, next) {
  10. res.send('登录')
  11. })
  12. module.exports = router

routes/signup.js

  1. const express = require('express')
  2. const router = express.Router()
  3. const checkNotLogin = require('../middlewares/check').checkNotLogin
  4. // GET /signup 注册页
  5. router.get('/', checkNotLogin, function (req, res, next) {
  6. res.send('注册页')
  7. })
  8. // POST /signup 用户注册
  9. router.post('/', checkNotLogin, function (req, res, next) {
  10. res.send('注册')
  11. })
  12. module.exports = router

routes/signout.js

  1. const express = require('express')
  2. const router = express.Router()
  3. const checkLogin = require('../middlewares/check').checkLogin
  4. // GET /signout 登出
  5. router.get('/', checkLogin, function (req, res, next) {
  6. res.send('登出')
  7. })
  8. module.exports = router

最后,修改 index.js 如下:

index.js

  1. const path = require('path')
  2. const express = require('express')
  3. const session = require('express-session')
  4. const MongoStore = require('connect-mongo')(session)
  5. const flash = require('connect-flash')
  6. const config = require('config-lite')(__dirname)
  7. const routes = require('./routes')
  8. const pkg = require('./package')
  9. const app = express()
  10. // 设置模板目录
  11. app.set('views', path.join(__dirname, 'views'))
  12. // 设置模板引擎为 ejs
  13. app.set('view engine', 'ejs')
  14. // 设置静态文件目录
  15. app.use(express.static(path.join(__dirname, 'public')))
  16. // session 中间件
  17. app.use(session({
  18. name: config.session.key, // 设置 cookie 中保存 session id 的字段名称
  19. secret: config.session.secret, // 通过设置 secret 来计算 hash 值并放在 cookie 中,使产生的 signedCookie 防篡改
  20. resave: true, // 强制更新 session
  21. saveUninitialized: false, // 设置为 false,强制创建一个 session,即使用户未登录
  22. cookie: {
  23. maxAge: config.session.maxAge// 过期时间,过期后 cookie 中的 session id 自动删除
  24. },
  25. store: new MongoStore({// 将 session 存储到 mongodb
  26. url: config.mongodb// mongodb 地址
  27. })
  28. }))
  29. // flash 中间件,用来显示通知
  30. app.use(flash())
  31. // 路由
  32. routes(app)
  33. // 监听端口,启动程序
  34. app.listen(config.port, function () {
  35. console.log(`${pkg.name} listening on port ${config.port}`)
  36. })

注意:中间件的加载顺序很重要。如上面设置静态文件目录的中间件应该放到 routes(app) 之前加载,这样静态文件的请求就不会落到业务逻辑的路由里;flash 中间件应该放到 session 中间件之后加载,因为 flash 是基于 session 实现的。

运行 supervisor index 启动博客,访问以下地址查看效果:

  1. http://localhost:3000/posts
  2. http://localhost:3000/signout
  3. http://localhost:3000/signup