赞
踩
##项目:博客搭建
1.博客内容展示
2.博客管理功能
建立项目所需文件夹
public 静态资源
model 数据库操作
route 路由
views 模板
初始化项目描述文件:npm init -y
下载所需第三方模块:npm install express mongoose art-template express-art-template
创建网站服务器
const express = require('express');
const app = express();
app.listen(80);
console.log('网站服务器启动成功,请访问localhost');
构建模块化路由
app.js
const home = require('./route/home');
const admin = require('./route/admin');
//拦截客户端请求,如果输入的是/home,去找home这个路由,/home是自定义的
app.use('/home',home);
app.use('/admin',admin);
构建博客管理页面模板
const path = require('path');
// 开放静态资源
app.use(express.static(path.join(__dirname,'public')));
//把配置模板
app.set('views',path.join(__dirname,'views'));
app.set('view engine','art');
app.engine('art',require('express-art-template'));
地址栏输入:http://localhost/admin/login
看效果。首先我们设置的是app.use(’/admin’,admin);
,所以路由第一步去找admin.art
,
//**admin.art**
const express = require('express');
//路由对象
const admin = express.Router();
admin.get('/login',(req,res)=>{
// res.send('欢迎来到博客登录页');
res.render('admin/login');
});
module.exports = admin;
第二步内部会渲染 模板文件login.art:res.render(‘admin/login’);
效果图
app.use(’/abc’,admin);
http://localhost/abc/login
总结: app.use(’/xxx’,admin)
是动态的,导致外链资源css也是动态的,即http://localhost/xxx/css/base
,所以在模板文件中要以绝对路径代替相对路径,前面加/代表绝对路径。xxx代表引导路由。外链正确格式写法: <linkrel=“stylesheet” href="/xxx/css/base.css">
,比如 <linkrel=“stylesheet” href="/abc/css/base.css">
抽离后的模板文件之一:user.art
,以此类推
1.创建用户集合,初始化用户
//connect.js
// 引入mongoose第三方模块
const mongoose = require('mongoose');
// 连接数据库
mongoose.connect('mongodb://localhost/blog', {useNewUrlParser: true })
.then(() => console.log('数据库连接成功'))
.catch(() => console.log('数据库连接失败'))
//user.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
minlength: 2,
maxlength: 20
},
email: {
type: String,
// 保证邮箱地址在插入数据库时不重复
unique: true,
required: true
},
password: {
type: String,
required: true
},
// admin 超级管理员
// normal 普通用户
role: {
type: String,
required: true
},
// 0 启用状态
// 1 禁用状态
state: {
type: Number,
default: 0
}
});
// 创建集合
const User = mongoose.model('User', userSchema);
//user.js
const user = await User.create({
username: 'iteheima',
email: 'itheima@itcast.cn',
password: pass,
role: 'admin',
state: 0
});
// 将用户集合做为模块成员进行导出
module.exports = {
User
}
//app.js
//数据库连接
...
require('./model/connect');
require('./model/user');
...
为登陆表单设置请求地址,请求方式和表单项name属性
当用户点击登录按钮时,客户端验证用户是否填写了登录表单
如果其中一项没有输入,阻止表单提交
//新建common.js
function serializeToJson(form){
var result ={};
var f = form.serializeArray();
f.forEach(function(item){
result[item.name]= item.value;
});
return result;}
//login.art
...
//注意提交地址写法/admin/login
<form action="/admin/login" method="post">
<div class="form-group">
<label>邮件</label>
<input name="email" type="email" class="form-control" placeholder="请输入邮件地址">
</div>
<div class="form-group">
<label>密码</label>
<input name="password" type="password" class="form-control" placeholder="请输入密码">
</div>
<button type="submit" class="btn btn-primary">登录</button>
</form>
...
<script src="/admin/js/common.js"></script>
<script>
$('#loginForm').on('submit',function(){
var result = serializeToJson($(this))
/* console.log(result);
{email: "1234353@admin.com", password: "yqs2891682"}
*/
if(result.email.trim().length==0){
alert('请输入邮件地址');
//阻止程序向下执行
return false;
}else if(result.email.trim().length==0){
alert('请输入密码')
//阻止程序向下执行
return false;
}
});
</script>
服务端接收请求参数,再次验证用户是否填写了登录表单,因为客户端可以禁止js验证
如果其中一项没有输入,客户端做出响应,阻止程序向下执行
//admin.js添加登陆功能路由
const express = require('express');
const admin = express.Router();
admin.get('/login',(req,res)=>{
res.render('admin/login');
});
//实现登陆功能
admin.post('/login',(req,res)=>{
//{"email":"123456@qq.com","password":"yqs2891682"}
const {email,password}=req.body;
if(email.trim().length==0||password.trim().length==0){
//res.end状态码默认200,客户端请求格式错误状态码一般设置成400
return res.status(400).send('<h4>邮件地址或者密码错误</h4>')
}
})
admin.get('/user',(req,res)=>{
res.render('admin/user');
})
module.exports = admin;
//app.js
...
const bodyPaser= require('body-parser');
app.use(bodyPaser.urlencoded({extended:false}));
...
根据邮箱地址查询用户数据库信息
如果用户不存在,为客户端做出响应,阻止程序向下执行
如果用户存在,将用户名和密码进行比对
比对成功,用户登录成功
比对失败,用户登录失败
//app.js
//导入用户集合构造函数
...
const { User }=require('../model/user');
admin.post('/login',async(req,res)=>{
....
let user = await User.findOne({email});
if(user){
if(password==user.password){
res.send('登陆成功');
}else{
return res.status(400).send('<h4>邮件地址或者密码错误</h4>')
}else{
return res.status(400).send('<h4>邮件地址或者密码错误</h4>')
}})
...
//导入bcrypt模块
const bcypt = require('bcrypt');
//生成随机字符串gen=>generate生成salt盐,数字越大越复杂,默认值就好
let salt = await bcrypt.genSalt(10);
//使用随机字符串对密码进行加密
let pass = await bcrypt.hash('明文密码',salt);
//密码比对
let isEqual=await bcrypt.compare('明文密码','加密密码');
//改动一下user.js
const User = mongoose.model('User',userSchema);
async function createUser(){
const salt = await bcrypt.genSalt(10);
const password = await bcrypt.hash('123456',salt);
const user1 = await User.create({
username:'yan',
email:'123456@qq.com',
password:password,
role:'admin',
state:0
});
};
createUser();
//改动一下admin.js
let user = await User.findOne({email});
if(user){
let isValid = await bcrypt.compare(password,user.password);
if(isValid){
res.send('登陆成功');
}else{
res.status(400).render('admin/error',{message:'邮件地址或者密码错误'});
}
}
cookie中的数据是以域名的形式进行区分的;
在node.js中需要借助express-session实现session功能。
//app.js
const session = require('express-session');
app.use(session({secret:'secret key'}));//secret固定的,secret key是自定义的
//admin.js
if(isValid){
//第一个username是req的自定义属性,值是查询出的用户名
req.session.username= user.username;
//重定向用户列表,用express自带的方法redirect
res.redirect('/admin/user');
}
//创建用户列表路由
admin.get('/user',(req,res)=>{
res.render('admin/user',{
msg:req.session.username
});
})
//user.art
...
<div class="title">
<h4>用户{{msg?msg:'用户名不存在'}}</h4>
<span>找到1个用户</span>
<a href="user-edit.html" class="btn btn-primary new">新增用户</a>
</div>
...
问题1:头部模块是公共区域,跳转到其他页面,每个路由跳转都要重复一遍render代码
解决:把一些公共数据暴露到模板中。在app对象下面有个locals对象,把数据放到这个对象里面,在模板中就可以直接拿到,不需要render了
//...
if(isValid){
//第一个username是req的自定义属性,值是查询出的用户名
req.session.username= user.username;
//app对象无需引入,在req对象下面有个app属性,实际就是app.js的app对象
req.app.locals.userInfo123=user;
//重定向用户列表,用express自带的方法redirect
res.redirect('/admin/user');
}
// ...
//创建用户列表路由
admin.get('/user',(req,res)=>{
res.render('admin/user');
})
//user.art
...
<div class="title">
<h4>用户{{userInfo123&&userInfo123.username}}</h4>
<span>找到1个用户</span>
<a href="user-edit.html" class="btn btn-primary new">新增用户</a>
</div>
...
问题2:没有登录依然可以输入localhost/admin/user,却能跳过登录验证直接访问到用户界面
解决:在跳转路由前拦截请求,判断用户登录状态,也就是请求地址必须是’/dmin/login’,并且session保存有用户名
//写在前面,第一个参数也可是表示以/admin开头的请求
app.use('/admin',(req,res,next)=>{
if (req.url !='/login' && !req.session.username) {
res.redirect('/admin/login');
} else {
next();
}
});
//拦截客户端请求,如果是home,去找home这个路由
app.use('/home',home);
app.use('/admin',admin);
问题3:优化代码,功能代码放到另外一个文件
根目录下新建文件夹middleware,再新建loginGuard.js文件
//app.js
app.use('/admin',require('./middleware/loginGuard'));
//loginGuard.js
const guard = (req,res,next)=>{
//如果用户访问的是除登录页之外的页面并且
if (req.url !='/login' && !req.session.username) {
res.redirect('/admin/login');
} else {
next();
}
}
module.exports = guard;
route下新建文件夹admin,把admin.js的功能代码抽离到这里
//admin.js
//渲染登录页面
admin.get('/login',require('./admin/loginPage'));
//实现登陆功能
admin.post('/login',require('./admin/login'));
//创建用户列表路由
admin.get('/user',require('./admin/userPage'));
//实现退出功能
admin.get('/logout',require('./admin/logout'));
为用户列表页面的新增用户按钮添加链接
<!-- 分类标题 -->
<div class="title">
<a href="/admin/user-edit" class="btn btn-primary new">新增用户</a>
</div>
<!-- /分类标题 -->
添加一个连接对应的路由,在路由处理函数中渲染新增用户模板
//admin.js
//创建用户编辑页面路由,渲染出编辑页面功能
admin.get('/user-edit',require('./admin/user-edit'));
新建user-edit.js
module.exports=(req,res)=>{
res.render('admin/user-edit');
}
为新增用户表单指定请求地址、请求方式、为表单项添加name属性
<form class="form-container" action="/admin/user-edit" method="post">
<div class="form-group">
<label>用户名</label>
<input name="username" type="text" class="form-control" placeholder="请输入用户名">
</div>
<div class="form-group">
<label>邮箱</label>
<input name="email" type="email" class="form-control" placeholder="请输入邮箱地址">
</div>
<div class="form-group">
<label>密码</label>
<input name="password" type="password" class="form-control" placeholder="请输入密码">
</div>
<div class="form-group">
<label>角色</label>
<select class="form-control" name="role">
<option value="normal">普通用户</option>
<option value="admin">超级管理员</option>
</select>
</div>
<div class="form-group">
<label>状态</label>
<select name="state" class="form-control">
<option value="0">启用</option>
<option value="1">禁用</option>
</select>
</div>
<div class="buttons">
<input type="submit" class="btn btn-primary">
</div>
</form>
增加实现添加用户的功能路由
admin.js
//创建实现用户添加功能路由,对页面进行验证功能
...
admin.post('/user-edit',require('./admin/user-edit-fn'));
...
新建user-edit-fn.js
接收到客户端传递过来的请求参数:req.body
对请求参数的格式进行验证:Joi.validate(req.body,schema);
user-edit-fn.js
//引入joi模块
const Joi = require('joi');
module.exports= async (req,res)=>{
//定义对象的验证规则
const schema = {
username:Joi.string().min(2).max(12).required().error(new Error('用户名不符合')),
password: Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/).required().error(new Error('密码格式不符合')),
email: Joi.string().email().required().error(new Error('邮箱格式不符合')),
role:Joi.string().valid('normal','admin').required().error(new Error('角色值非法')),
state:Joi.number().valid(0,1).required().error(new Error('状态值非法'))
};
try {
await Joi.validate(req.body,schema);
} catch (e) {
res.redirect(`/admin/user-edit?message=${e.message}`)
}
res.send(req.body);
}
验证当前要注册的邮箱地址是否已经注册过
let user = await User.findOne({email:req.body.email});
if (user) {
//1重定向响应后还做了res.end()这个事情;2重定向后代码就应该return终止了,不然后面继续res.end()会冲突报错
return res.redirect(`/admin/user-edit?message=邮箱地址被占用`)
}
res.send(user);
对密码进行加密处理
将用户信息添加到数据库中
重定向页面到用户列表页面
优化代码:
1.验证用户处理抽离到user.js,
2.格式验证错误和邮箱账号重复错误处理方式抽离到app.js
//app.js
//错误处理中间件
app.use((err,req,res,next)=>{
//将字符串类型转换为对象啊
const result = JSON.parse(err);
res.redirect(`${result.path}?message=${result.message}`);
})
//user.js
//....
//验证用户信息
const Joi = require('joi');
const validateUser=(user)=>{
//定义对象的验证规则
const schema = {
username:Joi.string().min(2).max(12).required().error(new Error('用户名不符合')),
password: Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/).required().error(new Error('密码格式不符合')),
email: Joi.string().email().required().error(new Error('邮箱格式不符合')),
role:Joi.string().valid('normal','admin').required().error(new Error('角色值非法')),
state:Joi.number().valid(0,1).required().error(new Error('状态值非法'))
};
//实施验证
return Joi.validate(user,schema);
}
//user-edit-fn.js
const { User,validateUser } = require('../../model/user');
//引入加密模块
const bcrypt = require('bcrypt');
module.exports= async (req,res,next)=>{
try {
await validateUser(req.body);
} catch (e) {
//return res.redirect(`/admin/user-edit?message=${e.message}`);
//next()只接受字符串;加return停止向下运行;next调用后触发错误处理中间件
return next(JSON.stringify({path:'/admin/user-edit',message:e.message}));
}
let user = await User.findOne({email:req.body.email});
if (user) {
//1重定向响应后还做了res.end()这个事情;2重定向后代码就应该return终止了,不然后面继续res.end()会冲突报错
//return res.redirect(`/admin/user-edit?message=邮箱地址被占用`);
return next(JSON.stringify({path:'/admin/user-edit',message:'邮箱地址被占用'}))
}
//对密码加密处理
const salt = await bcrypt.genSalt(10);
const password = await bcrypt.hash(req.body.password,salt);
//替换
req.body.password = password;
//添加数据库
await User.create(req.body);
res.redirect('/admin/user');
}
当访问用户列表页面时,现在用户列表对应路由处理函数中,将所有用户信息从数据库中查询出来,完成后用res.render()方法渲染用户模板,并且将查询出的用户数据传递到模板中展示出来。
//userPage.js
//1.引入用户集合函数
const {User} = require('../../model/user');
module.exports=async(req,res)=>{
//将全部用户信息从数据库查询出来
let users = await User.find({});
//渲染模板
res.render('admin/user',{
users:users
});
}
模板展示
<!-- 内容列表 -->
<table>
<tbody>
{{each users}}
<tr>
<td>{{@$value._id}}</td>
<td>{{$value.username}}</td>
<td>{{$value.email}}</td>
<td>{{$value.role=='admin'?'超级管理员':'普通用户'}}</td>
<td>{{$value.state==0?'启用':'禁用'}}</td>
<td>
<a href="user-edit.html" class="glyphicon glyphicon-edit"></a>
<i class="glyphicon glyphicon-remove" data-toggle="modal" data-target=".confirm-modal"></i>
</td>
</tr>
{{/each}}
</tbody>
</table>
<!-- /内容列表 -->
数据分页
分页功能核心要素:
userPage.js
const {User} = require('../../model/user');
module.exports=async(req,res)=>{
//接受客户端传递过来的当前页参数
let page = req.query.page||1;
//每一页显示的数据条数
let pagesize =3;
//查询用户数据的总数的集合方法
let count =await User.countDocuments({});
//总页数
let total = Math.ceil(count/pagesize);
//页码对应的数据查询开始位置
let start = (page-1)*pagesize;
//将用户信息从数据库查询出来
let users = await User.find({}).limit(pagesize).skip(start);
res.render('admin/user',{
users:users,
page:page,
total:total
});
}
user.art
<!-- 分页 -->
<ul class="pagination">
<li style="display:<%= page-1<1?'none':'inline'%>">
<a href="/admin/user?page=<%=page-1 %>">
<span>«</span>
</a>
</li>
<% for(var i =1;i<=total;i++){%>
<li><a href="/admin/user?page=<%=i %>">{{i}}</a></li>
<% } %>
<li style="display:<%= page-0+1>total?'none':'inline'%>">
<a href="/admin/user?page=<%=page-0+1 %>">//减0是为了隐式转换
<span>»</span>
</a>
</li>
</ul>
<!-- /分页 -->
对列表用户进行编辑,页面是跟添加用户一样,那怎么区分呢?
1.如果是添加操作,直接跳转;跳转地址:http://localhost/admin/user-edit
2.如果是修改操作,跳转时候将当前用户的id通过get方式传递,这样在跳转到用户编辑页面的时候,可以根据get参数中是否有id参数来区分
当前是添加还是编辑。修改地址:http://localhost/admin/user-edit?id=5ffec57c28
一.用户列表页面user.art中,编辑按钮的请求地址后加上id,跳转:user-edit.art
<!-- 内容列表 -->
<table>
<tbody>
{{each users}}
<tr>
<td>
<a href="/admin/user-edit?id={{@$value._id}}" class="glyphicon glyphicon-edit"></a>
<i class="glyphicon glyphicon-remove" data-toggle="modal" data-target=".confirm-modal"></i>
</td>
</tr>
{{/each}}
</tbody>
</table>
<!-- /内容列表 -->
<!-- 分页 -->
<!-- /分页 -->
二.根据是否有带id判断进入的是不是用户编辑页面:user-edit.art
//引入User
const {User} = require('../../model/user');
module.exports=async (req,res)=>{
//获取到地址栏中的id参数
const {message,id}=req.query;
//如果当前传递了id参数就是修改操作
if (id) {
//修改操作
let user = await User.findOne({_id:id});
//渲染用户编辑页面(修改)
res.render('admin/user-edit',{
message:message,
user:user,
link:'/admin/user-add',
button:'修改'
});
} else {
//增加操作
res.render('admin/user-edit',{
message:message,
link:'/admin/user-edit',
button:'添加'
});
}
}
三.给模板添加参数,注意因为是增加和修改都是这个页面,而增加操作未带user参数,所以先判断是否存在,即user&&user.username
<form class="form-container" action="{{link}}" method="post">
<div class="form-group">
<label>用户名</label>
<input name="username" type="text" class="form-control" placeholder="请输入用户名" value="{{user&&user.username}}">
</div>
<div class="form-group">
<label>邮箱</label>
<input name="email" type="email" class="form-control" placeholder="请输入邮箱地址" value="{{user&&user.email}}">
</div>
<div class="form-group">
<label>密码</label>
<input name="password" type="password" class="form-control" placeholder="请输入密码">
</div>
<div class="form-group">
<label>角色</label>
<select class="form-control" name="role">
<option value="normal" {{user&&user.role=='normal'?'selected':''}}>普通用户</option>
<option value="admin" {{user&&user.role=='admin'?'selected':''}}>超级管理员</option>
</select>
</div>
<div class="form-group">
<label>状态</label>
<select name="state" class="form-control">
<option value="0" {{user&&user.state=='0'?'selected':''}}>启用</option>
<option value="1" {{user&&user.state=='1'?'selected':''}}>禁用</option>
</select>
</div>
<div class="buttons">
<input type="submit" class="btn btn-primary" value='{{button}}'>
</div>
</form>
将要修改的用户ID传递到服务器端
//user-edit.js
if (id) {
//修改操作
let user = await User.findOne({_id:id});
//渲染用户编辑页面(修改)
res.render('admin/user-edit',{
message:message,
user:user,
link:'/admin/user-modify?id='+id,
button:'修改'
});
}
}
建立用户信息修改功能对应的路由
//admin.js
admin.post('/user-modify',require('./admin/user-modify'));
接收客户端表单传递过来的请求参数
//user-modify.js
const body =req.body;
//即将要修改的id,id是get方式传过来的
const id =req.query.id;
根据id查询用户信息,并将客户端传递过来的密码和数据库中的密码进行比对
//user-modify.js
const {User} = require('../../model/user');
const bcrypt = require('bcrypt');
module.exports=async(req,res,next)=>{
let user = await User.findOne({_id:id});
const isValid = await bcrypt.compare(req.body.password,user.password);
if(isValid){
res.send('密码比对成功')
}else{
let obj={path:'/admin/user-edit',message:'密码比对失败了',id:'id'}
next(JSON.stringify(obj));
}
}
如果比对失败,对客户端做出响应,触发错误处理中间件
//app.js
//改写错误处理中间件
app.use((err,req,res,next)=>{
//将字符串类型转换为对象啊
const result = JSON.parse(err);
//result{path:'/admin/user-edit',message:'密码比对失败了',id:'id'}
let params=[];
for (let attr in result) {
if (attr!='path') {
params.push(attr+'='+result[attr]);
}
}
// res.redirect(`${result.path}?message=${result.message}`);
res.redirect(`${result.path}?${params.join('&')}`);
})
如果密码对比成功,将用户信息更新到数据库中
if(isValid){
//res.send('密码比对成功');
//将用户信息更新到数据库,密码不能在这里更新
await User.updateOne({_id:id},{
username:username,
email:email,
role:role,
state:state
});
//重定向到用户列表页
res.redirect('/admin/user');
}else{
//注意id值不要加冒号
let obj={path:'/admin/user-edit',message:'密码比对失败了',id:id}
next(JSON.stringify(obj));
}
在确认删除框中添加隐藏域用以存储要删除用户的ID值
<form class="modal-content" action="/admin/delete" method="get">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
<h4 class="modal-title">请确认</h4>
</div>
<div class="modal-body">
<p>您确定要删除这个用户吗?</p>
<input type="hidden" name="id" id="deleteId"><!-- 隐藏域 -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
<input type="submit" class="btn btn-primary">
</div>
</form>
为操作选项下的删除按钮添自定义属性用以存储要删除用户的ID值
<i class=" delete" data-toggle="modal" data-target=".confirm-modal" data-id="{{@$value._id}}"></i>
为删除按钮添加点击事件,在点击事件处理函数中获取自定义属性中存储的ID值并将ID值存储在表单的隐藏域中
{{block 'script'}}
<script>
$(".delete").on('click',function(){
let id = $(this).attr('data-id');
//将要删除的用户id存储在隐藏域
$('#deleteId').val(id);
})
</script>
{{/block}}
为删除表单添加提交地址以及提交方式
在服务器端建立删除功能路由
//admin.js
admin.get('/delete',require('./admin/user-delete'));
接收客户端传递过来的id参数
根据id删除用户
//user-delete.js
const {User} =require("../../model/user");
module.exports= async(req,res)=>{
await User.findByIdAndDelete({_id:req.query.id});
res.redirect('/admin/user');
}
添加访问路由:文章列表页面和文章编辑页面
//admin.js
//文章列表路由
admin.get('/article',require('./admin/article'));
//文章编辑路由
admin.get('/article-edit',require('./admin/article-edit'));
//article.js
module.exports= (req,res)=>{
res.render('admin/article.art');
}
//article-edit.js
module.exports= (req,res)=>{
res.render('admin/article-edit.art');
}
1、实现点击切换文章管理页面和用户管理页面
用户列表userPage.js添加标识
用户编辑user-edit.js添加标识
//标识当前访问的是用户管理页面
req.app.locals.currentLink="user";
用户列表userPage.js添加标识
用户编辑user-edit.js添加标识
//标识当前访问的是文章管理页面
req.app.locals.currentLink="article";
三元表达式判断当前选中状态
//aside.art
<li>
<a class="item {{currentLink=='user'?'active':''}}" href="/admin/user">
<span class="glyphicon glyphicon-user"></span>
用户管理
</a>
/li>
<li>
<a class="item {{currentLink=='article'?'active':''}}" href="/admin/article">
<span class="glyphicon glyphicon-th-list"></span>
文章管理
</a>
</li>
1、创建文章集合
语法:
//引入mongoose模块
const mongoose = require('mongoose');
//创建文章集合规则
const articleSchema = new mongoose.Schema({
title:{
type:String,
maxlength:20,
minlength:4,
require:[true,'请填写文章标题']
},
author:{
type:mongoose.Schema.Types.ObjectId,
ref:'User',
required:[true,'请传递作者姓名']
},
publishDate:{
type:Date,
default:Date.now
},
cover:{
type:String,
default:null
},
content:{
type:String
}
})
//根据规则创建集合
const Article = mongoose.model('Article',articleSchema);
//将集合规则作为模块成员进行导出
module.exports={
Article
}
2、点击发布按钮,跳转至文章编辑页面article-edit.art
//article.art
<a href="/admin/article-edit" class="btn btn-primary new">发布新文章</a>
3、为表单添加请求方式和地址,由于往数据库添加数据,所以为post。添加的name属性最好与数据库保持一致
// enctype指定表单数据的编码类型,默认用
//application/x-wwww-form-urlencoded的类型:name=zhangsan&age=20
//multipart/form-data将表单数据编码成二进制类型,用于涉及文件上传
<form class="form-container" action="/admin/article-add" method="post" enctype="multipart/form-data">
<div class="form-group">
<label>标题</label>
<input type="text" class="form-control" placeholder="请输入文章标题" name="title">
</div>
<div class="form-group">
<label>作者</label>
<input name="author" type="text" class="form-control" readonly>
</div>
<div class="form-group">
<label>发布时间</label>
<input name="publishDate" type="date" class="form-control">
</div>
<div class="form-group">
<label for="exampleInputFile">文章封面</label>
<input type="file" name="cover">
<div class="thumbnail-waper">
<img class="img-thumbnail" src="">
</div>
</div>
<div class="form-group">
<label>内容</label>
<textarea name="content" class="form-control" id="editor"></textarea>
</div>
<div class="buttons">
<input type="submit" class="btn btn-primary">
</div>
</form>
3、在服务器端建立点击提交按钮后,文章添加功能的路由
//admin.js
admin.post('/article-add',require('./admin/article-add'));
bodyPaser不能处理客户端发送的二级制数据
formidable替代:解析表单,支持get请求参数,post请求参数、文件上传。
//article-add.js
const path =require('path');
// 引入formidable模块
const formidable = require('formidable');
module.exports= (req,res)=>{
// 创建表单解析对象
const form = new formidable.IncomingForm();
// 设置文件上传路径
form.uploadDir = path.join(__dirname,'../','../','/public','uploads')
// 是否保留表单上传文件的扩展名
form.keepExtensions = true;
// 对表单进行解析
form.parse(req, (err, fields, files) => {
//err错误对象,如果解析成功则为null
// fields对象 存储普通表单数据
// files对象 存储上传文件的信息
res.send(files);
});
}
作者author就是集合User的_id值,之前login.js里登录成功后已经把用户信息挂载到公用属性了 :req.app.locals.userInfo123=user;
//article-edit.art
<label>作者</label>
<input name="author" type="text" class="form-control" readonly value="{{@userInfo123._id}}">
4、封面图片显示在预览中
//article-edit.art
<label for="exampleInputFile">文章封面</label>
<input type="file" name="cover" id="file" >
<div class="thumbnail-waper">
<img class="img-thumbnail" src="" id="preview">
</div>
<script>
var file =document.querySelector("#file");
var preview = document.querySelector("#preview");
//onchange触发说明选择完文件
file.onchange=function(){
//1创建文件读取对象
var reader = new FileReader();
//2读取文件
reader.readAsDataURL(this.files[0]);
//3readAsDataURL是异步方法不能通过返回值的方式来获取结果,所以监听文件读取对象的onload事件
reader.onload=function(){
//reader.result文件读取的结果
preview.src= reader.result
}
}
</script>
5、将客户端传递到服务器的文件传递到数据库
//article-add.js
const path =require('path');
const formidable = require('formidable');
//引入文章规则
const {Article}=require('../../model/article.js');
module.exports= (req,res)=>{
// 创建表单解析对象
const form = new formidable.IncomingForm();
// 设置文件上传路径
form.uploadDir = path.join(__dirname,'../','../','/public','uploads')
// 是否保留表单上传文件的扩展名
form.keepExtensions = true;
// 对表单进行解析,
form.parse(req,async (err, fields, files) => {
await Article.create({
title:fields.title,
author:fields.author,
publishDate:fields.publishDate,
//files对象 存储上传文件的信息中路径path保存的是硬盘绝对路径,需要以public为分割字符去截取uploads后的路径
cover:files.cover.path.split('public')[1],
content:fields.content
});
res.redirect('/admin/article')
});
}
6、实现文章列表页面的数据展示功能
跟用户列表的展示功能步骤是一样的,从数据库中查询数据再渲染模板
1.多集合联合查询:populate(‘要查询的字段名称’);
2.告诉mongoose返回普通对象,不返回mongoose文档对象,防止报错要加上:.lean()
//将文章结合的构造函数导入到当前文件中
const {Article}=require('../../model/article');
module.exports= async(req,res)=>{
//标识当前访问的是文章管理页面
req.app.locals.currentLink="article";
//查询所有文章数据
let articles = await Article.find().populate('author').lean();
// res.send(articles);
res.render('admin/article.art',{
articles:articles
});
}
//article.art
{{each articles}}
<tr>
<td>{{@$value._id}}</td>
<td>{{$value.title}}</td>
<td>{{$value.publishDate}}</td>
<td>{{$value.author.username}}</td>
<td>
<a href="article-edit.html" class="glyphicon glyphicon-edit"></a>
<i class="glyphicon glyphicon-remove" data-toggle="modal" data-target=".confirm-modal"></i>
</td>
</tr>
{{/each }}
日期格式的处理npm i dateformat
dateformat是处理js的方法,要想在模板文件art中用这个方法要先配置
//app.js
//导入art-tempate模板引擎
const template = require('art-template');
//导入dateformat第三方模块格式日期
const dateFormat = require('dateformat');
//向模板内部导入dateFormate变量
template.defaults.imports.dateFormat = dateFormat;
<td>{{dateFormat($value.publishDate,'yyyy-mm-dd')}}</td>
数据分页 npm i mongoose-sex-page
const pagination = require('mongoose-sex-page');
pagination(集合构造函数).page(1) .size(20) .display(8) .exec();
7、实现分页展示
article.js
//将文章结合的构造函数导入到当前文件中
const {Article}=require('../../model/article');
//导入mongoose-sex-page模块
const pagination = require('mongoose-sex-page');
module.exports= async(req,res)=>{
//接受客户端传递过来的页码
const page = req.query.page;
//标识当前访问的是文章管理页面
req.app.locals.currentLink="article";
//page 指定当前页
//size 指定每页显示的数据条数
//display 指定客户端要显示的页码数量
//exec 向数据库中发送查询请求
//返回的是一个对象,数据在records里
// 查询所有文章数据
let articles = await pagination(Article).find().page(page).size(2).display(3).populate('author').exec();
//res.send(articles);
/*articles{
"page": 1,
"size": 2,
"total": 4,
"records": [{},{}],
"pages": 2,
"display": [1,2]
}
*/
//使用了第三方模块mongoose-sex-page控制查询数据,则不能使用lean(),换下面一种方式
articles = JSON.stringify(articles);
articles = JSON.parse(articles);
res.render('admin/article.art',{
articles:articles
});
}
article.art
<!-- 分页 -->
<ul class="pagination">
{{if articles.page>1}}
<li>
<a href="/admin/article?page={{articles.page-1}}">
<span>«</span>
</a>
</li>
{{/if}}
{{each articles.display}}
<li><a href="/admin/article?page={{$value}}">{{$value}}</a></li>
{{/each}}
{{if articles.page<articles.pages}}
<li>
<a href="/admin/article?page={{articles.page-0+1}}">
<span>»</span>
</a>
</li>
{{/if}}
</ul>
<!-- /分页 -->
给数据库添加超级管理员账号,只有登录账号才能操作数据库;普通账号只能操作单独的数据库,比如当前这个blogyqs数据库
以系统管理员的方式运行powershell
连接数据库: mongo
查看数据库: show dbs
切换到admin数据库 use admin
创建超级管理员账户(键都是固定的) db.createUser({user:'root',pwd:'root',roles:['root']})
切换到blog数据 use blogyqs
创建普通账号 db.createUser({user:'yqs4739',pwd:'yqs4739',roles:['readWrite']})
卸载原有的mongodb服务:
net stop mongodb
mongod --remove
创建mongodb服务
mongod --logpath="C:\Program Files\MongoDB\Server\4.1\log\mongod.log" --dbpath="C:\Program Files\MongoDB\Server\4.1\data" --install –-auth
在项目中使用账号连接数据库
//app.js
mongoose.connect('mongodb://yqs4739:yqs4739@localhost:27017/blogyqs',
{useNewUrlParser:true,useUnifiedTopology: true})
当想再多创建一个数据库账号时,必须先登录超级管理员账号
在开发环境将客户端向服务器端发送的请求信息打印到控制台
安装第三方模块
如何区分开发环境与生产环境
1.通过电脑操作系统中的系统环境变量区分
2.配置后必须重启代码编辑器,再安装morgan
npm install morgan
3.获取当前系统下的环境变量方法:
console.log(process.env)
4.把客户端的请求信息打印到控制台:
//导入morgan第三方模块
const morgan = require('morgan');
if(process.env.NODE_ENV == 'development'){
//当前是开发环境
console.log('当前是开发环境');
//在开发环境中将客户端向服务器端发送的请求信息打印到控制台
//app.use(morgan('dev'))
}else{
console.log('当前是生产环境');
};
5.第三方模块config实现上面的需求
作用:允许开发人员将不同运行环境下的应用配置信息如数据库连接地址、用户名、密码等,抽离到单独的文件中,模块内部自动判断当前应用的运行环境,并读取对应的配置信息,极大提供应用配置信息的维护成本,避免了当运行环境重复的多次切换时,手动到项目代码中修改配置信息。
使用步骤:
使用npm install config命令下载模块
在项目的根目录下新建config文件夹
在config文件夹下面新建default.json、development.json、production.json文件
//development.json
{
"db":{
"user":"yqs4739",
"pwd":"yqs4739",
"host":"localhost",
"port":"27017",
"name":"blogyqs"
}
}
在项目中通过require方法,将模块进行导入
使用模块内部提供的get方法获取配置信息
//connect.js
const mongoose = require('mongoose');
//导入config模块
const config=require('config');
mongoose.connect(`mongodb://${config.get('db.user')}:${config.get('db.pwd')}@${config.get('db.host')}:${config.get('db.port')}/${config.get('db.name')}`,{useNewUrlParser:true,useUnifiedTopology: true, useCreateIndex: true})
.then(()=>console.log('数据库连接成功'))
.catch(()=>console.log('数据库连接失败'))
6.将敏感信息存在环境变量中
1.在config文件夹中建立固定文件custom-environment-variables.json
2.先去电脑系统设置环境变量,再文件里填写系统环境变量的名字
//custom-environment-variables.json
{
"db":{
"pwd":"yqs_PASSWORD"
}
}
//development.json
{
"db":{
"user":"yqs4739",
"host":"localhost",
"port":"27017",
"name":"blogyqs"
}
}
3.项目运行时config模块会查找系统环境变量,并读取其值作为当前配置项的值
//home.js
const express = require('express');
const home = express.Router();
//博客前台首页的展示页面
home.get('/',require('./home/index'));
//博客前台文章的展示页面
home.get('/article',require('./home/article'))
module.exports = home;
module.exports = (req,res)=>{
res.render('home/article.art');
}
module.exports = (req,res)=>{
res.render('home/default.art');
}
外链资源相对路径不安全,需要修改成绝对路径
<link rel="stylesheet" href="/home/css/base.css">
<link rel="stylesheet" href="/home/css/article.css">
1.抽离骨架layout.art
骨架模板layout.art:
1.抽离HTML整体框架,不同的地方用{{block}}
挖坑做标记
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
<link rel="stylesheet" href="/home/css/base.css">
{{block 'link'}}{{/block}}
</head>
<body>
{{block 'main'}}{{/block}}
</body>
</html>
default.art示例:
extend
继承骨架模板{{block}}
填坑{{extend './common/layout.art'}}
{{block 'link'}}
<link rel="stylesheet" href="/home/css/index.css">
{{/block}}
{{block 'main'}}
<!-- 头部框架开始 -->
<div class="header">这里是头部</div>
<!-- 头部框架结束 -->
<!-- 文章列表开始 -->
//....
<!-- 文章列表结束 -->
{{/block}}
2.抽离公共部分
公共模板header.art:
1.抽离共有的头部代码
<div class="header">这里是头部</div>
default.art示例:
include
把头部再包含进来```javascript
{{extend './common/layout.art'}}
{{block 'link'}}
<link rel="stylesheet" href="/home/css/index.css">
{{/block}}
{{block 'main'}}
<!-- 头部框架开始 -->
{{include './common/header.art'}}
<!-- 头部框架结束 -->
<!-- 文章列表开始 -->
//....
<!-- 文章列表结束 -->
{{/block}}
由于对数据库的操作属于异步操作,我们要不用异步函数接收异步方法的返回值,要不用promise的方式,最好的方法是用异步函数async await
//index.js
const {Article} = require('../../model/article');
//导入分页模块
const pagination = require('mongoose-sex-page');
module.exports = async(req,res)=>{
//从数据库中查询数据
let result = await pagination(Article).page(1).size(4).display(5).find().populate('author').exec();
//res.send(result);
result = JSON.stringify(result);
result = JSON.parse(result);
//传递数据并渲染模板
res.render('home/default.art',{
result:result
});
}
<!-- 文章列表开始 -->
<ul class="list w1100">
{{each result.records}}
<li class="{{$index%2==0?'fl':'fr'}}">
<a href="article.html" class="thumbnail">
<img src="{{$value.cover}}">
</a>
<div class="content">
<a class="article-title" href="article.html">{{$value.title}}</a>
<div class="article-info">
<span class="author">{{$value.author.username}}</span>
<span>{{dateFormat($value.publishDate,'yyyy-mm-dd')}}</span>
</div>
<div class="brief">
{{@$value.content.replace(/<[^>]+>/g,'').substr(0,150)+'...'}}
</div>
</div>
</li>
{{/each}}
</ul>
<!-- 文章列表结束 -->
<!-- 分页开始 -->
<div class="page w1100">
{{if result.page>1}}
<a href="/home/?page={{result.page-1}}">上一页</a>
{{/if}}
{{each result.display}}
<a href="/home/?page={{$value}}" class="{{$value==result.page?'active':''}}">{{$value}}</a>
{{/each}}
{{if result.page<result.pages}}
<a href="/home/?page={{result.page-0+1}}">下一页</a>
{{/if}}
</div>
<!-- 分页结束 -->
点击文章进入详情页
通过get方式将文章id传递给服务端
如果我们想传递HTML代码,那么模板引擎并不会将HTML代码中的特殊符号进行解码,还是以编码的形式显示。例如:< h1> 在模板中最后显示为 0; h1 2;想让模板中原文显示的标准语法:{{@ 数据 }}
//default.art
<a class="article-title" href="/home/article?id={{@$value._id}}">id记得要原文输出</a>
//article.js
const {Article} = require('../../model/article');
module.exports = async (req,res)=>{
const id = req.query.id;
let article = await Article.findOne({_id:id}).populate('author');
// res.send(article);
article = JSON.stringify(article);
article = JSON.parse(article);
res.render('home/article.art',{
article:article
});
}
const mongoose = require('mongoose');
//创建评论集合规则
const commentSchema= new mongoose.Schema({
//aid就是文章的id
aid:{
type:mongoose.Schema.Types.ObjectId,
//关联写上文章集合的名字
ref:'Article'
},
//用户id
uid:{
type:mongoose.Schema.Types.ObjectId,
ref:'User'
},
//评论时间
time:{
type:Date
},
//评论内容
content:{
type:String
}
});
//创建评论集合
const Comment = mongoose.model('Comment',commentSchema);
module.exports={
Comment
}
//login.js
let user = await User.findOne({email});
if(user){
let isValid = await bcrypt.compare(password,user.password);
if(isValid){
//登录成功
req.session.username= user.username;
//同理将用户角色存储在session对象中
req.session.role= user.role;
req.app.locals.userInfo123=user;
//对用户角色进行判断
if(user.role=="admin"){
//重定向用户列表,用express自带的方法redirect
res.redirect('/admin/user');
}else{
//重定向博客列表
res.redirect('/home/');
}
}else{
res.status(400).render('admin/error',{message:'邮件地址或者密码错误'});
}
}else{
res.status(400).render('admin/error',{message:'邮件地址或者密码错误'});
}
//loginGuard.js
const guard = (req,res,next)=>{
//login.js已经提前设置了session,这里可以直接拿到里面的用户名username和角色role
if (req.url !='/login' && !req.session.username) {
res.redirect('/admin/login');
} else {
//进入这一步表明登陆成功
//判断用户权限
if(req.session.role=='normal'){
//普通用户跳转博客首页完成后,阻止程序向下执行
return res.redirect('/home/')
}
//不是普通用户就继续向下执行
next();
}
}
module.exports = guard;
在用户模板文件中如何判断用户是否登录了呢?有个对象叫
req.app.locals
在模板中是可以访问到的,可以通过判断这个对象下的自定义属性是否存在,代码就在上面第一步的login.js里,里面有这样一句:req.app.locals.userInfo123=user;
下面是在模板里做判断
//article.art
<h4>评论</h4>
{{if userInfo123}}
<form class="comment-form">
<textarea class="comment"></textarea>
<div class="items">
<input type="submit" value="提交">
</div>
</form>
{{else}}
<h2>先进行登陆,再对文章进行评论</h2>
{{/if}}
小bug:退出账号后,评论区依然还显示 。
原因:删除了服务器端的session,并没有再去删除userInfo123
//logout.js
module.exports=(req,res)=>{
//删除session
req.session.destroy(function(){
//删除cookie
res.clearCookie('connect.sid');
//重定向到用户登录页面
res.redirect('/admin/login');
//清除模板中的用户信息
req.app.locals.userInfo123=null;
})
}
添加评论表单提交地址和方式,增加两个隐藏域存储文章id和用户id
//article.art
<form class="comment-form" action="/home/comment" method="post">
<textarea class="comment" name="content"></textarea>//name跟数据库的评论对象名对应
<input type="hidden" name="aid" value="{{@article._id}}">//article.js已经将文章数据article导入到模块当中,这里可以直接用
<input type ="hidden" name="uid" value="{{@userInfo123._id}}">
<div class="items">
<input type="submit" value="提交">
</div>
</form>
//home.js创建评论功能路由
home.post('/comment',require('./home/comment'));
const {content,uid,aid}=req.body;
//导入评论集合构造函数 comment.js
const {Comment} =require('../../model/comment');
module.exports=async (req,res)=>{
//res.send(req.body);
// {
// "content": "",
// "aid": "600a834b0c38c715d0d1f3a7",
// "uid": "5ffec57c28bd41166c02db5b"
// }
const {content,uid,aid}=req.body;
await Comment.create({
content:content,
uid:uid,
aid:aid,
time:new Date()
});
//提交完成后将页面重定向回文章详情页面。还有地址栏带上文章id
res.redirect("/home/article?id="+aid);
}
//article.js
const {Article} = require('../../model/article');
//导入评论集合构造函数
const {Comment} = require('../../model/comment');
module.exports = async (req,res)=>{
//接收客户端传递过来的文章id
const id = req.query.id;
let article = await Article.findOne({_id:id}).populate('author');
//根据文章aid的值,来查询当前所对应的文章
let comments =await Comment.find({aid:id}).populate('uid');
article = JSON.stringify(article);
article = JSON.parse(article);
comments = JSON.stringify(comments);
comments = JSON.parse(comments);
res.render('home/article.art',{
article,
comments
});
}
//article.art
{{each comments}}
<div class="mb10">
<div class="article-info">
<span class="author">{{$value.uid.username}}</span>
<span>{{dateFormat($value.time,'yyyy-mm-dd')}}</span>
<span>{{$value.uid.email}}</span>
</div>
<div class="comment-content">
{{$value.content}}
</div>
</div>
{{/each}}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。