Node+Koa2+Mysql 搭建簡易博客

Koa2-blog

2018-1-5 更新教程(新增上傳頭像不皆、新增分頁进苍、樣式改版、發(fā)布文章和評論支持markdown語法)
現(xiàn)在GitHub的代碼結(jié)構(gòu)有變現(xiàn)在GitHub的代碼結(jié)構(gòu)有變鸭叙,接口名也有變動(dòng)觉啊。

Node+Koa2+Mysql 搭建簡易博客

預(yù)覽地址

http://blog.wclimb.site

寫在前面

本篇教程一方面是為了自己在學(xué)習(xí)的過程加深記憶,也是總結(jié)的過程沈贝。另一方面給對這方面不太了解的同學(xué)以借鑒經(jīng)驗(yàn)杠人。如發(fā)現(xiàn)問題還望指正,
如果你覺得這篇文章幫助到了你宋下,那就賞臉給個(gè)star吧嗡善,https://github.com/wclimb/Koa2-blog
下一篇可能是 Node + express + mongoose 或 zepto源碼系列
感謝您的閱讀_
ps:關(guān)于markdown代碼縮進(jìn)問題,看起來不太舒服学歧,但復(fù)制到編輯器是正常的喲罩引!

演示效果

img

開發(fā)環(huán)境

  • nodejs v8.1.0
  • koa v2.3.0
  • mysql v5.7.0

準(zhǔn)備工作

文中用到了promise、async await等語法枝笨,所以你需要一點(diǎn)es6的語法袁铐,傳送門當(dāng)然是阮老師的教程了 http://es6.ruanyifeng.com/

如果你已經(jīng)配置好node和mysql可以跳過

經(jīng)常會有人問報(bào)錯(cuò)的問題,運(yùn)行出錯(cuò)大部分是因?yàn)椴恢С謅sync横浑,升級node版本可解決

$ node -v   查看你的node版本剔桨,如果過低則去nodejs官網(wǎng)下載替換之前的版本

下載mysql,并設(shè)置好用戶名和密碼徙融,默認(rèn)可以為用戶名:root洒缀,密碼:123456

進(jìn)入到 bin 目錄下 比如 cd C:\Program Files\MySQL\MySQL Server 5.7\bin

然后開啟mysql

$ mysql -u root -p  

輸入密碼之后創(chuàng)建database(數(shù)據(jù)庫),nodesql是我們創(chuàng)建的數(shù)據(jù)庫

$ create database nodesql;

記住sql語句后面一定要跟;符號欺冀,接下來看看我們創(chuàng)建好的數(shù)據(jù)庫列表

$ show databases;
img

啟用創(chuàng)建的數(shù)據(jù)庫

$ use nodesql;

查看數(shù)據(jù)庫中的表

$ show tables;

顯示Empty set (0.00 sec)帝洪,因?yàn)槲覀冞€沒有建表,稍后會用代碼建表
注釋:
這是后面建表之后的狀態(tài)

img

目錄結(jié)構(gòu)

img
  • config 存放默認(rèn)文件
  • lib 存放操作數(shù)據(jù)庫文件
  • middlewares 存放判斷登錄與否文件
  • public 存放樣式和頭像文件
  • routes 存放路由文件
  • views 存放模板文件
  • index 程序主文件
  • package.json 包括項(xiàng)目名脚猾、作者葱峡、依賴等等

首先我們創(chuàng)建koa2-blog文件夾,然后cd koa2-blog

 接著使用 npm init 來創(chuàng)建package.json

接著安裝包龙助,安裝之前我們使用cnpm安裝

$ npm install -g cnpm --registry=https://registry.npm.taobao.org
$ cnpm i koa koa-bodyparser koa-mysql-session koa-router koa-session-minimal koa-static koa-views md5 moment mysql ejs markdown-it chai mocha koa-static-cache --save-dev

各模塊用處

  1. koa node框架
  2. koa-bodyparser 表單解析中間件
  3. koa-mysql-session砰奕、koa-session-minimal 處理數(shù)據(jù)庫的中間件
  4. koa-router 路由中間件
  5. koa-static 靜態(tài)資源加載中間件
  6. ejs 模板引擎
  7. md5 密碼加密
  8. moment 時(shí)間中間件
  9. mysql 數(shù)據(jù)庫
  10. markdown-it markdown語法
  11. koa-views 模板呈現(xiàn)中間件
  12. chai mocha 測試使用
  13. koa-static-cache 文件緩存

在文件夾里面新建所需文件

img

首先配置config

我們新建default.js文件

const config = {
  // 啟動(dòng)端口
  port: 3000,

  // 數(shù)據(jù)庫配置
  database: {
    DATABASE: 'nodesql',
    USERNAME: 'root',
    PASSWORD: '123456',
    PORT: '3306',
    HOST: 'localhost'
  }
}

module.exports = config

這是我們所需的一些字段,包括端口和數(shù)據(jù)庫連接所需提鸟,最后我們把它exports暴露出去军援,以便可以在別的地方使用

配置index.js文件

index.js

const Koa = require('koa');
const path = require('path')
const bodyParser = require('koa-bodyparser');
const ejs = require('ejs');
const session = require('koa-session-minimal');
const MysqlStore = require('koa-mysql-session');
const config = require('./config/default.js');
const router=require('koa-router')
const views = require('koa-views')
// const koaStatic = require('koa-static')
const staticCache = require('koa-static-cache')
const app = new Koa()


// session存儲配置
const sessionMysqlConfig = {
  user: config.database.USERNAME,
  password: config.database.PASSWORD,
  database: config.database.DATABASE,
  host: config.database.HOST,
}

// 配置session中間件
app.use(session({
  key: 'USER_SID',
  store: new MysqlStore(sessionMysqlConfig)
}))


// 配置靜態(tài)資源加載中間件
// app.use(koaStatic(
//   path.join(__dirname , './public')
// ))
// 緩存
app.use(staticCache(path.join(__dirname, './public'), { dynamic: true }, {
  maxAge: 365 * 24 * 60 * 60
}))
app.use(staticCache(path.join(__dirname, './images'), { dynamic: true }, {
  maxAge: 365 * 24 * 60 * 60
}))

// 配置服務(wù)端模板渲染引擎中間件
app.use(views(path.join(__dirname, './views'), {
  extension: 'ejs'
}))
app.use(bodyParser({
  formLimit: '1mb'
}))

//  路由(我們先注釋三個(gè),等后面添加好了再取消注釋称勋,因?yàn)槲覀冞€沒有定義路由胸哥,稍后會先實(shí)現(xiàn)注冊)
//app.use(require('./routers/signin.js').routes())
app.use(require('./routers/signup.js').routes())
//app.use(require('./routers/posts.js').routes())
//app.use(require('./routers/signout.js').routes())


app.listen(3000)

console.log(`listening on port ${config.port}`)

我們使用koa-session-minimal``koa-mysql-session來進(jìn)行數(shù)據(jù)庫的操作
使用koa-static配置靜態(tài)資源,目錄設(shè)置為public
使用ejs模板引擎
使用koa-bodyparser來解析提交的表單信息
使用koa-router做路由
使用koa-static-cache來緩存文件
之前我們配置了default.js赡鲜,我們就可以在這里使用了
首先引入進(jìn)來 var config = require('./config/default.js');
然后在數(shù)據(jù)庫的操作的時(shí)候空厌,如config.database.USERNAME庐船,得到的就是root。

如果你覺得這篇文章幫助到了你嘲更,那就賞臉給個(gè)star吧筐钟,https://github.com/wclimb/Koa2-blog

配置lib的mysql.js文件

關(guān)于數(shù)據(jù)庫的使用這里介紹一下氨距,首先我們建立了數(shù)據(jù)庫的連接池霎桅,以便后面的操作都可以使用到菩浙,我們創(chuàng)建了一個(gè)函數(shù)query聚凹,通過返回promise的方式以便可以方便用.then()來獲取數(shù)據(jù)庫返回的數(shù)據(jù),然后我們定義了三個(gè)表的字段蕴侧,通過createTable來創(chuàng)建我們后面所需的三個(gè)表府蛇,包括posts(存儲文章)腺怯,users(存儲用戶)毛嫉,comment(存儲評論)诽俯,create table if not exists users()表示如果users表不存在則創(chuàng)建該表,避免每次重復(fù)建表報(bào)錯(cuò)的情況狱庇。后面我們定義了一系列的方法,最后把他們exports暴露出去恶耽。

這里只介紹注冊用戶insertData密任,后續(xù)的可以自行查看,都差不多

// 注冊用戶
let insertData = function( value ) {
  let _sql = "insert into users set name=?,pass=?,avator=?,moment=?;"
  return query( _sql, value )
}

我們寫了一個(gè)_sql的sql語句偷俭,意思是插入到users的表中(在這之前我們已經(jīng)建立了users表)然后要插入的數(shù)據(jù)分別是name浪讳、pass、avator涌萤、moment淹遵,就是用戶名、密碼负溪、頭像透揣、注冊時(shí)間,最后調(diào)用query函數(shù)把sql語句傳進(jìn)去(之前的寫法是"insert into users(name,pass) values(?,?);",換成現(xiàn)在得更容易理解)

