課程管理系統

weixin_33766168發表於2018-11-22

使用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')
});

12754077-7df9606dd827c5ae.png

使用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
      })
    })
});

12754077-8aee87203182f9c0.png

進行編輯(改)
修改通常是使用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進行狀態的控制

12754077-9afd424dbff28d1e.png

從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
        })
      }
    })
});

相關文章