iT邦幫忙

2017 iT 邦幫忙鐵人賽
DAY 17
4
Modern Web

Half-Stack Developer 養成計畫系列 第 17

想~簡簡單單愛:超簡單留言板

想~簡簡單單愛:超簡單留言板

經歷了前面的重重難關之後,我們要在今天來做一個超級簡單的留言板。有多簡單呢?我們只要有三項功能就好:

  1. 管理員登入之後可以刪除留言
  2. 首頁可以顯示所有留言
  3. 任何人都可以發表留言

在實際動手 coding 之前,我習慣會先想一下根據上面的需求,大概需要有哪些更細的東西?你要慢慢去想說怎麼把這些需求轉變為程式碼跟檔案。以上面的這三個功能來說,我們大概會需要:

  1. 一個顯示所有留言的 ejs 檔案
  2. 一個發表留言的 ejs 檔案
  3. 一個管理後台的 ejs(或者是併入 1.,直接做在首頁也可以,為了方便起見,我們併入 1 好了)
  4. 管理員登入的介面,一樣是 ejs 檔案
  5. 儲存留言的地方

上面幾點,最重要的就是 5. 儲存留言的地方。我們要怎麼儲存留言呢?這個問題其實很簡單,就是寫到檔案裡面就好了嘛,之前我們有稍微講過怎麼讀檔寫檔了,所以相信對你來說不是一件難事。

可是,檔案的格式呢?我要怎麼把留言存進去?舉例來說,你可以把每一行當作一筆資料,然後用逗號來分割:

1,nick,大家好
2,peter,啊啊啊啊啊啊啊啊
3,pop,你好啊

這樣就是一個不錯的儲存格式。總之格式只要你自己定好,照著實作就沒問題了。不過既然上一章介紹了 JSON,我們就用 JSON 來實作吧,大概會長這樣:

[
  {
    "id": 1,
    "author": "nick",
    "content": "大家好",
    "createTime": "2016-12-22T16:53:17.263Z"
  }, {
    "id": 2,
    "author": "peter",
    "content": "啊啊啊啊啊啊啊",
    "createTime": "2016-12-22T16:53:17.263Z"
  }, {
    "id": 3,
    "author": "pop",
    "content": "你好啊",
    "createTime": "2016-12-22T16:53:17.263Z"
  }
]

我們先來寫一隻叫做 db.js 的檔案,來處理這些檔案操作吧!然後把上面的那個 JSON 儲存成 data.json

var fs = require('fs');
var FILENAME = 'data.json'

var DB = {
  addPost: function (post, cb) {
    DB.getPosts(function (err, posts) {
      if (err) {
        return cb(err);
      }
      posts.push(post);
      DB.savePosts(posts, cb);
    })
  },

  deletePost: function (id, cb) {
    DB.getPosts(function (err, posts) {
      if (err) {
        return cb(err);
      }
      var index = -1;
      for(var i = 0; i < posts.length; i++) {
        if(posts[i].id == id) {
          index = i;
          break;
        }
      }
      if (index >=0 ){
        posts.splice(index, 1);
      }
      DB.savePosts(posts, cb);
    })
  },

  savePosts: function (data, cb) {
    fs.writeFile(FILENAME, JSON.stringify(data), cb);
  },

  getPosts: function (cb) {
    fs.readFile(FILENAME, 'utf8', function (err, data) {
      if (err) {
        cb(err);
      } else {
        cb(err, JSON.parse(data));
      }
    })
  }
}

module.exports = DB;

這一段如果看不懂的需要去複習一下 callback 是什麼意思。其實 js 麻煩的就是在 callback,因為你有時候會忘記你必須去用 callback 接收結果。要記得,幾乎所有耗時的操作都是非同步的,都是額外用 callback function 去接收結果。

有了儲存的程式碼以後,先來寫我們的 index.js,也就是主要負責處理 request 以及 response 的程式碼(記得先把該裝的套件裝一裝,不知道裝什麼的話可以看程式碼去找。其實直接拿上一篇的來改就好):

var express = require('express');
var session = require('express-session')
var bodyParser = require('body-parser')
var db = require('./db');

var app = express();

// 使用 session,要設定一個 secret key
app.use(session({
  secret: 'keyboard cat',
}))

// 有了這個才能透過 req.body 取東西
app.use(bodyParser.urlencoded({ extended: false }))

app.set('view engine', 'ejs');

// 首頁,直接輸出所有留言
app.get('/', function (req, res) {

  // 試著看看 session 裡面有沒有 username 可以拿
  // 判斷是否是管理員
  var username = req.session.username;
  var isAdmin = false;
  if (username) {
    isAdmin = true;
  }

  // 拿出所有的留言
  db.getPosts(function (err, posts) {
    if (err) {
      res.send(err);
    } else {

      // 記得要把 posts 反過來,才是正確的順序
      // 把所有東西丟給 ejs 去處理
      res.render('index', {
        username: username,
        isAdmin: isAdmin,
        posts: posts.reverse()
      });
    }
  })
});

// 刪除文章
app.get('/posts/delete/:id', function (req, res) {
  var id = req.params.id;
  db.deletePost(id, function (err) {
    if (err) {
      res.send(err);
    } else {

      // 成功後導回首頁
      res.redirect('/');
    }
  })
})

// 發表新文章的頁面
app.get('/posts', function (req, res) {
  res.render('newpost');
})

