数据库表
blog_users
名 | 类型 | 长度 | 其他 |
---|---|---|---|
id | int | 11 | 主键 |
username | varchar | 255 | |
password | varchar | 255 | |
nickname | varchar | 255 | |
ctime | varchar | 255 | |
iddel | tinyint | 4 | 0未删除 1删除 |
博客项目:blog
博客,使用{
传统的开发方式
},后端开发人员,一边定义接口,一边写页面调用接口;
- 初始化项目:
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">«</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">»</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
}