Description:
使用 monk (ORM to Mongodb), express-validator, multer, moment (form date & time)基本的Blog系统功能:single view with comment \ Add Post \ Add Comment \ Add Category没有包含登入登出验证系统,可以自己参考Proejct 3的做法加上去安装 nodemon
到这里决定先安装 nodemon,透过nodemon 启动 Server 的话,只要 js 有更动,就会自动重启
虽然讲师重启得很开心,但我有点懒得一直手动重启
注:jade & css 除外,改这两个不需要重启Server,nodemon 也不会侦测到这个改动
使用 npm install 安装
加上 --save-dev option 代表这个 dependencies 是给开发人员使用的,也会自动加到package.jsonnpm install --save-dev nodemon
安装完打开package.json,在 scripts 区块加上执行 nodemon 指令: "dev"
... "scripts": { "start": "node ./bin/www", "dev": "nodemon ./bin/www" },...
使用以下指定启动,之后程式变更时就会自动重启 Servernpm run dev
App & Module Setup
安装express-generator globallynpm install -g express-generator
透过express建立新project目录 4_nodeblogexpress 4_nodeblog
修改package.json,加入要用的 dependencies
{ "name": "4-nodeblog", "version": "0.0.0", "private": true, "scripts": { "start": "node ./bin/www", "dev": "nodemon ./bin/www" }, "dependencies": { "body-parser": "~1.16.0", "cookie-parser": "~1.4.3", "debug": "~2.6.0", "express": "~4.14.1", "jade": "~1.11.0", "morgan": "~1.7.0", "serve-favicon": "~2.3.2", "monk": "https://github.com/vccabral/monk.git", "connect-flash": "*", "express-session": "*", "express-validator": "*", "express-messages": "*", "multer": "*", "moment": "*", "mongodb": "*" }, "devDependencies": { "nodemon": "^1.11.0" }}
monk: 类似于mongoose,MongoDB ORM,这边用monk是想提供多种练习,再去选择自己喜欢哪一种
moment: javascript library,用来format 日期时间格式
其他 module 都和 nodeauth project 类似,就不再多说明
安装 modulesnpm install
修改 app.js,import module
var session = require('session');var multer = require('multer');var upload = multer({ dest: './public/images' })var expressValidator = require('express-validator');var mongo = require('mongodb');var db = require('monk')('localhost/nodeblog');app.locals.moment = require('moment');
routing,让 router 可以存取到 DB
// Make our db accessible to our routerapp.use(function(req, res, next){ req.db = db; next();});
加入 connect-flash, validator, session middleware (从 project 3 copy过来)
// Connect-Flashapp.use(require('connect-flash')());app.use(function (req, res, next) { res.locals.messages = require('express-messages')(req, res); next();});// validatorapp.use(expressValidator({ errorFormatter: function(param, msg, value) { var namespace = param.split('.') , root = namespace.shift() , formParam = root; while(namespace.length) { formParam += '[' + namespace.shift() + ']'; } return { param : formParam, msg : msg, value : value }; }}));// Handle Sessionsapp.use(session({ secret:'secret', saveUninitialized: true, resave: true}));
Layout template
这次不使用bootstrap,只使用jade
layout.jade
doctype htmlhtml head title= title link(rel='stylesheet', href='/stylesheets/style.css') body .container img.logo(src='/images/nodebloglogo.png') nav ul li a(href='/') Home li a(href='/posts/add') Add Post li a(href='/categories/add') Add Category block content footer p NodeBlog © 2017
这边会需要一张 logo 图,可以随意放自己喜欢的图,或是到这个免费建logo的网址做一个
把图片放到 project 下的 public\images 中,档名需与 layout.jade 中定义的一致 (nodebloglogo.png)
修改style.css,撰写css样式
body { font: 15px Helvetica, Arial, sans-serif; background: #f4f4f4; color: #666;}.logo { text-align: center; margin: auto; padding-bottom: 10px; display: block;}.container { width: 750px; border: 1px solid #ccc; margin: 20px auto; padding: 20px; border-top: #83cd39 3px solid;}.clr { clear: both;}ul { padding: 0; margin: 0;}h1,h2,h3,p { padding: 5px 0; margin-bottom: 0;}p { margin: 0;}a { color: #00B7FF;}nav { background: #404137; overflow: auto; height: 40px; padding: 20px 0 0 10px; font-size: 10px;}nav li { float: left; list-style: none;}nav a { padding: 10px; margin: 0 10px; color: #fff;}nav a.current, nav a:hover { background: #83cd29; color: #000;}
除了图片之外,其他样式应该如下图
上面的配色可以随意调配,分享两个之前我自己有在用的网站:
Color Drop 这个网站提供多种颜色并排的比较,可以用来查看网站多种色彩的搭配效果
Paletton 也提供网站色彩搭配,而且附有一个大大的调色盘
首页显示贴文
在 Mongo Shell create nodeblog DB,并新增 categories 和 posts 两个 collection
use nodeblogdb.createCollection('categories');db.createCollection('posts');
新增两笔资料,等下测试要用
db.posts.insert({title:"Blog Post One", category:"Technology", arthor:"yuki", body:"This is the bo dy", date:ISODate()});db.posts.insert({title:"Blog Post Two", category:"Science", arthor:"grace", body:"This is the body ", date:ISODate()});
query posts collection,确认资料有塞进去db.posts.find().pretty();
修改 routes\index.js,加入 mongo db module 及 HTTP GET request
var express = require('express');var router = express.Router();//mongo dbvar mongo = require('mongodb');var db = require('monk')('localhost/nodeblog');/* GET home page. */router.get('/', function(req, res, next) { var db = req.db; var posts = db.get('posts'); posts.find({}, {}, function(err, posts){ res.render('index', { posts: posts }); });});module.exports = router;
修改 index.jade,如果有任何posts,把每个post列出来
title含有超连结,利用post的 _id 作为routing path
extends layoutblock content if posts each posts, i in posts .post h1 a(href='/posts/show/#{post._id}') =post.title
修改 style.css
美化几个地方
/* 分类 */.meta{ padding: 7px; border: 1px solid #ccc; background: #ccc; margin-bottom: 10px;}/* Read More 连结 */a.more{ display: block; width: 80px; background: #404137; color: #fff; padding: 10px; margin-top: 30px; text-decoration: none;}/* 贴文 */.post{ border-bottom: 1px solid #ccc; padding-bottom: 20px;}/* 贴文Title连结 */.post h1 a{ color: #666; text-decoration: none;}
使用 Moment 加上贴文的分类、作者、日期和贴文内容,最后再加上 Read More 连结
extends layoutblock content if posts each post, i in posts .post h1 a(href='/posts/show/#{post._id}') =post.title p.meta Posted in #{post.category} by #{post.author} on #{moment(post.date).format("MM-DD-YYYY")} =post.body a.more(href='/posts/show/#{post._id}') Read More
弄完之后应该长得像这样
新增贴文功能
接下来要撰写新增贴文的功能
修改 app.js 的 routing,把 users 改成 posts
...var index = require('./routes/index');var posts = require('./routes/posts');...app.use('/', index);app.use('/posts', posts);...
在 routes 下新增 posts.js,内容从 user.js copy过来,把user.js删掉(这边用不到)
修改成下面这样:
var express = require('express');var router = express.Router();router.get('/add', function(req, res, next) { res.render('addpost', { 'title': 'Add Post' });});module.exports = router;
接下来要为 posts 新增 view
在 view 下新增 addpost.jade,撰写template
extends layoutextends layoutblock content h1=title ul.errors if errors each error, i in errors li.alert.alert-danger #{error.msg} form(method='post', action='/posts/add', enctype="multipart/form-data") .form-group label Title: input.form-control(name='title', type='text') .form-group label Category: select.form-control(name='category') .form-group label Body: textarea.form-control(name='body', id='body') .form-group label Main Image: input.form-control(name='mainimage', type='file') .form-group label Author: select.form-control(name='author') option(value='byakuinss') byakuinss option(value='yuki') yuki input.btn.btn-default(name='submit', type='submit', value='Save')
html 写完了,再来修改 style.css,为 addpost.jade 加上css样式
input, select, textarea{ margin-bottom: 15px;}label{ display: inline-block; width: 180px;}input[type='text'], select, textarea{ padding: 3px; height: 20px; width: 200px; border: 1px #ccc solid;}select{ height: 28px;}textarea{ height: 70px; width: 400px;}
接下来要将贴文存到DB,需要在 posts.js 加入 HTTP POST request
POST request 主要有以下几个动作
req.body.{variable},variable 内容需要和 addpost.jade 中的 form-control(name='variable', ...) 相同检查是否有上传图片、检查必要栏位是否为空如果有任何 error 会回传,若无问题,则使用 flash 显示 "Post Added" message 并切换回首页
//Require multer to handle imagevar multer = require('multer');var upload = multer({ dest: './public/images' }); ......router.post('/add', upload.single('mainimage'), function(req, res, next) { //Get Form Values var title = req.body.title; var category = req.body.category; var body = req.body.body; var author = req.body.author; var date = new Date(); //Check Image Upload if(req.file){ var mainimage = req.file.filename; } else { var mainimage = 'no-image.jpg' } //Form Validation req.checkBody('title', 'Title field is required').notEmpty(); req.checkBody('body', 'Body field is required').notEmpty(); //Check Errors var errors = req.validationErrors(); if(errors){ res.render('addpost', { "errors": errors }); } else { var posts = db.get('posts'); posts.insert({ "title": title, "body": body, "category": category, "date": date, "author": author, "mainimage": mainimage }, function(err, post){ if(err) { res.send(err); } else { req.flash('success', 'Post Added'); res.location('/'); res.redirect('/'); } }); }}); ......
重启 Server,试着新增一篇贴文,此时的 Category 没有资料,这项先跳过不选
新增完应该会看到刚刚新增的贴文出现在首页
现在来补足 Category 选单,先到 Mongo Shell在 categories collection 手动新增几笔资料
db.categories.insert({name:'Technology'});db.categories.insert({name:'Science'});db.categories.insert({name:'Business'});
要让 Category 选单从 categories collection 抓出资料,需要在 GET Add Post 页面时加入DB连线,并将资料存到 categories
修改 posts.js 的 GET request 内容,将取得的DB资料存入 categories 参数
router.get('/add', function(req, res, next) { var categories = db.get('categories'); categories.find({}, {}, function(err, categories){ res.render('addpost', { 'title': 'Add Post', 'categories': categories }); });});
接下来要从 categories 参数中取出值,并串连到 addpost.jade 的 category 选单
修改 addpost.jade,在 Category 选单下加入两行,针对每一个 category,在选单中显示
category.name ... .form-group label Category: select.form-control(name='category') each category, i in categories option(value='#{category.name}') #{category.name} ...
在选单中可以看到所有 categories 了
新增一篇带有 category 的贴文,测试成功
文字编辑器
在新增贴文时,如果有文字编辑器,贴文的内容就可以有更多变化
这边选择的是 CKEditor,因为比较容易Setup
到CKEditor官网下载 Standard Package
下载后解压会产生ckeditor folder,把整个 folder 複製到 project folder 的 public 资料夹下
修改 addpost.jade,在最下方加入script,import ckeditor.js,并且用来取代原本 body 区块
... input.btn.btn-default(name='submit', type='submit', value='Save') script(src='/ckeditor/ckeditor.js') script | CKEDITOR.replace('body');...
重新进入 Add Post 页面,就会看到原本 body 区块的 textarea 已经变成文字编辑器啰
新增Category功能
修改 app.js,加入 categories routing
...var index = require('./routes/index');var posts = require('./routes/posts');var categories = require('./routes/categories')...app.use('/', index);app.use('/posts', posts);app.use('/categories', categories);...
新增两个档案: addcategory.jade \ categories.js
複製 addpost.jade 到 addcategory.jade,只留下一个 text 和 button,如下
extends layoutblock content h1=title ul.errors if errors each error, i in errors li.alert.alert-danger #{error.msg} form(method='post', action='/categories/add') .form-group label Name: input.form-control(name='name', type='text') input.btn.btn-default(name='submit', type='submit', value='Save')
同样複製 posts.js 到 categories.js,只留下需要的 module,修改 GET \ POST request 内容
var express = require('express');var router = express.Router();var mongo = require('mongodb');var db = require('monk')('localhost/nodeblog');router.get('/add', function(req, res, next) { res.render('addcategory', { 'title': 'Add Category' }); });router.post('/add', function(req, res, next) { //Get Form Values var name = req.body.name; //Form Validation req.checkBody('name', 'Name field is required').notEmpty(); //Check Errors var errors = req.validationErrors(); if(errors){ res.render('addcategories', { "errors": errors }); } else { var categories = db.get('categories'); categories.insert({ "name": name }, function(err, category){ if(err) { res.send(err); } else { req.flash('success', 'Category Added'); res.location('/'); res.redirect('/'); } }); }});module.exports = router;
新增一个 Category 测试,新增完再到 Add Post,新category已经出现在选单中了
缩短文字内容 (truncate text) & 显示上传图片
把之前测试的新增贴文都删除db.posts.remove("");
修改layout.jade,加入success message
... li a(href='/categories/add') Add Category != messages() block content ...
修改style.css,加入 success message 显示的css样式
...ul.success li{ padding: 15px; margin-top: 10px; margin-bottom: 20px; border: 1px solid transparent; border-radius: 4px; color: #3c763d; background-color: #dff0d8; border-color: #d6e9c6; list-style: none;}
接下来加入truncate text效果
新增一篇很长的贴文
修改app.js,加入一个新function: truncateText
...app.locals.moment = require('moment');app.locals.truncateText = function(text, length){ var truncateText = text.substring(0, length); return truncateText;}...
修改index.jade,将=post.body改成 !=truncateText(post.body,400)
...... p.meta Posted in #{post.category} by #{post.author} on #{moment(post.date).format("MM-DD-YYYY")} !=truncateText(post.body,400) a.more(href='/posts/show/#{post._id}') ......
重启server,可以看到过长的贴文被截掉了
接下来要把 image 加入贴文
先确认新增贴文时上传的图片有在 public\images中
修改 index.jade,在 p.meta 下方加入图片label (可以依自己喜好随意放)
... p.meta Posted in #{post.category} by #{post.author} on #{moment(post.date).format("MM-DD-YYYY")} img(src='/images/#{post.mainimage}') !=truncateText(post.body,400) ...
修改 style.css加上图片css样式,因为上传的图片可能大小不一,我希望图片都是随视窗改变大小
.post img { width: 100%;}
重新整理网页,图片就出来啰
注:没看到图片怎么办
检查 app.js 和 post.js 的 upload 路径是否一致检查 public\images 中是否有应该显示的图片,如果没有,可能是 post.js 的路径没改到或写错以Category View检视贴文
需要新增一个新页面,用Category当作query条件show出对应的贴文
修改categories.js,加入新的 route,将要query的category条件放入 posts.find({query_condition}, {}, function ...)
router.get('/show/:category', function(req, res, next) { var posts = db.get('posts'); posts.find({category: req.params.category}, {}, function(err, posts){ res.render('index', { //切换回index以显示贴文 'title': req.params.category, //标题为query的category 'posts': posts //显示query到的贴文 }); }); });
修改index.jade,将category文字改成连结,点选连结就会显示出该分类的所有贴文
.... p.meta Posted in a(href='/categories/show/#{post.category}') #{post.category} by #{post.author} on #{moment(post.date).format("MM-DD-YYYY")} img(src='/images/#{post.mainimage}') ...
重启 Server 测试,看到 Category 文字带有连结,且点选后只出现该分类的贴文
检视单篇贴文内容
目前点选 Read More 按钮,还无法显示单篇贴文内容
和分类显示相同,需要为单篇贴文显示建立新的 route
修改 posts.js,透过 id 找到对应的贴文并以 show view 显示
...router.get('/show/:id', function(req, res, next) { var posts = db.get('posts'); posts.findById(req.params.id, function(err, post){ res.render('show', { 'post': post }); });});...
新增 views\show.jade 档案,内容从 index.jade copy 过来修改
因为show只需要显示单篇贴文,所以去掉 loop, title 连结,truncate text改成显示完整body
extends layoutblock content .post h1=post.title p.meta Posted in a(href='/categories/show/#{post.category}') #{post.category} by #{post.author} on #{moment(post.date).format("MM-DD-YYYY")} img(src='/images/#{post.mainimage}') !=post.body
重启 Server,点选 Read More 就可以显示完整的单篇贴文了
在单篇贴文中加入 comments 区块
修改 show.js,从 post.body 往下新增 comments 区块
如果目前有任何 comments就会显示,无论是否有已存在 comments 都会显示新增comment的表单
... img(src='/images/#{post.mainimage}') !=post.body br hr if post.comments h3 Comments each comment, i in post.comments .comment p.comment-name #{comment.name} p.comment-body #{comment.body} br h3 Add Comment if errors ul.errors each error, i in errors li.alert.alert-danger #{error.msg} form.comment-form(method='post', action='/posts/addcomment') input(name='postid', type='hidden', value='#{post._id}') .form-group label Name input.form-control(type='text', name='name') .form-group label Email input.form-control(type='text', name='email') .form-group label Body textarea.form-control(type='text', name='body') br input.btn.btn-default(type='submit', name='submit', value='Add Comment')
接下来为 Add Comment 按钮撰写 POST request
修改 post.js,加入新的 POST route (从 add post copy)
router.post('/addcomment', function(req, res, next) { //修改名称为addcomment //Get Form Values var name = req.body.name; var email = req.body.email; var body = req.body.body; var postid = req.body.postid; var commentdate = new Date(); //Form Validation req.checkBody('name', 'Name field is required').notEmpty(); req.checkBody('email', 'Email field is required but never displayed').notEmpty(); req.checkBody('email', 'Email field is not formatted properly').isEmail(); req.checkBody('body', 'Body field is required').notEmpty(); //Check Errors var errors = req.validationErrors(); if(errors){ //如果add comment有error仍要显示贴文 var posts = db.get('posts'); posts.findById(postid, function(err, post){ res.render('show', { "errors": errors, "post": post }); }); } else { //如果没有错误,将comment内容update到该篇贴文的comments栏位 var comment = { "name": name, "email": email, "body": body, "commentdate": commentdate } var posts = db.get('posts'); posts.update({ "_id": postid }, { $push: { "comments": comment } }, function(err, doc){ //都没有问题就切换到单篇贴文页面 if(err){ throw err; } else { req.flash('success', 'Comment Added'); res.location('/posts/show/'+postid); res.redirect('/posts/show/'+postid); } }); }});
重启 Server,新增一篇 comment 测试看看,网页会自动切换,也会显示成功新增comment的讯息