lib/mysql.js

var mysql = require('mysql');
var config = require('../config/default.js')

var pool  = mysql.createPool({
  host     : config.database.HOST,
  user     : config.database.USERNAME,
  password : config.database.PASSWORD,
  database : config.database.DATABASE
});

let query = function( sql, values ) {

  return new Promise(( resolve, reject ) => {
    pool.getConnection(function(err, connection) {
      if (err) {
        reject( err )
      } else {
        connection.query(sql, values, ( err, rows) => {

          if ( err ) {
            reject( err )
          } else {
            resolve( rows )
          }
          connection.release()
        })
      }
    })
  })

}


// let query = function( sql, values ) {
// pool.getConnection(function(err, connection) {
//   // 使用連接
//   connection.query( sql,values, function(err, rows) {
//     // 使用連接執(zhí)行查詢
//     console.log(rows)
//     connection.release();
//     //連接不再使用川抡,返回到連接池
//   });
// });
// }

let users =
    `create table if not exists users(
     id INT NOT NULL AUTO_INCREMENT,
     name VARCHAR(100) NOT NULL,
     pass VARCHAR(100) NOT NULL,
     avator VARCHAR(100) NOT NULL,
     moment VARCHAR(100) NOT NULL,
     PRIMARY KEY ( id )
    );`

let posts =
    `create table if not exists posts(
     id INT NOT NULL AUTO_INCREMENT,
     name VARCHAR(100) NOT NULL,
     title TEXT(0) NOT NULL,
     content TEXT(0) NOT NULL,
     md TEXT(0) NOT NULL,
     uid VARCHAR(40) NOT NULL,
     moment VARCHAR(100) NOT NULL,
     comments VARCHAR(200) NOT NULL DEFAULT '0',
     pv VARCHAR(40) NOT NULL DEFAULT '0',
     avator VARCHAR(100) NOT NULL,
     PRIMARY KEY ( id )
    );`

let comment =
    `create table if not exists comment(
     id INT NOT NULL AUTO_INCREMENT,
     name VARCHAR(100) NOT NULL,
     content TEXT(0) NOT NULL,
     moment VARCHAR(40) NOT NULL,
     postid VARCHAR(40) NOT NULL,
     avator VARCHAR(100) NOT NULL,
     PRIMARY KEY ( id )
    );`

let createTable = function( sql ) {
  return query( sql, [] )
}

// 建表
createTable(users)
createTable(posts)
createTable(comment)

// 注冊用戶
let insertData = function( value ) {
  let _sql = "insert into users set name=?,pass=?,avator=?,moment=?;"
  return query( _sql, value )
}
// 刪除用戶
let deleteUserData = function( name ) {
  let _sql = `delete from users where name="${name}";`
  return query( _sql )
}
// 查找用戶
let findUserData = function( name ) {
  let _sql = `select * from users where name="${name}";`
  return query( _sql )
}
// 發(fā)表文章
let insertPost = function( value ) {
  let _sql = "insert into posts set name=?,title=?,content=?,md=?,uid=?,moment=?,avator=?;"
  return query( _sql, value )
}
// 更新文章評論數(shù)
let updatePostComment = function( value ) {
  let _sql = "update posts set comments=? where id=?"
  return query( _sql, value )
}

// 更新瀏覽數(shù)
let updatePostPv = function( value ) {
  let _sql = "update posts set pv=? where id=?"
  return query( _sql, value )
}

// 發(fā)表評論
let insertComment = function( value ) {
  let _sql = "insert into comment set name=?,content=?,moment=?,postid=?,avator=?;"
  return query( _sql, value )
}
// 通過名字查找用戶
let findDataByName = function ( name ) {
  let _sql = `select * from users where name="${name}";`
  return query( _sql)
}
// 通過文章的名字查找用戶
let findDataByUser = function ( name ) {
  let _sql = `select * from posts where name="${name}";`
  return query( _sql)
}
// 通過文章id查找
let findDataById = function ( id ) {
  let _sql = `select * from posts where id="${id}";`
  return query( _sql)
}
// 通過評論id查找
let findCommentById = function ( id ) {
  let _sql = `select * FROM comment where postid="${id}";`
  return query( _sql)
}

// 查詢所有文章
let findAllPost = function () {
  let _sql = ` select * FROM posts;`
  return query( _sql)
}
// 查詢分頁文章
let findPostByPage = function (page) {
  let _sql = ` select * FROM posts limit ${(page-1)*10},10;`
  return query( _sql)
}
// 查詢個(gè)人分頁文章
let findPostByUserPage = function (name,page) {
  let _sql = ` select * FROM posts where name="${name}" order by id desc limit ${(page-1)*10},10 ;`
  return query( _sql)
}
// 更新修改文章
let updatePost = function(values){
  let _sql = `update posts set  title=?,content=?,md=? where id=?`
  return query(_sql,values)
}
// 刪除文章
let deletePost = function(id){
  let _sql = `delete from posts where id = ${id}`
  return query(_sql)
}
// 刪除評論
let deleteComment = function(id){
  let _sql = `delete from comment where id=${id}`
  return query(_sql)
}
// 刪除所有評論
let deleteAllPostComment = function(id){
  let _sql = `delete from comment where postid=${id}`
  return query(_sql)
}
// 查找評論數(shù)
let findCommentLength = function(id){
  let _sql = `select content from comment where postid in (select id from posts where id=${id})`
  return query(_sql)
}

// 滾動(dòng)無限加載數(shù)據(jù)
let findPageById = function(page){
  let _sql = `select * from posts limit ${(page-1)*5},5;`
  return query(_sql)
}
// 評論分頁
let findCommentByPage = function(page,postId){
  let _sql = `select * from comment where postid=${postId} order by id desc limit ${(page-1)*10},10;`
  return query(_sql)
}

module.exports = {
    query,
    createTable,
    insertData,
    deleteUserData,
    findUserData,
    findDataByName,
    insertPost,
    findAllPost,
    findPostByPage,
    findPostByUserPage,
    findDataByUser,
    findDataById,
    insertComment,
    findCommentById,
    updatePost,
    deletePost,
    deleteComment,
    findCommentLength,
    updatePostComment,
    deleteAllPostComment,
    updatePostPv,
    findPageById,
    findCommentByPage
}


下面是我們建的表

users posts comment
id id id
name name name
pass title content
avator content moment
moment md postid
- uid avator
- moment -
- comments -
- pv -
- avator -
  • id主鍵遞增
  • name: 用戶名
  • pass:密碼
  • avator:頭像
  • title:文章標(biāo)題
  • content:文章內(nèi)容和評論
  • md:markdown語法
  • uid:發(fā)表文章的用戶id
  • moment:創(chuàng)建時(shí)間
  • comments:文章評論數(shù)
  • pv:文章瀏覽數(shù)
  • postid:文章id

現(xiàn)在感覺有點(diǎn)枯燥辐真,那我們先來實(shí)現(xiàn)一下注冊吧

實(shí)現(xiàn)注冊頁面

routers/singup.js

const router = require('koa-router')();
const userModel = require('../lib/mysql.js');
const md5 = require('md5')
const checkNotLogin = require('../middlewares/check.js').checkNotLogin
const checkLogin = require('../middlewares/check.js').checkLogin
const moment = require('moment');
const fs = require('fs')
// 注冊頁面
router.get('/signup', async(ctx, next) => {
    await checkNotLogin(ctx)
    await ctx.render('signup', {
        session: ctx.session,
    })
})
    
module.exports = router

使用get方式得到'/signup'頁面,然后渲染signup模板崖堤,這里我們還沒有在寫signup.ejs

views/signup.ejs

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>注冊</title>
</head>
<body>
    <div class="container">
        <form class="form create" method="post">
            <div>
                <label>用戶名:</label> 
                <input placeholder="請輸入用戶名" type="text" name="name">
            </div>
            <div>
                <label>密碼:</label> 
                <input placeholder="請輸入密碼" class="password" type="password" name="password">
            </div>
            <div>
                <label>重復(fù)密碼:</label> 
                <input placeholder="請確認(rèn)密碼" class="repeatpass" type="password" name="repeatpass">
            </div>
            <div>
                <label>上傳頭像:</label>
                <input type="file" name="avator" id="avator">
                <input type="hidden" id="avatorVal">
                <img class="preview" alt="預(yù)覽頭像">
            </div>
            <div class="submit">注冊</div>
        </form>
    </div>
</body>
</html>

我們先安裝supervisor

$ cnpm i supervisor -g

supervisor的作用是會監(jiān)聽文件的變化侍咱,而我們修改文件之后不必去重啟程序

supervisor --harmony index

現(xiàn)在訪問 localhost:3000/signup 看看效果吧。注意數(shù)據(jù)庫一定要是開啟的狀態(tài)密幔,不能關(guān)閉

完善注冊功能

