nodejs+express+mongodb搭建博客

https://github.com/lanleilin/sayHelloBlogcss

是能夠運行的html

https://github.com/lanleilin/sayHelloBlog

 

文件結構以下:git

config存放配置文件,github

lib存放鏈接數據庫文件mongodb

middlewares存放中間件數據庫

public存放靜態文件express

views存放模版文件json

routes存放路由文件markdown

model存放操做數據庫文件cookie

logs存放日誌

index.js主程序

配置文件 config/default.js

module.exports = {
  port: 3000,
  session: {
    secret: 'myblog',
    key: 'myblog',
    maxAge: 2592000000
  },
  mongodb: 'mongodb://localhost:27017/myblog'
};

lib/mongo.js

var config = require('config-lite');
var Mongolass = require('mongolass');
var mongolass = new Mongolass();
mongolass.connect(config.mongodb);

var moment = require('moment');
var objectIdToTimestamp = require('objectid-to-timestamp');

// 根據 id 生成建立時間 created_at
mongolass.plugin('addCreatedAt', {
  afterFind: function (results) {
    results.forEach(function (item) {
      item.created_at = moment(objectIdToTimestamp(item._id)).format('YYYY-MM-DD HH:mm');
    });
    return results;
  },
  afterFindOne: function (result) {
    if (result) {
      result.created_at = moment(objectIdToTimestamp(result._id)).format('YYYY-MM-DD HH:mm');
    }
    return result;
  }
});

exports.User = mongolass.model('User', {
  name: { type: 'string' },
  password: { type: 'string' },
  avatar: { type: 'string' },
  gender: { type: 'string', enum: ['m', 'f', 'x'] },
  bio: { type: 'string' }
});
exports.User.index({ name: 1 }, { unique: true }).exec();// 根據用戶名找到用戶,用戶名全局惟一

exports.Post = mongolass.model('Post', {
  author: { type: Mongolass.Types.ObjectId },
  title: { type: 'string' },
  content: { type: 'string' },
  pv: { type: 'number' }
});
exports.Post.index({ author: 1, _id: -1 }).exec();// 按建立時間降序查看用戶的文章列表

exports.Comment = mongolass.model('Comment', {
  author: { type: Mongolass.Types.ObjectId },
  content: { type: 'string' },
  postId: { type: Mongolass.Types.ObjectId }
});
exports.Comment.index({ postId: 1, _id: 1 }).exec();// 經過文章 id 獲取該文章下全部留言,按留言建立時間升序
exports.Comment.index({ author: 1, _id: 1 }).exec();// 經過用戶 id 和留言 id 刪除一個留言

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

models/post.js

var marked = require('marked');
var Post = require('../lib/mongo').Post;
var CommentModel = require('./comments');

// 給 post 添加留言數 commentsCount
Post.plugin('addCommentsCount', {
  afterFind: function (posts) {
    return Promise.all(posts.map(function (post) {
      return CommentModel.getCommentsCount(post._id).then(function (commentsCount) {
        post.commentsCount = commentsCount;
        return post;
      });
    }));
  },
  afterFindOne: function (post) {
    if (post) {
      return CommentModel.getCommentsCount(post._id).then(function (count) {
        post.commentsCount = count;
        return post;
      });
    }
    return post;
  }
});

// 將 post 的 content 從 markdown 轉換成 html
Post.plugin('contentToHtml', {
  afterFind: function (posts) {
    return posts.map(function (post) {
      post.content = marked(post.content);
      return post;
    });
  },
  afterFindOne: function (post) {
    if (post) {
      post.content = marked(post.content);
    }
    return post;
  }
});

module.exports = {
  // 建立一篇文章
  create: function create(post) {
    return Post.create(post).exec();
  },

  // 經過文章 id 獲取一篇文章
  getPostById: function getPostById(postId) {
    return Post
      .findOne({ _id: postId })
      .populate({ path: 'author', model: 'User' })
      .addCreatedAt()
      .addCommentsCount()
      .contentToHtml()
      .exec();
  },

  // 按建立時間降序獲取全部用戶文章或者某個特定用戶的全部文章
  getPosts: function getPosts(author) {
    var query = {};
    if (author) {
      query.author = author;
    }
    return Post
      .find(query)
      .populate({ path: 'author', model: 'User' })
      .sort({ _id: -1 })
      .addCreatedAt()
      .addCommentsCount()
      .contentToHtml()
      .exec();
  },

  // 經過文章 id 給 pv 加 1
  incPv: function incPv(postId) {
    return Post
      .update({ _id: postId }, { $inc: { pv: 1 } })
      .exec();
  },

  // 經過文章 id 獲取一篇原生文章(編輯文章)
  getRawPostById: function getRawPostById(postId) {
    return Post
      .findOne({ _id: postId })
      .populate({ path: 'author', model: 'User' })
      .exec();
  },

  // 經過用戶 id 和文章 id 更新一篇文章
  updatePostById: function updatePostById(postId, author, data) {
    return Post.update({ author: author, _id: postId }, { $set: data }).exec();
  },

  // 經過用戶 id 和文章 id 刪除一篇文章
  delPostById: function delPostById(postId, author) {
    return Post.remove({ author: author, _id: postId })
      .exec()
      .then(function (res) {
        // 文章刪除後,再刪除該文章下的全部留言
        if (res.result.ok && res.result.n > 0) {
          return CommentModel.delCommentsByPostId(postId);
        }
      });
  }
};

