博客项目:传统开发模式--前后端不分离

数据库表

blog_users

类型长度其他
idint11主键
usernamevarchar255
passwordvarchar255
nicknamevarchar255
ctimevarchar255
iddeltinyint40未删除 1删除

博客项目:blog

博客,使用{传统的开发方式},后端开发人员,一边定义接口,一边写页面调用接口;

在这里插入图片描述

  1. 初始化项目:npm init -y

package.json

{
  "name": "heima_blog",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "bcrypt": "^3.0.0",
    "body-parser": "^1.18.3",
    "bootstrap": "^3.3.7",
    "ejs": "^2.6.1",
    "express": "^4.16.3",
    "express-session": "^1.15.6",
    "jquery": "^3.3.1",
    "marked": "^0.4.0",
    "mditor": "^1.3.3",
    "moment": "^2.22.2",
    "mysql": "^2.15.0"
  }
}

app.js

const express = require('express')
const fs = require('fs')
const path = require('path')
const app = express()
const bodyParser = require('body-parser')
// 导入 session 中间件
const session = require('express-session')

// 注册 session 中间件
// 只要注册了 session 中间件,那么,今后只要能访问到 req 这个对象,必然能访问到 req.session
app.use(
  session({
    secret: '这是加密的密钥',
    resave: false,
    saveUninitialized: false
  })
)

// 设置 默认采用的模板引擎名称
app.set('view engine', 'ejs')
// 设置模板页面的存放路径
app.set('views', './views')

// 注册解析表单数据的中间件
app.use(bodyParser.urlencoded({ extended: false }))

// 把 node_modules 文件夹,托管为静态资源目录
app.use('/node_modules', express.static('./node_modules'))

/* 方式一:手动注册路由模块
   导入 router/index.js 路由模块:
   const router1 = require('./router/index.js')
   app.use(router1)
   导入 用户相关的 路由模块
   const router2 = require('./router/user.js')
   app.use(router2)
*/ 

// 方式二:使用 循环的方式,进行路由的自动注册 【重点】
fs.readdir(path.join(__dirname, './router'), (err, filenames) => {
  if (err) return console.log('读取 router 目录中的路由失败!')
  // 循环router目录下的每一个文件名
  filenames.forEach(fname => {
    // 每循环一次,拼接出一个完整的路由模块地址
    // 然后,使用 require 导入这个路由模块
    const router = require(path.join(__dirname, './router', fname))
    app.use(router)
  })
})

app.listen(80, () => {
  console.log('server running at http://127.0.0.1')
})

db / index.js

const mysql = require('mysql')

const conn = mysql.createConnection({
  host: '127.0.0.1',
  database: 'mysql_001',
  user: 'root',
  password: 'root',
  // 开启执行多条Sql语句的功能
  multipleStatements: true
})

// 把当前模块中创建的 conn 数据库连接对象,暴露出去
module.exports = conn

router

article.js

const express = require('express')
const router = express.Router()

const ctrl = require('../controller/article.js')

// 监听客户端的 get 请求地址,显示 文章添加页面
router.get('/article/add', ctrl.showAddArticlePage)

// 监听客户端发表文章的请求
router.post('/article/add', ctrl.addArticle)

// 监听 客户端 查看文章详情的请求
router.get('/article/info/:id', ctrl.showArticleDetail)

// 监听 客户端 请求 文章编辑页面
router.get('/article/edit/:id', ctrl.showEditPage)

// 用户要编辑文章
router.post('/article/edit', ctrl.editAticle)

module.exports = router

index.js

// 封装路由模块的目的,是为了保证每个模块的职能单一性;
// 对于路由模块来说:只需要分配 URL 地址到 处理函数之间的对应关系即可;
// 路由模块,并不关心如何处理这一次请求;
const express = require('express')
const router = express.Router()

// 导入自己的业务处理模块
const ctrl = require('../controller/index.js')

// 用户请求的 项目首页
router.get('/', ctrl.showIndexPage)

// 把路由对象暴露出去
module.exports = router

user.js

const express = require('express')
const router = express.Router()

// 导入 用户相关的 处理函数模块
const ctrl = require('../controller/user.js')

// 用户请求的 是注册页面
router.get('/register', ctrl.showRegisterPage)

// 用户请求的 是登录页面
router.get('/login', ctrl.showLoginPage)

// 要注册新用户了
router.post('/register', ctrl.reg)

// 监听 登录的请求
router.post('/login', ctrl.login)

// 监听 注销请求
router.get('/logout', ctrl.logout)

module.exports = router

views

article / add.ejs

<%- include('../layout/header.ejs') %>