首先我們來完善一下樣式吧楔脯,稍微美化一下

public/index.css

body,
header,
ul,
li,
p,
div,
html,
span,
h3,
a,
blockquote {
    margin: 0;
    padding: 0;
    color: #333;
}

body {
    margin-bottom: 20px;
}
ul,li{
    list-style-type: none;
}
a {
    text-decoration: none;
}

header {
    width: 60%;
    margin: 20px auto;
}
header:after{
    content: '';
    clear: both;
    display: table;
}
header .user_right{
    float: right
}
header .user_right .active{
    color: #5FB878;
    background: #fff;
    border: 1px solid #5FB878;
    box-shadow: 0 5px 5px #ccc;
}
header .user_name {
    float: left
}
.user_name {
    font-size: 20px;
}

.has_user a,
.has_user span,
.none_user a {
    padding: 5px 15px;
    background: #5FB878;
    border-radius: 15px;
    color: #fff;
    cursor: pointer;
    border: 1px solid #fff;
    transition: all 0.3s;
}

.has_user a:hover,.has_user span:hover{
    color: #5FB878;
    background: #fff;
    border: 1px solid #5FB878;
    box-shadow: 0 5px 5px #ccc;
}

.posts{
    border-radius: 4px; 
    border: 1px solid #ddd;
}
.posts > li{
    padding: 10px;
    position: relative;
    padding-bottom: 40px;
}
.posts .comment_pv{
    position: absolute;
    bottom: 5px;
    right: 10px;
}
.posts .author{
    position: absolute;
    left: 10px;
    bottom: 5px;
}
.posts .author span{
    margin-right: 5px;
}
.posts > li:hover {
    background: #f2f2f2;
}
.posts > li:hover pre{
    border: 1px solid #666;
}
.posts > li:hover .content{
    border-top: 1px solid #fff;
    border-bottom: 1px solid #fff;
}
.posts > li + li{
    border-top: 1px solid #ddd;
}
.posts li .title span{
    position: absolute;
    left: 10px;
    top: 10px;
    color: #5FB878;
    font-size: 14px;
}
.posts li .title{
     margin-left: 40px;
     font-size: 20px;
     color: #222;
}
.posts .userAvator{
    position: absolute;
    left: 3px;
    top: 3px;
    width: 40px;
    height: 40px;
    border-radius: 5px;
}
.posts .content{
    border-top: 1px solid #f2f2f2;
    border-bottom: 1px solid #f2f2f2;
    margin: 10px 0 0 0 ;
    padding: 10px 0;
    margin-left: 40px;
}
.userMsg img{
    width: 40px;
    height: 40px;
    border-radius: 5px;
    margin-right: 10px;
    vertical-align: middle;
    display: inline-block;
}
.userMsg span{
    font-size: 18px;
    color:#333;
    position: relative;
    top: 2px;
}
.posts li img{
    max-width: 100%;
}
.spost .comment_pv{
    position: absolute;
    top: 10px;
}
.spost .edit {
    position: absolute;
    right: 20px;
    bottom: 5px;
}

.spost .edit p {
    display: inline-block;
    margin-left: 10px;
}

.comment_wrap {
    width: 60%;
    margin: 20px auto;
}

.submit {
    display: block;
    width: 100px;
    height: 40px;
    line-height: 40px;
    text-align: center;
    border-radius: 4px;
    background: #5FB878;
    cursor: pointer;
    color: #fff;
    float: left;
    margin-top: 20px ;
    border:1px solid #fff;
}
.submit:hover{
    background: #fff;
    color: #5FB878;
    border:1px solid #5FB878;
}
.comment_list{
    border: 1px solid #ddd;
    border-radius: 4px;
}
.cmt_lists:hover{
    background: #f2f2f2;
}
.cmt_lists + .cmt_lists{
    border-top: 1px solid #ddd;
}
.cmt_content {
    padding: 10px;
    position: relative;
    border-radius: 4px;
    word-break: break-all;
}
.cmt_detail{
    margin-left: 48px;
}
.cmt_content img{
    max-width: 100%;
}
/* .cmt_content:after {
    content: '#content';
    position: absolute;
    top: 5px;
    right: 5px;
    color: #aaa;
    font-size: 13px;
}
 */
.cmt_name {
    position: absolute;
    right: 8px;
    bottom: 5px;
    color: #333;
}

.cmt_name a {
    margin-left: 5px;
    color: #1E9FFF;
}
.cmt_time{
    position: absolute;
    font-size: 12px;
    right: 5px;
    top: 5px;
    color: #aaa
}
.form {
    margin: 0 auto;
    width: 50%;
    margin-top: 20px;
}

textarea {
    width: 100%;
    height: 150px;
    padding:10px 0 0 10px;
    font-size: 20px;
    border-radius: 4px;   
    border: 1px solid #d7dde4;
    -webkit-appearance: none;
    resize: none;
}

textarea#spContent{
    width: 98%;
}

.tips {
    margin: 20px 0;
    color: #ec5051;
    text-align: center;
}

.container {
    width: 60%;
    margin: 0 auto;
}
.form img.preview {
    width:100px;
    height:100px;
    border-radius: 50%;
    display: none;
    margin-top:10px;
}
input {
    display: block;
    width: 100%;
    height: 35px;
    font-size: 18px;
    padding: 6px 7px;   
    border-radius: 4px;   
    border: 1px solid #d7dde4;
    -webkit-appearance: none;
}

input:focus,textarea:focus{
    outline: 0;
    box-shadow: 0 0 0 2px rgba(51,153,255,.2);
    border-color: #5cadff;
}

input:hover,input:active,textarea:hover,textarea:active{
    border-color: #5cadff;
}

.create label {
    display: block;
    margin: 10px 0;
}

.comment_wrap form {
    width: 100%;
    margin-bottom: 85px;
}

.delete_comment,
.delete_post {
    cursor: pointer;
}

.delete_comment:hover,
.delete_post:hover,
a:hover {
    color: #ec5051;
}
.disabled{
    user-select: none;
    cursor: not-allowed !important;
}
.error{
    color: #ec5051;
}
.success{
    color: #1E9FFF;
}
.container{
    width: 60%;
    margin:0 auto;
}
.message{
    position: fixed;
    top: -100%;
    left: 50%;
    transform: translateX(-50%);
    padding: 10px 20px;
    background: rgba(0, 0, 0, 0.7);
    color: #fff;
    border-bottom-left-radius: 15px;
    border-bottom-right-radius: 15px;
    z-index: 99999;
}
.markdown pre{
    display: block;
    overflow-x: auto;
    padding: 0.5em;
    background: #F0F0F0;
    border-radius: 3px;
    border: 1px solid #fff;
}
.markdown blockquote{
    padding: 0 1em;
    color: #6a737d;
    border-left: 0.25em solid #dfe2e5;
    margin: 10px 0;
}
.markdown ul li{
    list-style: circle;
    margin-top: 5px;
}

我們再把模板引擎的header和footer獨(dú)立出來

/views/header.ejs
順便引入index.css和jq

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>koa2-blog</title>
    <link rel="icon" >
    <link rel="stylesheet" href="/index.css">
    <script src="http://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
    <script>
        function fade(txt){
            $('.message').text(txt)
            $('.message').animate({
                top:0
            })
            setTimeout(function(){
                $('.message').animate({
                    top: '-100%'
                })
            },1500)
        }
        $(function(){
            $('.signout').click(()=>{
                $.ajax({
                    url: "/signout",
                    type: "GET",
                    cache: false,
                    dataType: 'json',
                    success: function (msg) {
                        if (msg) {
                            fade('登出成功')
                            setTimeout(()=>{
                                window.location.href = "/posts"
                            },1500) 
                        }
                    },
                    error: function () {
                        alert('異常');
                    }
                })
            })
        })
    </script>
</head>
<body>
    <header>
        <div class="user_name">
            <% if(session.user){ %>
                 Hello,<%= session.user %>
            <% } %>
            <% if(!session.user){ %>
                歡迎注冊登錄^_^
            <% } %>
        </div>
        <div class="message">登錄成功</div>
        <div class="user_right">
            <%  if(session.user){ %>
                <div class="has_user">
                    <a target="__blank" >GitHub</a>
                    <% if(type == 'all'){ %>
                        <a class="active" href="/posts">全部文章</a>
                    <% }else{ %>
                        <a href="/posts">全部文章</a>
                    <% }%>
                    <% if(type == 'my'){ %>
                        <a class="active" href="/posts?author=<%= session.user %>">我的文章</a>
                    <% }else{ %>
                        <a href="/posts?author=<%= session.user %>">我的文章</a>
                    <% }%>
                    <% if(type == 'create'){ %>
                        <a class="active" href="/create">發(fā)表文章</a>
                    <% }else{ %>
                        <a href="/create">發(fā)表文章</a>
                    <% }%>
                    
                    <span class="signout">登出</span>
                </div>
            <% } %>
            <% if(!session.user){ %>
                <div class="none_user has_user">
                    <a target="__blank" >GitHub</a>
                    <% if(type == 'all'){ %>
                        <a class="active" href="/posts">全部文章</a>
                    <% }else{ %>
                        <a href="/posts">全部文章</a>
                    <% }%>
                    <% if(type == 'signup'){ %>
                        <a class="active" href="/signup">注冊</a>
                    <% }else{ %>
                        <a href="/signup">注冊</a>
                    <% }%>
                    <% if(type == 'signin'){ %>
                        <a class="active" href="/signin">登錄</a>
                    <% }else{ %>
                        <a href="/signin">登錄</a>
                    <% }%>
                </div>
            <% } %>
        </div>
    </header>

