文章管理系统 – Express学习
仓库:https://gitee.com/aeipyuan/articles_manage
1.项目搭建
生成express
项目
express -e article_managemet 创建项目 -e 表示使用ejs模板引擎
mongodb
创建数据库
mongo - 开启mongodb
use articles_db - 创建/使用数据库
db.createCollection('users') - 创建users集合
db.createCollection('articles') - 创建article集合
show collections - 查询是否创建成功
测试:
db.users.insertOne({username:'admin',passsword:'000'})
db.users.find()
创建连接mongodb
连接的模块
文件位置:model > index.js
const MongoClient = require('mongodb').MongoClient;
/* 连接数据库的url,在mongo命令下可以找到 */
let url = 'mongodb://localhost:27017';
/* 连接的数据库名字 */
let dbName = 'articles_db';
/* 封装数据库连接方法 */
function connect(callback) {
MongoClient.connect(url, (err, client) => {
if (err) {
console.log('数据库连接错误!', err);
} else {
/* 根据数据库名获取数据库返回给callback处理 */
let db = client.db(dbName);
callback && callback(db);
client.close();
}
})
}
module.exports = { connect };
/* 测试 */
connect(db => {
db.collection('users').findOne({ username: 'admin' }, (err, docs) => {
if (err)
console.log(err)
else
console.log(docs);
});
});//{ _id: 5ea442dbbc0dfbff14afd728, username: 'admin', passsword: '000'
2.注册页
注册路由
//位置:routes > index.js
router.get('/regist', (req, res, next) => {
res.render('regist');
})
页面结构
<div class='form-box'>
<form action="/users/regist" method="post">
<input type="text" name="username" placeholder="请输入用户名">
<input type="password" name="password" placeholder="请输入密码">
<input type="password" name="password2" placeholder="请确认密码">
<input type="submit" value="注册">
</form>
<div>已有帐户,<a href="/login">立即登录</a></div>
</div>
提交处理
//位置 routes > user.js
router.post('/regist', (req, res, next) => {
/* 取出提交数据 */
let { username, password, password2 } = req.body;
let data = { username, password, password2 };
/* 校验数据 */
if (password !== password2) {
console.log("两次输入的密码不一致");
res.redirect('/regist');
} else {
model.connect(db => {/* 写入数据库 */
db.collection('users').insertOne(data, (err, docs) => {
if (err) {
console.log('注册失败');
res.redirect('/regist');//返回注册页
} else {
console.log('注册成功');
res.redirect('/login');//返回登陆页
}
})
})
}
})
3.登录页
页面结构
<div class="form-box">
<form action="/users/login" method="post">
<input type="text" name="username" placeholder="请输入用户名">
<input type="password" name="password" placeholder="请输入密码">
<input type="submit" value="登录">
</form>
<div>没有帐号,<a href="/regist">立即注册</a>!</div>
</div>
提交处理
(1)安装express-session
拦截登录状态
安装
npm i express-session -S
/* -------------app.js配置------------- */
/* 配置session */
app.use(session({
secret: 'qf project',
resave: false,
saveUninitialized: true,
cookie: { maxAge: 1000 * 60 * 10 } // 指定登录会话的有效时长
}))
/* 拦截登陆状态 */
app.get('*', (req, res, next) => {
let username = req.session.username;
let path = req.path;
console.log("session-" + username);
if (path != '/login' && path != '/regist') {
if (!username)
res.redirect('/login');
}
next();
})
(2)从数据库查询信息并存入session
/* 登录提交 */
router.post('/login', (req, res, next) => {
let data = {
username: req.body.username,
password: req.body.password
}
/* 连接数据库 */
model.connect(db => {
db.collection('users').findOne(data, (err, docs) => {
if (err) {
console.log(err);
res.redirect('/login');
} else {
req.session.username = data.username;
res.redirect('/');
}
})
})
})
4. 通用顶部组件
结构
<!-- bar.ejs -->
<div class='bar'>
<span><%- username %></span>
<a href='/write'>写文章</a>
<a href='/users/logout'>退出</a>
<a href='/' class='home'>
<img src="/images/home.png" alt="首页">
</a>
</div>
处理退出请求
router.get('/logout', (req, res, next) => {
req.session.username = null;
res.redirect('/login');
})
5. 写文章
页面结构
<%- include('bar',{username:username}) %>
<form class="article" action="/article/add" method="post">
<input type="text" name="title" placeholder="请输入文章标题">
<textarea name="content" class="xheditor" cols="30" rows="10"></textarea>
<input type="submit" value="发布">
</form>
<!-- xheditor富文本编辑器 -->
<script type="text/javascript" src="/xheditor/jquery/jquery-1.4.4.min.js"></script>
<script type="text/javascript" src="/xheditor/xheditor-1.2.2.min.js"></script>
<script type="text/javascript" src="/xheditor/xheditor_lang/zh-cn.js"></script>
<script>
$('.xheditor').xheditor({
tools: 'full',
skin: 'default',
upImgUrl: '/article/upload',
html5Upload: false,
upMultiple: 1
});
</script>
数据提交
(1)创建article
路由处理文件
/* app.js配置 */
var articleRouter=require('./routes/article');
app.use('/article', articleRouter);
(2)处理提交请求
/* routes > article.js */
router.post('/add', (req, res, next) => {
/* 获取数据 */
let username = req.session.username;
let data = {
title: req.body.title,
content: req.body.content,
id: Date.now(),
username: username
}
/* 写入数据库 */
model.connect(db => {
db.collection('articles').insertOne(data, (err, docs) => {
if (err) {
console.log('提交失败', err);
res.redirect('/write');
} else {
console.log('提交成功');
res.redirect('/');
}
})
})
})
6. 首页
页面结构
<%- include('bar',{username}) %>
<div class='list'>
<% data.list.map((item,index)=>{ %>
<div class='row'>
<span><%- index+1 %></span><!-- 序号 -->
<span><%- item.username %></span><!-- 作者 -->
<span>
<a href="/detail?id=<%- item.id %>&pageIndex=<%- data.pageIndex %> "> <%- item.title %></a><!-- 标题 -->
</span>
<span><%- item.time %></span><!-- 时间 -->
<div>
<a href=" /write?id=<%- item.id %>&pageIndex=<%- data.pageIndex %>">编辑</a>
<a href="/article/delete?id=<%- item.id %>&pageIndex=<%- data.pageIndex %>">删除</a>
</div>
</div>
<% }) %>
<!-- 显示页码 -->
<div class="pages">
<% for(let i=1;i<=data.total;i++){ %>
<a href="/?pageIndex=<%- i %>"><%- i %></a><!-- 根据页码重新请求数据 -->
<% } %>
</div>
</div>
<!--
data数据结构:
{
total: 1,
pageIndex: 1,
list: [{
_id: 5ea5538ad8c2e22d908155eb,
title: '1dxsxx',
content: 'dccfwvfr',
id: 1587893130840,
username: 'a',
time: '2020-04-26 05:25:30'
}]
} -->
数据请求实现分页
通过url
将当前页码pageIndex
传入,然后第一次查询总的数量处理pageSize
获得总页数total
,第二次查询添加限制条件获取页面需要显示的数据列表,针对不是第一页时页面数据为空的问题,将pageIndex-1
重新加载
/* http://localhost:8888/?pageIndex=1 */
router.get('/', async (req, res, next) => {
/* 页面数据 */
let pageIndex = req.query.pageIndex || 1;
let pageInfo = {
total: 0,/* 总共页数 */
pageIndex,/* 当前页 */
list: []/* 页面数据 */
}
let pageSize = 3;
model.connect(db => {/* 查询所有数据 */
db.collection('articles').find().toArray((err, docs) => {
if (err) {
console.log('首页数据查询错误');
res.render('index', { username, pageInfo });
} else {
pageInfo.total = Math.ceil(docs.length / pageSize);//获取总的页面数
model.connect(db => {/* 限制条件查询 */
db.collection('articles').find()/* 查找所有 */
.sort({ id: -1 })/* 按时间倒序 */
.limit(pageSize)/* 限制数量 */
.skip((pageIndex - 1) * pageSize)/* 跳过数量 */
.toArray((err2, list) => {
if (err2) {
console.log('首页数据查询错误', err2);
} else {/* 除第一页若页面数据条数为0则请求前一页 */
if (pageIndex != 1 && !list.length) {
res.redirect('/?pageIndex=' + (pageIndex - 1));
} else {
/* 格式化时间 */
list.forEach(v => v.time = moment(v.id).format('YYYY-MM-DD hh:mm:ss'))
pageInfo.list = list;
}
/* 将数据传给页面 */
res.render('index', {
username: req.session.username,
data: pageInfo
});
}
})
})
}
})
})
})
7.文章详情
页面结构
<!-- index.ejs入口 -->
<a href="/detail?id=<%- item.id %>&pageIndex=<%- data.pageIndex %> ">
<%- item.title %>
</a><!-- 标题 -->
<!-- detail.ejs -->
<%- include('bar',{username}) %>
<div class='detail'>
<div class='title'><%- data.title %></div>
<div class="desc"><span>作者:<%- data.title %></span> <span> 发布时间:<%- data.time %></span></div>
<div class="content"><%- data.content %></div>
</div>
<!--
data格式:
{
_id: 5ea5538ad8c2e22d908155eb,
title: '1dxsxx',
content: 'dccfwvfr',
id: 1587893130840,
username: 'a',
time: '2020-04-26 05:25:30'
}
-->
数据处理
/* 文章详情 http://localhost:8888/detail?id=1587900351782&pageIndex=1 */
router.get('/detail', (req, res, next) => {
let id = parseInt(req.query.id);
let pageIndex = req.query.pageIndex;
model.connect(db => {
db.collection('articles').findOne({ id }, (err, docs) => {
if (err) {
console.log('详情获取失败' + err);
res.redirect('/?=' + pageIndex);//返回主页
} else {
/* 时间格式化 */
docs.time = moment(docs.id).format('YYYY-MM-DD hh:mm:ss');
console.log(docs)
res.render('detail', {
username: req.session.username,
data: docs
})
}
})
})
})
8.文章编辑
页面结构
编辑与添加文章结构相同,区别在于数据id
和pageIndex
记录,修改write
页结构即可
<!-- index.ejs的入口 -->
<a href=" /write?id=<%- item.id %>&pageIndex=<%- data.pageIndex %>">编辑</a>
<!-- write.ejs -->
<form class="article" action="/article/add" method="post">
<!-- 传递参数使用hidden -->
<input type="hidden" name="id" value="<%- data.id %>">
<input type="hidden" name="pageIndex" value="<%- data.pageIndex %>">
<input type="text" name="title" placeholder="请输入文章标题" value="<%- data.title %>">
<textarea class="xheditor" name="content" cols="30" rows="10"><%- data.content %></textarea>
<input type="submit" value="<%- data.id?'修改':'发布' %>">
</form>
<!-- data数据结构
{
id: 1587893123696,
title: 'qqqqq',
content: 'qcdxcdxcxzczx',
pageIndex: '1'
}
-->
数据处理
1.进入write
页面数据获取
写文章进入write
页时没有id
和pageIndex
传入,修改文章时可以通过id
查询到文章title
和content
,通过pageIndex
可以标识修改完成后要返回的页面
/* 文章 http://localhost:8888/write?id=1587900351782&pageIndex=1*/
router.get('/write', (req, res, next) => {
/* 获取数据 */
let id = parseInt(req.query.id) || null;
let pageIndex = req.query.pageIndex || 1;
let data = {
id,
title: '',
content: '',
pageIndex
}
/* 查询数据 */
model.connect(db => {
db.collection('articles').findOne({ id: id }, (err, docs) => {
if (err) {
console.log('获取文章数据失败', err);
res.redirect('/?pageIndex=' + pageIndex);
} else {
if (docs) {/* 查询结果为空是新增文章 */
data.title = docs.title;
data.content = docs.content;
}
res.render('write', {
username: req.session.username,
data
})
}
})
})
})
2.点击修改按钮更新数据
通过是否含有id
来判断是更新操作还是插入操作,操作失败返回当前页,成功则返回主页
/* 文章发布/修改 req.body => id pageIndex title content*/
router.post('/add', (req, res, next) => {
/* 获取数据 */
let username = req.session.username;
let id = parseInt(req.body.id);
let pageIndex = req.body.pageIndex;
let data = {
title: req.body.title,
content: req.body.content,
id: id || Date.now(),/* 修改是id,添加是Date.now() */
username: username,
}
model.connect(db => {
if (id) {/* 修改 */
db.collection('articles').updateOne({ id }, { $set: data }, (err, docs) => {
if (err) {
console.log('修改失败', err);
res.redirect(`/write?id=${id}&pageIndex=${pageIndex}`);/* 重新操作 */
} else {
res.redirect(`/?pageIndex=${pageIndex}`);//回主页
}
})
} else {/* 添加 */
db.collection('articles').insertOne(data, (err, docs) => {
if (err) {
console.log('发布失败', err);
res.redirect('/write');/* 重新操作 */
} else {
console.log('发布成功');
res.redirect('/');/* 回主页 */
}
})
}
})
})
9.删除文章
<a href="/article/delete?id=<%- item.id %>&pageIndex=<%- data.pageIndex %>">删除</a>
在主页加入删除文章选项,id
用于找到指定文章,pageIndex
确定删除后返回的页面
/* 文章删除 http://localhost:8888/?id=1587874300950&pageIndex=1 */
router.get('/delete', (req, res, next) => {
/* 获取id和当前页码 */
let id = parseInt(req.query.id);
let pageIndex = req.query.pageIndex;
/* 删除数据 */
model.connect(db => {
db.collection('articles').deleteOne({ id }, (err, ret) => {
if (err) {
console.log('删除失败', err);
res.redirect(`/?pageIndex=${pageIndex}`);
} else {
console.log('删除成功', ret);
res.redirect(`/?pageIndex=${pageIndex}`);
}
})
})
})
10.实现图片上传
安装multiparty
插件解析请求
npm i multiparty -S
配置xheditor
富文本编辑器
$('.xheditor').xheditor({
tools: 'full',
skin: 'default',
upImgUrl: '/article/upload',/* 上传路由 */
html5Upload: false,
upMultiple: 1
});
配置上传路由
/* 图片上传 */
router.post('/upload', (req, res, next) => {
var form = new multiparty.Form();
form.parse(req, (err, fields, files) => {
if (err) {
console.log('上传失败', err);
} else {
let file = files.filedata[0];
/* 创建读写流 */
let rs = fs.createReadStream(file.path);
/* 存图片的路径为public下的/uploads/ */
let newPath = '/uploads/' + file.originalFilename;
let ws = fs.createWriteStream('./public' + newPath);
/* 边读边写 */
rs.pipe(ws);
ws.on('close', () => {
console.log('文件上传成功');
res.send({ err: '', msg: newPath });
})
}
})
})
/*
files {
filedata: [
{
fieldName: 'filedata',
originalFilename: 'Snipaste_2019-09-23_00-10-40.png',
path: 'C:\Users\14329\AppData\Local\Temp\qWOzXsQRXhVyqiO57W3qjMgC.png',
headers: [Object],
size: 146188
}
]
}
*/