课程管理系统

2018-11-22  本文已影响0人  海因斯坦

使用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
        })
      }
    })
});
上一篇下一篇

猜你喜欢

热点阅读