<link rel="stylesheet" href="/node_modules/mditor/dist/css/mditor.min.css">
<script src="/node_modules/mditor/dist/js/mditor.min.js"></script>

<div class="container">
  <h1>发表文章页</h1>
  <hr>
  <form id="form">
    <!-- 在进入文章添加页面的一瞬间,就立即把 文章的 作者Id,保存到 一个隐藏域中,防止 session 失效的问题 -->
    <input type="hidden" name="authorId" value="<%= user.id %>">
    <div class="form-group">
      <label>文章标题:</label>
      <input type="text" name="title" class="form-control" required>
    </div>

    <div class="form-group">
      <label>文章内容:</label>
      <textarea name="content" class="form-control" id="editor"></textarea>
    </div>

    <div class="form-group">
      <input type="submit" value="发表文章" class="btn btn-primary">
    </div>
  </form>
</div>

<script>
  $(function () {
    // 初始化编辑器
    var mditor = Mditor.fromTextarea(document.getElementById('editor'));

    $('#form').on('submit', function (e) {
      e.preventDefault()
      $.ajax({
        url: '/article/add',
        data: $('#form').serialize(),
        type: 'POST',
        dataType: 'json',
        success: function (result) {
          if (result.status !== 200) return alert('发表文章失败!')
          location.href = '/article/info/' + result.insertId
        }
      })
    })
  })
</script>

<%- include('../layout/footer.ejs') %>

article / edit.ejs

<%- include('../layout/header.ejs') %>

<link rel="stylesheet" href="/node_modules/mditor/dist/css/mditor.min.css">
<script src="/node_modules/mditor/dist/js/mditor.min.js"></script>

<div class="container">
  <h1>编辑文章页</h1>
  <hr>
  <form id="form">
    <!--应该把文章的标题,作为 隐藏域,保存到 表单中-->
    <input type="hidden" name="id" value="<%= article.id %>">
    <div class="form-group">
      <label>文章标题:</label>
      <input type="text" name="title" class="form-control" required value="<%= article.title %>">
    </div>

    <div class="form-group">
      <label>文章内容:</label>
      <textarea name="content" class="form-control" id="editor"><%= article.content %></textarea>
    </div>

    <div class="form-group">
      <input type="submit" value="保存文章" class="btn btn-primary">
    </div>
  </form>
</div>

<script>
  $(function () {
    // 初始化编辑器
    var mditor = Mditor.fromTextarea(document.getElementById('editor'));

    $('#form').on('submit', function (e) {
      e.preventDefault()

      $.ajax({
        url: '/article/edit',
        data: $('#form').serialize(),
        type: 'POST',
        dataType: 'json',
        success: function (result) {
          if (result.status !== 200) return alert('修改文章失败!')
          location.href = '/article/info/<%= article.id %>'
        }
      })
    })
  })
</script>

<%- include('../layout/footer.ejs') %>

article / info.ejs

<%- include('../layout/header.ejs') %>

<div class="container">
  <h1 class="text-center">
    <%= article.title %>
    <!--只有登录,且登录人的Id和文章作者Id相同,才应该展示编辑按钮-->
    <% if(islogin && user.id === article.authorId){ %>
    <a href="/article/edit/<%= article.id %>" class="btn btn-info pull-right">编辑</a>
    <% } %>
  </h1>
  <hr>

  <div><%- article.content %></div>
</div>

<%- include('../layout/footer.ejs') %>

layout / header.ejs

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap.min.css">
  <script src="/node_modules/jquery/dist/jquery.min.js"></script>
  <!-- 注意:bootstrap 的JS文件,需要依赖于Jquery -->
  <script src="/node_modules/bootstrap/dist/js/bootstrap.min.js"></script>
</head>

<body>

  <!-- 导航条区域 -->
  <nav class="navbar navbar-default">
    <div class="container-fluid">
      <!-- Brand and toggle get grouped for better mobile display -->
      <div class="navbar-header">
        <button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
          data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <a class="navbar-brand" href="/">黑马博客</a>
      </div>

      <!-- Collect the nav links, forms, and other content for toggling -->
      <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">

        <% if(islogin){ %>
        <!-- 注销和用户名按钮组 -->
        <div class="nav navbar-nav navbar-right navbar-form">
          <button class="btn btn-warning">欢迎
            <strong><%= user.nickname %></strong></button>
          <a class="btn btn-danger" href="/logout">注销</a>
        </div>

        <!-- 发表文章按钮组 -->
        <ul class="nav navbar-nav navbar-right">
          <li class="dropdown">
            <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
              aria-expanded="false">发表
              <span class="caret"></span>
            </a>
            <ul class="dropdown-menu">
              <li>
                <a href="/article/add">文章</a>
              </li>
              <li>
                <a href="javascript:;">问题</a>
              </li>
            </ul>
          </li>
        </ul>
        <% } else { %>
        <!-- 登录注册按钮组 -->
        <div class="nav navbar-nav navbar-right navbar-form">
          <a class="btn btn-success" href="/register">注册</a>
          <a class="btn btn-primary" href="/login">登录</a>
        </div>
        <% } %>
      </div>
      <!-- /.navbar-collapse -->
    </div>
    <!-- /.container-fluid -->
  </nav>

