在学习了koa2和express并写了一些demo后,打算本身写一个项目练练手,因为是在校生,没什么好的项目作,即以开发一个前端论坛为目标,功能需求参照一下一些社区拟定,主要有:javascript
github地址:github.com/Jay214/mybl…,若是以为对你有帮助或者还看得下去,欢迎star~~鼓励鼓励我这前端渣渣辉。css
node: v8.3.0html
koa: ^2.4.1
前端
mysql: 5.7.1java
npm: 5.3.0及以上node
git clone git@github.com:Jay214/myblog-koa2.git
npm install
node index 或npm supervisor index
若发现项目有存在什么bug或有比较好的建议欢迎多多提议,qq:2752402930。mysql
因为koa2是基于es6的promise和es7的await/async语法,因此若是对es6/es7不懂的话请先过一遍文档,后台搭建数据库是关键,因此请先安装好mysql,mysql建议安装5.7版本如下的,由于5.7.0版本有个bug,须要更改配置文件,具体若大家安装的时候便知道了。jquery
安装node环境,使用node -v
查看node版本,node须要较新版本可以支持es6的promise和es7的await/async语法,如今node版本都会自带npm的,因此不须要再去安装npm。git
项目初始化:cd myblog1 -> npm init 此时已经建立好了package.json文件了。es6
因为koa2是轻量级的框架,小巧精悍,因此咱们为了促进咱们的开发效率和方便性,咱们须要安装一些koa2的模块中间件:
npm install i koa koa-bodyparser koa-mysql-session koa-router koa-session-minimal koa-static koa-views md5 moment mysql ejs koa-static-cache --save-dev复制代码
各模块用处
koa node
框架koa-bodyparser
表单解析中间件koa-mysql-session
、koa-session-minimal
处理数据库的中间件koa-router
路由中间件koa-static
静态资源加载中间件ejs
模板引擎md5
密码加密moment
时间中间件mysql
数据库koa-views
模板呈现中间件koa-static-cache
文件缓存在config文件夹新建default.js :
const config = {
//启动端口
port: 8080,
//数据库配置
database: {
DATABASE: 'nodesql',
USERNAME: 'root',
PASSWORD: '123456',
PORT: '3306',
HOST: 'localhost'
}
}
module.exports = config; 复制代码
而后在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(); //为每个请求都创建一个connection使用完后调用connection.release(); 直接释放资源。
//query用来操做数据库表
})
}
})
})}复制代码
这里创建了一个数据库链接池和封装了一个操做数据库表的函数,若是对于数据库链接有不懂的话请自行百度。
在主目录新建index.js,即项目入口文件:
const koa = require("koa"); //node框架
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 router = require("koa-router"); //路由中间件
const config = require('./config/default.js'); //引入默认文件
const views = require("koa-views"); //模板呈现中间件
const koaStatic = require("koa-static"); //静态资源加载中间件
const staticCache = require('koa-static-cache')
const app = new koa();
//session存储配置,将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)
}))
//配置静态资源加载中间件
app.use(koaStatic(
path.join(__dirname , './public')
))
//配置服务端模板渲染引擎中间件
app.use(views(path.join(__dirname, './views'),{
extension: 'ejs'
}))
//使用表单解析中间件
app.use(bodyParser({
"formLimit":"5mb",
"jsonLimit":"5mb",
"textLimit":"5mb"
}));
//使用新建的路由文件
//登陆
app.use(require('./routers/signin.js').routes())
//注册
app.use(require('./routers/signup.js').routes())
//退出登陆
app.use(require('./routers/signout.js').routes())
//首页
app.use(require('./routers/home.js').routes())
//我的主页
app.use(require('./routers/personal').routes())
//文章页
app.use(require('./routers/articles').routes())
//资源分享
app.use(require('./routers/share').routes())
//我的日记
app.use(require('./routers/selfNote').routes())
//监听在8080端口
app.listen(8080)
console.log(`listening on port ${config.port}`)
复制代码
上面代码都有注释,我就不一一说明了,因为资源分享和我的日记还没写,因此暂时统一share...替代。
接下来向mysql.js添加数据库操做语句,建表、增删改查。。。
var users = `create table if not exists users(
id INT(200) NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
pass VARCHAR(40) NOT NULL,
avator VARCHAR(100) DEFAULT 'default.jpg',
job VARCHAR(40),
company VARCHAR(40),
introdu VARCHAR(255),
userhome VARCHAR(100),
github VARCHAR(100),
PRIMARY KEY (id)
);`
var posts = `create table if not exists posts(
id INT(200) NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
title VARCHAR(100) NOT NULL,
content TEXT NOT NULL,
uid INT(200) NOT NULL,
moment VARCHAR(40) NOT NULL,
comments VARCHAR(255) NOT NULL DEFAULT '0',
pv VARCHAR(40) NOT NULL DEFAULT '0',
likes INT(200) NOT NULL DEFAULT '0',
type VARCHAR(20) NOT NULL,
avator VARCHAR(100),
collection INT(200) NOT NULL DEFAULT '0',
PRIMARY KEY (id) ,
FOREIGN KEY (uid) REFERENCES users(id)
ON DELETE CASCADE
);`
var comment= `create table if not exists comment(
id INT(200) NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
content TEXT NOT NULL,
moment VARCHAR(40) NOT NULL,
postid INT(200) NOT NULL,
avator VARCHAR(100),
PRIMARY KEY ( id ),
FOREIGN KEY (postid) REFERENCES posts(id)
ON DELETE CASCADE
);`
var likes = `create table if not exists likes(
id INT(200) NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
postid INT(200) NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (postid) REFERENCES posts(id)
ON DELETE CASCADE
);`
var collection = `create table if not exists collection(
id INT(200) NOT NULL AUTO_INCREMENT,
uid VARCHAR(100) NOT NULL,
postid INT(200) NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (postid) REFERENCES posts(id)
ON DELETE CASCADE
);`
var follow = `create table if not exists follow(
id INT(200) NOT NULL AUTO_INCREMENT,
uid INT(200) NOT NULL,
fwid INT(200) NOT NULL DEFAULT '0',
PRIMARY KEY (id),
FOREIGN KEY (uid) REFERENCES users(id)
ON DELETE CASCADE
)
`
let createTable = function(sql){
return query(sql, []);
}
//建表
createTable(users);
createTable(posts);
createTable(comment);
createTable(likes);
createTable(collection);
createTable(follow);
//createTable(follower);
//注册用户
let insertData = function(value){
let _sql = "insert into users(name,pass) values(?,?);"
return query(_sql,value);
}
//更新头像
let updateUserImg = function(value){
let _sql = "update users set avator=? where id=?"
return query(_sql,value);
}
//更新用户信息
let updateUser = function(value){
let _sql = "update users set name=?,job=?,company=?,introdu=?,userhome=?,github=? where id=?"
return query(_sql,value);
}
//发表文章
let insertPost = function(value){
let _sql = "insert into posts(name,title,content,uid,moment,type,avator) values(?,?,?,?,?,?,?);"
return query(_sql,value);
}
//更新文章评论数
let updatePostComment = function(value){
let _sql = "update posts set comments=? where id=?"
return query(_sql,value);
}
.......复制代码
总共六张表:用户表、文章表、文章评论表、文章收藏表、文章点赞表、用户关注表。
这里引用了外键,可是如今的开发不推荐使用外键了,因此大家能够自行修改,这里在项目第一次启动时会出现数据库建立失败(因为外键缘由),只要从新启动就ok了,若是对mysql还不了解的,这里附送你们一个传送门:mysql入门视频教程 密码:c2q7
。
项目基本结构搭建好后,就能够进行前端页面的编写了。用node开发web时咱们通常会配合模板引擎,这个项目我采用的是ejs,除了ejs以外较为经常使用的还有jade,可是jade相对ejs来讲的话代码结构不够清晰。关于ejs语法,这里作个简单的介绍:
前端页面开发也应该是有清晰的结构的,为了实现代码复用,这里先创建四个公共文件:header 、 footer、 nav、 login:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Myblog</title>
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/index.css">
<script src="/js/jquery-3.2.1.min.js" type="text/javascript"></script>
<script src="/js/bootstrap.min.js" type="text/javascript"></script>
复制代码
</head>
<body>
<header class="nav-head">
<div class="nav container">
<ul>
<li><a href="/home">首页</a></li>
<li> <a href="/share">资源分享</a></li>
<li> <a href="/share">推荐</a></li>
<li> <a href="/share">我的日记</a></li>
<li><a href="/about">关于做者</a></li>
<li><input type="text" placeholder="搜索" class="input-sm search"></li>
<% if(session.user){ %>
<li>
<img src="/images/<%= session.avator %>" alt="" class="img-circle img-title">
<ul class="menu">
<li class="personal menulist"><a href="/personal/<%= session.user %>">主页</a></li>
<!-- <li class="collection menulist"><a href="#">收藏集</a></li>
-->
<li class="menulist"><a href="/articles">写文章</a></li>
<li class="out"><a href="/signout">登出</a></li>
</ul>
</li>
<script>
var imgTitle = document.getElementsByClassName('img-title')[0],
menu = document.getElementsByClassName('menu')[0];
imgTitle.onclick = function (event) {
showTap();
event.stopPropagation(); //阻止事件冒泡
}
document.body.addEventListener('click',function (event) {
menu.style.display = 'none';
// event.stopPropagation();
},true)
function showTap(){
if(menu.style.display == 'block'){
menu.style.display = 'none';
}else {
menu.style.display = 'block';
}
}
//退出登陆
var signOut = document.getElementsByClassName('out')[0];
/* signOut.onclick = function(){
ajax('get','/signout',null);
xhr.onreadystatechange = function () {
if(xhr.readyState==4&&xhr.status>=200&&xhr.status<300){
let text = xhr.responseText; //服务器返回的对象
if(text){
window.location.reload = 'localhost:8080/home';
}
}
}
}*/
</script>
<% }else{ %>
<li class="login">
<a class="loginup" href="javascript:;"><span class="glyphicon glyphicon-user"></span> 注册 | 登陆</a>
</li>
<% } %>
</ul>
</div>
</header>
<script>
var searchInput = document.getElementsByClassName('search')[0];
searchInput.onfocus = function () {
this.style.width = "300px";
}
searchInput.onblur = function () {
this.style.width = "180px";
}
</script>
复制代码
<div class="sign">
<a href="javascript:;" title="关闭" class="login-close close">×</a>
<div class="sign-title">
<h1>用户注册</h1>
<h3>来吧骚年们!</h3>
</div>
<form class="form signup" role="form">
<div class="form-group">
<input type="text" name="username" placeholder="帐号很多于两个字符" class="form-control">
</div>
<div class="form-group">
<input type="password" name="pass" class="pass form-control" placeholder="密码">
</div>
<div class="form-group">
<input type="password" name="repeatpass" id="repeat" placeholder="重复密码" class="form-control">
</div>
<div class="form-group">
<input type="button" value="注册" class="btn btn-primary login-up">
</div>
</form>
<form class="form signin" role="form">
<div class="form-group">
<input type="text" name="username" placeholder="请输入用户名" class="form-control">
</div>
<div class="form-group">
<input type="password" name="pass" class="pass form-control" placeholder="请输入密码">
</div>
<div class="form-group">
<input type="button" value="登陆" class="btn btn-primary login-in">
</div>
</form>
<div class="form-tips">
<span>已有帐号?</span>
<a href="javascript:;" class="register">登陆</a>
</div>
</div>
<div class="login-form-mask"></div>
<script>
// $(document).ready(function () {
var $close = $('.login-close');
var $sign = $('.sign');
$close.click(function () {
$sign.css("display","none");
})
var $register = $('.register'), //login/loginup切换
$span = $('.form-tips span'),
$signup = $('.signup'),
$signTitle = $('.sign-title h1'),
$signin = $('.signin');
$register.click(function () {
if($span.html() == "已有帐号?"){
$signin.css('display','block');
$signup.css('display','none');
$(this).html('注册');
$span.html("没有帐号?");
$signTitle.html("欢迎登陆");
}else{
$signin.css('display','none');
$signup.css('display','block');
$(this).html('登陆');
$span.html("已有帐号?");
$signTitle.html("欢迎注册");
}
})
var $loginup = $('.loginup'); //点击登陆/注册,阻止事件冒泡
$loginup.click(function () {
$mask.fadeIn(100);
$sign.slideDown(200);
return false;
})
var $close = $('.login-close'),
$mask = $('.login-form-mask'),
$sign = $('.sign');
$sign.click(function () {
return false;
})
$close.click(function (e) {
// e.stopPropagation();
fadeOut();
})
$(document).click(function (e) { //点击任意位置取消登陆框
//e.stopPropagation();
fadeOut();
})
function fadeOut(){
$mask.fadeOut(100);
$sign.slideUp(200);
}
var loginUp = document.getElementsByClassName('login-up')[0],
loginIn = document.getElementsByClassName('login-in')[0],
signUp = document.getElementsByClassName('signup')[0],
signIn = document.getElementsByClassName('signin')[0];
loginUp.onclick = function () { //注册
var data1 = 'username=' + signUp["username"].value + '&' + 'pass='+ signUp["pass"].value + '&' + 'repeatpass=' + signUp["repeatpass"].value;
var reg = /^[\u4E00-\u9FA5]{2,5}$/;
/* if(!reg.test(signUp["username"].value)){
signUp["username"].classList.add("tips");
signUp['username'].value()
} */
ajax('post','/signup',data1,"application/x-www-form-urlencoded");
xhr.onreadystatechange = function () {
if(xhr.readyState==4&&xhr.status>=200&&xhr.status<300){
let text = JSON.parse(xhr.responseText).code;
console.log(text) //服务器返回的对象
if(text == 3){
fadeOut();
alert("注册成功")
setTimeout(()=>{
window.location.reload();
},1000)
// document.getElementsByClassName('login')[0].outerHTML = "<li class='users'><a href='/'>"+signUp["username"].value+ "(=^ ^=)" +"</a></li>"
}else{
fadeOut();
alert("用户已存在")
}
}
}
}
loginIn.onclick = function () { //登陆
var data2 = 'username=' + signIn["username"].value + '&' + 'pass=' + signIn["pass"].value;
ajax('post','/signin',data2,"application/x-www-form-urlencoded");
xhr.onreadystatechange = function () {
if(xhr.readyState==4&&xhr.status>=200&&xhr.status<300){
let text = JSON.parse(xhr.responseText).code; //服务器返回的对象
console.log(text);
// document.getElementsByClassName('login')[0].outerHTML = "<li class='users'><a href='/'>"+signUp["username"].value+ "(=^ ^=)" +"</a></li>"
if(text===1){
fadeOut();
// let imgTitle = document.getElementsByClassName('img-title')[0];
// imgTitle.setAttribute('src','/images/' + JSON.parse(xhr.responseText).avator)
setTimeout(()=>{
window.location.reload();
},1000)
}else if(text === 2){
alert('密码错误')
}else{
alert('帐号不存在')
}
}
}
}
</script>
复制代码
</body>
</html>复制代码
header为页面头部结构,nav为页面导航条,login为登陆、注册内容、footer为页面顶部结构。能够看到我在ejs文件里有不少的if else
判断语句,这是根据session来判断用户是否登陆渲染不一样的内容。如今咱们须要咱们的页面编写样式:分别是home.css和index.css
为了加强对原生js的理解,在项目里我用了大量的原生ajax(显然jquery封装的ajax比较好哈哈),所以这里先封装一个原生ajax请求:
var xhr = null;
function ajax(method,url,data,types) { //封装一个ajax方法
// var text;
if(window.XMLHttpRequest){
xhr = new XMLHttpRequest();
}else if(window.ActiveXObject){
xhr = new ActiveXObject("Microsoft.XMLHTTP");
}else {
alert('你的浏览器不支持ajax');
return false;
}
xhr.onerror = function (err) {
alert("some err have hapened:",err);
}
xhr.open(method,url,true);
if(method=="post"){
xhr.setRequestHeader("Content-type",types);
// xhr.setRequestHeader("Conent-Type",'application/json'"application/x-www-form-urlencoded")
}
try{
setTimeout(()=>{
xhr.send(data);
},0);
}catch(err) {
alert("some error have hapened in font:",err);
}
return xhr;
}
复制代码
前端基本页面开发好后,咱们就能够写后台登陆接口了:
var router = require('koa-router')();
var userModel = require('../lib/mysql.js');
var md5 = require('md5')
// 注册页面
// post 注册
router.post('/signup', async(ctx, next) => {
console.log(ctx.request.body)
var user = {
name: ctx.request.body.username,
pass: ctx.request.body.pass,
repeatpass: ctx.request.body.repeatpass
}
let flag = 0;
await userModel.findDataByName(user.name)
.then(result => {
console.log(result)
if (result.length) {
//处理err
console.log('用户已存在')
ctx.body = {
code: 1
};
} else if (user.pass !== user.repeatpass || user.pass == '') {
ctx.body = { //应把这个逻辑放到前端
code: 2
};
} else {
flag = 1;
}
})
if(flag==1){
let res = await userModel.insertData([user.name, md5(user.pass + 'asd&$BH&*') ])
console.log(res.insertId)
await userModel.findDataByName(user.name)
.then((result)=>{
// var res = JSON.parse(JSON.stringify(result))
console.log(result[0]['avator'])
ctx.session.id = res.insertId;
ctx.session.user=user.name;
ctx.session.avator = 'default.jpg';
ctx.body = {
code: 3
};
console.log('注册成功')
})
}
})
module.exports = router复制代码
密码采用md5加密,注册后为用户建立session并将其添加到数据库,写完别忘了在最后加上module.exports = router
将接口暴露出来。
登陆:signin.js
var router = require('koa-router')();
var userModel = require('../lib/mysql.js')
var md5 = require('md5')
router.post('/signin', async(ctx, next) => {
console.log(ctx.request.body)
var name = ctx.request.body.username;
var pass = ctx.request.body.pass;
await userModel.findDataByName(name)
.then(result => {
var res = JSON.parse(JSON.stringify(result))
if (name === res[0]['name']&&(md5(pass + 'asd&$BH&*') === res[0]['pass'])) {
console.log('登陆成功')
ctx.body = {
code: 1,
}
ctx.session.user = res[0]['name']
ctx.session.id = res[0]['id']
ctx.session.avator = res[0]['avator']
}else if(md5(pass + 'asd&$BH&*') != res[0]['pass']){
ctx.body = {
code: 2 //密码错误
}
}
}).catch(err => {
ctx.body = {
code: 3 //帐号不存在+
}
console.log('用户名或密码错误!')
})
})
module.exports = router复制代码
var router = require('koa-router')();
var checkUser = require('../midllewares/checkUser');
router.get('/signout', async(ctx, next) => {
ctx.session = null;
console.log('退出成功');
ctx.body = true;
ctx.redirect('/home');
//return;
//ctx.redirect('/home');
})
module.exports = router
module.exports = router复制代码
//使用新建的路由文件
//登陆
app.use(require('./routers/signin.js').routes())
//注册
app.use(require('./routers/signup.js').routes())
//退出登陆
app.use(require('./routers/signout.js').routes())复制代码
登陆注册完成后下面就是咱们的首页了,首页部分咱们先写后端接口:
var router = require('koa-router')();
var userModel = require('../lib/mysql');
router.get('/home', async(ctx, next)=>{
let types;
console.log(ctx.headers['accept'])
//判断是否带文章类型参数,若没有则type=all
if(!ctx.request.querystring){
types = 'all';
await userModel.findPostByPage(1)
.then(result => {
//console.log(result)
post = result
})
await userModel.findAllPost()
.then(result=>{
postsLength = result.length
})
if(ctx.session.user){
await userModel.findDataByName(ctx.session.user)
.then(res=>{
ctx.session.avator = res[0]['avator'];
})
}
}else{
//若带有文章类型参数则根据类型查询数据库
types = ctx.request.querystring.split('=')[1];
console.log(types)
let _sql = `select * from posts where type = "${types}" limit 0,10`;
await userModel.query(_sql)
.then(result=>{
post = result;
})
_sql = `select * from posts where type = "${types}"`;
await userModel.query(_sql)
.then(result=>{
postsLength = result.length;
})
}
await ctx.render('home', {
session: ctx.session,
articles: post,
type: types,
postsLength: postsLength,
postsPageLength: Math.ceil(postsLength / 10),
})
})
复制代码
上面数据库操做采用分页查询,首次仅查询十条数据,以便前端文章列表分页,咱们向前端传递所有文章总数也是为了分页的查询。除此以外咱们还要为前端分页查询再写一个接口:
// 首页分页,每次输出10条
router.post('/articles/page', async(ctx, next) => {
let page = ctx.request.body.page,
type = ctx.request.querystring.split('=')[1];
console.log(type)
if(type=='all'){
await userModel.findPostByPage(page)
.then(result=>{
//console.log(result)
ctx.body = result
}).catch(()=>{
ctx.body = false;
})
}else{
let _sql = `select * from posts where type = "${type}" limit ${(page-1)*10},10`;
await userModel.query(_sql)
.then(result=>{
ctx.body = result;
}).catch(err=>{
console.log(err);
ctx.body = false;
})
}
})
复制代码
接口写好后咱们就能够来写前端了:
<% include header %>
<% if(!session.user){ %>
<link rel="stylesheet" href="/css/login.css">
<% } %>
<link rel="stylesheet" href="/css/home.css">
<script src="./js/ajax.js"></script>
<% include nav %>
<% if(!session.user){ %>
<% include login %>
<% } %>
<header class="article-tap padmar">
<ul class="tip">
<li class="all"><a href="javascript:;" class="label label-default">所有</a></li>
<li class="javascript"><a href="javascript:;" class="label label-default">javascript</a></li>
<li class="html"><a href="javascript:;" class="label label-default">html</a></li>
<li class="css"><a href="javascript:;" class="label label-default">css</a></li>
<li class="node"><a href="javascript:;" class="label label-default">node</a></li>
<li class="other"><a href="javascript:;" class="label label-default">其余</a></li>
</ul>
</header>
<article class="article-list">
<% articles.forEach(function(post){ %>
<div class="content">
<a href="/personal/<%= post.name %>" target="_blank" title=" <%= post.name %> " class="post-author">
<span class="author"><%= post.name %></span>
<span class="times"><%= post.moment %></span>
<span class="label label-info"><%= post.type %></span>
</a>
<a href="/articledetail/<%= post.id %>" target="_blank">
<h4 class="title"><%= post.title %></h4>
<div class="content-fo">
<span class="glyphicon glyphicon-heart"></span><span><%= post.likes %></span>
<span class="glyphicon glyphicon-comment"></span><span><%= post.comments %></span>
<span class="pv-item">阅读量 <span class="pv"><%= post.pv %></span></span>
</div>
</a>
</div>
<% }) %>
<% if(!postsPageLength){ %>
<div class="nothing">
<p><span class="glyphicon glyphicon-list-alt"></span></p>
<p>这里什么都没有</p>
</div>
<% } %>
</article>
<div style="width:50%;margin-left:25%;margin-top: 30px;text-align:center;" class="pagination" id="page"></div>
<script src="/js/pagination.js"></script>
<script>
window.onload = function () {
if('<%= type %>'==0){
$('.tip li').eq(0).find('a').attr('class','label label-info');
}else{
$(".<%=type%>").find('a').attr('class','label label-info');
}
}
pagination({
selector: '#page',
totalPage: '<%= postsPageLength %>',
currentPage: 1,
prev: '上一页',
next: '下一页',
first: true,
last: true,
showTotalPage: true,
count: 2//当前页先后显示的数量
},function(val){
// 当前页
$.ajax({
url: "articles/page?type=<%=type%>",
type: 'POST',
data:{
page: val
},
cache: false,
success: function (msg) {
console.log(msg)
if (msg != false) {
$('.article-list').html(' ')
$.each(msg,function(i,val){
console.log(val.name)
$('.article-list').append(
"<div class='content'>" +
'<a href="/personal/' + val.name + '" title="' +val.name + '" target="_blank" class="post-author">' +
"<span class='author'>" + val.name + "</span>" + " " +
"<span class='times'>" + val.moment + '</span>' + " " +
'<span class="label label-info">' + val.type + '</span> ' +
"</a>" +
'<a href="/articledetail/' + val.id + '" target="_blank">' +
"<h4 class='title'>" + val.title + "</h4>" +
"<div class='content-fo'>" +
"<span class='glyphicon glyphicon-heart'></span><span>" + val.likes + "</span>" +
"<span class='glyphicon glyphicon-comment'></span><span>" + val.comments + "</span> " +
' <span class="pv-item">阅读量 ' + "<span class='pv'>" + val.pv + "</span>" + '</span>' +
"</div>" +
"</a>" +
"</div>"
)
})
}else{
alert('分页不存在')
}
}
})
})
let articleTap = document.getElementsByClassName('article-tap')[0].getElementsByClassName('tip')[0];
articleTap.onclick = function(e){
// let e = e||window.e;
let target = e.target||e.srcElement;
// let lis = articleTap.getElementsByTagName('li');
// console.log(lis)
if(target.nodeName=='A'){
let type = target.parentNode.getAttribute('class');
if(type==='all'){
window.location.href = '/home';
}else{
window.location.href ='/home?type=' + type;
target.setAttribute('class','label label-info');
}
}
}
</script>
</body>
</html>复制代码
使用<% if(!session.user){ %> <% include login %> <% } %>
根据用户是否登陆加载不一样文件,文章列表直接for循环渲染输出,pagination.js
是一个封装好的分页组件,须要传递两个参数:一个json对象表示分页组件的内容;一个ajax请求实现分页加载。
下面介绍咱们的文章发表功能,文章发表的富文本编辑器我采用的是wangEditor.min.js,
简单易上手,直接引入压缩文件便可,可根据本身的需求配置选项,话很少说:
<% include header %>
<link rel="stylesheet" href="/css/articles.css">
<script src="./js/wangEditor.min.js"></script>
<script src="./js/ajax.js"></script>
<% include nav %>
<div class="editor1"></div>
<div id="edit">
<span class="edit-tips">发表成功</span>
<span class="num">5字</span>
<select name="derection" id="derect">
<option value="0">文章类型</option>
<option value="javascript">javascript</option>
<option value="html">html</option>
<option value="css">css</option>
<option value="node">node</option>
<option value="other">其余</option>
</select>
<a href="javascript:;" class="editorUp"><span class="glyphicon glyphicon-share-alt"></span>发布文章</a>
</div>
<div id="article">
<input type="text" placeholder="文章标题" name="title" class="article-title"/>
<div class="editor2"></div>
</div>
<script src="/js/mask.js"></script>
<script>
//配置编辑器
var E = window.wangEditor
var editor = new E('.editor1','.editor2')
// 或者 var editor = new E( document.getElementById('editor') )
editor.customConfig.uploadImgShowBase64 = true // 使用 base64 保存图片
//editor.customConfig.uploadImgServer = '/upload' // 上传图片到服务器
// 将图片大小限制为 3M
editor.customConfig.uploadImgMaxSize = 3 * 1024 * 1024;
// 限制一次最多上传 5 张图片
editor.customConfig.uploadImgMaxLength = 5;
editor.customConfig.pasteFilterStyle = true;
editor.create(); //建立富文本编辑器
editor.txt.html('<span>编辑文章内容.......</span>');
//统计字数
var num = document.getElementsByClassName('num')[0],
editType = document.getElementById('derect'),
editContent = document.getElementsByClassName('editor2')[0];
editContent.onkeyup = function () {
num.innerHTML = (editContent.innerHTML.length - 120) + '字';
}
//ajax上传文章
var editorUp = document.getElementsByClassName('editorUp')[0],
editTitle = document.getElementsByClassName('article-title')[0],
editTips = document.getElementsByClassName('edit-tips')[0];
editorUp.onclick = function () {
// console.log(editContent.innerHTML)
if(editTitle.value&&editContent.innerHTML.length>132){
console.log(editor.txt.text())
if(editType.value==0){
fadeout('请选择文章类型');
return false;
}
let data = { "title": editTitle.value,"content":editor.txt.html(),"type":editType.value };
let data2 = JSON.stringify(data);
console.log(data2);
ajax('post','/articles',data2,'application/json');
xhr.onreadystatechange = function () {
if(xhr.readyState==4&&xhr.status>=200&&xhr.status<300){
let text = xhr.responseText; //服务器返回的对象
console.log(text)
if(text==1){
mask('发表成功');
setTimeout(()=>{
window.location.href = '/home';
},1500);
}else{
fadeout('发表失败');
}
}
}
}else{
fadeout('文章内容和标题不能为空');
// editTips.innerHTML = '文章内容和标题不能为空';
// editTips.style.animation = 'fadeout 2s';
}
}
function fadeout(text){
editTips.innerHTML = text;
editTips.style.opacity = 1;
setTimeout(()=>{
editTips.style.opacity = 0;
},1000)
}
</script>
<% include footer %>复制代码
若插入图片则使用 base64 保存图片,由于文章内容(包括标签)是直接以字符串形式存入数据库的,注意上面我引入了一个mask.js文件,这个文件封装了一个mask函数用于弹出提示信息,因为没有用jquery因此采用的是触发的时候建立元素,setTimeout()定时器延迟删除,这样能够避免页面上有没必要要的标签存在。有点相似于单例模式。文章发表后台很简单我就不解释了,如今发表文章的话就能够在咱们的首页看到内容了。
function mask(text) {
let div1 = document.createElement('div');
div1.setAttribute('class','login-form-mask');
let div2 = document.createElement('div');
div2.setAttribute('class','tip-box');
div2.innerHTML = text;
document.body.appendChild(div1);
document.body.appendChild(div2);
setTimeout(()=>{
let dd = document.getElementsByClassName('.login-form-mask')[0];
console.log(dd);
document.body.removeChild(document.getElementsByClassName('login-form-mask')[0]);
document.body.removeChild(document.getElementsByClassName('tip-box')[0]);
},1500)
}
复制代码
文章虽然发表了,可是咱们尚未写评论和收藏等功能,下面开始入手,首先要先写咱们的文章详情页:
<% include header %>
<link rel="stylesheet" href="/css/login.css">
<link rel="stylesheet" href="/css/articledetail.css">
<script src="/js/ajax.js"></script>
<% include nav %>
<% if(!session.user){ %>
<% include login %>
<% } %>
<div class="article-list padmar">
<div class="post-head">
<span class="post-type label label-info">
<%= article.type %>
</span>
<a href="/personal/<%= article.name %>" class="author">
<img src="/images/<%= article.avator %>" alt="" class="img-circle">
<h4><%= article.name %></h4>
<span><%= article.moment %></span>
</a>
<% if(session.user){ %>
<% if(session.user === article.name){ %>
<a href="/" class="editself">编辑</a>
<a href="/delete?id=<%= article.id %>" class="delete"> 删除</a>
<% } %>
<% if(session.user != article.name){ %>
<span class="follow btn btn-default">关注做者</span>
<% } %>
<% } %>
</div>
<aside>
<ul class="comment-taps">
<li class="list"><a href="javascript:;" title="点赞"><span class="glyphicon glyphicon-heart-empty"></span></a></li>
<li style="text-align: center;color: gray;" class="likes"><%= article.likes %></li>
<li class="list"><a href="#comment" title="评论"><span class="glyphicon glyphicon-comment"></span></a></li>
<li class="list"><a href="javascript:;" title="收藏"><span class="glyphicon glyphicon-tasks"></span></a></li>
</ul>
</aside>
<h1 class="article-title"><%= article.title %><span class="article-pv glyphicon glyphicon-folder-open"> <%= article.pv %></span></h1>
<section class="content-section">
<%- article.content %>
</section>
</div>
<div class="comment padmar">
<% if(session.user){ %>
<div class="create-comment">
<form >
<textarea name="comment" id="comment" cols="30" rows="5"></textarea>
<input type="button" class="btn btn-success" value="发表评论">
</form>
</div>
<% } %>
<% if(commentPageLenght) { %>
<div class="comment-list">
<% commentPage.forEach(function(val){ %>
<div class="comment-item">
<h4><a href="/personal/<%= val['name'] %>"><img src="/images/<%= val['avator'] %>" alt="<%= val['name'] %>"><%= val['name'] %></a></h4>
<span><%= val['moment'] %></span>
<div class="comment-content">
<%= val['content'] %>
<% if(session.user == val['name']){ %>
<a href="javascript:deleteComment('/deleteComment/<%= article.id %>/<%= val['id'] %>',document.getElementsByClassName('deleteComment')[0])" class="deleteComment">删除</a>
<% } %>
</div>
</div>
<% }) %>
</div>
<div style="display:block;width:50%;margin:auto;margin-top: 30px;" class="pagination" id="page"></div>
<% }else{ %>
<p style="text-align:center;line-height:80px;fon-size:30px;color:gray">暂无任何评论</p>
<% } %>
</div>
<script src="/js/pagination.js"></script>
<script src="/js/mask.js"></script>
<script>
$(document).ready(function(){
var userName = "<%- session.user %>"
pagination({
selector: '#page',
totalPage: '<%= commentPageLenght %>',
currentPage: 1,
prev: '上一页',
next: '下一页',
first: true,
last: true,
showTotalPage:true,
count: 2//当前页前面显示的数量
},function(val){
// 当前页
var _comment = ''
$.ajax({
url: "/article/<%= article.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){
_comment +=
" <div class='comment-item'>" +
"<h4><a href='/personal/" + val.name + "'>" + "<img src='images/" + val.avator + "' alt=" + val.name + "'>" + val.name + "</a></h4>" +
"<span>" + val.moment + "</span>" +
"<div class='comment-content'>" ;
if(val.name == userName){
_comment += val.content + "<a href='javascript:deleteComment('/deleteComment/<%= article.id%>/'" + val.id +",this)' class='deleteComment'>" + "删除</a>" +
"</div>" +
"</div>"
}else{
_comment += val.content +
"</div>" +
"</div>"
}
})
console.log( _comment)
$('.comment-list').append(_comment)
}else{
alert('分页不存在')
}
}
})
})
})
//点赞
var postTaps = document.getElementsByClassName('comment-taps')[0];
var addHeart = postTaps.getElementsByTagName('li')[0],
gly = addHeart.getElementsByClassName('glyphicon')[0],
likes = postTaps.getElementsByClassName('likes')[0];
var collects = postTaps.getElementsByTagName('li')[3];
'<% if(session.user){ %>'
window.onload = function(){
console.log('<%- session.id%>')
'<% if(session.user != article.name){ %>'
var follow = document.getElementsByClassName('follow')[0];
if('<%= follow %>'=='<%= session.id%>'){ //已关注
follow.innerHTML = '已关注'
}
follow.onclick = function(){
if(follow.innerHTML==='关注做者'){
ajax('get','/follow/<%= article.uid %>?flag=1',null);
xhr.onreadystatechange = function () {
if(xhr.readyState==4&&xhr.status>=200&&xhr.status<300){
console.log(xhr.responseText)
if(xhr.responseText){
follow.innerHTML = '已关注';
console.log('ok')
}
}
}
}else{
ajax('get','/follow/<%= article.uid %>?flag=2',null);
xhr.onreadystatechange = function () {
if(xhr.readyState==4&&xhr.status>=200&&xhr.status<300){
console.log(xhr.responseText)
if(xhr.responseText){
follow.innerHTML = '关注做者'
console.log('ok2')
}
}
}
}
}
'<% } %>'
console.log('<%= likes %>')
if('<%= likes %>'==='<%= session.user %>'){ //已点赞
gly.setAttribute('class','glyphicon '+'glyphicon-heart');
}
}
addHeart.onclick = function () {
if(gly.getAttribute('class').indexOf('glyphicon-heart-empty')>-1){
ajax('get','/addHeart/<%= article.id %>?flag=1',null);
xhr.onreadystatechange = function () {
if(xhr.readyState==4&&xhr.status>=200&&xhr.status<300){
if(xhr.responseText){
gly.setAttribute('class','glyphicon '+'glyphicon-heart');
likes.innerHTML = parseInt(likes.innerHTML) + 1;
}
}
}
}else{ //取消赞
ajax('get','/addHeart/<%= article.id %>?flag=2',null);
console.log('<%= likes %>')
xhr.onreadystatechange = function () {
if(xhr.readyState==4&&xhr.status>=200&&xhr.status<300){
if(xhr.responseText){
gly.setAttribute('class','glyphicon '+'glyphicon-heart-empty');
likes.innerHTML = parseInt(likes.innerHTML) - 1;
}
}
}
}
}
//收藏
//取消收藏或收藏
let cotitle = collects.getElementsByTagName('a')[0];
if('<%= collects %>'=='<%= session.user %>'){
cotitle.setAttribute('title','取消收藏');
}
collects.onclick = function () {
if(cotitle.getAttribute('title')=='收藏'){
// cotitle.setAttribute('title','收藏');
ajax('get','/collects/<%= article.id %>?flag=1',null);
xhr.onreadystatechange = function () {
if(xhr.readyState==4&&xhr.status>=200&&xhr.status<300){
if(xhr.responseText){
alert('收藏成功');
cotitle.setAttribute('title','取消收藏');
}
}
}
}else{
ajax('get','/collects/<%= article.id %>?flag=2',null);
xhr.onreadystatechange = function(){
if(xhr.readyState==4&&xhr.status>=200&&xhr.status<300){
if(xhr.responseText){
alert('取消成功');
cotitle.setAttribute('title','收藏');
}
}
}
}
}
//评论
let comment = document.getElementsByClassName('create-comment')[0].getElementsByClassName('btn')[0];
let Allow = true;
comment.onclick = function(){
let comments = document.getElementById('comment');
if(comments.value==''){
comments.value = '请输入评论内容!';
setTimeout(()=>{
comments.value = '';
},500)
return 0;
}
if(!Allow) return 0;
Allow = false;
// + '&articleId=' + '<%= article.id %>'
ajax('post','/comment/<%= article.id %>',"comments=" + comments.value,'application/x-www-form-urlencoded');
xhr.onreadystatechange = function(){
if(xhr.readyState==4&xhr.status>=200&&xhr.status<300){
if(xhr.responseText){
mask('评论成功!');
setTimeout(()=>{
window.location.reload();
},1500)
Allow = true;
}
}
}
}
//删除评论
function deleteComment(url,item){
ajax('get',url,null);
console.log(item)
xhr.onreadystatechange = function(){
if(xhr.readyState==4&xhr.status>=200&&xhr.status<300){
if(xhr.responseText){
console.log('删除评论成功');
mask('删除成功!');
console.log(item)
console.log(item.parentNode)
item.parentNode.parentNode.outerHTML = '';
}
}
}
}
'<% }else{ %>'
addHeart.onclick = function(){
alert("请先登陆")
}
collects.onclick = function(){
alert("请先登陆")
}
'<% } %>'
</script>
</body>
</html>复制代码
文章详情页算是最复杂的一个页面了,涉及到了访问量,评论,点赞、收藏、关注用户等等功能,因此能够看到上面代码很长很长,优秀的小伙伴能够帮我整理整理这个模块啊哈哈。
虽然看起来内容不少,可是慢慢分析的话仍是不难的,一句话,有登陆就给权限,没登陆啥都不给,根据session来判断用户是否登陆以及是不是文章做者。还有根据后台传过来的信息判断用户是否已点赞或收藏文章:if('<%= follow %>'=='<%= session.id%>')
if('<%= likes %>'==='<%= session.user %>'){ //已点赞。
这里较好的逻辑应该是:在用户点击收藏或点赞按钮时请求后台判断用户是否已点赞或收藏,这样的话加载页面时后台就不用一会儿处理那么多逻辑了。然而为了方便,个人逻辑是在请求页面时将所有逻辑信息查出来一次性传给前端了。
这里的评论也采用了分页的形式,循环渲染评论列表时判断本条评论是否是该用户发表:
<% if(session.user == val['name']){ %>
<a href="javascript:deleteComment('/deleteComment/<%= article.id %>/<%= val['id'] %>',document.getElementsByClassName('deleteComment')[0])" class="deleteComment">删除</a>
<% } %>
复制代码
再看看咱们的后台代码:
/ 单篇文章页
router.get('/articledetail/:postId', async(ctx, next) => {
let comments,
article,
pageOne,
article_pv,
collects,
follow,
likes;
let postId = ctx.params.postId;
console.log(postId,'potid')
await userModel.findPostById(postId)
.then(result => {
article = result;
article_pv = parseInt(result[0]['pv'])
article_pv += 1
}).catch(err=>{
console.log(err);
ctx.body = false;
})
await userModel.updatePostPv([article_pv, postId])
await userModel.findCommentByPage(1,postId)
.then(result => {
commentPage = result
}).catch(err=>{
console.log(err);
ctx.body = false;
})
await userModel.findCommentById(postId)
.then(result => {
comments = result
console.log('comment', Math.ceil(comments.length/10))
}).catch(err=>{
console.log(err);
ctx.body = false;
})
if(ctx.session.user!=postId){
await userModel.findFollowByUserId([ctx.session.id,article[0]['uid']])
.then(result=>{
// console.log(result[0])
if(result[0]!=undefined){
// console.log(result[0])
follow = result[0]['uid'];
}else{
follow = null;
}
}).catch(err=>{
console.log(err)
})
}
await userModel.findLikeByPostId([ctx.session.user,article[0]['id']])
.then((result)=>{
if(result[0]!=undefined){
// console.log(result[0])//未解决
likes = result[0]['name']
}else{
likes = null;
}
})
await userModel.findCollectionByData([ctx.session.id,article[0]['id']])
.then((result)=>{
if(result[0]!=undefined){
// console.log(result[0])
collects = result[0]['name']
// console.log(collects)
}else{
collects = null;
}
})
await ctx.render('articledetail', {
session: ctx.session,
article: article[0],
likes: likes,
collects :collects,
follow,
commentLenght: comments.length,
commentPageLenght: Math.ceil(comments.length/10),
commentPage:commentPage
})
})
复制代码
首先是用户进入该页面,文章访问量加一,而后根据文章id查询文章的评论表,接着就是查询用户关注表、点赞表、收藏表了,最后将全部查询的结果经过渲染ejs模板传给前端。
下面是咱们文章详情页各个功能的后台代码:
//删除文章
router.get('/delete',async(ctx,next)=>{
let articleId = ctx.query.id;
console.log(articleId)
await userModel.deletePost(articleId)
.then(()=>{
ctx.redirect('/home');
console.log('删除成功')
}).catch(err=>{
console.log(err)
ctx.body = false;
})
})
//关注
router.get('/follow/:articleAuthor',async(ctx,next)=>{
console.log(ctx.params.articleAuthor)
let flag = ctx.query.flag,
fwid = ctx.params.articleAuthor;
if(flag==1){
await userModel.insertFollow([ctx.session.id,fwid])
.then(()=>{
console.log('关注成功');
ctx.body = true;
}).catch(err=>{
console.log(err);
ctx.body = false;
})
}else{
await userModel.deleteFollow([ctx.session.id,fwid])
.then(()=>{
console.log('取消成功')
ctx.body = 1;
}).catch(err=>{
console.log(err);
ctx.body = 0;
})
}
})
//点赞
router.get('/addHeart/:articleId',async(ctx,next)=>{
console.log(ctx.query.flag)
let flag = ctx.query.flag,
likes,
likeId,
articleId = ctx.params.articleId;
await userModel.findPostById(ctx.params.articleId)
.then((result)=>{
likes = parseInt(result[0]['likes']);
})
if(flag==1){
likes += 1;
await userModel.insertLikes([ctx.session.user,articleId])
.then(()=>{
console.log('点赞OK');
}).catch((err)=>{
console.log(err);
})
await userModel.updatePostLike([likes,articleId])
.then(()=>{
ctx.body = true;
console.log('点同意功')
}).catch((err)=>{
console.log(err)
ctx.body = false;
})
}else if(flag==2){ //取消赞
await userModel.findLikeByPostId([ctx.session.user,articleId])
.then((result)=>{
likeId = result[0]['id'];
}).catch(err=>{
console.log(err);
});
await userModel.poseLikes(likeId)
.then(()=>{
console.log('取消同意功');
}).catch((err)=>{
console.log(err);
})
likes -= 1;
await userModel.updatePostLike([likes,articleId])
.then(()=>{
ctx.body = true;
console.log('取消赞了')
}).catch((err)=>{
console.log(err)
ctx.body = false;
})
}
})
//收藏文章、取消收藏
router.get('/collects/:articleId',async(ctx,next)=>{
let flag = ctx.query.flag,
articleId = ctx.params.articleId,
collects,
collectId;
await userModel.findPostById(articleId)
.then((result)=>{
collects = result[0]['collection'];
}).catch(err=>{
console.log(err)
})
if(flag==1){
await userModel.insertCollection([ctx.session.id,articleId])
.then(()=>{
console.log('收藏成功1')
}).catch((err)=>{
console.log(err)
})
collects++;
await userModel.updatePostCollection([collects,articleId])
.then(()=>{
console.log('收藏成功')
ctx.body = true;
}).catch(err=>{
console.log(err)
ctx.body = false;
})
}else{
await userModel.findCollectionByNaId([ctx.session.id,articleId])
.then(result=>{
collectId = result[0]['id'];
}).catch(err=>{
console.log(err)
})
await userModel.deleteCollection(collectId)
.then(()=>{
console.log('取消成功2')
}).catch(err=>{
console.log(err)
})
collects--;
await userModel.updatePostCollection([collects,articleId])
.then(()=>{
console.log('取消成功3')
ctx.body = true;
}).catch(err=>{
console.log(err);
})
}
})
//评论
router.post('/comment/:articleId', async(ctx,next)=>{
console.log('test')
console.log(ctx.request.body.comments)
let articleId = ctx.params.articleId,
content = ctx.request.body.comments,
name = ctx.session.user,
avator = ctx.session.avator;
// moment = moment().format('YYYY-MM-DD HH:mm');
let comments = 0;
await userModel.insertComment([name,content,moment().format('YYYY-MM-DD HH:mm'),articleId,avator])
.then(result=>{
console.log(result[0]);
}).catch(err=>{
console.log(err);
});
await userModel.findPostById(articleId)
.then(result=>{
// console.log(result[0]);
console.log(result[0]['comments'])
comments = parseInt(result[0]['comments']) + 1;
}).catch(err=>{
console.log(err);
});
await userModel.updatePostComment([comments,articleId])
.then(result=>{
console.log(result);
ctx.body = true;
}).catch(err=>{
console.log(err);
ctx.body = false;
});
})
//评论分页
router.post('/article/:articleId/commentPage', async(ctx,next)=>{
let articleId = parseInt(ctx.params.articleId),
page = parseInt(ctx.request.body.page);
console.log(articleId,page)
await userModel.findCommentByPage(page,articleId)
.then(result=>{
ctx.body = result;
console.log(result);
}).catch(err=>{
ctx.body = 'error';
console.log(err);
})
})
//删除评论
router.get('/deleteComment/:articleId/:commentId', async(ctx,next)=>{
let commentId = ctx.params.commentId;
let articleId = ctx.params.articleId,
comment = 0;
await userModel.deleteComment(commentId)
.then(result=>{
console.log(result);//需更新文章评论数
}).catch(err=>{
console.log(err);
ctx.body = false;
})
await userModel.findPostById(articleId)
.then(result=>{
console.log(result[0]['comments']);
comment = parseInt(result[0]['comments']) -1;
}).catch(err=>{
console.log(err);
})
await userModel.updatePostComment([comment,articleId])
.then(result=>{
console.log(result);
ctx.body = true;
}).catch(err=>{
console.log(err);
ctx.body = false;
})
})
复制代码
因为学业繁忙,后续内容持续更新。。。