首先我們看到用到了session.user,這個(gè)值從哪來呢胯甩?請看下面的代碼

// 注冊頁面
router.get('/signup', async(ctx, next) => {
    await checkNotLogin(ctx)
    await ctx.render('signup', {
        session: ctx.session,
    })
})

我們可以看到我們向模板傳了一個(gè)session值昧廷,session:ctx.session堪嫂,這個(gè)值存取的就是用戶的信息,包括用戶名麸粮、登錄之后的id等溉苛,session一般是你關(guān)閉瀏覽器就過期了,等于下次打開瀏覽器的時(shí)候就得重新登錄了弄诲,用if判斷他存不存在愚战,就可以知道用戶是否需要登錄,如果不存在用戶齐遵,則只顯示全部文章 注冊 登錄 ,如果session.user存在則有登出的按鈕寂玲。

在上面我們會看到我用了另外一個(gè)if判斷,判斷type類型梗摇,這樣做的目的是比如我們登錄注冊頁面拓哟,注冊頁面的導(dǎo)航會高亮,其實(shí)就是添加了class:active;
之后我們每個(gè)ejs文件的頭部會這樣寫<%- include("header",{type:'signup'}) %> 登錄頁面則是<%- include("header",{type:'signin'}) %>

/views/footer.ejs

    
</body>
</html>

修改views/signup.ejs

<%- include("header",{type:'signup'}) %>
    <div class="container">
        <form class="form create" method="post">
            <div>
                <label>用戶名:</label> 
                <input placeholder="請輸入用戶名" type="text" name="name">
            </div>
            <div>
                <label>密碼:</label> 
                <input placeholder="請輸入密碼" class="password" type="password" name="password">
            </div>
            <div>
                <label>重復(fù)密碼:</label> 
                <input placeholder="請確認(rèn)密碼" class="repeatpass" type="password" name="repeatpass">
            </div>
            <div>
                <label>上傳頭像:</label>
                <input type="file" name="avator" id="avator">
                <input type="hidden" id="avatorVal">
                <img class="preview" alt="預(yù)覽頭像">
            </div>
            <div class="submit">注冊</div>
        </form>
    </div>
    <script>
        $(window).keyup(function (e) {
            //console.log(e.keyCode)
            if (e.keyCode == 13) {
                $('.submit').click()
            }
        })
        $('#avator').change(function(){
            if (this.files.length != 0) {
                var file = this.files[0],
                    reader = new FileReader();
                if (!reader) {
                    this.value = '';
                    return;
                };
                console.log(file.size)
                if (file.size >= 1024 * 1024 / 2) {
                    fade("請上傳小于512kb的圖片!")
                    return 
                }
                reader.onload = function (e) {
                    this.value = '';
                    $('form .preview').attr('src', e.target.result)
                    $('form .preview').fadeIn()
                    $('#avatorVal').val(e.target.result)
                };
                reader.readAsDataURL(file);
            };
        })
        $('.submit').click(()=>{
            // console.log($('.form').serialize())
            if ($('input[name=name]').val().trim() == '') {
                fade('請輸入用戶名伶授!')
            }else if($('input[name=name]').val().match(/[<'">]/g)){
                fade('請輸入合法字符断序!')
            }else if($('#avatorVal').val() == ''){
                fade('請上傳頭像!')
            }else{
                $.ajax({
                    url: "/signup",
                    data: {
                        name: $('input[name=name]').val(),
                        password: $('input[name=password]').val(),
                        repeatpass: $('input[name=repeatpass]').val(),
                        avator: $('#avatorVal').val(),
                    },
                    type: "POST",
                    cache: false,
                    dataType: 'json',
                    success: function (msg) {
                       if (msg.data == 1) {                 
                           $('input').val('')
                           fade('用戶名存在')
                       }
                       else if (msg.data == 2){
                            fade('請輸入重復(fù)的密碼')                        
                       }
                       else if(msg.data == 3){
                            fade('注冊成功')
                            setTimeout(()=>{
                                window.location.href = "/signin"      
                            },1000)
                        }
                    },
                    error: function () {
                        alert('異常');
    
                    }
                })          
            }
        })      
    </script>
<% include footer %>

先看我們請求的url地址糜烹,是'/signup'违诗,為什么是這個(gè)呢?我們看下面這段代碼(后面有完整的)

router.post('/signup', async(ctx, next) => {
    //console.log(ctx.request.body)
    let user = {
        name: ctx.request.body.name,
        pass: ctx.request.body.password,
        repeatpass: ctx.request.body.repeatpass,
        avator: ctx.request.body.avator
    }
    ....
}

我們的請求方式是post疮蹦,地址是/signup所以走了這段代碼诸迟,之后會獲取我們輸入的信息,通過ctx.request.body拿到

這里重點(diǎn)就在于ajax提交了愕乎,提交之后換回?cái)?shù)據(jù) 1 2 3阵苇,然后分別做正確錯(cuò)誤處理,把信息寫在error和success中

修改routers/signup.js

const router = require('koa-router')();
const userModel = require('../lib/mysql.js');
const md5 = require('md5')
const checkNotLogin = require('../middlewares/check.js').checkNotLogin
const checkLogin = require('../middlewares/check.js').checkLogin
const moment = require('moment');
const fs = require('fs')
// 注冊頁面
router.get('/signup', async(ctx, next) => {
    await checkNotLogin(ctx)
    await ctx.render('signup', {
        session: ctx.session,
    })
})
// post 注冊
router.post('/signup', async(ctx, next) => {
    //console.log(ctx.request.body)
    let user = {
        name: ctx.request.body.name,
        pass: ctx.request.body.password,
        repeatpass: ctx.request.body.repeatpass,
        avator: ctx.request.body.avator
    }
    await userModel.findDataByName(user.name)
        .then(async (result) => {
            console.log(result)
            if (result.length) {
                try {
                    throw Error('用戶已經(jīng)存在')
                } catch (error) {
                    //處理err
                    console.log(error)
                }
                // 用戶存在
                ctx.body = {
                    data: 1
                };;
                
            } else if (user.pass !== user.repeatpass || user.pass === '') {
                ctx.body = {
                    data: 2
                };
            } else {
                // ctx.session.user=ctx.request.body.name   
                let base64Data = user.avator.replace(/^data:image\/\w+;base64,/, "");
                let dataBuffer = new Buffer(base64Data, 'base64');
                let getName = Number(Math.random().toString().substr(3)).toString(36) + Date.now()
                await fs.writeFile('./public/images/' + getName + '.png', dataBuffer, err => { 
                    if (err) throw err;
                    console.log('頭像上傳成功') 
                });            
                await userModel.insertData([user.name, md5(user.pass), getName, moment().format('YYYY-MM-DD HH:mm:ss')])
                    .then(res=>{
                        console.log('注冊成功',res)
                        //注冊成功
                        ctx.body = {
                            data: 3
                        };
                    })
            }
        })
})
module.exports = router
  • 我們使用md5實(shí)現(xiàn)密碼加密感论,長度是32位的
  • 使用我們之前說的bodyParse來解析提交的數(shù)據(jù)绅项,通過ctx.request.body得到
  • 我們引入了數(shù)據(jù)庫的操作 findDataByName和insertData,因?yàn)橹拔覀冊?lib/mysql.js中已經(jīng)把他們寫好比肄,并暴露出來了趁怔。意思是先從數(shù)據(jù)庫里面查找注冊的用戶名,如果找到了證明該用戶名已經(jīng)被注冊過了薪前,如果沒有找到則使用insertData增加到數(shù)據(jù)庫中
  • ctx.body 是我們通過ajax提交之后給頁面返回的數(shù)據(jù)润努,比如提交ajax成功之后msg.data=1的時(shí)候就代表用戶存在,msg.data出現(xiàn)在后面的signup.ejs模板ajax請求中
  • 上傳頭像之前要新建好文件夾示括,我們ajax發(fā)送的是base64的格式铺浇,然后使用fs.writeFile來生成圖片

我們使用ajax來提交數(shù)據(jù),方便來做成功錯(cuò)誤的處理

模板引擎ejs

我們使用的是ejs垛膝,語法可以見ejs

之前我們寫了這么一段代碼

router.get('/signup',async (ctx,next)=>{
    await ctx.render('signup',{
        session:ctx.session,
    })
})