layout / footer.ejs

<!-- 版权区域 -->
<div class="text-center text-muted">
  传智播客 © 黑马程序员 2018
</div>

</body>

</html>

index.ejs

<%- include('./layout/header.ejs') %>

<h1>文章列表</h1>

<div class="list-group" style="margin: 10px;">
  <% articles.forEach(item => { %>
  <a href="/article/info/<%= item.id %>" class="list-group-item">
    <%= item.title %>
    <span class="badge" style="background-color: #5bc0de;">发表时间:<%= item.ctime %></span>
    <span class="badge" style="background-color: #f0ad4e;">作者昵称:<%= item.nickname %></span>
  </a>
  <% }) %>
</div>

<!-- 分页区域 -->
<nav aria-label="Page navigation">
  <ul class="pagination">
    <li class="<%= nowpage-1 === 0 ? 'disabled' : '' %>">
      <<%= nowpage-1 === 0 ? 'span' : 'a' %> href="?page=<%= nowpage-1 === 0 ? 1 : nowpage-1 %>" aria-label="Previous">
        <span aria-hidden="true">&laquo;</span>
      </<%= nowpage-1 === 0 ? 'span' : 'a' %>>
    </li>
    <% for(var i = 0; i < totalPage; i++){ %>
    <li class="<%= nowpage === (i+1) ? 'active' : '' %>"><a href="?page=<%= i+1 %>"><%= i+1 %></a></li>
    <% } %>
    <li class="<%= nowpage+1 > totalPage ? 'disabled' : ''  %>">
      <<%= nowpage+1 > totalPage ? 'span' : 'a'  %> href="?page=<%= nowpage+1 > totalPage ? totalPage : nowpage+1  %>"
        aria-label="Next">
        <span aria-hidden="true">&raquo;</span>
      </<%= nowpage+1 > totalPage ? 'span' : 'a'  %>>
    </li>
  </ul>
</nav>

<%- include('./layout/footer.ejs') %>

user / login.ejs

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <link rel="stylesheet" type="text/css" href="/node_modules/bootstrap/dist/css/bootstrap.min.css" />
  <style>
    #form {
      width: 400px;
      margin: 0 auto;
      margin-top: 100px;
    }

    h1 {
      text-align: center;
    }

    input[type='submit'] {
      width: 100%;
    }
  </style>
</head>

<body>
  <form id="form">
    <h1>登录页面</h1>
    <div class="form-group">
      <input type="text" name="username" id="username" class="form-control input-lg" placeholder="用户名" required
        value="ls">
    </div>

    <div class="form-group">
      <input type="password" name="password" id="password" class="form-control input-lg" placeholder="密码" required
        value="123">
    </div>

    <div class="form-group">
      <a href="/register" class="pull-right">去注册</a>
    </div>

    <div class="form-group">
      <input type="submit" value="登录" class="btn btn-primary btn-lg">
    </div>
  </form>

  <script src="/node_modules/jquery/dist/jquery.min.js"></script>
  <script>
    $(function () {
      $('#form').on('submit', function (e) {
        // 组织表单的默认提交行为
        e.preventDefault()
        $.ajax({
          url: '/login',
          data: $('#form').serialize(),
          type: 'POST',
          dataType: 'json',
          success: function (result) {
            if (result.status !== 200) {
              // 登录失败
              return alert(result.msg)
            }
            location.href = '/'
          }
        })
      })
    })
  </script>
</body>

</html>

user / register.ejs

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <link rel="stylesheet" type="text/css" href="/node_modules/bootstrap/dist/css/bootstrap.min.css" />
  <style>
    #form {
      width: 400px;
      margin: 0 auto;
      margin-top: 100px;
    }

    h1 {
      text-align: center;
    }

    input[type='submit'] {
      width: 100%;
    }
  </style>
</head>

