├── .gitignore ├── README.md ├── config └── default.js ├── controller ├── c-posts.js ├── c-signin.js └── c-signup.js ├── index.js ├── lib └── mysql.js ├── middlewares └── check.js ├── package.json ├── public ├── images │ └── avatar.png ├── index.css └── pagination.js ├── routers ├── posts.js ├── signin.js ├── signout.js └── signup.js ├── test └── blog-test.js └── views ├── create.ejs ├── edit.ejs ├── footer.ejs ├── header.ejs ├── posts.ejs ├── sPost.ejs ├── selfPosts.ejs ├── signin.ejs └── signup.ejs /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Koa2-blog 2 | node+koa2+mysql 3 | 4 | > 现在最新的代码有变动,请参照最新的代码,新增了上传头像、分页、markdown语法等 5 | 6 | 7 | 教程 [Node+Koa2+Mysql 搭建简易博客] 8 | 9 | ### 创建数据库 10 | 11 | 登录数据库 12 | ``` 13 | $ mysql -u root -p 14 | ``` 15 | 创建数据库 16 | ``` 17 | $ create database nodesql; 18 | ``` 19 | 使用创建的数据库 20 | ``` 21 | $ use nodesql; 22 | ``` 23 | 24 | > database: nodesql tables: users posts comment (已经在lib/mysql建表) 25 | 26 | 27 | | users | posts | comment | 28 | | :----: | :----: | :----: | 29 | | id | id | id | 30 | | name | name | name | 31 | | pass | title | content | 32 | | avator | content | moment | 33 | | moment | md | postid | 34 | | - | uid | avator | 35 | | - | moment | - | 36 | | - | comments | - | 37 | | - | pv | - | 38 | | - | avator | - | 39 | 40 | 41 | * id主键递增 42 | * name: 用户名 43 | * pass:密码 44 | * avator:头像 45 | * title:文章标题 46 | * content:文章内容和评论 47 | * md:markdown语法 48 | * uid:发表文章的用户id 49 | * moment:创建时间 50 | * comments:文章评论数 51 | * pv:文章浏览数 52 | * postid:文章id 53 | 54 | ``` 55 | $ git clone https://github.com/wunci/Koa2-blog.git 56 | ``` 57 | ``` 58 | $ cd Koa2-blog 59 | ``` 60 | ``` 61 | $ cnpm i supervisor -g 62 | ``` 63 | ``` 64 | $ cnpm i 65 | ``` 66 | ``` 67 | $ npm run dev(运行项目) 68 | ``` 69 | ``` 70 | $ npm test(测试项目) 71 | ``` 72 | 73 | 74 | -------------------------------------------------------------------------------- /config/default.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | // 启动端口 3 | port: 3000, 4 | // 数据库配置 5 | database: { 6 | DATABASE: 'nodesql', 7 | USERNAME: 'root', 8 | PASSWORD: '123456', 9 | PORT: '3306', 10 | HOST: 'localhost' 11 | } 12 | } 13 | 14 | module.exports = config -------------------------------------------------------------------------------- /controller/c-posts.js: -------------------------------------------------------------------------------- 1 | 2 | const userModel = require('../lib/mysql.js') 3 | const moment = require('moment') 4 | const checkNotLogin = require('../middlewares/check.js').checkNotLogin 5 | const checkLogin = require('../middlewares/check.js').checkLogin; 6 | const md = require('markdown-it')(); 7 | /** 8 | * 重置到文章页 9 | */ 10 | exports.getRedirectPosts = async ctx => { 11 | ctx.redirect('/posts') 12 | } 13 | /** 14 | * 文章页 15 | */ 16 | exports.getPosts = async ctx => { 17 | let res, 18 | postCount, 19 | name = decodeURIComponent(ctx.request.querystring.split('=')[1]); 20 | if (ctx.request.querystring) { 21 | await userModel.findPostCountByName(name) 22 | .then(result => { 23 | postCount = result[0].count 24 | }) 25 | await userModel.findPostByUserPage(name, 1) 26 | .then(result => { 27 | res = result 28 | }) 29 | await ctx.render('selfPosts', { 30 | session: ctx.session, 31 | posts: res, 32 | postsPageLength: Math.ceil(postCount / 10), 33 | }) 34 | } else { 35 | await userModel.findPostByPage(1) 36 | .then(result => { 37 | res = result 38 | }) 39 | await userModel.findAllPostCount() 40 | .then(result => { 41 | postCount = result[0].count 42 | }) 43 | await ctx.render('posts', { 44 | session: ctx.session, 45 | posts: res, 46 | postsLength: postCount, 47 | postsPageLength: Math.ceil(postCount / 10), 48 | 49 | }) 50 | } 51 | } 52 | /** 53 | * 首页分页, 每次输出10条 54 | */ 55 | exports.postPostsPage = async ctx => { 56 | let page = ctx.request.body.page; 57 | await userModel.findPostByPage(page) 58 | .then(result => { 59 | ctx.body = result 60 | }).catch(() => { 61 | ctx.body = 'error' 62 | }) 63 | } 64 | /** 65 | * 个人文章分页, 每次输出10条 66 | */ 67 | exports.postSelfPage = async ctx => { 68 | let data = ctx.request.body 69 | await userModel.findPostByUserPage(decodeURIComponent(data.name), data.page) 70 | .then(result => { 71 | ctx.body = result 72 | }).catch(() => { 73 | ctx.body = 'error' 74 | }) 75 | } 76 | /** 77 | * 单篇文章页 78 | */ 79 | exports.getSinglePosts = async ctx => { 80 | let postId = ctx.params.postId, 81 | count, 82 | res, 83 | pageOne; 84 | await userModel.findDataById(postId) 85 | .then(result => { 86 | res = result 87 | }) 88 | await userModel.updatePostPv(postId) 89 | await userModel.findCommentByPage(1, postId) 90 | .then(result => { 91 | pageOne = result 92 | }) 93 | await userModel.findCommentCountById(postId) 94 | .then(result => { 95 | count = result[0].count 96 | }) 97 | await ctx.render('sPost', { 98 | session: ctx.session, 99 | posts: res[0], 100 | commentLength: count, 101 | commentPageLength: Math.ceil(count / 10), 102 | pageOne: pageOne 103 | }) 104 | 105 | } 106 | /** 107 | * 发表文章页面 108 | */ 109 | exports.getCreate = async ctx => { 110 | await checkLogin(ctx) 111 | await ctx.render('create', { 112 | session: ctx.session, 113 | }) 114 | } 115 | /** 116 | * 发表文章 117 | */ 118 | exports.postCreate = async ctx => { 119 | let {title,content} = ctx.request.body, 120 | id = ctx.session.id, 121 | name = ctx.session.user, 122 | time = moment().format('YYYY-MM-DD HH:mm:ss'), 123 | avator, 124 | // 现在使用markdown不需要单独转义 125 | newContent = content.replace(/[<">']/g, (target) => { 126 | return { 127 | '<': '<', 128 | '"': '"', 129 | '>': '>', 130 | "'": ''' 131 | }[target] 132 | }), 133 | newTitle = title.replace(/[<">']/g, (target) => { 134 | return { 135 | '<': '<', 136 | '"': '"', 137 | '>': '>', 138 | "'": ''' 139 | }[target] 140 | }); 141 | 142 | await userModel.findUserData(ctx.session.user) 143 | .then(res => { 144 | avator = res[0]['avator'] 145 | }) 146 | await userModel.insertPost([name, newTitle, md.render(content), content, id, time, avator]) 147 | .then(() => { 148 | ctx.body = { 149 | code:200, 150 | message:'发表文章成功' 151 | } 152 | }).catch(() => { 153 | ctx.body = { 154 | code: 500, 155 | message: '发表文章失败' 156 | } 157 | }) 158 | } 159 | /** 160 | * 发表评论 161 | */ 162 | exports.postComment = async ctx => { 163 | let name = ctx.session.user, 164 | content = ctx.request.body.content, 165 | postId = ctx.params.postId, 166 | time = moment().format('YYYY-MM-DD HH:mm:ss'), 167 | avator; 168 | await userModel.findUserData(ctx.session.user) 169 | .then(res => { 170 | avator = res[0]['avator'] 171 | }) 172 | await userModel.insertComment([name, md.render(content), time, postId, avator]) 173 | await userModel.addPostCommentCount(postId) 174 | .then(() => { 175 | ctx.body = { 176 | code:200, 177 | message:'评论成功' 178 | } 179 | }).catch(() => { 180 | ctx.body = { 181 | code: 500, 182 | message: '评论失败' 183 | } 184 | }) 185 | } 186 | /** 187 | * 编辑单篇文章页面 188 | */ 189 | exports.getEditPage = async ctx => { 190 | let name = ctx.session.user, 191 | postId = ctx.params.postId, 192 | res; 193 | await checkLogin(ctx) 194 | await userModel.findDataById(postId) 195 | .then(result => { 196 | res = result[0] 197 | }) 198 | await ctx.render('edit', { 199 | session: ctx.session, 200 | postsContent: res.md, 201 | postsTitle: res.title 202 | }) 203 | 204 | } 205 | /** 206 | * post 编辑单篇文章 207 | */ 208 | exports.postEditPage = async ctx => { 209 | let title = ctx.request.body.title, 210 | content = ctx.request.body.content, 211 | id = ctx.session.id, 212 | postId = ctx.params.postId, 213 | allowEdit = true, 214 | // 现在使用markdown不需要单独转义 215 | newTitle = title.replace(/[<">']/g, (target) => { 216 | return { 217 | '<': '<', 218 | '"': '"', 219 | '>': '>', 220 | "'": ''' 221 | }[target] 222 | }), 223 | newContent = content.replace(/[<">']/g, (target) => { 224 | return { 225 | '<': '<', 226 | '"': '"', 227 | '>': '>', 228 | "'": ''' 229 | }[target] 230 | }); 231 | await userModel.findDataById(postId) 232 | .then(res => { 233 | if (res[0].name != ctx.session.user) { 234 | allowEdit = false 235 | } else { 236 | allowEdit = true 237 | } 238 | }) 239 | if (allowEdit) { 240 | await userModel.updatePost([newTitle, md.render(content), content, postId]) 241 | .then(() => { 242 | ctx.body = { 243 | code: 200, 244 | message: '编辑成功' 245 | } 246 | }).catch(() => { 247 | ctx.body = { 248 | code: 500, 249 | message: '编辑失败' 250 | } 251 | }) 252 | } else { 253 | ctx.body = { 254 | code: 404, 255 | message: '无权限' 256 | } 257 | } 258 | } 259 | /** 260 | * 删除单篇文章 261 | */ 262 | exports.postDeletePost = async ctx => { 263 | let postId = ctx.params.postId, 264 | allow; 265 | await userModel.findDataById(postId) 266 | .then(res => { 267 | if (res[0].name != ctx.session.user) { 268 | allow = false 269 | } else { 270 | allow = true 271 | } 272 | }) 273 | if (allow) { 274 | await userModel.deleteAllPostComment(postId) 275 | await userModel.deletePost(postId) 276 | .then(() => { 277 | ctx.body = { 278 | code: 200, 279 | message: '删除文章成功' 280 | } 281 | }).catch(() => { 282 | ctx.body = { 283 | code: 500, 284 | message: '删除文章失败' 285 | } 286 | }) 287 | } else { 288 | ctx.body = { 289 | code: 404, 290 | message: '无权限' 291 | } 292 | } 293 | } 294 | /** 295 | * 删除评论 296 | */ 297 | exports.postDeleteComment = async ctx => { 298 | let postId = ctx.params.postId, 299 | commentId = ctx.params.commentId, 300 | allow; 301 | await userModel.findComment(commentId) 302 | .then(res => { 303 | if (res[0].name != ctx.session.user) { 304 | allow = false 305 | } else { 306 | allow = true 307 | } 308 | }) 309 | if (allow) { 310 | await userModel.reducePostCommentCount(postId) 311 | await userModel.deleteComment(commentId) 312 | .then(() => { 313 | ctx.body = { 314 | code: 200, 315 | message: '删除评论成功' 316 | } 317 | }).catch(() => { 318 | ctx.body = { 319 | code: 500, 320 | message: '删除评论失败' 321 | } 322 | 323 | }) 324 | } else { 325 | ctx.body = { 326 | code: 404, 327 | message: '无权限' 328 | } 329 | } 330 | } 331 | /** 332 | * 评论分页 333 | */ 334 | exports.postCommentPage = async function (ctx) { 335 | let postId = ctx.params.postId, 336 | page = ctx.request.body.page; 337 | await userModel.findCommentByPage(page, postId) 338 | .then(res => { 339 | ctx.body = res 340 | }).catch(() => { 341 | ctx.body = 'error' 342 | }) 343 | } -------------------------------------------------------------------------------- /controller/c-signin.js: -------------------------------------------------------------------------------- 1 | const userModel = require('../lib/mysql.js') 2 | const md5 = require('md5') 3 | const checkNotLogin = require('../middlewares/check.js').checkNotLogin 4 | const checkLogin = require('../middlewares/check.js').checkLogin 5 | 6 | exports.getSignin = async ctx => { 7 | await checkNotLogin(ctx) 8 | await ctx.render('signin', { 9 | session: ctx.session, 10 | }) 11 | } 12 | exports.postSignin = async ctx => { 13 | console.log(ctx.request.body) 14 | let { name, password } = ctx.request.body 15 | await userModel.findDataByName(name) 16 | .then(result => { 17 | let res = result 18 | if (res.length && name === res[0]['name'] && md5(password) === res[0]['pass']) { 19 | ctx.session = { 20 | user: res[0]['name'], 21 | id: res[0]['id'] 22 | } 23 | ctx.body = { 24 | code: 200, 25 | message: '登录成功' 26 | } 27 | console.log('ctx.session.id', ctx.session.id) 28 | console.log('session', ctx.session) 29 | console.log('登录成功') 30 | } else { 31 | ctx.body = { 32 | code: 500, 33 | message: '用户名或密码错误' 34 | } 35 | console.log('用户名或密码错误!') 36 | } 37 | }).catch(err => { 38 | console.log(err) 39 | }) 40 | 41 | } -------------------------------------------------------------------------------- /controller/c-signup.js: -------------------------------------------------------------------------------- 1 | const userModel = require('../lib/mysql.js'); 2 | const md5 = require('md5') 3 | const checkNotLogin = require('../middlewares/check.js').checkNotLogin 4 | const checkLogin = require('../middlewares/check.js').checkLogin 5 | const moment = require('moment'); 6 | const fs = require('fs') 7 | 8 | exports.getSignup = async ctx => { 9 | await checkNotLogin(ctx) 10 | await ctx.render('signup', { 11 | session: ctx.session, 12 | }) 13 | } 14 | exports.postSignup = async ctx => { 15 | let { name, password, repeatpass, avator } = ctx.request.body 16 | console.log(typeof password) 17 | await userModel.findDataCountByName(name) 18 | .then(async (result) => { 19 | console.log(result) 20 | if (result[0].count >= 1) { 21 | // 用户存在 22 | ctx.body = { 23 | code: 500, 24 | message: '用户存在' 25 | }; 26 | } else if (password !== repeatpass || password.trim() === '') { 27 | ctx.body = { 28 | code: 500, 29 | message: '两次输入的密码不一致' 30 | }; 31 | } else if(avator && avator.trim() === ''){ 32 | ctx.body = { 33 | code: 500, 34 | message: '请上传头像' 35 | }; 36 | } else { 37 | let base64Data = avator.replace(/^data:image\/\w+;base64,/, ""), 38 | dataBuffer = new Buffer(base64Data, 'base64'), 39 | getName = Number(Math.random().toString().substr(3)).toString(36) + Date.now(), 40 | upload = await new Promise((reslove, reject) => { 41 | fs.writeFile('./public/images/' + getName + '.png', dataBuffer, err => { 42 | if (err) { 43 | throw err; 44 | reject(false) 45 | }; 46 | reslove(true) 47 | console.log('头像上传成功') 48 | }); 49 | }); 50 | // console.log('upload', upload) 51 | if (upload) { 52 | await userModel.insertData([name, md5(password), getName + '.png', moment().format('YYYY-MM-DD HH:mm:ss')]) 53 | .then(res => { 54 | console.log('注册成功', res) 55 | //注册成功 56 | ctx.body = { 57 | code: 200, 58 | message: '注册成功' 59 | }; 60 | }) 61 | } else { 62 | console.log('头像上传失败') 63 | ctx.body = { 64 | code: 500, 65 | message: '头像上传失败' 66 | } 67 | } 68 | } 69 | }) 70 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | const path = require('path') 3 | const bodyParser = require('koa-bodyparser'); 4 | const ejs = require('ejs'); 5 | const session = require('koa-session-minimal'); 6 | const MysqlStore = require('koa-mysql-session'); 7 | const config = require('./config/default.js'); 8 | const router = require('koa-router') 9 | const views = require('koa-views') 10 | // const koaStatic = require('koa-static') 11 | const staticCache = require('koa-static-cache') 12 | const app = new Koa() 13 | 14 | 15 | // session存储配置 16 | const sessionMysqlConfig= { 17 | user: config.database.USERNAME, 18 | password: config.database.PASSWORD, 19 | database: config.database.DATABASE, 20 | host: config.database.HOST, 21 | } 22 | 23 | // 配置session中间件 24 | app.use(session({ 25 | key: 'USER_SID', 26 | store: new MysqlStore(sessionMysqlConfig) 27 | })) 28 | 29 | 30 | // 配置静态资源加载中间件 31 | // app.use(koaStatic( 32 | // path.join(__dirname , './public') 33 | // )) 34 | // 缓存 35 | app.use(staticCache(path.join(__dirname, './public'), { dynamic: true }, { 36 | maxAge: 365 * 24 * 60 * 60 37 | })) 38 | app.use(staticCache(path.join(__dirname, './images'), { dynamic: true }, { 39 | maxAge: 365 * 24 * 60 * 60 40 | })) 41 | 42 | // 配置服务端模板渲染引擎中间件 43 | app.use(views(path.join(__dirname, './views'), { 44 | extension: 'ejs' 45 | })) 46 | app.use(bodyParser({ 47 | formLimit: '1mb' 48 | })) 49 | 50 | // 路由 51 | app.use(require('./routers/signin.js').routes()) 52 | app.use(require('./routers/signup.js').routes()) 53 | app.use(require('./routers/posts.js').routes()) 54 | app.use(require('./routers/signout.js').routes()) 55 | 56 | 57 | app.listen(config.port) 58 | 59 | console.log(`listening on port ${config.port}`) 60 | -------------------------------------------------------------------------------- /lib/mysql.js: -------------------------------------------------------------------------------- 1 | var mysql = require('mysql'); 2 | var config = require('../config/default.js') 3 | 4 | var pool = mysql.createPool({ 5 | host : config.database.HOST, 6 | user : config.database.USERNAME, 7 | password : config.database.PASSWORD, 8 | database : config.database.DATABASE, 9 | port : config.database.PORT 10 | }); 11 | 12 | let query = ( sql, values ) => { 13 | 14 | return new Promise(( resolve, reject ) => { 15 | pool.getConnection( (err, connection) => { 16 | if (err) { 17 | reject( err ) 18 | } else { 19 | connection.query(sql, values, ( err, rows) => { 20 | if ( err ) { 21 | reject( err ) 22 | } else { 23 | resolve( rows ) 24 | } 25 | connection.release() 26 | }) 27 | } 28 | }) 29 | }) 30 | 31 | } 32 | 33 | 34 | // let query = function( sql, values ) { 35 | // pool.getConnection(function(err, connection) { 36 | // // 使用连接 37 | // connection.query( sql,values, function(err, rows) { 38 | // // 使用连接执行查询 39 | // console.log(rows) 40 | // connection.release(); 41 | // //连接不再使用,返回到连接池 42 | // }); 43 | // }); 44 | // } 45 | 46 | let users = 47 | `create table if not exists users( 48 | id INT NOT NULL AUTO_INCREMENT, 49 | name VARCHAR(100) NOT NULL COMMENT '用户名', 50 | pass VARCHAR(100) NOT NULL COMMENT '密码', 51 | avator VARCHAR(100) NOT NULL COMMENT '头像', 52 | moment VARCHAR(100) NOT NULL COMMENT '注册时间', 53 | PRIMARY KEY ( id ) 54 | );` 55 | 56 | let posts = 57 | `create table if not exists posts( 58 | id INT NOT NULL AUTO_INCREMENT, 59 | name VARCHAR(100) NOT NULL COMMENT '文章作者', 60 | title TEXT(0) NOT NULL COMMENT '评论题目', 61 | content TEXT(0) NOT NULL COMMENT '评论内容', 62 | md TEXT(0) NOT NULL COMMENT 'markdown', 63 | uid VARCHAR(40) NOT NULL COMMENT '用户id', 64 | moment VARCHAR(100) NOT NULL COMMENT '发表时间', 65 | comments VARCHAR(200) NOT NULL DEFAULT '0' COMMENT '文章评论数', 66 | pv VARCHAR(40) NOT NULL DEFAULT '0' COMMENT '浏览量', 67 | avator VARCHAR(100) NOT NULL COMMENT '用户头像', 68 | PRIMARY KEY(id) 69 | );` 70 | 71 | let comment = 72 | `create table if not exists comment( 73 | id INT NOT NULL AUTO_INCREMENT, 74 | name VARCHAR(100) NOT NULL COMMENT '用户名称', 75 | content TEXT(0) NOT NULL COMMENT '评论内容', 76 | moment VARCHAR(40) NOT NULL COMMENT '评论时间', 77 | postid VARCHAR(40) NOT NULL COMMENT '文章id', 78 | avator VARCHAR(100) NOT NULL COMMENT '用户头像', 79 | PRIMARY KEY(id) 80 | );` 81 | 82 | let createTable = ( sql ) => { 83 | return query( sql, [] ) 84 | } 85 | 86 | // 建表 87 | createTable(users) 88 | createTable(posts) 89 | createTable(comment) 90 | 91 | // 注册用户 92 | exports.insertData = ( value ) => { 93 | let _sql = "insert into users set name=?,pass=?,avator=?,moment=?;" 94 | return query( _sql, value ) 95 | } 96 | // 删除用户 97 | exports.deleteUserData = ( name ) => { 98 | let _sql = `delete from users where name="${name}";` 99 | return query( _sql ) 100 | } 101 | // 查找用户 102 | exports.findUserData = ( name ) => { 103 | let _sql = `select * from users where name="${name}";` 104 | return query( _sql ) 105 | } 106 | // 发表文章 107 | exports.insertPost = ( value ) => { 108 | let _sql = "insert into posts set name=?,title=?,content=?,md=?,uid=?,moment=?,avator=?;" 109 | return query( _sql, value ) 110 | } 111 | // 增加文章评论数 112 | exports.addPostCommentCount = ( value ) => { 113 | let _sql = "update posts set comments = comments + 1 where id=?" 114 | return query( _sql, value ) 115 | } 116 | // 减少文章评论数 117 | exports.reducePostCommentCount = ( value ) => { 118 | let _sql = "update posts set comments = comments - 1 where id=?" 119 | return query( _sql, value ) 120 | } 121 | 122 | // 更新浏览数 123 | exports.updatePostPv = ( value ) => { 124 | let _sql = "update posts set pv= pv + 1 where id=?" 125 | return query( _sql, value ) 126 | } 127 | 128 | // 发表评论 129 | exports.insertComment = ( value ) => { 130 | let _sql = "insert into comment set name=?,content=?,moment=?,postid=?,avator=?;" 131 | return query( _sql, value ) 132 | } 133 | // 通过名字查找用户 134 | exports.findDataByName = ( name ) => { 135 | let _sql = `select * from users where name="${name}";` 136 | return query( _sql) 137 | } 138 | // 通过名字查找用户数量判断是否已经存在 139 | exports.findDataCountByName = ( name ) => { 140 | let _sql = `select count(*) as count from users where name="${name}";` 141 | return query( _sql) 142 | } 143 | // 通过文章的名字查找用户 144 | exports.findDataByUser = ( name ) => { 145 | let _sql = `select * from posts where name="${name}";` 146 | return query( _sql) 147 | } 148 | // 通过文章id查找 149 | exports.findDataById = ( id ) => { 150 | let _sql = `select * from posts where id="${id}";` 151 | return query( _sql) 152 | } 153 | // 通过文章id查找 154 | exports.findCommentById = ( id ) => { 155 | let _sql = `select * from comment where postid="${id}";` 156 | return query( _sql) 157 | } 158 | 159 | // 通过文章id查找评论数 160 | exports.findCommentCountById = ( id ) => { 161 | let _sql = `select count(*) as count from comment where postid="${id}";` 162 | return query( _sql) 163 | } 164 | 165 | // 通过评论id查找 166 | exports.findComment = ( id ) => { 167 | let _sql = `select * from comment where id="${id}";` 168 | return query( _sql) 169 | } 170 | // 查询所有文章 171 | exports.findAllPost = () => { 172 | let _sql = `select * from posts;` 173 | return query( _sql) 174 | } 175 | // 查询所有文章数量 176 | exports.findAllPostCount = () => { 177 | let _sql = `select count(*) as count from posts;` 178 | return query( _sql) 179 | } 180 | // 查询分页文章 181 | exports.findPostByPage = ( page ) => { 182 | let _sql = ` select * from posts limit ${(page-1)*10},10;` 183 | return query( _sql) 184 | } 185 | // 查询所有个人用户文章数量 186 | exports.findPostCountByName = (name) => { 187 | let _sql = `select count(*) as count from posts where name="${name}";` 188 | return query( _sql) 189 | } 190 | // 查询个人分页文章 191 | exports.findPostByUserPage = (name,page) => { 192 | let _sql = ` select * from posts where name="${name}" order by id desc limit ${(page-1)*10},10 ;` 193 | return query( _sql) 194 | } 195 | // 更新修改文章 196 | exports.updatePost = (values) => { 197 | let _sql = `update posts set title=?,content=?,md=? where id=?` 198 | return query(_sql,values) 199 | } 200 | // 删除文章 201 | exports.deletePost = (id) => { 202 | let _sql = `delete from posts where id = ${id}` 203 | return query(_sql) 204 | } 205 | // 删除评论 206 | exports.deleteComment = (id) => { 207 | let _sql = `delete from comment where id=${id}` 208 | return query(_sql) 209 | } 210 | // 删除所有评论 211 | exports.deleteAllPostComment = (id) => { 212 | let _sql = `delete from comment where postid=${id}` 213 | return query(_sql) 214 | } 215 | 216 | // 滚动无限加载数据 217 | exports.findPageById = (page) => { 218 | let _sql = `select * from posts limit ${(page-1)*5},5;` 219 | return query(_sql) 220 | } 221 | // 评论分页 222 | exports.findCommentByPage = (page,postId) => { 223 | let _sql = `select * from comment where postid=${postId} order by id desc limit ${(page-1)*10},10;` 224 | return query(_sql) 225 | } 226 | 227 | 228 | 229 | -------------------------------------------------------------------------------- /middlewares/check.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports ={ 3 | // 已经登录了 4 | checkNotLogin: (ctx) => { 5 | if (ctx.session && ctx.session.user) { 6 | ctx.redirect('/posts'); 7 | return false; 8 | } 9 | return true; 10 | }, 11 | //没有登录 12 | checkLogin: (ctx) => { 13 | if (!ctx.session || !ctx.session.user) { 14 | ctx.redirect('/signin'); 15 | return false; 16 | } 17 | return true; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa2-blog", 3 | "version": "1.0.0", 4 | "description": "blog", 5 | "main": "index.js", 6 | "directories": { 7 | "lib": "lib" 8 | }, 9 | "scripts": { 10 | "dev": "supervisor --harmony index", 11 | "test": "./node_modules/mocha/bin/mocha --harmony test" 12 | }, 13 | "author": "wclimb", 14 | "license": "MIT", 15 | "dependencies": { 16 | "config-lite": "^2.0.0", 17 | "ejs": "^2.5.6", 18 | "koa": "^2.3.0", 19 | "koa-bodyparser": "^4.2.0", 20 | "koa-mysql-session": "^0.0.2", 21 | "koa-router": "^7.2.1", 22 | "koa-session-minimal": "^3.0.4", 23 | "koa-static": "^4.0.0", 24 | "koa-static-cache": "^5.1.1", 25 | "koa-views": "^6.0.2", 26 | "markdown-it": "^8.4.0", 27 | "md5": "^2.2.1", 28 | "moment": "^2.18.1", 29 | "mysql": "^2.13.0" 30 | }, 31 | "devDependencies": { 32 | "chai": "^4.1.2", 33 | "mocha": "^4.0.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /public/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wunci/Koa2-blog/fc8ac61702e46b51cda877fccfc0c42f00f7bcd8/public/images/avatar.png -------------------------------------------------------------------------------- /public/index.css: -------------------------------------------------------------------------------- 1 | body, 2 | header, 3 | ul, 4 | li, 5 | p, 6 | div, 7 | html, 8 | span, 9 | h3, 10 | a, 11 | blockquote { 12 | margin: 0; 13 | padding: 0; 14 | color: #333; 15 | } 16 | 17 | body { 18 | margin-bottom: 20px; 19 | } 20 | ul,li{ 21 | list-style-type: none; 22 | } 23 | a { 24 | text-decoration: none; 25 | } 26 | 27 | header { 28 | width: 60%; 29 | margin: 20px auto; 30 | } 31 | header:after{ 32 | content: ''; 33 | clear: both; 34 | display: table; 35 | } 36 | header .user_right{ 37 | float: right 38 | } 39 | header .user_right .active{ 40 | color: #5FB878; 41 | background: #fff; 42 | border: 1px solid #5FB878; 43 | box-shadow: 0 5px 5px #ccc; 44 | } 45 | header .user_name { 46 | float: left 47 | } 48 | .user_name { 49 | font-size: 20px; 50 | } 51 | 52 | .has_user a, 53 | .has_user span, 54 | .none_user a { 55 | padding: 5px 15px; 56 | background: #5FB878; 57 | border-radius: 15px; 58 | color: #fff; 59 | cursor: pointer; 60 | border: 1px solid #fff; 61 | transition: all 0.3s; 62 | } 63 | 64 | .has_user a:hover,.has_user span:hover{ 65 | color: #5FB878; 66 | background: #fff; 67 | border: 1px solid #5FB878; 68 | box-shadow: 0 5px 5px #ccc; 69 | } 70 | 71 | .posts{ 72 | border-radius: 4px; 73 | border: 1px solid #ddd; 74 | } 75 | .posts > li{ 76 | padding: 10px; 77 | position: relative; 78 | padding-bottom: 40px; 79 | } 80 | .posts .comment_pv{ 81 | position: absolute; 82 | bottom: 5px; 83 | right: 10px; 84 | } 85 | .posts .author{ 86 | position: absolute; 87 | left: 10px; 88 | bottom: 5px; 89 | font-size:0; 90 | } 91 | .posts .author span{ 92 | margin-right: 5px; 93 | font-size: 16px; 94 | } 95 | .posts > li:hover { 96 | background: #f2f2f2; 97 | } 98 | .posts > li:hover pre{ 99 | border: 1px solid #666; 100 | } 101 | .posts > li:hover .content{ 102 | border-top: 1px solid #fff; 103 | border-bottom: 1px solid #fff; 104 | } 105 | .posts > li + li{ 106 | border-top: 1px solid #ddd; 107 | } 108 | .posts li .title span{ 109 | position: absolute; 110 | left: 10px; 111 | top: 10px; 112 | color: #5FB878; 113 | font-size: 14px; 114 | } 115 | .posts li .title{ 116 | margin-left: 40px; 117 | font-size: 20px; 118 | color: #222; 119 | } 120 | .posts .userAvator{ 121 | position: absolute; 122 | left: 3px; 123 | top: 3px; 124 | width: 40px; 125 | height: 40px; 126 | border-radius: 5px; 127 | } 128 | .posts .content{ 129 | border-top: 1px solid #f2f2f2; 130 | border-bottom: 1px solid #f2f2f2; 131 | margin: 10px 0 0 0 ; 132 | padding: 10px 0; 133 | margin-left: 40px; 134 | word-wrap: break-word; 135 | word-break: break-all; 136 | } 137 | .userMsg img{ 138 | width: 40px; 139 | height: 40px; 140 | border-radius: 5px; 141 | margin-right: 10px; 142 | vertical-align: middle; 143 | display: inline-block; 144 | } 145 | .userMsg span{ 146 | font-size: 18px; 147 | color:#333; 148 | position: relative; 149 | top: 2px; 150 | } 151 | .posts li img{ 152 | max-width: 100%; 153 | } 154 | .spost .comment_pv{ 155 | position: absolute; 156 | top: 10px; 157 | } 158 | .spost .edit { 159 | position: absolute; 160 | right: 20px; 161 | bottom: 5px; 162 | } 163 | 164 | .spost .edit p { 165 | display: inline-block; 166 | margin-left: 10px; 167 | } 168 | 169 | .comment_wrap { 170 | width: 60%; 171 | margin: 20px auto; 172 | } 173 | 174 | .submit { 175 | display: block; 176 | width: 100px; 177 | height: 40px; 178 | line-height: 40px; 179 | text-align: center; 180 | border-radius: 4px; 181 | background: #5FB878; 182 | cursor: pointer; 183 | color: #fff; 184 | float: left; 185 | margin-top: 20px ; 186 | border:1px solid #fff; 187 | } 188 | .submit:hover{ 189 | background: #fff; 190 | color: #5FB878; 191 | border:1px solid #5FB878; 192 | } 193 | .comment_list{ 194 | border: 1px solid #ddd; 195 | border-radius: 4px; 196 | } 197 | .cmt_lists:hover{ 198 | background: #f2f2f2; 199 | } 200 | .cmt_lists + .cmt_lists{ 201 | border-top: 1px solid #ddd; 202 | } 203 | .cmt_content { 204 | padding: 10px; 205 | position: relative; 206 | border-radius: 4px; 207 | word-break: break-all; 208 | } 209 | .cmt_detail{ 210 | margin-left: 48px; 211 | } 212 | .cmt_content img{ 213 | max-width: 100%; 214 | } 215 | /* .cmt_content:after { 216 | content: '#content'; 217 | position: absolute; 218 | top: 5px; 219 | right: 5px; 220 | color: #aaa; 221 | font-size: 13px; 222 | } 223 | */ 224 | .cmt_name { 225 | position: absolute; 226 | right: 8px; 227 | bottom: 5px; 228 | color: #333; 229 | } 230 | 231 | .cmt_name a { 232 | margin-left: 5px; 233 | color: #1E9FFF; 234 | } 235 | .cmt_time{ 236 | position: absolute; 237 | font-size: 12px; 238 | right: 5px; 239 | top: 5px; 240 | color: #aaa 241 | } 242 | .form { 243 | margin: 0 auto; 244 | width: 50%; 245 | margin-top: 20px; 246 | } 247 | 248 | textarea { 249 | width: 100%; 250 | height: 150px; 251 | padding:10px 0 0 10px; 252 | font-size: 20px; 253 | border-radius: 4px; 254 | border: 1px solid #d7dde4; 255 | -webkit-appearance: none; 256 | resize: none; 257 | } 258 | 259 | textarea#spContent{ 260 | width: 98%; 261 | } 262 | 263 | .tips { 264 | margin: 20px 0; 265 | color: #ec5051; 266 | text-align: center; 267 | } 268 | 269 | .container { 270 | width: 60%; 271 | margin: 0 auto; 272 | } 273 | .form img.preview { 274 | width:100px; 275 | height:100px; 276 | border-radius: 50%; 277 | display: none; 278 | margin-top:10px; 279 | } 280 | input { 281 | display: block; 282 | width: 100%; 283 | height: 35px; 284 | font-size: 18px; 285 | padding: 6px 7px; 286 | border-radius: 4px; 287 | border: 1px solid #d7dde4; 288 | -webkit-appearance: none; 289 | } 290 | 291 | input:focus,textarea:focus{ 292 | outline: 0; 293 | box-shadow: 0 0 0 2px rgba(51,153,255,.2); 294 | border-color: #5cadff; 295 | } 296 | 297 | input:hover,input:active,textarea:hover,textarea:active{ 298 | border-color: #5cadff; 299 | } 300 | 301 | .create label { 302 | display: block; 303 | margin: 10px 0; 304 | } 305 | 306 | .comment_wrap form { 307 | width: 100%; 308 | margin-bottom: 85px; 309 | } 310 | 311 | .delete_comment, 312 | .delete_post { 313 | cursor: pointer; 314 | } 315 | 316 | .delete_comment:hover, 317 | .delete_post:hover, 318 | .author a:hover { 319 | color: #ec5051; 320 | } 321 | .disabled{ 322 | user-select: none; 323 | cursor: not-allowed !important; 324 | } 325 | .error{ 326 | color: #ec5051; 327 | } 328 | .success{ 329 | color: #1E9FFF; 330 | } 331 | .container{ 332 | width: 60%; 333 | margin:0 auto; 334 | } 335 | .message{ 336 | position: fixed; 337 | top: -100%; 338 | left: 50%; 339 | transform: translateX(-50%); 340 | padding: 10px 20px; 341 | background: rgba(0, 0, 0, 0.7); 342 | color: #fff; 343 | border-bottom-left-radius: 15px; 344 | border-bottom-right-radius: 15px; 345 | z-index: 99999; 346 | } 347 | .markdown pre{ 348 | display: block; 349 | overflow-x: auto; 350 | padding: 0.5em; 351 | background: #F0F0F0; 352 | border-radius: 3px; 353 | border: 1px solid #fff; 354 | } 355 | .markdown blockquote{ 356 | padding: 0 1em; 357 | color: #6a737d; 358 | border-left: 0.25em solid #dfe2e5; 359 | margin: 10px 0; 360 | } 361 | .markdown ul li{ 362 | list-style: circle; 363 | margin-top: 5px; 364 | } -------------------------------------------------------------------------------- /public/pagination.js: -------------------------------------------------------------------------------- 1 | function pagination(data, callback) { 2 | // css样式 3 | if (!document.getElementById('pageStyle')) { 4 | var style = document.createElement('style') 5 | style.id = 'pageStyle' 6 | style.innerHTML = '.pagination{text-align:center;margin-top:100px}.pagination a,.pagination span{margin:0 2px;padding:4px 8px;color:#428bca;background:#fff;text-decoration:none;border:1px solid #ddd;border-radius:4px;user-select:none;cursor:pointer}.pagination a:hover,.pagination span:hover{color:#fff;background:#428bca}.pagination .active{color:#fff;background:#428bca;cursor:default;}.pagination input{width:40px;padding:7px 0;border:none;outline:0;border:1px solid #ddd;border-radius:4px;text-align:center;margin:0 5px}.pagination i{font-style: normal;margin:0 5px;color:#999}.pagination input:focus{border:1px solid #428bca}' 7 | document.getElementsByTagName('head')[0].appendChild(style) 8 | } 9 | var page = document.getElementById(data.selector.slice(1)), 10 | nextPage = document.getElementById('nextPage'), 11 | prevPage = document.getElementById('prevPage'), 12 | inputGo = document.getElementById('inputGo'), 13 | currentPage = data.currentPage, 14 | nowPage = currentPage ? currentPage : 1, 15 | visiblePage = Math.ceil(data.visiblePage / 2), 16 | i_html = '', 17 | pageOneLoad = data.pageOneLoad ? false : true; 18 | // 初始化 19 | pageAction(nowPage) 20 | function pageAction(dataPage) { 21 | nowPage = dataPage; 22 | i_html = ''; 23 | var count = data.count <= 1 ? 1 : data.count ? data.count : 2 24 | startPage = dataPage - data.count <= 1 ? 1 : dataPage - data.count, 25 | endPage = dataPage + data.count >= data.totalPage ? data.totalPage : dataPage + data.count, 26 | prevPage = data.prev ? data.prev : '<', 27 | nextPage = data.next ? data.next : '>'; 28 | if (dataPage > 1) { 29 | i_html += '' + prevPage + '' 30 | if (data.first) { 31 | i_html += '首页' 32 | } 33 | } 34 | if (dataPage >= 5) { 35 | for (var i = 1; i <= 2; i++) { 36 | i_html += '' + i + '' 37 | } 38 | i_html += '...' 39 | } 40 | for (var j = startPage; j <= endPage; j++) { 41 | i_html += '' + j + '' 42 | } 43 | if (endPage + 1 < data.totalPage) { 44 | i_html += '...' 45 | for (var i = (endPage > data.totalPage - 2 ? data.totalPage : data.totalPage - 1); i <= data.totalPage; i++) { 46 | i_html += '' + i + '' 47 | } 48 | if (data.last) { 49 | i_html += '尾页' 50 | } 51 | i_html += '' + nextPage + '' 52 | } 53 | if (data.showTotalPage && data.totalPage >= 1) { 54 | i_html += '' + nowPage + '/' + data.totalPage + '' 55 | } 56 | if (data.jumpBtn && data.totalPage >= 1) { 57 | i_html += '前往页 确定' 58 | } 59 | page.innerHTML = i_html; 60 | var pageA = page.getElementsByTagName('a'); 61 | for (var i = 0, pageALength = pageA.length; i < pageALength; i++) { 62 | pageA[i].className = '' 63 | if (pageA[i].getAttribute('data-page') == dataPage) { 64 | pageA[i].className = "active" 65 | } 66 | } 67 | // 第一页不请求 68 | if (!pageOneLoad) { 69 | callback && callback.call(null, dataPage) 70 | } 71 | } 72 | page.onclick = function (event) { 73 | var event = event || window.event, 74 | target = event.target || event.srcElement, 75 | dataPage = parseInt(target.getAttribute('data-page')); 76 | pageOneLoad = false; 77 | if (target.className == 'active') return 78 | if (target.nodeName.toLowerCase() == 'a') { 79 | pageAction(dataPage) 80 | } 81 | if (target.id == 'nextPage') { 82 | nowPage++ 83 | pageAction(nowPage) 84 | } 85 | if (target.id == 'prevPage') { 86 | nowPage-- 87 | pageAction(nowPage) 88 | } 89 | if (target.id == 'inputGo') { 90 | var pageInput = document.getElementById('pageInput'), 91 | goPage = pageInput.value > data.totalPage ? 1 : /[1-9]+/g.test(pageInput.value) ? pageInput.value : 1; 92 | pageAction(parseInt(goPage)) 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /routers/posts.js: -------------------------------------------------------------------------------- 1 | const router = require('koa-router')(); 2 | const controller = require('../controller/c-posts') 3 | 4 | // 重置到文章页 5 | router.get('/', controller.getRedirectPosts) 6 | // 文章页 7 | router.get('/posts', controller.getPosts) 8 | // 首页分页,每次输出10条 9 | router.post('/posts/page', controller.postPostsPage) 10 | // 个人文章分页,每次输出10条 11 | router.post('/posts/self/page', controller.postSelfPage) 12 | // 单篇文章页 13 | router.get('/posts/:postId', controller.getSinglePosts) 14 | // 发表文章页面 15 | router.get('/create', controller.getCreate) 16 | // post 发表文章 17 | router.post('/create', controller.postCreate) 18 | // 发表评论 19 | router.post('/:postId',controller.postComment) 20 | // 编辑单篇文章页面 21 | router.get('/posts/:postId/edit', controller.getEditPage) 22 | // post 编辑单篇文章 23 | router.post('/posts/:postId/edit', controller.postEditPage) 24 | // 删除单篇文章 25 | router.post('/posts/:postId/remove', controller.postDeletePost) 26 | // 删除评论 27 | router.post('/posts/:postId/comment/:commentId/remove', controller.postDeleteComment) 28 | // 评论分页 29 | router.post('/posts/:postId/commentPage', controller.postCommentPage) 30 | 31 | module.exports = router -------------------------------------------------------------------------------- /routers/signin.js: -------------------------------------------------------------------------------- 1 | const router = require('koa-router')(); 2 | const controller = require('../controller/c-signin') 3 | 4 | router.get('/signin', controller.getSignin) 5 | router.post('/signin', controller.postSignin) 6 | 7 | module.exports = router -------------------------------------------------------------------------------- /routers/signout.js: -------------------------------------------------------------------------------- 1 | const router = require('koa-router')(); 2 | 3 | router.get('/signout', async(ctx, next) => { 4 | ctx.session = null; 5 | console.log('登出成功') 6 | ctx.body = true 7 | }) 8 | 9 | module.exports = router -------------------------------------------------------------------------------- /routers/signup.js: -------------------------------------------------------------------------------- 1 | const router = require('koa-router')(); 2 | const controller = require('../controller/c-signup') 3 | 4 | // 注册页面 5 | router.get('/signup', controller.getSignup) 6 | // post 注册 7 | router.post('/signup', controller.postSignup) 8 | 9 | module.exports = router -------------------------------------------------------------------------------- /test/blog-test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var apiModel = require('../lib/mysql.js') 3 | 4 | describe('add User', function() { 5 | // 创建一个用户 6 | before((done) => { 7 | apiModel.insertData(['wclimb','123456','avator','time']).then(()=>{ 8 | done() 9 | }); 10 | }); 11 | // 删除一个用户 12 | after((done) => { 13 | apiModel.deleteUserData('wclimb').then(()=>{ 14 | done() 15 | }); 16 | }) 17 | // 查找用户 18 | it('should return an Array contain {} when find by name="wclimb"', (done) => { 19 | apiModel.findUserData('wclimb').then((user) => { 20 | var data = JSON.parse(JSON.stringify(user)); 21 | console.log(data) 22 | expect(data).to.have.lengthOf(1); 23 | done(); 24 | }).catch((err)=>{ 25 | if (err) { 26 | return done(err); 27 | } 28 | }) 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /views/create.ejs: -------------------------------------------------------------------------------- 1 | <%- include("header",{type:'create'}) %> 2 |