[12 Project 学 Node.js] Project 4 : Node Blog System

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.json
npm install --save-dev nodemon

安装完打开package.json,在 scripts 区块加上执行 nodemon 指令: "dev"

...  "scripts": {    "start": "node ./bin/www",    "dev": "nodemon ./bin/www"  },...

使用以下指定启动,之后程式变更时就会自动重启 Server
npm run dev

http://img2.58codes.com/2024/20104222uVPmocUxcS.png


App & Module Setup

安装express-generator globally
npm install -g express-generator

透过express建立新project目录 4_nodeblog
express 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 类似,就不再多说明

安装 modules
npm 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;}

除了图片之外,其他样式应该如下图
http://img2.58codes.com/2024/20104222kvEqITZnru.png

上面的配色可以随意调配,分享两个之前我自己有在用的网站:

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

http://img2.58codes.com/2024/20104222ek8WvOJhVj.png

修改 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

弄完之后应该长得像这样
http://img2.58codes.com/2024/20104222C6Z8WEbPTF.png


新增贴文功能

接下来要撰写新增贴文的功能
修改 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 没有资料,这项先跳过不选
http://img2.58codes.com/2024/20104222LQoYVEh0ED.png

新增完应该会看到刚刚新增的贴文出现在首页
http://img2.58codes.com/2024/20104222IM8mZyfdZM.png

现在来补足 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 了
http://img2.58codes.com/2024/20104222mZLJ9wo2SI.png

新增一篇带有 category 的贴文,测试成功
http://img2.58codes.com/2024/20104222iGq3cgk64F.png


文字编辑器

在新增贴文时,如果有文字编辑器,贴文的内容就可以有更多变化
这边选择的是 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 已经变成文字编辑器啰
http://img2.58codes.com/2024/20104222E06aBu00Pv.png


新增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已经出现在选单中了
http://img2.58codes.com/2024/20104222Hfrqc4K1J7.pnghttp://img2.58codes.com/2024/20104222tEvTK1TReu.png


缩短文字内容 (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效果

新增一篇很长的贴文
http://img2.58codes.com/2024/20104222ywTU6wvfSR.png

修改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,可以看到过长的贴文被截掉了
http://img2.58codes.com/2024/201042222w8t4lHytE.png

接下来要把 image 加入贴文
先确认新增贴文时上传的图片有在 public\images中
http://img2.58codes.com/2024/20104222ps4bvRfAyx.png

修改 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%;}

重新整理网页,图片就出来啰
http://img2.58codes.com/2024/20104222wvomH12bt4.png

注:没看到图片怎么办

检查 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 文字带有连结,且点选后只出现该分类的贴文
http://img2.58codes.com/2024/20104222N6gyx62lDG.png


检视单篇贴文内容

目前点选 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 就可以显示完整的单篇贴文了
http://img2.58codes.com/2024/20104222ogHgfSrYWy.png


在单篇贴文中加入 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')

http://img2.58codes.com/2024/2010422241T43bS3Y6.png

接下来为 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的讯息
http://img2.58codes.com/2024/201042221PsgGNNstm.pnghttp://img2.58codes.com/2024/20104222cPl8ArxYow.png


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章