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>