models/users.js

var User = require('../lib/mongo').User;

module.exports = {
  // 註冊一個用戶
  create: function create(user) {
    return User.create(user).exec();
  },

  // 經過用戶名獲取用戶信息
  getUserByName: function getUserByName(name) {
    return User
      .findOne({ name: name })
      .addCreatedAt()
      .exec();
  }
};

models/comment.js

var marked = require('marked');
var Comment = require('../lib/mongo').Comment;

// 將 comment 的 content 從 markdown 轉換成 html
Comment.plugin('contentToHtml', {
  afterFind: function (comments) {
    return comments.map(function (comment) {
      comment.content = marked(comment.content);
      return comment;
    });
  }
});

module.exports = {
  // 建立一個留言
  create: function create(comment) {
    return Comment.create(comment).exec();
  },

  // 經過用戶 id 和留言 id 刪除一個留言
  delCommentById: function delCommentById(commentId, author) {
    return Comment.remove({ author: author, _id: commentId }).exec();
  },

  // 經過文章 id 刪除該文章下全部留言
  delCommentsByPostId: function delCommentsByPostId(postId) {
    return Comment.remove({ postId: postId }).exec();
  },

  // 經過文章 id 獲取該文章下全部留言,按留言建立時間升序
  getComments: function getComments(postId) {
    return Comment
      .find({ postId: postId })
      .populate({ path: 'author', model: 'User' })
      .sort({ _id: 1 })
      .addCreatedAt()
      .contentToHtml()
      .exec();
  },

  // 經過文章 id 獲取該文章下留言數
  getCommentsCount: function getCommentsCount(postId) {
    return Comment.count({ postId: postId }).exec();
  }
};

public中存放css文件

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'));

  // 404 page
  app.use(function (req, res) {
    if (!res.headersSent) {
      res.render('404');
    }
  });
};

routes/post.js

var express = require('express');
var router = express.Router();

var PostModel = require('../models/posts');
var CommentModel = require('../models/comments');
var checkLogin = require('../middlewares/check').checkLogin;

// GET /posts 全部用戶或者特定用戶的文章頁
//   eg: GET /posts?author=xxx
router.get('/', function(req, res, next) {
  var author = req.query.author;

  PostModel.getPosts(author)
    .then(function (posts) {
      res.render('posts', {
        posts: posts
      });
    })
    .catch(next);
});

// GET /posts/create 發表文章頁
router.get('/create', checkLogin, function(req, res, next) {
  res.render('create');
});

// POST /posts 發表一篇文章
router.post('/', checkLogin, function(req, res, next) {
  var author = req.session.user._id;
  var title = req.fields.title;
  var content = req.fields.content;

  // 校驗參數
  try {
    if (!title.length) {
      throw new Error('請填寫標題');
    }
    if (!content.length) {
      throw new Error('請填寫內容');
    }
  } catch (e) {
    req.flash('error', e.message);
    return res.redirect('back');
  }

  var post = {
    author: author,
    title: title,
    content: content,
    pv: 0
  };

  PostModel.create(post)
    .then(function (result) {
      // 此 post 是插入 mongodb 後的值,包含 _id
      post = result.ops[0];
      req.flash('success', '發表成功');
      // 發表成功後跳轉到該文章頁
      res.redirect(`/posts/${post._id}`);
    })
    .catch(next);
});

// GET /posts/:postId 單獨一篇的文章頁
router.get('/:postId', function(req, res, next) {
  var postId = req.params.postId;
  
  Promise.all([
    PostModel.getPostById(postId),// 獲取文章信息
    CommentModel.getComments(postId),// 獲取該文章全部留言
    PostModel.incPv(postId)// pv 加 1
  ])
  .then(function (result) {
    var post = result[0];
    var comments = result[1];
    if (!post) {
      throw new Error('該文章不存在');
    }

    res.render('post', {
      post: post,
      comments: comments
    });
  })
  .catch(next);
});

