markdown测试内容
41期 Node.js 综合案例
准备
- 创建 github 仓库
- 克隆到本地
- 创建
.gitignore
,并配置忽略node_modules
目录 - npm init -y
- npm i express
创建 app.js
编写一个响应 hello world
的服务
// 0. 加载 Express
const express = require('express')
// 1. 调用 express() 得到一个 app
// 类似于 http.createServer()
const app = express()
// 2. 设置请求对应的处理函数
// 当客户端以 GET 方法请求 / 的时候就会调用第二个参数:请求处理函数
app.get('/', (req, res) => {
res.send('hello world')
})
// 3. 监听端口号,启动 Web 服务
app.listen(3000, () => console.log('app listening on port 3000!'))
抽取路由模块 router.js
// 0. 加载 express
const express = require('express')
// 1. 调用 express.Router() 创建一个路由实例
const router = express.Router()
// 2. 配置路由规则
router.get('/', (req, res) => {
res.send('hello world')
})
// 3. 导出路由对象
module.exports = router
// 4. 在 app.js 中通过 app.use(路由对象) 挂载使之生效
设计路由表
请求方法 | 请求路径 | 说明 |
---|---|---|
GET | / | 渲染首页 |
GET | /signin | 渲染登陆页面 |
POST | /signin | 处理登陆请求 |
GET | /signup | 渲染注册页面 |
POST | /signup | 处理注册请求 |
POST | /signout | 处理退出请求 |
GET | /topic/create | 渲染发布话题页面 |
POST | /topic/create | 处理发布请求请求 |
GET | /topic/:topicID | 渲染话题详情页面 |
GET | /topic/:topicID/edit | 渲染编辑话题页面 |
POST | /topic/:topicID/edit | 处理编辑话题请求 |
POST | /topic/:topicID/delete | 处理删除话题请求 |
POST | 发表评论 | |
修改评论 | ||
删除评论 | ||
个人主页 | ||
基本信息 | ||
设置 |
根据路由表配置路由规则
// 首页路由
router
.get('/', index.showIndex)
// 用户路由
router
.get('/signin', user.showSignin)
.post('/signin', user.signin)
.get('/signup', user.showSignup)
.post('/signup', user.signup)
.post('/signout', user.signout)
// 话题相关
router
.get('/topic/create', topic.showCreate)
.post('/topic/create', topic.create)
.get('/topic/:topicID', topic.show)
.get('/topic/:topicID/edit', topic.showEdit)
.post('/topic/:topicID/edit', topic.edit)
.post('/topic/:topicID/delete', topic.delete)
创建控制器(处理函数)模块
- 创建
controllers
目录 - 在
controllers
目录中根据业务划分创建处理函数模块
index.js
文件:
exports.showIndex = (req, res) => {
res.send('showIndex')
}
user.js
文件:
exports.showSignin = (req, res) => {
res.send('showSignin')
}
exports.signin = (req, res) => {
res.send('signin')
}
exports.showSignup = (req, res) => {
res.send('showSignup')
}
exports.signup = (req, res) => {
res.send('singup')
}
exports.signout = (req, res) => {
res.send('signout')
}
topic.js
文件:
exports.showCreate = (req, res) => {
res.send('showCreate')
}
exports.create = (req, res) => {
res.send('create')
}
exports.show = (req, res) => {
res.send('show')
}
exports.showEdit = (req, res) => {
res.send('showEdit')
}
exports.edit = (req, res) => {
res.send('edit')
}
exports.delete = (req, res) => {
res.send('delete')
}
然后访问几个路由规则测试一下。
拷贝静态资源到项目中
下载项目需要的静态资源:
git clone https://github.com/lipengzhou/itcast-resource.git
把下面的目录拷贝到项目中:
- public/
- views/
- ithub.sql
配置模板引擎,渲染发送页面
- 配置
art-template
模板引擎 - 渲染登陆、注册、首页、创建话题… 页面
页面渲染出来,发现没有样式。
公开静态资源、下载第三方包
下载:
npm i [email protected] jquery
公开:
app.use('/public', express.static('./public/'))
app.use('/node_modules', express.static('./node_modules/'))
再测试页面样式。
用户注册
前端页面处理
// 异步提交表单
// 1. 监听表单的 submit 提交事件
// 设置事件处理函数
// 2. 在事件处理函数中
// 阻止表单默认的提交行为
// 采集表单数据
// 表单验证
// https://github.com/jquery-validation/jquery-validation
// 自己尝试一下这个 jQuery 表单验证插件
// 发起 ajax 异步请求
// 根据服务端响应结果做交互处理
$('#signup_form').on('submit', handleSubmit)
function handleSubmit(e) {
e.preventDefault()
var formData = $(this).serialize()
$.ajax({
url: '/signup',
type: 'post',
data: formData,
dataType: 'json',
success: function (data) {
console.log(data)
switch(data.code) {
case 200:
window.location.href = '/'
break
case 1:
window.alert('邮箱已存在,请更换重试')
break
case 2:
window.alert('昵称已存在,请更换重试')
break
case 500:
window.alert('服务器内部错误,请稍后重试')
break
}
// 判断 data 中的数据是否成功,成功则跳转到首页
},
error: function () {
}
})
}
服务端后台处理
- 接收处理
- 处理请求
- 发送响应
// 1. 接收获取客户端提交的表单数据
// 配置 body-parser 插件用来解析获取表单 POST 请求体数据
// 2. 数据验证
// 普通数据校验,例如数据有没有,格式是否正确
// 业务数据校验,例如校验用户名是否被占用
// 这里校验邮箱和昵称是否被占用
// 邮箱和昵称都校验没有问题了,可以注册了
// 3. 当数据验证都通过之后,在数据库写入一条新的用户数据
// 4. 注册成功,发送成功响应
exports.signup = (req, res) => {
// 1. 接收获取客户端提交的表单数据
// 配置 body-parser 插件用来解析获取表单 POST 请求体数据
const body = req.body
// 2. 数据验证
// 普通数据校验,例如数据有没有,格式是否正确
// 业务数据校验,例如校验用户名是否被占用
// 这里校验邮箱和昵称是否被占用
// 校验邮箱是否被占用
connection.query(
'SELECT * FROM `users` WHERE `email`=?', [body.email],
(err, results) => {
if (err) {
return res.send({
code: 500,
message: err.message // 把错误对象中的错误消息发送给客户端
})
}
if (results[0]) {
return res.send({
code: 1,
message: '邮箱已被占用了'
})
}
// 校验昵称是否存在
connection.query(
'SELECT * FROM `users` WHERE `nickname`=?',
[body.nickname],
(err, results) => {
if (err) {
return res.send({
code: 500,
message: err.message // 把错误对象中的错误消息发送给客户端
})
}
if (results[0]) {
return res.send({
code: 2,
message: '昵称已被占用'
})
}
// 邮箱和昵称都校验没有问题了,可以注册了
// 3. 当数据验证都通过之后,在数据库写入一条新的用户数据
// 添加更新时间
// moment 是一个专门处理时间的 JavaScript 库,这个库既可以在浏览器使用,也可以在 Node 中使用
// JavaScript 被称之为全栈式语言
// moment() 用来获取当前时间
// format() 方法用来格式化输出
body.createdAt = moment().format('YYYY-MM-DD HH:mm:ss')
const sqlStr = 'INSERT INTO `users` SET ?'
connection.query(sqlStr, body, (err, results) => {
if (err) {
// 服务器异常,通知客户端
return res.send({
code: 500,
message: err.message
})
}
// 注册成功,告诉客户端成功了
res.send({
code: 200,
message: 'ok'
})
// 用户注册成功之后需要跳转到首页
// 1. 服务端重定向(只对同步请求有效)
// res.send('注册成功')
// 2. 让客户端自己跳
// res.redirect('/')
})
}
)
}
)
}
提取 db-helper.js
文件模块
const mysql = require('mysql')
const connection = mysql.createConnection({
host: 'localhost', // 要连接的主机名
user: 'root', // 要连接的数据库的用户名
password: '123456', // 数据库密码
database: 'ithub' // 数据库
})
module.exports = connection
划分 MVC
- 在项目根目录下创建
models
目录 - 在
models
目录中分别创建user.js
、topic.js
、comment.js
等文件 - 把所有的数据库操作都封装到 Model 对应的业务模块中
例如,model/user.js
:
// 我们把用户相关的数据库操作方法都封装到当前模块
const db = require('../controllers/db-helper')
exports.findAll = callback => {
const sqlStr = 'SELECT * FROM `users`'
db.query(sqlStr, (err, results) => {
if (err) {
return callback(err)
}
callback(null, results)
})
}
exports.getByEmail = (email, callback) => {
const sqlStr = 'SELECT * FROM `users` WHERE `email`=?'
db.query(
sqlStr,
[email],
(err, results) => {
if (err) {
return callback(err)
}
callback(null, results[0])
}
)
}
exports.getByNickname = (nickname, callback) => {
const sqlStr = 'SELECT * FROM `users` WHERE `nickname`=?'
db.query(
sqlStr,
[nickname],
(err, results) => {
if (err) {
return callback(err)
}
callback(null, results[0])
}
)
}
exports.create = (user, callback) => {
const sqlStr = 'INSERT INTO `users` SET ?'
db.query(
sqlStr,
user,
(err, results) => {
if (err) {
return callback(err)
}
callback(null, results)
}
)
}
用户登陆(客户端处理)
$('#signin_form').on('submit', handleSubmit)
function handleSubmit (e) {
e.preventDefault()
var formData = $(this).serialize()
$.post('/signin', formData, function (data) {
switch(data.code) {
case 200:
window.location.href = '/'
break
case 1:
window.alert('用户名不存在')
break
case 2:
window.alert('密码不正确')
break
}
})
}
用户登陆(没有存储登陆状态)
exports.signin = (req, res) => {
const body = req.body
// TODO: 基本数据校验
User.getByEmail(body.email, (err, user) => {
if (err) {
return res.send({
code: 500,
message: err.message
})
}
// 如果用户不存在,告诉客户端
if (!user) {
return res.send({
code: 1,
message: '用户不存在'
})
}
// 如果用户存在了,则校验密码
if (md5(body.password) !== user.password) {
return res.send({
code: 2,
message: '密码不正确'
})
}
// TODO: 使用Session保存会话状态
// 代码执行到这里,就意味着验证通过,可以登陆了
res.send({
code: 200,
message: '恭喜你,登陆成功'
})
})
}
用户登陆(使用 Session 存储登陆状态)
express 需要安装配置 express-session 插件才可以使用 Session 功能。
安装:
npm i express-session
在 app.js
入口模块中配置:
// ...
const session = require('express-session')
// ...
// 配置 session
// 只要配置了该插件,则在后续请求的任何处理函数中都可以使用 req.session 来访问或者设置 Session 数据了
// req.session 就是一个对象,所以:
// 读取 Session 数据:req.session.xxx
// 保存 Session 数据:req.session.xxx = xxx
app.use(session({
secret: 'keyboard cat', // 加密规则私钥,用来保证不同的丰巢快递柜的密码规则都是不一样的,
resave: false,
saveUninitialized: true // 是否在初始化的时候就给客户端发送一个 Cookie
}))
// ...
接下来找到 controllers/user.js
文件中,将 sign
方法修改为:
// ...
// TODO: 使用Session保存会话状态
req.session.user = user
// ...
根据用户登陆状态动态展示网页头部内容
- 登陆前:显示登陆和注册按钮
- 登陆后:显示个人中心和发起按钮
找到 controllers/index.js
文件,将 showIndex
方法修改为:
exports.showIndex = (req, res) => {
res.render('index.html', {
user: req.session.user // 把会话用户信息传递到模板中,模板就可以使用当前登陆的用户了
})
}
然后找到 views/_incldes/header.html
文件进行条件判定渲染:
<!-- 如果用户已登陆,则显示如下内容块 -->
{{ if user }}
<a class="btn btn-default navbar-btn" href="/topic/new">发起</a>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><img width="20" height="20" src="../public/img/avatar-max-img.png" alt=""> <span class="caret"></span></a>
<ul class="dropdown-menu">
<li class="dropdown-current-user">
当前登录用户: {{ user.nickname }}
</li>
<li role="separator" class="divider"></li>
<li><a href="#">个人主页</a></li>
<li><a href="/settings/profile">设置</a></li>
<li><a href="/signout">退出</a></li>
</ul>
</li>
{{ else }}
<!-- 如果用户未登录,则显示该内容块 -->
<a class="btn btn-primary navbar-btn" href="/signin">登录</a>
<a class="btn btn-success navbar-btn" href="/signup">注册</a>
{{ /if }}
持久化存储 Session 数据到 MySQL 数据库
默认Session是内存存储,服务器一旦重启就会导致Session数据丢失,用户就需要重新登陆。 为了解决这个,我们只需要将 Session 数据持久化存储到数据库中就可以了。
这里我们需要使用一个插件:express-mysql-session。
安装:
npm i express-mysql-session
在 app.js
中配置如下:
const express = require('express')
const bodyParser = require('body-parser')
const router = require('./router')
const session = require('express-session')
const MySQLStore = require('express-mysql-session')(session)
const options = {
host: 'localhost',
port: 3306,
user: 'root',
password: '123456',
database: 'ithub'
}
const sessionStore = new MySQLStore(options)
const app = express()
// 配置 Session 插件
// 只要配置了该插件,则在后续请求的任何处理函数中都可以使用 req.session 来访问或者设置 Session 数据了
app.use(session({
key: 'session_cookie_name',
secret: 'keyboard cat',
resave: false,
saveUninitialized: true,
store: sessionStore // 将 Session 数据存储到数据库中(默认是内存存储)
}))
// ...
用户退出
exports.signout = (req, res) => {
// 1. 清除登陆状态
delete req.session.user
// 2. 跳转到登录页
res.redirect('/signin')
}
发布话题
- 处理前端页面
- 处理服务端
处理创建话题的客户端
$('#form').on('submit', handleSubmit)
function handleSubmit(e) {
e.preventDefault()
var formData = $(this).serialize()
$.post('/topic/create', formData, function (data) {
console.log(data)
})
}
编写话题数据库操作模块 models/topic/js
// 我们把话题相关的数据库操作方法都封装到当前模块
const db = require('../controllers/db-helper')
/**
* 获取所有话题列表
* @param {Function} callback 回调函数
* @return {undefined} 没有返回值
*/
exports.findAll = callback => {
const sqlStr = 'SELECT * FROM `topics`'
db.query(sqlStr, (err, results) => {
if (err) {
return callback(err)
}
callback(null, results)
})
}
/**
* 插件一个话题
* @param {Object} topic 话题对象
* @param {Function} callback 回调函数(返回值都是通过回调函数来接收)
* @return {undefined} 没有返回值
*/
exports.create = (topic, callback) => {
const sqlStr = 'INSERT INTO `topics` SET ?'
db.query(
sqlStr,
topic,
(err, results) => {
if (err) {
return callback(err)
}
callback(null, results)
}
)
}
/**
* 根据话题id更新话题内容
* @param {Object} topic 要更新话题对象
* @param {Function} callback 回调函数
* @return {undefined} 没有返回值
*/
exports.updateById = (topic, callback) => {
const sqlStr = 'UPDATE `topics` SET `title`=?, `content`=? WHERE `id`=?'
db.query(
sqlStr,
[
topic.title,
topic.content,
topic.id
],
(err, results) => {
if (err) {
return callback(err)
}
callback(null, results)
}
)
}
/**
* 根据话题id删除某个话题
* @param {Number} id 话题id
* @param {Function} callback 回调函数
* @return {undefined} 没有返回值
*/
exports.deleteById = (id, callback) => {
const sqlStr = 'DELETE FROM `topics` WHERE `id`=?'
db.query(
sqlStr,
[
id
],
(err, results) => {
if (err) {
return callback(err)
}
callback(null, results)
}
)
}
处理创建话题服务端
exports.create = (req, res) => {
const body = req.body
body.userId = req.session.user.id // 话题的作者,就是当前登陆用户
body.createdAt = moment().format('YYYY-MM-DD HH:mm:ss') // 话题的创建时间
Topic.create(body, (err, results) => {
if (err) {
return res.send({
code: 500,
message: err.message
})
}
res.send({
code: 200,
message: '创建话题成功了'
})
})
}
配置全局错误处理中间件
在 app.js
挂载路由之后添加以下代码:
// ...
app.use(router)
// 一个特殊的中间件:错误处理中间件
// 一般一个 Express 应用,配一个就够了
// 作用:全局统一错误处理
app.use((err, req, res, next) => {
res.send({
code: 500,
message: err.message
})
})
// ...
配置处理 404 页面
查看话题
删除话题
编辑话题
1 回复
设计得好精致,可是我看到jQuery了。😂😂😂😂