// 新增文章
app.post('/posts', function (req, res) {
  var author = req.body.author;
  var content = req.body.content;

  // 這邊因為我很懶,所以文章 id 採用現在時間 + 一個亂數
  // 不能保證不會重複,這絕對是錯誤作法,請勿參考
  db.addPost({
    author: author,
    content: content,
    createTime: new Date(),
    id: new Date()*1 + Math.floor(Math.random()*99999)
  }, function (err, data) {
    if(err) {
      res.send(err)
    } else {
      res.redirect('/');
    }
  })
})

// 輸出登入頁面
app.get('/login', function (req, res) {
  res.render('login');
})

// 登入,如果帳號密碼是 peter 123 就登入通過
app.post('/login', function(req, res) {
  var username = req.body.username;
  var password = req.body.password;
  if (username === 'peter' && password === '123') {
    console.log('login success');
    req.session.username = 'peter';
  }
  res.redirect('/');
})

// 登出,清除 session
app.get('/logout', function(req, res) {
  req.session.destroy();
  res.redirect('/')
})

app.listen(3000, function () {
  console.log('Example app listening on port 3000!')
})

這邊程式碼有點多,但因為分成很多個 function 的關係,所以還是滿容易讀的。如果有哪邊不懂你就專心研究那個 function 就好。基本上這邊做的事情就是去跟 db 拿資料,處理以後丟到 views 那邊去。

views 的話則是有三個,我們先來看看首頁 index.ejs

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" />
</head>
<body>
  <div class="container">
    <% if (username) { %>
      <a class="btn btn-default" href="/logout">登出</a>
    <% } else { %>
      <a class="btn btn-default" href="/login">登入</a>
    <% } %>

    <a class="btn btn-primary" href="/posts">發表新留言</a>
    <h2>留言列表</h2>
    <div>
      <% for(var i=0; i<posts.length; i++) { %>
        <div class="panel panel-default">
          <div class="panel-heading">
            <h3 class="panel-title"><%= posts[i].author %>, 發佈時間:<%= posts[i].createTime %></h3>
          </div>
          <div class="panel-body">
            <%= posts[i].content  %>
            <% if (isAdmin) { %>
              <a href="/posts/delete/<%= posts[i].id %>">刪除</a>
            <% } %>
          </div>
        </div>
      <% } %>
    </div>
  </div>
</body>
</html>

可以發現這邊有很多的邏輯判斷,因為你必須根據現在有沒有登入來決定要不要顯示登入還是登出的按鈕。刪除按鈕也是只有在有權限的時候才會出現。其他 class 就是直接用 bootstrap 提供的排版了。

再來有兩個 view,分別是讓管理員登入的 login.ejs 以及發表文章的 newpost.ejs,這兩個其實都只是一個 html 的表單而已,沒什麼好講的。

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" />
</head>
<body>
  <div class="container">
    <form method="post" action="/login">
      <div class="form-group">
        <label for="username">帳號</label>
        <input name="username" class="form-control" id="username" placeholder="username">
      </div>
      <div class="form-group">
        <label for="exampleInputPassword1">密碼</label>
        <input name="password" type="password" class="form-control" id="exampleInputPassword1" placeholder="Password">
      </div>
      <button type="submit" class="btn btn-default">送出</button>
    </form>
  </div>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" />
</head>
<body>
  <div class="container">
    <form method="post" action="/posts">
      <div class="form-group">
        <label for="author">作者</label>
        <input name="author" class="form-control" id="author" placeholder="author">
      </div>
      <div class="form-group">
        <label >內容</label>
        <textarea name="content"></textarea>
      </div>
      <button type="submit" class="btn btn-default">送出</button>
    </form>
  </div>
</body>
</html>

最後來看做出來的效果圖:

首頁:
http://ithelp.ithome.com.tw/upload/images/20161225/20091346uayC38X0cB.png

登入之後看到的首頁:
http://ithelp.ithome.com.tw/upload/images/20161225/20091346QwRS504Ykq.png

發表文章頁面:
http://ithelp.ithome.com.tw/upload/images/20161225/20091346OMd6ZMWdwP.png

發表完成以後的頁面:
http://ithelp.ithome.com.tw/upload/images/20161225/20091346PjvDp3pkKz.png

這一章講的內容請你務必全部搞懂,因為這會是你做出來的第一個真的可以動而且功能豐富的作品。你想想看這一篇你學會了什麼?你學會登入跟登出,意思就是任何會員功能的基礎你已經會了;再來你學會了儲存資料跟拿資料,雖然有一些問題(你可以想想看有哪些可能會碰到的問題),但至少可以動。最後你也學會了各種不同的路由設定以及 GET 與 POST 的操作。

跟你講,這些就是寫網頁的最最最基礎,所有困難的功能全部都是這些基礎堆疊起來的。例如說你看 iT 邦,你要寫出一個陽春版的需要什麼?

  1. 會員功能(就登入登出加上記住資料)
  2. 發表文章功能(就跟我們剛做的留言板一樣)
  3. 評論功能(其實就是文章底下再有一個留言板的感覺)
  4. 分類功能(幫每篇文章加上分類而已)
  5. 按讚功能(一樣,再開一個檔案來儲存按讚數目跟文章 id 的關聯就好)

所以你只要能把這一章搞懂,並且加上一些自訂的新功能,我相信你對網頁後端就沒什麼問題了。你有發現我們現在帳號密碼是寫死的嗎?要請你把它改成跟文章一樣,從檔案讀取,並且加上更換密碼的功能。等你完成之後,就可以前往下一章了。


上一篇
你喜歡吃餅乾嗎?我是還好(session 與 cookie)
下一篇
閃開!讓專業的來:SQL 與 NoSQL
系列文
Half-Stack Developer 養成計畫30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言