<body>
  <form id="form">
    <h1>注册页面</h1>
    <div class="form-group">
      <input type="text" name="username" id="username" class="form-control input-lg" placeholder="用户名" required>
    </div>

    <div class="form-group">
      <input type="password" name="password" id="password" class="form-control input-lg" placeholder="密码" required>
    </div>

    <div class="form-group">
      <input type="text" name="nickname" id="nickname" class="form-control input-lg" placeholder="昵称" required>
    </div>

    <div class="form-group">
      <input type="submit" value="注册新用户" class="btn btn-primary btn-lg">
    </div>
  </form>

  <script src="/node_modules/jquery/dist/jquery.min.js"></script>
  <script>
    $(function () {
      $('#form').on('submit', function (e) {
        // 取消表单的默认提交事件
        e.preventDefault()
        // 当取消了表单的默认提交行为之后,手动发起Ajax请求,提交表单
        $.ajax({
          url: '/register',
          data: $('#form').serialize(),
          type: 'POST',
          dataType: 'json',
          success: function (result) {
            if (result.status !== 200) {
              return alert(result.msg)
            }
            location.href = '/login'
          }
        })
      })
    })
  </script>
</body>

</html>

controller

article.js

const moment = require('moment')
const conn = require('../db/index.js')
const marked = require('marked')

const showAddArticlePage = (req, res) => {
  // 如果用户没有登录,则不允许访问文章添加页
  if (!req.session.islogin) return res.redirect('/')
  res.render('./article/add.ejs', {
    user: req.session.user,
    islogin: req.session.islogin
  })
}

// 添加新文章
const addArticle = (req, res) => {
  const body = req.body
  // 如果在服务器端获取作者的Id,会有问题;如果文章编写了很长的时间,则 session 很可能会失效;
  // body.authorId = req.session.user.id
  body.ctime = moment().format('YYYY-MM-DD HH:mm:ss')
  // console.log(body)
  const sql = 'insert into blog_articles set ?'
  conn.query(sql, body, (err, result) => {
    if (err) return res.send({ msg: '发表文章失败!', status: 500 })
    // console.log(result)
    if (result.affectedRows !== 1) return res.send({ msg: '发表文章失败!', status: 501 })
    res.send({ msg: '发表文章成功!', status: 200, insertId: result.insertId })
  })
}

// 展示文章详情页
const showArticleDetail = (req, res) => {
  // 获取文章Id
  const id = req.params.id
  // 根据 Id 查询文章信息
  const sql = 'select * from blog_articles where id=?'
  conn.query(sql, id, (err, result) => {
    if (err) return res.send({ msg: '获取文章详情失败!', status: 500 })
    if (result.length !== 1) return res.redirect('/')
    // 在 调用 res.render 方法之前,要先把 markdown 文本,转为 html 文本
    const html = marked(result[0].content)
    // 把转换好的 HTML 文本,赋值给 content 属性
    result[0].content = html
    // 渲染详情页面
    res.render('./article/info.ejs', { user: req.session.user, islogin: req.session.islogin, article: result[0] })
  })
}

// 展示编辑页面
const showEditPage = (req, res) => {
  // 如果用户没有登录,则不允许查看文章编辑页面
  if (!req.session.islogin) return res.redirect('/')
  const sql = 'select * from blog_articles where id=?'
  conn.query(sql, req.params.id, (err, result) => {
    if (err) return res.redirect('/')
    if (result.length !== 1) return res.redirect('/')
    // 渲染详情页
    res.render('./article/edit.ejs', { user: req.session.user, islogin: req.session.islogin, article: result[0] })
  })
}

// 编辑文章
const editAticle = (req, res) => {
  const sql = 'update blog_articles set ? where id=?'
  conn.query(sql, [req.body, req.body.id], (err, result) => {
    if (err) return res.send({ msg: '修改文章失败!', status: 501 })
    if (result.affectedRows !== 1) return res.send({ msg: '修改文章失败!', status: 502 })
    res.send({ msg: 'ok', status: 200 })
  })
}

module.exports = {
  showAddArticlePage,
  addArticle,
  showArticleDetail,
  showEditPage,
  editAticle
}

index.js

const conn = require('../db/index.js')

// 展示首页页面
const showIndexPage = (req, res) => {
  // 每页显示3条数据
  const pagesize = 3
  const nowpage = Number(req.query.page) || 1
  console.log(nowpage)

  const sql = `select blog_articles.id, blog_articles.title, blog_articles.ctime, blog_users.nickname 
    from blog_articles 
    LEFT JOIN blog_users 
    ON blog_articles.authorId=blog_users.id
    ORDER BY blog_articles.id desc limit ${(nowpage - 1) * pagesize}, ${pagesize};
    select count(*) as count from blog_articles`
  conn.query(sql, (err, result) => {
    if (err) {
      return res.render('index.ejs', {
        user: req.session.user,
        islogin: req.session.islogin,
        // 文章列表
        articles: []
      })
    }

    // 总页数
    const totalPage = Math.ceil(result[1][0].count / pagesize)

    // 使用 render 函数之前,一定要保证安装和配置了 ejs 模板引擎
    res.render('index.ejs', {
      user: req.session.user,
      islogin: req.session.islogin,
      articles: result[0],
      // 总页数
      totalPage: totalPage,
      // 当前展示的是第几页
      nowpage: nowpage
    })
  })
}