這里就用到了ejs所需的session 我們通過渲染signup.ejs模板鳍侣,將值ctx.session賦值給session丁稀,之后我們就可以在signup.ejs中使用了
ejs的常用標(biāo)簽為:

  • <% code %>:運(yùn)行 JavaScript 代碼,不輸出
  • <%= code %>:顯示轉(zhuǎn)義后的 HTML內(nèi)容
  • <%- code %>:顯示原始 HTML 內(nèi)容

<%= code %><%- code %>的區(qū)別在于倚聚,<%= code %>不管你寫什么都會原樣輸出线衫,比如code為 <strong>hello</strong>的時(shí)候 <%= code %> 會顯示<strong>hello</strong>
<%- code %>則顯示加粗的hello

實(shí)現(xiàn)登錄頁面

img

修改 /routers/signin.js

const router = require('koa-router')();
const userModel = require('../lib/mysql.js')
const md5 = require('md5')
const checkNotLogin = require('../middlewares/check.js').checkNotLogin
const checkLogin = require('../middlewares/check.js').checkLogin

router.get('/signin', async(ctx, next) => {
    await checkNotLogin(ctx)
    await ctx.render('signin', {
        session: ctx.session,
    })
})
module.exports=router

修改 /views/signin.ejs

<%- include("header",{type:'signin'}) %>
    <div class="container">
        <form class="form create" method="post ">
            <div>
                <label>用戶名:</label> 
                <input placeholder="用戶名" type="text" name="name">
            </div>
            <div>
                <label>密碼:</label> 
                <input placeholder="密碼" type="password" name="password">
            </div>
            <div class="submit">登錄</div>
        </form>     
    </div>
<% include footer %>

修改 index.js 文件 把下面這段代碼注釋去掉,之前注釋是因?yàn)槲覀儧]有寫signin的路由惑折,以免報(bào)錯(cuò)授账,后面還有文章頁和登出頁的路由,大家記住一下

app.use(require('./routers/signin.js').routes())

現(xiàn)在注冊一下來看看效果吧

$ supervisor --harmony index
img

我們怎么查看我們注冊好的賬號和密碼呢惨驶?打開mysql控制臺

$ select * from users;

這樣剛剛我們注冊的用戶信息都出現(xiàn)了


img

如果你覺得這篇文章幫助到了你白热,那就賞臉給個(gè)star吧,https://github.com/wclimb/Koa2-blog

登錄頁面

修改signin
routers/signin.js

const router = require('koa-router')();
const userModel = require('../lib/mysql.js')
const md5 = require('md5')
const checkNotLogin = require('../middlewares/check.js').checkNotLogin
const checkLogin = require('../middlewares/check.js').checkLogin

router.get('/signin', async(ctx, next) => {
    await checkNotLogin(ctx)
    await ctx.render('signin', {
        session: ctx.session,
    })
})

router.post('/signin', async(ctx, next) => {
    console.log(ctx.request.body)
    let name = ctx.request.body.name;
    let pass = ctx.request.body.password;

    await userModel.findDataByName(name)
        .then(result => {
            let res = result
            if (name === res[0]['name'] && md5(pass) === res[0]['pass']) {
                ctx.body = true
                ctx.session.user = res[0]['name']
                ctx.session.id = res[0]['id']
                console.log('ctx.session.id', ctx.session.id)
                console.log('session', ctx.session)
                console.log('登錄成功')
            }else{
                ctx.body = false
                console.log('用戶名或密碼錯(cuò)誤!')
            }
        }).catch(err => {
            console.log(err)
        })

})

module.exports = router

我們進(jìn)行登錄操作粗卜,判斷登錄的用戶名和密碼是否有誤屋确,使用md5加密
我們可以看到登錄成功返回的結(jié)果是result 結(jié)果是這樣的一個(gè)json數(shù)組:id:4 name:rrr pass:...
[ RowDataPacket { id: 4, name: 'rrr', pass: '44f437ced647ec3f40fa0841041871cd' } ]

修改views/signin.ejs
signin.ejs

<%- include("header",{type:'signin'}) %>
    <div class="container">
        <form class="form create" method="post ">
            <div>
                <label>用戶名:</label> 
                <input placeholder="用戶名" type="text" name="name">
            </div>
            <div>
                <label>密碼:</label> 
                <input placeholder="密碼" type="password" name="password">
            </div>
            <div class="submit">登錄</div>
        </form>     
    </div>
    <script>
        $(window).keyup(function(e){
            //console.log(e.keyCode)
            if (e.keyCode == 13) {
                $('.submit').click()
            }
        })
        $('.submit').click(()=>{
            if ($('input[name=name]').val().trim() == '' || $('input[name=password]').val().trim() == '' ) {
                fade('請輸入用戶名或密碼')
            }else{
                console.log($('.form').serialize())
                $.ajax({
                    url: "/signin",
                    data: $('.form').serialize(),
                    type: "POST",
                    cache: false,
                    dataType: 'json',
                    success: function (msg) {
                        if (!msg) {
                            $('input').val('')
                            fade('用戶名或密碼錯(cuò)誤')
                        } else{
                            fade('登錄成功')
                            setTimeout(()=>{
                                window.location.href = "/posts"
                            },1500)                 
                        }
                    },
                    error: function () {
                        alert('異常');
                    }
                })          
            }
        })      
    </script>
<% include footer %>

我們增加了ajax請求,在routers/signin.js里续扔,我們設(shè)置如果登錄失敗就返回false攻臀,登錄成功返回true

ctx.body = false
ctx.body = true

那我們登錄成功后要做跳轉(zhuǎn),可以看到window.location.href="/posts"跳轉(zhuǎn)到posts頁面

全部文章

img

修改routers/posts.js

posts.js

const router = require('koa-router')();
const userModel = require('../lib/mysql.js')
const moment = require('moment')
const checkNotLogin = require('../middlewares/check.js').checkNotLogin
const checkLogin = require('../middlewares/check.js').checkLogin;
const md = require('markdown-it')();  
// 重置到文章頁
router.get('/', async(ctx, next) => {
    ctx.redirect('/posts')
})
// 文章頁
router.get('/posts', async(ctx, next) => {
    let res,
        postsLength,
        name = decodeURIComponent(ctx.request.querystring.split('=')[1]);
    if (ctx.request.querystring) {
        console.log('ctx.request.querystring', name)
        await userModel.findDataByUser(name)
            .then(result => {
                postsLength = result.length
            })
        await userModel.findPostByUserPage(name,1)
            .then(result => {
                res = result
            })
        await ctx.render('selfPosts', {
            session: ctx.session,
            posts: res,
            postsPageLength:Math.ceil(postsLength / 10),
        })
    } else {
        await userModel.findPostByPage(1)
            .then(result => {
                //console.log(result)
                res = result
            })
        await userModel.findAllPost()
            .then(result=>{
                postsLength = result.length
            })    
        await ctx.render('posts', {
            session: ctx.session,
            posts: res,
            postsLength: postsLength,
            postsPageLength: Math.ceil(postsLength / 10),
            
        })
    }
})
// 首頁分頁纱昧,每次輸出10條
router.post('/posts/page', async(ctx, next) => {
    let page = ctx.request.body.page;
    await userModel.findPostByPage(page)
            .then(result=>{
                //console.log(result)
                ctx.body = result   
            }).catch(()=>{
            ctx.body = 'error'
        })  
})
// 個(gè)人文章分頁刨啸,每次輸出10條
router.post('/posts/self/page', async(ctx, next) => {
    let data = ctx.request.body
    await userModel.findPostByUserPage(data.name,data.page)
            .then(result=>{
                //console.log(result)
                ctx.body = result   
            }).catch(()=>{
            ctx.body = 'error'
        })  
})
module.exports = router

修改 index.js

app.use(require('./routers/posts.js').routes())的注釋去掉

修改 views/posts.ejs

<%- include("header",{type:'posts'}) %>

    posts

<% include footer %>

現(xiàn)在看看登錄成功之后的頁面吧

接下來我們實(shí)現(xiàn)登出頁面

登出頁面

修改 router/signout.js

signout.js

const router = require('koa-router')();

router.get('/signout', async(ctx, next) => {
    ctx.session = null;
    console.log('登出成功')
    ctx.body = true
})

module.exports = router

把session設(shè)置為null即可

修改 index.js

app.use(require('./routers/posts.js').routes())的注釋去掉,現(xiàn)在把注釋的路由全部取消注釋就對了

然后我們看看 views/header.ejs

我們點(diǎn)擊登出后的ajax 的提交砌些,成功之后回到posts頁面

發(fā)表文章

修改router/posts
在后面增加

// 發(fā)表文章頁面
router.get('/create', async(ctx, next) => {
    await ctx.render('create', {
        session: ctx.session,
    })
})