// GET /posts/:postId/edit 更新文章頁
router.get('/:postId/edit', checkLogin, function(req, res, next) {
  var postId = req.params.postId;
  var author = req.session.user._id;

  PostModel.getRawPostById(postId)
    .then(function (post) {
      if (!post) {
        throw new Error('該文章不存在');
      }
      if (author.toString() !== post.author._id.toString()) {
        throw new Error('權限不足');
      }
      res.render('edit', {
        post: post
      });
    })
    .catch(next);
});

// POST /posts/:postId/edit 更新一篇文章
router.post('/:postId/edit', checkLogin, function(req, res, next) {
  var postId = req.params.postId;
  var author = req.session.user._id;
  var title = req.fields.title;
  var content = req.fields.content;

  PostModel.updatePostById(postId, author, { title: title, content: content })
    .then(function () {
      req.flash('success', '編輯文章成功');
      // 編輯成功後跳轉到上一頁
      res.redirect(`/posts/${postId}`);
    })
    .catch(next);
});

// GET /posts/:postId/remove 刪除一篇文章
router.get('/:postId/remove', checkLogin, function(req, res, next) {
  var postId = req.params.postId;
  var author = req.session.user._id;

  PostModel.delPostById(postId, author)
    .then(function () {
      req.flash('success', '刪除文章成功');
      // 刪除成功後跳轉到主頁
      res.redirect('/posts');
    })
    .catch(next);
});

// POST /posts/:postId/comment 建立一條留言
router.post('/:postId/comment', checkLogin, function(req, res, next) {
  var author = req.session.user._id;
  var postId = req.params.postId;
  var content = req.fields.content;
  var comment = {
    author: author,
    postId: postId,
    content: content
  };

  CommentModel.create(comment)
    .then(function () {
      req.flash('success', '留言成功');
      // 留言成功後跳轉到上一頁
      res.redirect('back');
    })
    .catch(next);
});

// GET /posts/:postId/comment/:commentId/remove 刪除一條留言
router.get('/:postId/comment/:commentId/remove', checkLogin, function(req, res, next) {
  var commentId = req.params.commentId;
  var author = req.session.user._id;

  CommentModel.delCommentById(commentId, author)
    .then(function () {
      req.flash('success', '刪除留言成功');
      // 刪除成功後跳轉到上一頁
      res.redirect('back');
    })
    .catch(next);
});

module.exports = router;

routes/signin.js

var sha1 = require('sha1');
var express = require('express');
var router = express.Router();

var UserModel = require('../models/users');
var checkNotLogin = require('../middlewares/check').checkNotLogin;

// GET /signin 登陸頁
router.get('/', checkNotLogin, function(req, res, next) {
  res.render('signin');
});

// POST /signin 用戶登陸
router.post('/', checkNotLogin, function(req, res, next) {
  var name = req.fields.name;
  var password = req.fields.password;

  UserModel.getUserByName(name)
    .then(function (user) {
      if (!user) {
        req.flash('error', '用戶不存在');
        return res.redirect('back');
      }
      // 檢查密碼是否匹配
      if (sha1(password) !== user.password) {
        req.flash('error', '用戶名或密碼錯誤');
        return res.redirect('back');
      }
      req.flash('success', '登陸成功');
      // 用戶信息寫入 session
      delete user.password;
      req.session.user = user;
      // 跳轉到主頁
      res.redirect('/posts');
    })
    .catch(next);
});

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) {
  // 清空 session 中用戶信息
  req.session.user = null;
  req.flash('success', '登出成功');
  // 登出成功後跳轉到主頁
  res.redirect('/posts');
});

module.exports = router;

routes/signup.js

var fs = require('fs');
var path = require('path');
var sha1 = require('sha1');
var express = require('express');
var router = express.Router();

var UserModel = require('../models/users');
var checkNotLogin = require('../middlewares/check').checkNotLogin;

// GET /signup 註冊頁
router.get('/', checkNotLogin, function(req, res, next) {
  res.render('signup');
});

