课程管理系统
使用node实现简单的增删改查
一.handlebars模板引擎的使用
handlebars的安装
npm i express
npm i express-hndlebars
handlebars的使用
模板引擎的目录结构,必须如下图所示:
文件夹名称必须是views,views目录下必须有一个layouts文件夹,layouts文件夹下有一个handlebars文件,作为模板渲染的主文件,所有的其他handlebars都会渲染到这个文件中。
|---app.js
|---views
|---layouts
|---main.handlebars
|---index.handlebars
app.js中设置模板引擎:
const express = require('express');
const exphbs = require('express-handlebars');
const app = express();
//设置模板引擎
app.engine('handlebars', exphbs({defaultLayout: 'main'}));
app.set('view engine', 'handlebars');
app.get('/',(req,res) => {
res.render('index');
});
main.handlebars:所有的handlebars都会被渲染到这个文件中
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no>
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
{{{body}}}
</body>
</html>
index.handlebars:是你路由指定的渲染模板
<h1>这里是课程项目</h1>
二.添加课程
//添加课程
app.get('/ideas/add',(req,res) => {
res.render('ideas/add')
});
使用body-parser解析请求体
添加课程时,我们需要使用post请求,post请求包含请求体,在Node原生的http模块中,请求体需要使用流来进行接收和解析。body-parser是express使用的HTTP请求体解析的中间件,可以解析JSON、Raw、文本、URL-encoded格式的请求体。
body-parser的使用
通过使用body-parser.json()方法可以解析application/json格式的文件
通过使用body-parser.urlencoded()方法可以解析application/x-www-form-urlencoded表单格式的数据
//使用body-parser中间件解析请求体
const bodyParser = require('body-parser');
// 解析 application/json
app.use(bodyParser.urlencoded());
const jsonParser = bodyParser.json();
// 解析 application/x-www-form-urlencoded
const urlencodedParser = bodyParser.urlencoded({ extended: false })
//解析后的数据在req.body中
app.post('/ideas',urlencodedParser,(req,res) => {
res.render('ideas/index',{
title:req.body.title,
details:req.body.details
})
});
后台错误验证
表单提交时,需要进行错误验证。表单提交的信息是否正确(是否全部填写,填写部分是否有要求等),在这里我们需要提交两个数据。
如下面代码所示:创建一个error数组,如果req.body.title不存在表示没有输入这个内容,将提示文字添加到error数组中。根据数组的长度来验证是否有错误,如果有错误,需要错误提示。error.handlebars用来描述错误提示。
app.post('/ideas',urlencodedParser,(req,res) => {
//后台错误验证
const error = [];
if(!req.body.title){
error.push({text:'请输入标题'});
}
if(!req.body.details){
error.push({text:'请输入详情'});
}
if(error.length > 0){
res.render('ideas/add',{
//实现自动填写
title:req.body.title,
details:req.body.details,
//实现错误提示
errors:error
})
}else{
res.render('ideas/index',{
title:req.body.title,
details:req.body.details
})
}
});
error.handlebars
{{#each errors}}
<div class="alert alert-danger">{{text}}</div>
{{/each}}
如果errors数组不存在,那么就没有渲染的内容。
main.handlebars
<div class="container">
{{> error}}
{{{body}}}
</div>
三.增
添加课程以后,我们需要将添加的课程存入数据库中。在需要展示时载从数据库中调取。
mongoose的使用
1.连接数据库 :node-course是我们自己定义的数据库名称
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/node-course');
2.创建集合
数据库集合通常创建在models文件中,每一个文件代表一个集合。比如Idea.js表示的是集合Idea
|---app.js
|---models
|---Idea.js
Idea.js:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
//创建一个Schema
const IdeaSchema = new Schema({
title:{
type:String,
required:true
},
details:{
type:String,
require:true
}
});
//创建集合idea
const Idea = mongoose.model('ideas',IdeaSchema);
//导出集合对象
module.exports = Idea;
3.保存数据
const Idea = require('./models/Idea');
//添加到数据库
const newCourse = {
title:req.body.title,
details:req.body.details
};
//集合创建的示例对象就是要保存的文档(数据)
new Idea(newCourse)
.save()
.then(()=>{
res.redirect('/ideas');
})
四.查
通过调用数据库查看数据
app.get('/ideas',(req,res) => {
Idea.find({})
.then((idea)=>{
res.render('ideas/index',{
ideas:idea
})
})
});
查看数据可以直接使用Idea.find({})静态方法,获取到的是一个数组,我们需要将这个数组渲染到ideas/index.handlebars.
ideas/index.handlebars
{{#each ideas}}
<div class="card card-body">
<h3>{{title}}</h3>
<h3>{{details}}</h3>
</div>
{{/each}}
五.改
我们有时候需要对课程信息进行编辑
跳转到编辑页面:
{{#each ideas}}
<div class="card card-body">
<h3>{{title}}</h3>
<h3>{{details}}</h3>
<a href="/ideas/edit/{{id}}" class="btn btn-dark btn-block">编辑</a>
</div>
{{/each}}
注意:上面的编辑按钮中的{{id}}来自数据库,每一个文档都有一个特定的id。我们在这里可以直接获取到。
跳转到编辑页面
app.get('/ideas/edit/:id',(req,res) => {
Idea.findOne({_id:req.params.id})
.then((idea) => {
res.render('ideas/edit',{
title:idea.title,
details:idea.details
})
})
});
进行编辑(改)
修改通常是使用put方法,但是表单一般只支持get和post方法,想要让form支持put或者delete等方法,需要使用中间件method-override
method-override中间件的使用
1.使用method-override
//使用method-override支持put和delete等http请求方法
const methodOverride = require('method-override');
app.use(methodOverride('_method'))
2.修改form表单内容
修改action和添加一个隐藏的input框
<form action="/ideas/{{id}}?_method=PUT" method = "post">
//需要在这里添加一个隐藏的input
<input type="hidden" name="_method" value="PUT">
<button type="submit" class="btn btn-primary">提交</button>
</form>
使用put进行修改:
数据库的修改时先查找到数据,然后跟操作对象一样将数据修改完,然后记得保存。
app.put('/ideas/:id',urlencodedParser,(req,res) => {
const course = {
title:req.body.title,
details:req.body.details
}
Idea.findOne({_id:req.params.id})
.then((idea) => {
idea.title = req.body.title;
idea.details = req.body.details;
idea.save()
.then(()=>{
res.redirect('/ideas')
})
})
});
六.删
如果我们想要删除课程,那么不需要跳转到新的页面,直接在当前页面删除就行。由于删除使用delete方法,因此我们同样需要使用method-override,因此将请求放到form表单中。
<form action="/ideas/{{id}}?_method=DELETE" method = 'POST'>
<input type="hidden" name="method" value = 'DELETE'>
<input type="submit" class="btn btn-danger btn-block" value="删除">
</form>
删除操作:
//删除
app.delete('/ideas/:id',urlencodedParser,(req,res) => {
Idea.remove({_id:req.params.id})
.then(()=>{
res.redirect('/ideas');
})
})
七.对用户的操作进行提醒
我们在进行增删改查的时候,需要对用户的操作进行提醒,比如修改成功后,提示修改成功,删除成功后,提示删除成功,以及出现错误时,提示错误。connect-flash是nodejs中的一个模块,flash是一个暂存器,而且暂存器里面的值使用过一次便被清空,适合用来做网站的提示信息。flash 是 session 中一个用于存储信息的特殊区域。消息写入到 flash 中,在跳转目标页中显示该消息。flash 是配置 redirect 一同使用的,以确保消息在目标页面中可用
安装
npm i express-session connect-flash
使用
在app.js中引入
const session = require('express-session');
const flash = require('connect-flash');
在app中使用flash中间件
app.use(session({
secret: 'secret',
resave: true,
saveUninitialized: true
}));
app.use(flash());
使用完flash中间件以后,所有的req中都存在一个flash方法,可以存储内容。req.flash('success')。将flash中存入的变量存入res.locals全局变量中,假如我要在网站中使用flash中存的error和success变量,加可以把它们传入locals变量中,这样所有的模板都可以拿到这个变量。注意flash存储的变量都是只能使用一次,使用完毕就会被移除
定义falsh变量:req.flash(success_msg)表示定义一个success_msg变量
//将flash中存入的变量存入res.locals对象中
app.use(function(req,res,next){
res.locals.success_msg = req.flash('success_msg');
res.locals.error_msg = req.flash('error_msg');
next();
});
给flash变量赋值,一般是在res.redirect()前面进行赋值req.flash('success_msg',"删除成功")
app.delete('/ideas/:id',urlencodedParser,(req,res) => {
Idea.remove({_id:req.params.id})
.then(()=>{
req.flash('success_msg',"数据删除成功");
res.redirect('/ideas');
})
})
req.flash赋值以后,res.locals中的变量就能够获取到这个值,那么在任何模板中,都可以使用这个值。
_msg.handlebars
{{#if success_msg}}
<div class="alert alert-success">{{success_msg}}</div>
{{/if}}
{{#if error_msg}}
<div class="alert alert-danger">{{error_msg}}</div>
{{/if}}
七.路由管理
目前,我们所有的中间件的使用和路由的设置都在app.js中,这样的话就导致整个app.js文件显得臃肿,而且之后可能还有新的路由设置,因此我们需要对路由进行管理。顶级express对象具有创建新的router对象的功能,这个新的router对象可以用来帮助我们实现路由管理。
app.js
const app = express();
const idea = require('./routes/idea');
//使用idea routes
//这里的/表示根目录,之后的router.get(/idea)都是在这个根目录下进行组合的
app.use('/',idea);
app.use('/',idea)表示所有的/下面的路由都在idea中进行管理(idea是一个迷你路由router)
idea.js
const express = require('express');
const router = express.Router();
//添加课程
router.get('/ideas/add',(req,res) => {
res.render('ideas/add')
});
module.exports = router;
通过express.Router()创建一个新的router对象,用来代替app管理指定路径下(/)的所有路由
八.注册页面的实现##
|---routes
|---user.js
//注册页面
router.get('/users/register',(req,res) => {
res.render('users/register.handlebars')
});
//注册
router.post('/users/register',urlencodedParser,(req,res) => {
console.log(res.body);
res.send('注册成功')
});
register.handlebars
<form action="/users/register" method="POST">
<div class="form-group">
<label for="name">用户名</label>
<input type="text" class="form-control" name="name" required>
</div>
<div class="form-group">
<label for="email">邮箱</label>
<input type="email" name="email" class="form-control" required>
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" name="password" class="form-control" required>
</div>
<div class="form-group">
<label for="password2">确认密码</label>
<input type="password" name="password2" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary">注册</button>
</form>
1.注册表单错误信息后台处理
只要是设计到表单的提交,通常都需要进行错误处理,比如密码验证,密码长度处理等。
router.post('/users/register',urlencodedParser,(req,res) => {
const errors = [];
if(req.body.password !== req.body.password2){
errors.push({text:'两次输入的密码不一致'})
}
if(req.body.password.length < 4){
errors.push({text:'密码长度不能小于4位'})
}
if(errors.length > 0){
res.render('users/register',{
name:req.body.name,
email:req.body.email,
password:req.body.password,
password2:req.body.password2,
errors:errors
})
}
});
2.如果没有错误,保存到数据库中
if(errors.length > 0){
res.render('users/register',{
name:req.body.name,
email:req.body.email,
password:req.body.password,
password2:req.body.password2,
errors:errors
})
}else{
// 如果没有错就保存到数据库中
const newUser = {
name:req.body.name,
email:req.body.email,
password:req.body.password
}
new User(newUser)
.save()
.then(() => {
res.redirect('/ideas');
})
}
3.保存到数据库之前,同样需要验证用户名,邮箱等是否已经注册过了
//验证邮箱是否存在
User.find({email:req.body.email})
.then((user) =>{
if(user.length > 0){
req.flash('error_msg',"使用的邮箱已注册,请使用新的邮箱")
res.redirect('/users/register');
}else{
//验证用户名是否存在
User.find({name:req.body.name})
.then((user) =>{
if(user.length > 0){
req.flash('error_msg',"用户名已存在,请使用其他的用户名")
res.redirect('/users/register');
}else{
const newUser = {
name:req.body.name,
email:req.body.email,
password:req.body.password
}
new User(newUser)
.save()
.then(() => {
req.flash('success_msg','注册成功');
res.redirect('/ideas');
})
}
})
}
})
4.加密操作
用户注册时,密码保存到数据库一定是明文的,而需要进行一定的加密。这里使用bcrypt进行加密。
安装:
npm i bcrypt
使用
const newUser =new User({
name:req.body.name,
email:req.body.email,
password:req.body.password
});
const saltRounds = 10;//加密强度
const myPlaintextPassword = req.body.password;//加密对象
bcrypt.genSalt(saltRounds, function(err, salt) {
bcrypt.hash(myPlaintextPassword, salt, function(err, hash) {
newUser.password = hash;
newUser.save()
.then(() => {
req.flash('success_msg','注册成功');
res.redirect('/ideas');
})
});
});
加密后的密码为:
"password" : "$2b$10$CRirGkbEvmwNbfBpx21Uyesju3MWyb9oU432dNFPAwvW5C9H8KzqW }
九.登陆
登陆时,首先通过用户名或者邮箱从数据库中查找用户,如果用户存在则进行密码验证。
router.post('/users/login',urlencodedParser,(req,res) => {
// 从数据库中通过用户名或者邮箱进行查询,如果有这个用户且密码正确则进行登陆
User.findOne({email:req.body.email})
.then((user) => {
if(user){
// 验证密码
bcrypt.compare(req.body.password, user.password, function(err, isMatch) {
// res == true
if(isMatch){
res.redirect('/ideas');
}else{
req.flash('error_msg',"您输入的密码不正确");
res.redirect('/users/login')
}
});
}
})
});
用户登陆状态的持久化
用户登陆成功以后,在退出之前应该都是登陆状态,这需要passport模块来帮助我们实现。同时,登陆,注册等提示应该消失。而且是在所有的页面消失,因此我们需要一个全局的变量来控制它。这就是app.locals.user。app.locals在整个应用生命周期内都是有效的,但是这里的app必须是app.js中唯一的那一个,不能是在一个文件内创建的新的app。
关于passport的使用可以查看passport
1.使用passport进行登陆验证和持久话
app.js中
const app = express();
const passport = require('passport');
app.use(passport.initialize());
app.use(passport.session());
//passport持久数据时涉及到session,需要对session进行序列化和反序列化。(同时需要安装session等npm)
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.findById(id, function (err, user) {
done(err, user);
});
});
//定义验证的策列
passport.use(new LocalStrategy(
{usernameField:"email"}, //验证对象改为email
function(email, password, done) {
User.findOne({ email: email })
.then((user) => {
if(!user){
return done(null,false,{message:'没有该用户'});
}else{
//用户存在密码验证
bcrypt.compare(password,user.password, (err, isMatch) => {
if(err){
throw err;
}else{
if(isMatch){
app.locals.user = true;
return done(null,user)
}else{
return done(null,false,{message:'密码错误'});
}
}
});
}
})
}
));
在登陆时进行验证
router.post('/users/login',urlencodedParser,(req,res,next) => {
// passport进行登陆验证
passport.authenticate('local', {
successRedirect:'/ideas',
failureRedirect: '/users/login',
failureFlash: true //是否使用flash进行提示,如果使用需要定义res.locals.error
})(req, res, next);
});
2.使用app.locals.user进行状态的控制
从app.js中引入app
const app = require('../app.js').app;
验证密码通过以后,通过app.locals.user = true;来持久登陆状态
router.post('/users/login',urlencodedParser,(req,res) => {
// 从数据库中通过用户名或者邮箱进行查询,如果有这个用户且密码正确则进行登陆
User.findOne({email:req.body.email})
.then((user) => {
if(user){
// 验证密码
bcrypt.compare(req.body.password, user.password, function(err, isMatch) {
// res == true
if(isMatch){
app.locals.user = true;
req.flash('success_msg','登陆成功');
res.redirect('/ideas');
}else{
req.flash('error_msg',"您输入的密码不正确");
res.redirect('/users/login')
}
});
}
})
});
handlebars文件中通过这个变量控制登陆和注册的显示:
<ul class="navbar-nav ml-auto">
{{#if user}}
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" id="navbarDropdownMenuLink">想学的课程</a>
<div class="dropdown-menu">
<a href="/ideas" class="dropdown-item">Idea</a>
<a href="/ideas/add" class="dropdown-item">添加</a>
</div>
</li>
<li class="nav-item">
<a href="/users/logout" class="nav-link">退出</a>
</li>
{{else}}
<li class="nav-item">
<a href="/users/login" class="nav-link">登录</a>
</li>
<li class="nav-item">
<a href="/users/register" class="nav-link">注册</a>
</li>
{{/if}}
</ul>
十.注销登陆
注销时,需要清理许多用户信息,这里使用passport来帮助我们进行注销。
安装
npm i pssport
使用:通过req.logout来实现注销,注销时需要将持久的变量设置为false。变成未登陆状态。
router.get('/users/logout',(req,res) => {
req.logout();
app.locals.user = false;
req.flash('success_msg',"退出登陆成功");
res.redirect('/users/login')
});
十一.导航守卫
在没有进行登陆时,用户应该不能访问任何页面。也就是说永不能通过输入网址进行页面访问。也就是说我们需要对所有的get请求进行守卫。这里同样需要用到passport模块通过自定义中间件来实现。
|---helpers
|---auth.js
module.exports = {
ensureAuthenticated:(req,res,next) => {
if(req.isAuthenticated()){
return next();
}else{
req.flash('error_msg',"请先登陆");
res.redirect('/users/login');
}
}
}
上面使用的req.isAuthenticated必须先安装passport模块才能够使用。
|---router
|---idea.js
//导航守卫
const {ensureAuthenticated} = require('../helpers/auth');
//添加课程
router.get('/ideas/add',ensureAuthenticated,(req,res) => {
res.render('ideas/add')
});
//查
router.get('/ideas',ensureAuthenticated,(req,res) => {
Idea.find({})
.then((idea)=>{
res.render('ideas/index',{
ideas:idea
})
})
});
在路由时,第二个参数时是导航守卫的中间件。
十二.数据的管理
每一个用户对应有自己的课程,也只能对自己的课程进行编辑和删除。因此需要对用户的数据进行管理。否则的话,无论什么人进行什么操作都会影响到其他的人的课程。
解决办法:在每次添加课程时,把用户的信息添加进去。
1.添加用户字段
model/Idea.js
const IdeaSchema = new Schema({
title:{
type:String,
required:true
},
details:{
type:String,
required:true
},
//把用户的信息添加进去
user:{
type:String,
required:true
}
});
router/idea.js:将user:req.user.id添加到数据库中。
//添加到数据库
const newCourse = {
title:req.body.title,
details:req.body.details,
user:req.user.id
};
new Idea(newCourse)
.save()
.then(()=>{
res.redirect('/ideas');
})
观察数据库中的结果:多了一个user字段
{ "_id" : ObjectId("5bf90f0ae5436e489cb3e29c"), "details" : "html" }
{ "_id" : ObjectId("5bf9139ec99ee92c70e3dc4f"), "details" : "test", "user" : "5bf904946607d339d8b8a30b" }
2.每次查看时,都通过这个user字段来进行筛选,只能查看具有这个字段的用户信息
//增加了筛选条件{user:req.user.id}
router.get('/ideas',ensureAuthenticated,(req,res) => {
Idea.find({user:req.user.id})
.then((idea)=>{
res.render('ideas/index',{
ideas:idea
})
})
});
3.编辑
如果我们每次进入一个账号先获取到他的url,然后再使用另外一个账号进行登陆,这样的话还是能够进行操作的。因此这里也需要进行设置。必须验证他的user和req.user.id是否相同。
//跳转到编辑页面
router.get('/ideas/edit/:id',ensureAuthenticated,(req,res) => {
Idea.findOne({_id:req.params.id})
.then((idea) => {
// 增加user和请求的id的验证
if(idea.user !== req.user.id ){
req.flash('error_msg','非法操作');
res.redirect('/ideas')
}else{
res.render('ideas/edit',{
title:idea.title,
details:idea.details,
id:idea._id
})
}
})
});