// post 發(fā)表文章
router.post('/create', async(ctx, next) => {
    let title = ctx.request.body.title,
        content = ctx.request.body.content,
        id = ctx.session.id,
        name = ctx.session.user,
        time = moment().format('YYYY-MM-DD HH:mm:ss'),
        avator,
        // 現(xiàn)在使用markdown不需要單獨(dú)轉(zhuǎn)義
        newContent = content.replace(/[<">']/g, (target) => { 
            return {
                '<': '&lt;',
                '"': '&quot;',
                '>': '&gt;',
                "'": '&#39;'
            }[target]
        }),
        newTitle = title.replace(/[<">']/g, (target) => {
            return {
                '<': '&lt;',
                '"': '&quot;',
                '>': '&gt;',
                "'": '&#39;'
            }[target]
        });

    //console.log([name, newTitle, content, id, time])
    await userModel.findUserData(ctx.session.user)
        .then(res => {
            console.log(res[0]['avator'])
            avator = res[0]['avator']       
        })
    await userModel.insertPost([name, newTitle, md.render(content), content, id, time,avator])
            .then(() => {
                ctx.body = true
            }).catch(() => {
                ctx.body = false
            })

})

修改 views/create.ejs

create.ejs


img
<%- include("header",{type:'create'}) %>
<div class="container">
    <form style="width:100%" method="post" class="form create">
        <div>
            <label>標(biāo)題:</label>
            <input placeholder="請輸入標(biāo)題" type="text" name="title">
        </div>
        <div>
            <label>內(nèi)容:</label>
            <textarea placeholder="請輸入內(nèi)容" name="content" id="" cols="42" rows="10"></textarea>
        </div>
        <div class="submit">發(fā)表</div>
    </form>
</div>
<script>
    $('.submit').click(()=>{
        if ($('input[name=title]').val().trim() == '') {
            fade('請輸入標(biāo)題')
        }else if ($('textarea').val().trim() == '') {
            fade('請輸入內(nèi)容')
        }else{          
            $.ajax({
                url: "/create",
                data: $('.form').serialize(),
                type: "POST",
                cache: false,
                dataType: 'json',
                success: function (msg) {
                    if (msg) {
                        fade('發(fā)表成功')
                        setTimeout(()=>{
                            window.location.href="/posts"
                        },1000)
                    }else{
                        fade('發(fā)表失敗')
                    }
                },
                error: function () {
                    alert('異常');
                }
            })          
        }   
    })
</script>
<% include footer %>

現(xiàn)在看看能不能發(fā)表吧

即使我們發(fā)表了文章呜投,但是當(dāng)前我們的posts的頁面沒有顯示加匈,因?yàn)檫€沒有獲取到數(shù)據(jù)

我們可以看我們之前寫的代碼 router.get('/posts', async(ctx, next) => {}路由

if (ctx.request.querystring) {
    ...
}else {
        await userModel.findPostByPage(1)
            .then(result => {
                //console.log(result)
                res = result
            })
        await userModel.findAllPost()
            .then(result=>{
                postsLength = result.length
            })    
        await ctx.render('posts', {
            session: ctx.session,
            posts: res,
            postsLength: postsLength,
            postsPageLength: Math.ceil(postsLength / 10),
            
        })
    }

if前面這部分我們先不用管存璃,后面會說。只需要看else后面的代碼我們通過userModel.findPostByPage(1)來獲取第一頁的數(shù)據(jù)雕拼,然后查找所有文章的數(shù)量纵东,最后除以10拿到首頁文章的頁數(shù),把數(shù)據(jù)postsPageLength的值傳給模板posts.ejs啥寇。這樣就可以渲染出來了

修改 views/posts.ejs

posts.ejs

<%- include("header",{type:'all'}) %>
    <div class="container">
        <ul class="posts">
            <% posts.forEach(function(res){ %>
                <li>
                    <div class="author">
                        <span title="<%= res.name %>"><a href="/posts?author=<%= res.name %> ">author: <%= res.name %></a></span>
                        <span>評論數(shù):<%= res.comments %></span>
                        <span>瀏覽量:<%= res.pv %></span>
                    </div>
                    <div class="comment_pv">
                        <span><%= res.moment %></span>
                    </div>
                    <a href="/posts/<%= res.id %>">
                        <div class="title">
                            <img class="userAvator" src="images/<%= res.avator %>.png">
                            <%= res.title %>
                        </div>
                        <div class="content markdown">
                            <%- res.content %>
                        </div>
                    </a>
                </li>
            <% }) %>
        </ul>
        <div style="margin-top: 30px" class="pagination" id="page"></div>   
    </div>
    <script src="http://www.wclimb.site/pagination/pagination.js"></script>
    <script>
        pagination({
            selector: '#page',
            totalPage: <%= postsPageLength %>,
            currentPage: 1,
            prev: '上一頁',
            next: '下一頁',
            first: true,
            last: true,
            showTotalPage: true,
            count: 2//當(dāng)前頁前后顯示的數(shù)量
        },function(val){
            // 當(dāng)前頁
            $.ajax({
                url: "posts/page",
                type: 'POST',
                data:{
                    page: val
                },
                cache: false,
                success: function (msg) {
                    console.log(msg)
                    if (msg != 'error') {
                        $('.posts').html(' ')
                        $.each(msg,function(i,val){
                            //console.log(val.content)
                            $('.posts').append(
                                '<li>'+
                                    '<div class=\"author\">'+
                                        '<span title=\"'+ val.name +'\"><a href=\"/posts?author='+ val.name +' \">author: '+ val.name +'</a></span>'+
                                        '<span>評論數(shù):'+ val.comments +'</span>'+
                                        '<span>瀏覽量:'+ val.pv +'</span>'+
                                    '</div>'+
                                    '<div class=\"comment_pv\">'+
                                        '<span>'+ val.moment +'</span>'+
                                    '</div>'+
                                    '<a href=\"/posts/'+ val.id +'\">'+
                                        '<div class=\"title\">'+
                                            '<img class="userAvator" src="images/'+ val.avator +'.png">'+
                                             val.title +
                                        '</div>'+
                                        '<div class=\"content\">'+
                                             val.content +
                                        '</div>'+
                                    '</a>'+
                                '</li>'
                            )
                        })
                    }else{
                        alert('分頁不存在')
                    } 
                }
            })
        })
    </script>
<% include footer %>

現(xiàn)在看看posts頁面有沒有文章出現(xiàn)了

我們看到是所第一頁的文章數(shù)據(jù)偎球,初始化的稍后我們是用服務(wù)端渲染的數(shù)據(jù),使用了分頁辑甜,每頁顯示10條數(shù)據(jù)衰絮,然后通過請求頁數(shù)。
下面是服務(wù)端請求拿到的第一頁的數(shù)據(jù)

await userModel.findPostByUserPage(name,1)
        .then(result => {
            res = result
        })

要拿到別的頁面數(shù)據(jù)的話要向服務(wù)器發(fā)送post請求磷醋,如下

// 首頁分頁猫牡,每次輸出10條
router.post('/posts/page', async(ctx, next) => {
    let page = ctx.request.body.page;
    await userModel.findPostByPage(page)
            .then(result=>{
                //console.log(result)
                ctx.body = result   
            }).catch(()=>{
            ctx.body = 'error'
        })  
})

單篇文章頁面

img

但是我需要點(diǎn)擊單篇文章的時(shí)候,顯示一篇文章怎么辦呢邓线?

修改 routers/posts.js

在posts.js后面增加

// 單篇文章頁
router.get('/posts/:postId', async(ctx, next) => {
    let comment_res,
        res,
        pageOne,
        res_pv; 
    await userModel.findDataById(ctx.params.postId)
        .then(result => {
            //console.log(result )
            res = result
            res_pv = parseInt(result[0]['pv'])
            res_pv += 1
           // console.log(res_pv)
        })
    await userModel.updatePostPv([res_pv, ctx.params.postId])
    await userModel.findCommentByPage(1,ctx.params.postId)
        .then(result => {
            pageOne = result
            //console.log('comment', comment_res)
        })
    await userModel.findCommentById(ctx.params.postId)
        .then(result => {
            comment_res = result
            //console.log('comment', comment_res)
        })
    await ctx.render('sPost', {
        session: ctx.session,
        posts: res[0],
        commentLenght: comment_res.length,
        commentPageLenght: Math.ceil(comment_res.length/10),
        pageOne:pageOne
    })

})

現(xiàn)在的設(shè)計(jì)是淌友,我們點(diǎn)進(jìn)去出現(xiàn)的url是 /posts/1 這類的 1代表該篇文章的id煌恢,我們在數(shù)據(jù)庫建表的時(shí)候就處理了,讓id為主鍵震庭,然后遞增

我們通過userModel.findDataById 文章的id來查找數(shù)據(jù)庫
我們通過userModel.findCommentById 文章的id來查找文章的評論瑰抵,因?yàn)閱纹恼吕锩嬗性u論的功能
最后通過sPost.ejs模板渲染單篇文章

修改 views/sPost.ejs

sPost.ejs