// POST /signup 用戶註冊
router.post('/', checkNotLogin, function(req, res, next) {
  var name = req.fields.name;
  var gender = req.fields.gender;
  var bio = req.fields.bio;
  var avatar = req.files.avatar.path.split(path.sep).pop();
  var password = req.fields.password;
  var repassword = req.fields.repassword;

  // 校驗參數
  try {
    if (!(name.length >= 1 && name.length <= 10)) {
      throw new Error('名字請限制在 1-10 個字符');
    }
    if (['m', 'f', 'x'].indexOf(gender) === -1) {
      throw new Error('性別只能是 m、f 或 x');
    }
    if (!(bio.length >= 1 && bio.length <= 30)) {
      throw new Error('我的簡介請限制在 1-30 個字符');
    }
    if (!req.files.avatar.name) {
      throw new Error('缺乏頭像');
    }
    if (password.length < 6) {
      throw new Error('密碼至少 6 個字符');
    }
    if (password !== repassword) {
      throw new Error('兩次輸入密碼不一致');
    }
  } catch (e) {
    // 註冊失敗,異步刪除上傳的頭像
    fs.unlink(req.files.avatar.path);
    req.flash('error', e.message);
    return res.redirect('/signup');
  }

  // 明文密碼加密
  password = sha1(password);

  // 待寫入數據庫的用戶信息
  var user = {
    name: name,
    password: password,
    gender: gender,
    bio: bio,
    avatar: avatar
  };
  // 用戶信息寫入數據庫
  UserModel.create(user)
    .then(function (result) {
      // 此 user 是插入 mongodb 後的值,包含 _id
      user = result.ops[0];
      // 將用戶信息存入 session
      delete user.password;
      req.session.user = user;
      // 寫入 flash
      req.flash('success', '註冊成功');
      // 跳轉到首頁
      res.redirect('/posts');
    })
    .catch(function (e) {
      // 註冊失敗,異步刪除上傳的頭像
      fs.unlink(req.files.avatar.path);
      // 用戶名被佔用則跳回註冊頁,而不是錯誤頁
      if (e.message.match('E11000 duplicate key')) {
        req.flash('error', '用戶名已被佔用');
        return res.redirect('/signup');
      }
      next(e);
    });
});

module.exports = router;

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 winston = require('winston');
var expressWinston = require('express-winston');

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());
// 處理表單及文件上傳的中間件
app.use(require('express-formidable')({
  uploadDir: path.join(__dirname, 'public/img'),// 上傳文件目錄
  keepExtensions: true// 保留後綴
}));

// 設置模板全局常量
app.locals.blog = {
  title: pkg.name,
  description: pkg.description
};

// 添加模板必需的三個變量
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();
});

// 正常請求的日誌
app.use(expressWinston.logger({
  transports: [
    new (winston.transports.Console)({
      json: true,
      colorize: true
    }),
    new winston.transports.File({
      filename: 'logs/success.log'
    })
  ]
}));
// 路由
routes(app);
// 錯誤請求的日誌
app.use(expressWinston.errorLogger({
  transports: [
    new winston.transports.Console({
      json: true,
      colorize: true
    }),
    new winston.transports.File({
      filename: 'logs/error.log'
    })
  ]
}));

// error page
app.use(function (err, req, res, next) {
  res.render('error', {
    error: err
  });
});

if (module.parent) {
  module.exports = app;
} else {
  // 監聽端口,啓動程序
  app.listen(config.port, function () {
    console.log(`${pkg.name} listening on port ${config.port}`);
  });
}

模版文件,模版引擎用的ejs

比較多,貼一個post.ejs

<%- include('header') %>

<% posts.forEach(function (post) { %>
  <%- include('components/post-content', { post: post }) %>
<% }) %>

<%- include('footer') %>

components/post-content.ejs

<div class="post-content">
  <div class="ui grid">
    <div class="four wide column">
      <a class="avatar"
         href="/posts?author=<%= post.author._id %>"
         data-title="<%= post.author.name %> | <%= ({m: '男', f: '女', x: '保密'})[post.author.gender] %>"
         data-content="<%= post.author.bio %>">
        <img class="avatar" src="/img/<%= post.author.avatar %>">
      </a>
    </div>

    <div class="eight wide column">
      <div class="ui segment">
        <h3><a href="/posts/<%= post._id %>"><%= post.title %></a></h3>
        <pre><%- post.content %></pre>
        <div>
          <span class="tag"><%= post.created_at %></span>
          <span class="tag right">
            <span>瀏覽(<%= post.pv %>)</span>
            <span>留言(<%= post.commentsCount %>)</span>
            
            <% if (user && post.author._id && user._id.toString() === post.author._id.toString()) { %>
              <div class="ui inline dropdown">
                <div class="text"></div>
                <i class="dropdown icon"></i>
                <div class="menu">
                  <div class="item"><a href="/posts/<%= post._id %>/edit">編輯</a></div>
                  <div class="item"><a href="/posts/<%= post._id %>/remove">刪除</a></div>
                </div>
              </div>
            <% } %>

          </span>
        </div>
      </div>
    </div>
  </div>
</div>
相關文章
相關標籤/搜索