module.exports = {
  showIndexPage
}

user.js

const moment = require('moment')
// 导入 数据库 操作模块
const conn = require('../db/index.js')
// 导入加密模块
const bcrypt = require('bcrypt')
// 定义一个 幂次
const saltRounds = 10 // 2^10

// 展示注册页面
const showRegisterPage = (req, res) => {
  // 注意:当 在 调用 模板引擎的 res.render 函数的时候, ./ 相对路径,是相对于 app.set('views') 指定的目录,来进行查找的
  res.render('./user/register.ejs', {})
}

// 展示登录页面
const showLoginPage = (req, res) => {
  res.render('./user/login.ejs', {})
}

// 注册新用户的请求处理函数
const reg = (req, res) => {
  // TODO: 完成用户注册的业务逻辑
  const body = req.body
  // 判断用户输入的数据是否完整
  if (body.username.trim().length <= 0 || body.password.trim().length <= 0 || body.nickname.trim().length <= 0) {
    return res.send({ msg: '请填写完整的表单数据后再注册用户!', status: 501 })
  }
  // 查询用户名是否重复
  const sql1 = 'select count(*) as count from blog_users where username=?'
  conn.query(sql1, body.username, (err, result) => {
    // 如果查询失败,则告知客户端失败
    if (err) return res.send({ msg: '用户名查重失败!', status: 502 })
    if (result[0].count !== 0) return res.send({ msg: '请更换其它用户名后重新注册!', status: 503 })
    // 执行注册的业务逻辑
    body.ctime = moment().format('YYYY-MM-DD HH:mm:ss')


    /**
     * 在执行Sql语句之前,先对用户提供的密码,做一层加密,防止密码被泄露之后,明文被盗取的清空
     *  bcrypt.hash('要被加密的密码', 循环的幂次, 回调函数)
     *  */

    bcrypt.hash(body.password, saltRounds, (err, pwd) => {
      // 加密失败了!!!
      if (err) return res.send({ msg: '注册用户失败!', status: 506 })
      // 把加密之后的新密码,赋值给 body.password
      body.password = pwd
      const sql2 = 'insert into blog_users set ?'
      conn.query(sql2, body, (err, result) => {
        if (err) return res.send({ msg: '注册新用户失败!', status: 504 })
        if (result.affectedRows !== 1) return res.send({ msg: '注册新用户失败!', status: 505 })
        res.send({ msg: '注册新用户成功!', status: 200 })
      })
    })

  })
}

// 登录的请求处理函数
const login = (req, res) => {
  
  // 1. 获取到表单中的数据
  const body = req.body
  // 2. 执行Sql语句,查询用户是否存在
  const sql1 = 'select * from blog_users where username=?'
  conn.query(sql1, [body.username], (err, result) => {
    // 如果查询期间,执行Sql语句失败,则认为登录失败!
    if (err) return res.send({ msg: '用户登录失败', status: 501 })
    // 如果查询的结果,记录条数不为 1, 则证明查询失败
    if (result.length !== 1) return res.send({ msg: '用户登录失败', status: 502 })

    // 对比 密码的方法
    // bcrypt.compare('用户输入的密码', '数据库中记录的密码', 回调函数)
    bcrypt.compare(body.password, result[0].password, (err, compireResult) => {
      if (err) return res.send({ msg: '用户登录失败', status: 503 })

      if (!compireResult) return res.send({ msg: '用户登录失败', status: 504 })

      // 把 登录成功之后的用户信息,挂载到 session 上
      req.session.user = result[0]
      // 把 用户登录成功之后的结果,挂载到 session 上
      req.session.islogin = true
      // 查询成功
      res.send({ msg: 'ok', status: 200 })
    })
  })
}

// 注销
const logout = (req, res) => {
  req.session.destroy(function () {
    // 使用 res.redirect 方法,可以让 客户端重新访问 指定的页面
    res.redirect('/')
  })
}

module.exports = {
  showRegisterPage,
  showLoginPage,
  reg,
  login,
  logout
}
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

落花流雨

你的鼓励将是我创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值