<%- include("header",{type:''}) %>
    <div class="container">
        <ul class="posts spost">
            <li>
                <div class="author">
                    <span title="<%= posts.name %>"><a href="/posts?author=<%= posts.name %> ">author: <%= posts.name %></a></span>
                    <span>評論數(shù):<%= posts.comments %></span>
                    <span>瀏覽量:<%= posts.pv %></span>
                </div>
                <div class="comment_pv">
                    <span><%= posts.moment %></span>
                </div>
                <a href="/posts/<%= posts.id %>">
                    <div class="title">
                        <img class="userAvator" src="../images/<%= posts.avator %>.png">
                        <%= posts.title %>
                    </div>
                    <div class="content markdown">
                        <%- posts.content %>
                    </div>
                </a>
                <div class="edit">
                    <% if(session && session.user ===  posts.name  ){ %>
                    <p><a href="<%= posts['id'] %>/edit">編輯</a></p>
                    <p><a class="delete_post">刪除</a></p>
                    <% } %>
                </div>
            </li>
        </ul>
    </div>
    <div class="comment_wrap">
        <% if(session.user){ %>
        <form class="form" method="post" action="/<%= posts.id %>">
            <textarea id="spContent" name="content" cols="82"></textarea>
            <div class="submit">發(fā)表留言</div>
        </form>
        <% } else{ %>
            <p class="tips">登錄之后才可以評論喲</p>
        <% } %>
        <% if (commentPageLenght > 0) { %>
        <div class="comment_list markdown">
            <% pageOne.forEach(function(res){ %>
                <div class="cmt_lists">
                    <div class="cmt_content">
                        <div class="userMsg">
                            <img src="../images/<%= res['avator'] %>.png" alt=""><span><%= res['name'] %></span>
                        </div>
                        <div class="cmt_detail">
                            <%- res['content'] %>
                        </div>
                        <span class="cmt_time"><%= res['moment'] %></span>
                        <span class="cmt_name">
                            <% if(session && session.user ===  res['name']){ %>
                                <a class="delete_comment" href="javascript:delete_comment(<%= res['id'] %>);"> 刪除</a>
                            <% } %>
                        </span>
                    </div>
                </div>
            <% }) %>
        </div>  
        <% } else{ %>
            <p class="tips">還沒有評論,趕快去評論吧器联!</p>
        <% } %>
        <div style="margin-top: 30px" class="pagination" id="page"></div>   
    </div>
    <script src="http://www.wclimb.site/pagination/pagination.js"></script>
    <script>
        var userName = "<%- session.user %>"
        pagination({
            selector: '#page',
            totalPage: <%= commentPageLenght %>,
            currentPage: 1,
            prev: '上一頁',
            next: '下一頁',
            first: true,
            last: true,
            showTotalPage:true,
            count: 2//當(dāng)前頁前面顯示的數(shù)量
        },function(val){
            // 當(dāng)前頁
            var _comment = ''
            $.ajax({
                url: "<%= posts.id %>/commentPage",
                type: 'POST',
                data:{
                    page: val
                },
                cache: false,
                success: function (msg) {
                    //console.log(msg)
                    _comment = ''
                    if (msg != 'error') {
                        $('.comment_list').html(' ')
                        $.each(msg,function(i,val){
                            //console.log(val.content)
                            _comment += '<div class=\"cmt_lists\"><div class=\"cmt_content\"><div class=\"userMsg\"><img src = \"../images/'+ val.avator +'.png\" ><span>'+ val.name +'</span></div ><div class="cmt_detail">'+ val.content + '</div><span class=\"cmt_time\">'+ val.moment +'</span><span class=\"cmt_name\">';
                                if (val.name == userName) {
                                    _comment += '<a class=\"delete_comment\" href=\"javascript:delete_comment('+ val.id +');\"> 刪除</a>'
                                }
                            _comment += '</span></div></div>'
                        })
                        $('.comment_list').append(_comment)
                    }else{
                        alert('分頁不存在')
                    } 
                }
            })
        
        })
        
        // 刪除文章
        $('.delete_post').click(() => {
            $.ajax({
                url: "<%= posts.id %>/remove",
                type: 'POST',
                cache: false,
                success: function (msg) {
                    if (msg.data == 1) {
                        fade('刪除文章成功')
                        setTimeout(() => {
                            window.location.href = "/posts"
                        }, 1000)
                    } else if (msg.data == 2) {
                        fade('刪除文章失敗');
                        setTimeout(() => {
                            window.location.reload()
                        }, 1000)
                    }
                }
            })
        })
        // 評論
        var isAllow = true
        $('.submit').click(function(){
            if (!isAllow) return
            isAllow = false
            if ($('textarea').val().trim() == '') {
                fade('請輸入評論二汛!')
            }else{
                $.ajax({
                    url: '/' + location.pathname.split('/')[2],
                    data:$('.form').serialize(),
                    type: "POST",
                    cache: false,
                    dataType: 'json',
                    success: function (msg) {
                        if (msg) {
                            fade('發(fā)表留言成功')                          
                            setTimeout(()=>{
                                isAllow = true
                                window.location.reload()
                            },1500)     
                        }
                    },
                    error: function () {
                        alert('異常');
                    }
                })
            }
        })
        // 刪除評論
        function delete_comment(id) {
            $.ajax({
                url: "<%= posts.id %>/comment/" + id + "/remove",
                type: 'POST',
                cache: false,
                success: function (msg) {
                    if (msg.data == 1) {
                        fade('刪除留言成功')
                        setTimeout(() => {
                            window.location.reload()
                        }, 1000)
                    } else if (msg.data == 2) {
                        fade('刪除留言失敗');
                        setTimeout(() => {
                            window.location.reload()
                        }, 1500)
                    }
                },
                error: function () {
                    alert('異常')
                }
            })
        }
    </script>
<% include footer %>

現(xiàn)在點(diǎn)擊單篇文章試試,進(jìn)入單篇文章頁面主籍,但是編輯习贫、刪除、評論都還沒有做千元,點(diǎn)擊無效苫昌,我們先不做,先實(shí)現(xiàn)每個(gè)用戶自己發(fā)表的文章列表幸海,我們之前在 get '/posts' 里面說先忽略if (ctx.request.querystring) {}里面的代碼祟身,這里是做了一個(gè)處理,假如用戶點(diǎn)擊了某個(gè)用戶物独,該用戶發(fā)表了幾篇文章袜硫,我們需要只顯示該用戶發(fā)表的文章,那么進(jìn)入的url應(yīng)該是 /posts?author=xxx ,這個(gè)處理在posts.ejs 就已經(jīng)加上了挡篓,就在文章的左下角婉陷,作者:xxx就是一個(gè)鏈接。我們通過判斷用戶來查找文章官研,繼而有了ctx.request.querystring 獲取到的是:author=xxx

注:這里我們處理了秽澳,通過判斷 session.user === res['name'] 如果不是該用戶發(fā)的文章,不能編輯和刪除戏羽,評論也是担神。這里面也可以注意一下包裹的<a href=""></a>標(biāo)簽

個(gè)人已發(fā)表文章列表里面

還記得之前在 get '/post' 里面的代碼嗎?
下面的代碼就是之前說先不處理的代碼片段始花,不過這個(gè)不用再次添加妄讯,之前已經(jīng)添加好了,這段代碼處理個(gè)人發(fā)布的文章列表酷宵,我們是通過selfPosts.ejs模板來渲染的亥贸,樣式和全部文章列表一樣,但是牽扯到分頁的問題浇垦,
分頁請求的是不同的接口地址

if (ctx.request.querystring) {
        console.log('ctx.request.querystring', name)
        await userModel.findDataByUser(name)
            .then(result => {
                postsLength = result.length
            })
        await userModel.findPostByUserPage(name,1)
            .then(result => {
                res = result
            })
        await ctx.render('selfPosts', {
            session: ctx.session,
            posts: res,
            postsPageLength:Math.ceil(postsLength / 10),
        })
    }

修改 selfPost.ejs

<%- include("header",{type:'my'}) %>
    <div class="container">
        <ul class="posts">
            <% posts.forEach(function(res){ %>
                <li>
                    <div class="author">
                        <span title="<%= res.name %>"><a href="/posts?author=<%= res.name %> ">author: <%= res.name %></a></span>
                        <span>評論數(shù):<%= res.comments %></span>
                        <span>瀏覽量:<%= res.pv %></span>
                    </div>
                    <div class="comment_pv">
                        <span><%= res.moment %></span>
                    </div>
                    <a href="/posts/<%= res.id %>">
                        <div class="title">
                            <img class="userAvator" src="images/<%= res.avator %>.png">
                            <%= res.title %>
                        </div>
                        <div class="content markdown">
                            <%- res.content %>
                        </div>
                    </a>
                </li>
            <% }) %>
        </ul>
        <div style="margin-top: 30px" class="pagination" id="page"></div>   
    </div>
    <script src="http://www.wclimb.site/pagination/pagination.js"></script>
    <script>
        pagination({
            selector: '#page',
            totalPage: <%= postsPageLength %>,
            currentPage: 1,
            prev: '上一頁',
            next: '下一頁',
            first: true,
            last: true,
            showTotalPage: true,
            count: 2//當(dāng)前頁前后顯示的數(shù)量
        },function(val){
            // 當(dāng)前頁
            $.ajax({
                url: "posts/self/page",
                type: 'POST',
                data:{
                    page: val,
                    name: location.search.slice(1).split('=')[1]
                },
                cache: false,
                success: function (msg) {
                    //console.log(msg)
                    if (msg != 'error') {
                        $('.posts').html(' ')
                        $.each(msg,function(i,val){
                            //console.log(val.content)
                            $('.posts').append(
                                '<li>'+
                                    '<div class=\"author\">'+
                                        '<span title=\"'+ val.name +'\"><a href=\"/posts?author='+ val.name +' \">author: '+ val.name +'</a></span>'+
                                        '<span>評論數(shù):'+ val.comments +'</span>'+
                                        '<span>瀏覽量:'+ val.pv +'</span>'+
                                    '</div>'+
                                    '<div class=\"comment_pv\">'+
                                        '<span>'+ val.moment +'</span>'+
                                    '</div>'+
                                    '<a href=\"/posts/'+ val.id +'\">'+
                                        '<div class=\"title\">'+
                                            '<img class="userAvator" src="images/' + val.avator + '.png">' +
                                             val.title +
                                        '</div>'+
                                        '<div class=\"content\">'+
                                             val.content +
                                        '</div>'+
                                    '</a>'+
                                '</li>'
                            )
                        })
                    }else{
                        alert('分頁不存在')
                    } 
                }
            })
        
        })
    </script>
<% include footer %>

編輯文章炕置、刪除文章、評論、刪除評論

評論

修改routers/posts.js

在post.js 后面增加

// 發(fā)表評論
router.post('/:postId', async(ctx, next) => {
    let name = ctx.session.user,
        content = ctx.request.body.content,
        postId = ctx.params.postId,
        res_comments,
        time = moment().format('YYYY-MM-DD HH:mm:ss'),
        avator;
    await userModel.findUserData(ctx.session.user)
        .then(res => {
            console.log(res[0]['avator'])
            avator = res[0]['avator']
        })   
    await userModel.insertComment([name, md.render(content),time, postId,avator])
    await userModel.findDataById(postId)
        .then(result => {
            res_comments = parseInt(result[0]['comments'])
            res_comments += 1
        })
    await userModel.updatePostComment([res_comments, postId])
        .then(() => {
            ctx.body = true
        }).catch(() => {
            ctx.body = false
        })
})
// 評論分頁
router.post('/posts/:postId/commentPage', async function(ctx){
    let postId = ctx.params.postId,
        page = ctx.request.body.page;
    await userModel.findCommentByPage(page,postId)
        .then(res=>{
            ctx.body = res
        }).catch(()=>{
            ctx.body = 'error'
        })  
})

現(xiàn)在試試發(fā)表評論的功能吧讹俊,之所以這樣簡單垦沉,因?yàn)槲覀冎熬驮趕Post.ejs做了好幾個(gè)ajax的處理,刪除文章和評論也是如此
評論我們也做了分頁仍劈,所以后面會有一個(gè)評論的分頁的接口厕倍,我們已經(jīng)在sPost.ejs里面寫好了ajax請求

刪除評論

修改routers/posts.js

繼續(xù)在post.js 后面增加

// 刪除評論
router.post('/posts/:postId/comment/:commentId/remove', async(ctx, next) => {
    let postId = ctx.params.postId,
        commentId = ctx.params.commentId,
        res_comments;
    await userModel.findDataById(postId)
        .then(result => {
            res_comments = parseInt(result[0]['comments'])
            //console.log('res', res_comments)
            res_comments -= 1
            //console.log(res_comments)
        })
    await userModel.updatePostComment([res_comments, postId])
    await userModel.deleteComment(commentId)
        .then(() => {
            ctx.body = {
                data: 1
            }
        }).catch(() => {
            ctx.body = {
                data: 2
            }

        })
})

現(xiàn)在試試刪除評論的功能吧

刪除文章

只有自己發(fā)表的文字刪除的文字才會顯示出來,才能被刪除贩疙,

修改routers/posts.js

繼續(xù)在post.js 后面增加

// 刪除單篇文章
router.post('/posts/:postId/remove', async(ctx, next) => {
    let postId = ctx.params.postId
    await userModel.deleteAllPostComment(postId)
    await userModel.deletePost(postId)
        .then(() => {
            ctx.body = {
                data: 1
            }
        }).catch(() => {
            ctx.body = {
                data: 2
            }
        })
})

現(xiàn)在試試刪除文章的功能吧

編輯文章

修改routers/posts.js

繼續(xù)在post.js 后面增加

// 編輯單篇文章頁面
router.get('/posts/:postId/edit', async(ctx, next) => {
    let name = ctx.session.user,
        postId = ctx.params.postId,
        res;
    await userModel.findDataById(postId)
        .then(result => {
            res = result[0]
            //console.log('修改文章', res)
        })
    await ctx.render('edit', {
        session: ctx.session,
        postsContent: res.md,
        postsTitle: res.title
    })

})

// post 編輯單篇文章
router.post('/posts/:postId/edit', async(ctx, next) => {
    let title = ctx.request.body.title,
        content = ctx.request.body.content,
        id = ctx.session.id,
        postId = ctx.params.postId,
         // 現(xiàn)在使用markdown不需要單獨(dú)轉(zhuǎn)義
        newTitle = title.replace(/[<">']/g, (target) => {
            return {
                '<': '&lt;',
                '"': '&quot;',
                '>': '&gt;',
                "'": '&#39;'
            }[target]
        }),
        newContent = content.replace(/[<">']/g, (target) => {
            return {
                '<': '&lt;',
                '"': '&quot;',
                '>': '&gt;',
                "'": '&#39;'
            }[target]
        });
    await userModel.updatePost([newTitle, md.render(content), content, postId])
        .then(() => {
            ctx.body = true
        }).catch(() => {
            ctx.body = false
        })
})

修改views/edit.js

<%- include("header",{type:''}) %>
<div class="container">
    <form style="width:100%" class="form create" method="post">
        <div>
            <label>標(biāo)題:</label>
            <input placeholder="標(biāo)題" type="text" name="title" value="<%- postsTitle %>">
        </div>
        <div>
            <label>內(nèi)容:</label>
            <textarea name="content" id="" cols="42" rows="10"><%= postsContent %></textarea>
        </div>
        <div class="submit">修改</div>
    </form>
</div>
<script>
    $('.submit').click(()=>{
        $.ajax({
            url: '',
            data: $('.form').serialize(),
            type: "POST",
            cache: false,
            dataType: 'json',
            success: function (msg) {
               if (msg) {
                    fade('修改成功')
                    setTimeout(()=>{
                        window.location.href="/posts"
                    },1000)
               }
            },
            error: function () {
                alert('異常');
            }
        })      
    })
</script>
<% include footer %>

現(xiàn)在試試編輯文字然后修改提交吧

結(jié)語

至此一個(gè)簡單的blog就已經(jīng)制作好了讹弯,其他擴(kuò)展功能相信你已經(jīng)會了吧!如果出現(xiàn)問題这溅,還望積極提問哈组民,我會盡快處理的

所有的代碼都在 https://github.com/wclimb/Koa2-blog 里面,如果覺得不錯(cuò)就star一下吧悲靴。有問題可以提問喲
下一篇可能是 Node + express + mongoose 或 zepto源碼系列
感謝您的閱讀_

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末臭胜,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子癞尚,更是在濱河造成了極大的恐慌耸三,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件浇揩,死亡現(xiàn)場離奇詭異仪壮,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)胳徽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門积锅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人养盗,你說我怎么就攤上這事缚陷。” “怎么了爪瓜?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵蹬跃,是天一觀的道長匙瘪。 經(jīng)常有香客問我铆铆,道長,這世上最難降的妖魔是什么丹喻? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任薄货,我火速辦了婚禮,結(jié)果婚禮上碍论,老公的妹妹穿的比我還像新娘谅猾。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布税娜。 她就那樣靜靜地躺著坐搔,像睡著了一般。 火紅的嫁衣襯著肌膚如雪敬矩。 梳的紋絲不亂的頭發(fā)上概行,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機(jī)與錄音弧岳,去河邊找鬼凳忙。 笑死,一個(gè)胖子當(dāng)著我的面吹牛禽炬,可吹牛的內(nèi)容都是我干的涧卵。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼腹尖,長吁一口氣:“原來是場噩夢啊……” “哼柳恐!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起热幔,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤胎撤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后断凶,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體伤提,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年认烁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了肿男。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡却嗡,死狀恐怖舶沛,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情窗价,我是刑警寧澤如庭,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站撼港,受9級特大地震影響坪它,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜帝牡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一往毡、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧靶溜,春花似錦开瞭、人聲如沸懒震。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽个扰。三九已至,卻和暖如春葱色,著一層夾襖步出監(jiān)牢的瞬間锨匆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工冬筒, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留恐锣,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓舞痰,卻偏偏與公主長得像土榴,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子响牛,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容