一、初始 Node.js
node.js是一个基于Chrome V8引擎的 JavaScript 运行环境
官网:https://nodejs.org/zh-cn
查看已安装的 Node.js 的版本号
- 打开终端,在终端输入命令 node –v 后,按下回车键,即可查看已安装的 Node.js 的版本号。
终端使用node命令执行js文件(推荐)
- vscode打开代码文件夹
- 在文件上shift+右键–> 在终端中打开
- 好处是,终端中执行node命令的文件夹,已经定位好了,我们不用切换文件夹了
- 终端中,
node js文件
,回车
二、fs 文件系统模块
fs 模块是 Node.js 官方提供的、用来操作文件的模块。它提供了一系列的方法和属性,用来满足用户对文件的操作需求。
例如:
- fs.readFile() 方法,用来读取指定文件中的内容
- fs.writeFile() 方法,用来向指定的文件中写入内容
const fs = require('fs')
2.1 fs.readFile() 语法格式
2.2 fs.writeFile() 语法格式
2.3 成绩录入案例
const fs = require('fs')
fs.readFile('../files/成绩.txt', 'utf8', function(err, dataStr){
if (err) {
return console.log('读取文件失败!' + err.message);
}
// console.log('读取文件成功!'+ dataStr);
// 把成绩字符串按照 空格 划分为数组
const arrOld = dataStr.split(' ')
// 循环追加每一项到新数组中,对每一项进行“=”替换操作
const arrNew = []
arrOld.forEach(item => {
arrNew.push(item.replace('=', ': '))
});
// 把数组转化为 回车符 划分的字符串
const newStr = arrNew.join('\r\n')
// console.log(newStr);
fs.writeFile('../files/成绩ok.txt', newStr, function(res){
if (res) {
return console.log('成绩录入失败' + res.message);
}
console.log('成绩录入成功!\n');
})
})
2.4 文件路径动态拼接问题
方法一:文件位置可以写绝对路径
'/'='\\'
方法二:_ _dirname 表示当前文件所在目录
可以拼接上相对文件路径
三、path 模块
处理路径模块,有一系列方法和属性
例如:
-
path.join() 方法:将多个路径片段拼接成一个完整的字符串 **
-
path.basename() 方法:从路径字符串中,找文件的文件名
-
path.extname() 方法:从路径字符串中,找文件的后缀
3.1 path.join()语法格式
3.2 path.basename()语法格式
3.3 path.extname() 语法格式
事例
const path = require('path')
const pathStr1 = path.join('/a', '/b/c', '../', './d', 'e')
console.log(pathStr1); // a\b\d\e
// 尽量使用join
const pathStr2 = path.join(__dirname, '../files/1.txt')
console.log(pathStr2);
let fpath = '/a/b/c/inde.html'
let fname1 = path.basename(fpath)
console.log(fname1);
let fname2 = path.basename(fpath, '.html')
console.log(fname2);
// 找字符串中,最后一个点及之后的字符
console.log( path.extname('index.html') ); // .html
console.log( path.extname('index..html') ); // .html
console.log( path.extname('a.b.c.d.html') ); // .html
console.log( path.extname('asdfas/asdfa/a.b.c.d.html') ); // .html
console.log( path.extname('adf.adsf') ); // .adsf
3.4 时钟分离案例
时钟.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>时钟首页</title>
<style>
.box {
width: 400px;
height: 250px;
border: 5px solid black;
margin: 100px auto;
line-height: 250px;
text-align: center;
font-size: 70px;
font-weight: 700;
background-color: pink;
}
</style>
</head>
<body>
<div class="box"></div>
<script>
setInterval(function () {
function getTimer() {
var time = new Date();
var h = time.getHours();
h = h < 10 ? '0' + h : h;
var m = time.getMinutes();
m = m < 10 ? '0' + m : m;
var s = time.getSeconds();
s = s < 10 ? '0' + s : s;
return h + ':' + m + ':' + s;
}
var box = document.querySelector('.box');
box.innerHTML = getTimer();
})
</script>
</body>
</html>
分离.js
// 1.1导入fs
const fs = require('fs')
// 1.2导入path
const path = require('path')
//1.3定义正则表达式,分别匹配<style></style>和<script></script>标签
// [\s\S]表示匹配空格和非空格
const regStyle = /<style>[\s\S]*<\/style>/
const regScript = /<script>[\s\S]*<\/script>/
// 2.1调用fs.readFile()方法读取文件
fs.readFile(path.join(__dirname, '../01-时钟.html'), 'utf8', function (err, dataStr) {
// 2.2文件读取失败
if (err) {
console.log('文件读取失败' + err.message)
}
// 2.3读取文件成功后,调用对应的三个方法,分别解析出css,js,html文件
resolveCSS(dataStr)
resolveJS(dataStr)
resolveHTML(dataStr)
})
// 3.1定义处理css样式的方法
function resolveCSS(htmlStr) {
// 3.2使用正则提取需要的内容
const r1 = regStyle.exec(htmlStr)
// 3.3将提取出来的样式字符串,进行字符串的replace替换操作
const newCSS = r1[0].replace('<style>', '').replace('</style>', '')
// console.log(newCss)
// 3.4调用fs.writeFile()方法,将提取的样式,写入到clock目录中index.css的文件里边
fs.writeFile(path.join(__dirname, '../clock/index.css'), newCSS, function (err) {
if (err) return console.log('写入css样式失败!' + err.message)
console.log('写入样式成功!')
})
}
// 4.1定义处理js脚本的方法
function resolveJS(htmlStr) {
// 4.2使用正则提取对应的<script></script>标签
const r2 = regScript.exec(htmlStr)
//4.3将提取出来的样式字符串,进行字符串的replace替换操作
const newJS = r2[0].replace('<script>', '').replace('</script>', '')
// console.log(newJS)
// 4.4调用fs.writeFile()方法,将提取的脚本,写入到clock目录中index.js的文件里边
fs.writeFile(path.join(__dirname, '../clock/index.js'), newJS, function (err) {
if (err) return console.log('写入js脚本失败!' + err.message)
console.log('写入脚本成功!')
})
}
// 5.1定义处理HTMl结构的方法
function resolveHTML(htmlStr) {
// 5.2将内嵌的style标签和script标签替换为外联的link和script标签
const newHTML = htmlStr.replace(regStyle, '<link rel="stylesheet" href="index.css"/>').replace(regScript, '<script src="index.js"></script>')
// 5.3写入index.html这个文件
fs.writeFile(path.join(__dirname, '../clock/index.html'), newHTML, function (err) {
if (err) return console.log('写入html失败!' + err.message)
console.log('写入html成功!')
})
}
四、querystring模块
- querystring.parse() – 把查询字符串转成对象
- querystring.stringify() – 把对象转成查询字符串
const querystring = require('querystring');
// querystring -- 查询字符串
/**
* 什么是查询字符串
* 发送请求的时候,携带的字符串参数
* booksname=xxx&author=xxx
*/
let str = 'id=3&bookname=xiyouji&author=tangseng';
// 一:把查询字符串转成对象
let obj = querystring.parse(str);
// console.log(obj); // {id: '3', bookname: 'xiyouji', author: 'tangseng'}
// 二:把对象转成查询字符串
console.log( querystring.stringify(obj) ); // id=3&bookname=xiyouji&author=tangseng
五、http模块
5.1 搭建服务器的步骤
① 导入 http 模块
② 创建 web 服务器实例
③ 为服务器实例绑定 request 事件,监听客户端的请求
④ 启动服务器
// ① 导入 http 模块
const http = require('http')
// ② 创建 web 服务器实例
const server = http.createServer()
// 只要客户端发来请求,则会触发下面的事件
server.on('request', (req, res) =>{
console.log('Someone visit our web server');
})
// ④ 启动服务器
server.listen(8000, ()=>{
// 服务器启动之后,会触发这个回调函数
console.log('http server running at http://127.0.0.1:8000');
})
req 请求对象
访问与客户端相关的数据或属性
// req 对象,是根据 http.IncomingMessage类得来的对象
// 通过这个对象,我们可以得到请求的相关信息
req.url // 表示请求的url
req.method // 表示请求方式
req.headers // 表示请求头
res 响应对象
访问与服务器相关的数据或属性
// 所有和响应相关的信息,都通过res来完成。res这里是response的意思。
// console.log(res instanceof http.ServerResponse); // true
// res.end(); // 用于做出响应
// res.setHeader(name, value); // 设置响应头
// res.statusCode // 设置响应状态码
// res.writeHead(状态码, {响应头})
// 告诉浏览器,响应的数据是什么类型、什么编码
res.setHeader('Content-Type', 'text/html; charset=utf-8');
// res.statusCode = 404; // 随便设置的状态码,实际中应该是200
res.writeHead(200, {
// key: value
'Content-Type': 'text/html; charset=utf-8',
'Author': 'LaoTang', // 随便写的,实验一下。值不能用中文
});
res.end('你好浏览器,你的请求我收到了,这是给你的回应');
当调用res.end()方法,向客户端发送中文内容的时候,会出现乱码问题,此时需要手动设置内容的编码格式
res.setHeader('Content-Type', 'text/html; charset=utf-8');
5.2 访问时钟案例
const fs = require('fs')
const path = require('path')
const http = require('http')
const server = http.createServer()
server.on('request', (req, res) => {
let url = req.url
let fpath = ''
// console.log(url);
if (url === '/' || url === '/index') {
fpath = path.join(__dirname, '../clock/index.html')
} else {
fpath = path.join(__dirname, '../clock', url)
}
// console.log(fpath);
fs.readFile(fpath, 'utf8', (err, dataStr) => {
if (err) return res.end('<h1>404 Not Found!</h1>')
res.end(dataStr)
})
})
server.listen(8000, () => {
console.log('http server running at http://127.0.0.1:8000');
})
六、模块化
把系统划分为若干模块的过程,模块是可组合、分解和更换的单元
要遵守模块化规范
好处:
① 提高了代码的复用性
② 提高了代码的可维护性
③ 可以实现按需加载
6.1 Node.js中的模块化
- 内置模块(官方提供)
- 自定义模块(用户自定义的 .js 文件)
- 第三方模块
6.1.1 加载模块
require() 方法,加载需要的内置模块、用户自定义模块和第三方模块
注意:加载用户自定义模块,可以省略 .js 后缀名
6.1.2 模块作用域
防止全局变量污染
6.1.3 向外共享模块作用域中的成员
在每个 .js 自定义模块中都有一个 module 对象,它里面存储了和当前模块有关的对象
可以使用 module.exports 对象。将模块内的成员共享出去,供外界使用。默认 module.exports={}
注意:require() 方法导入模块时,导入结果,永远以module.exports 指向的对象为准
还可以使用 exports 对象来向外共享模块作用域中的成员,他和 module.exports 指向同一个对象。最终也要以module.exports 指向的对象为准
注意:require() 模块得到的永远是 module.exports
6.1.4 Node.js 中的模块化规范
Node.js遵循CommonJS模块化规范,CommonJS 规定了 模块的特性
和 各模块之间如何相互依赖
CommoneJS规定:
6.2 npm 与包
npm,inc. 公司,包搜索平台:https://www.npmjs.com/
包下载:https://registry.npmjs.org/
npm包管理工具:Node Package Manager
npm -v // 查看已安装版本号
在项目中安装指定包,默认最新版本的包;指定版本的在包名后用 @ 符号
npm install 包的完整名称
简写:
npm i 完整的包名称
npm install 包的完整名称@2.22。2
6.2.1 包管理配置文件
初次安装后
注意:在今后的项目开发中,一定要把node_modules文件夹添加到gitignore忽略文件中,别人可以根据package.json配置文件来下载。
npm init -y
// 作用:在执行命令所处的目录中,快速新建 package.json 文件
注意:只能在英文目录下成功运行(不能使用中文,也不能使用空格);只要执行 npm -install 包名
就会自动把包版本号和名称记录到package.json中
注意:可以直接调用 npm install
(简写npm i
)命令,直接安装package.json文件中的dependencies下的所有包
卸载包:
npm uninstall 包名
简写(npm un 包名)
npm uninstall --save 包名
删除的同时也会把依赖信息全部删除
简写(npm un 包名)
安装包到devDependencies节点(开发会用到):
npm i 包名 -D // 简写
npm i 包名 --save-dev // 完整
npm help
- 查看使用帮助
npm 命令 --help
- 查看具体命令的使用帮助(npm uninstall --help)
6.2.2 解决下载包速度慢的问题
淘宝 NPM 镜像服务器:https://registry.npmmirror.com/
① 切换 npm 的下载包镜像源:
// 查看当前的下包镜像源
npm config get registry
// 切换为淘宝 NPM 镜像服务器下载包
npm config set registry=https://registry.npmmirror.com/
#查看npm配置信息
npm config list;
② nrm工具
方便切换包下载镜像源
// 通过 npm 包管理工具,将nrm安装为全局可用工具
npm i nrm -g
// 查看所有可用镜像源
nrm ls
// 将下包的镜像源切换为 taobao 镜像
nrm use taobao
nrm错误
get-ExecutionPolicy // 查看当前策略 Restricted
set-ExecutionPolicy RemoteSigned // 设置为 RemoteSigned
// 重新安装
npm install -g nrm open@8.4.2 -save
// 应该使用 open 的 CommonJs规范的包 ,现在 open v9.0.0 是 ES Module 版本的包
③ 安装淘宝的cnpm:
npm install -g cnpm --registry=https://registry.npmmirror.com/
#在任意目录执行都可以
#--global表示安装到全局,而非当前目录
#--global不能省略,否则不管用
npm install --global cnpm
安装包的时候把以前的npm
替换成cnpm
。
#走国外的npm服务器下载包,速度比较慢
npm install 包名;
#使用cnpm就会通过淘宝的服务器来下载包
cnpm install 包名;
如果不想安装cnpm又想使用淘宝的服务器来下载:
npm install 包名 --registry=https://registry.npmmirror.com/
④ 全局安装nodemon模块
-
安装命令
npm i nodemon -g
-
nodemon的作用:
-
代替node命令,启动服务的,当更改代码之后,nodemon会自动帮我们重启服务。
6.2.3 包分类
项目包:开发依赖包(devDependencies节点中)、核心依赖包(dependencies节点中)
全局包:一般是有工具性质的包,可以根据官方使用说明来安装
npm install -g 全局包名
npm uninstall -g 全局包名
i5ting_toc 全局包
可以把md文档转为 html 页面的小工具
npm install -g i5ting_toc // 安装为全局包
// md 转化为 html
i5ting_toc -f 要转换的md文件路径 -o
6.2.4 开发属于自己的包
规范的包内部结构
我的包:index.js(可划分到src根目录下)、package.json(name、version、main、description、keywords、license)、README.md(安装、导入、调用、开源协议)
特别地:es6展开运算符 ...
,展开所有属性
登录:npm login
或者npm adduser
发布包:npm publish
(注意包名不能重复、切换到包的根目录)
删除包:npm unpublish 包名 --force
(删除已发布的包)
6.3 模块的加载机制
模块在第一次加载后会被缓存
内置模块:优先加载
自定义模块:
第三方模块:
目录模块:
七、Express
Express是基于Node.js平台的快速、开放、极简的Web开发框架(作用和内置http模块类似)
可以方便快速创建 Web网站 和 API接口 的服务器
// 调用
const express = require('express')
// 创建服务器
const app = express()
// 监听GET请求
app.get('/user', (req, res) => {
// 调用 express 提供的 res.send() 向客户端响应一个 JSON 对象
res.send({ name: 'ssy', age: 20, gender: '男' })
})
// 监听POST请求
app.post('/user', (req, res) => {
// 调用 express 提供的 res.send() 向客户端响应一个 文本字符串
res.send('请求成功!')
})
app.get('/', (req, res) => {
// req.query 可以获取客户端发送过来的 查询参数
// 默认是一个空对象
console.log(req.query);
res.send(req.query)
})
// :id 为动态参数, id 可以为别的 ids 等等;可以有多个动态参数
app.get('/user/:ids/:name', (req, res) => {
// req.params 获取URL中动态匹配的参数,默认也是一个空对象
console.log(req.params);
res.send(req.params)
})
// 用将 express.static() clock 文件夹对外提供为静态资源,但是访问路径不能有clock,只能是它的子文件
app.use(express.static('./clock'))
// 如果访问index.html,会根据先后顺序,先调用clock中的,而不调用files中的
app.use(express.static('./files'))
// 挂载路径前缀,访问的是必须加 /clock
app.use('/files', express.static('./files'))
// 启动监听
app.listen(8001, () => {
console.log('express server runing at http://127.0.0.1:8001');
})
7.1 Express 路由
客户端与服务器之间的映射关系
7.1.1 简单挂载
分三部分:请求的类型、请求的URL地址、处理函数
app.METHOD(PATH, HANDLER)
7.1.2 模块化路由
为了方便对路由进行模块化的管理,Express 不建议将路由直接挂载到 app 上,而是推荐将路由抽离为单独的模块。
将路由抽离为单独模块的步骤如下:
-
创建路由模块对应的 .js 文件
- 创建router/login.js 存放 登录、注册、验证码三个路由
- 创建router/heroes.js 存放 和英雄相关的所有路由
-
调用 express.Router() 函数创建路由对象
const express = require('express');
const router = express.Router();
- 向路由对象上挂载具体的路由
// 把app换成router,比如
router.get('/xxx/xxx', (req, res) => {});
router.post('/xxx/xxx', (req, res) => {});
- 使用 module.exports 向外共享路由对象
module.exports = router;
- 使用 app.use() 函数注册路由模块 – app.js (
app.use()作用就是注册全局中间键件
)
// app.js 中,将路由导入,注册成中间件
const login = require('./router/logon.js');
app.use(login)
// app.use(require('./router/heroes.js'));
app.use( require(path.join(__dirname, 'router', 'heores.js')) );
7.2 Express 中间件
对请求进行预处理,本质上就是function处理函数
注意:中间件函数的形参列表中,必须包含 next 参数(代表一个函数) ,而路由处理函数只包含 req 和 res。
next函数:实现多个中间件连续调用
7.2.1 定义中间件函数
使中间件全局生效
app.use(中间件函数)
不使用
app.use(中间件函数)
定义的中间件,就是局部生效的中间件可以将其添加到路由中
const express = require('express')
const app = express()
const mv1 = function (req, res, next) {
console.log('这是第一个局部生效的中间件函数');
// 把流转关系,转交给下一个中间件或路由
next()
}
const mv2 = function (req, res, next) {
console.log('这是第二个局部生效的中间件函数');
// 把流转关系,转交给下一个中间件或路由
next()
}
// // 将 mv 注册为全局生效的中间件
// app.use(mv)
// 或者直接写中间件,可以拼接路径
app.use((req, res, next) => {
console.log('这是第一个最简单的中间件函数');
// 获取请求到达时间
const time = Date.now()
// 为req对象挂载自定义属性,从而共享给后面的路由
req.startTime = time
// 把流转关系,转交给下一个中间件或路由
next()
})
// 定义多个全局中间件
app.use((req, res, next) => {
console.log('这是第二个最简单的中间件函数');
// 把流转关系,转交给下一个中间件或路由
next()
})
app.get('/', (req, res) => {
console.log('调用了GET这个路由!');
res.send('Home page.' + req.startTime)
})
app.get('/user1', mv1, (req, res) => {
console.log('调用了GET这个路由!');
res.send('Home page.' + req.startTime)
})
// mv1,mv2 还可以写成 [mv1,mv2]
app.get('/user2', mv1, mv2, (req, res) => {
console.log('调用了GET这个路由!');
res.send('Home page.' + req.startTime)
})
app.post('/', (req, res) => {
console.log('调用了POST这个路由!');
res.send('User page.' + req.startTime)
})
// 启动监听
app.listen(8003, () => {
console.log('http://127.0.0.1:8003');
})
7.2.2 中间件作用
共享同一个req和res,可以为其挂载属性和方法
可以定义多个中间件(全局、局部个数随意)
7.2.3 中间件注意事项
7.2.4 中间件分类
① 应用级别中间件
- 绑定到
app.use()
上,或者app.get()
和app.post()
② 路由级别中间件
- 绑定到 router 实例上
③ 错误级别中间件
- 捕获错误
- 必须有四个形参,(err, req, res, next)
- 必须定义在所有路由之后
const express = require('express')
const app = express()
// 定义路由
app.get('/', (req, res) => {
// 人为制造错误
throw new Error('服务器内部发生了错误!')
res.send('Home Paeg!')
})
// 定义错误级别的中间件,捕获所有异常
app.use((err, req, res, next) => {
console.log('发生了错误!' + err.message);
res.send('Error!' + err.message)
})
app.listen(8004, ()=>{
console.log('http://127.0.0.1:8004');
})
④ Express 内置的中间件
const express = require('express')
const app = express()
// 配置中间件
// express.json() 解析表单中的JSON数据
app.use(express.json())
// express.urlencoded() 解析表单中的 urlencoded 格式数据(键值对)
app.use(express.urlencoded({ extended: false }))
// 定义路由
app.get('/', (req, res) => {
// 使用 req.body 来接收客户端发送的 请求体数据
// 默认情况下,如果不配置解析表单数据的中间件,则 req.body 默认等于 underfined
console.log(req.body);
res.send('Home Paeg!')
})
app.post('/', (req, res) => {
// 默认情况下,如果不配置解析表单数据的中间件,则 req.body 默认等于 {}
console.log(req.body);
res.send('Home Paeg!')
})
app.listen(8005, () => {
console.log('http://127.0.0.1:8005');
})
⑤ 第三方的中间件
可参考:http://expressjs.com/en/resources/middleware.html
7.2.5 自定义解析表单数据中间件
const express = require('express')
const querystring = require('querystring')
// 导入自定义处理表单数据的中间模块
const customBodyParser = require('./19-custom-body-parser')
const app = express()
// // 解析表单数据中间件
// app.use((req, res, next) => {
// // 业务逻辑
// // 顶一个空字符串,接收客户端发送过来的数据
// let str = ''
// // 监听 req 对象的 data 事件(客户端发送过来的新的请求体数据)
// req.on('data', (chunk) => {
// // 拼接请求体数据,隐式转换为字符串
// str += chunk
// })
// // 监听 req 对象的 end 事件,获取 str
// req.on('end', () => {
// // 在 str 中存放的是完整的请求体数据
// console.log(str);
// // TODO: 把字符串格式的请求体数据,转化为对象格式
// let body = querystring.parse(str)
// console.log(body);
// // 将body挂载到req上
// req.body = body
// next()
// })
// })
// 将自定义的中间件模块,注册为全局可用的中间件
app.use(customBodyParser)
app.post('/user', (req, res) => {
res.send(req.body)
})
app.listen(8006, () => {
console.log('http://127.0.0.1:8006');
})
7.3 使用 Express 写接口
- 创建 Express 服务器
- 创建 API 路由模块
- 编写 GET 接口
- 编写 POST 接口
API.js
const express = require('express')
const router = require('./21-apiRouter')
const app = express()
// 配置解析表单数据的中间件
app.use(express.urlencoded({ extended: false }))
app.use('/api', router)
app.listen(8007, () => {
console.log('http://127.0.0.1:8007');
})
21-apiRouter.js
// 路由模块
const express = require('express')
const router = express.Router()
// 挂载路由
router.get('/get', (req,res)=>{
// 通过 req.query 获取客户端通过查询字符串,发送到服务器的数据
const query = req.query
// 处理结果
res.send({
status: 0, // 0 表示处理成功, 1 表示处理失败
msg: 'GET 请求成功!', // 状态描述
data: query // 需要响应给客户端的数据
})
})
router.post('/post', (req,res)=>{
// 通过 req.body 获取请求体中包含的 url-encoded 格式数据
const body = req.body
// 处理结果
res.send({
status: 0, // 0 表示处理成功, 1 表示处理失败
msg: 'POST 请求成功!', // 状态描述
data: body // 需要响应给客户端的数据
})
})
// 导出路由对象
module.exports = router
7.4 CORS跨域资源共享
JSONP只支持GET请求(之前ajax部分讲到),推荐使用CORS中间件
npm install cors // 安装
const cors = require('cors') // 导入中间件
app.use(cors()) // 配置中间件
跨域访问
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>跨域访问</title>
<script src="https://cdn.staticfile.org/jquery/3.7.0/jquery.min.js"></script>
</head>
<body>
<button id="getbtn">GET请求</button>
<button id="postbtn">POST请求</button>
<script>
$('#getbtn').on('click', function() {
$.ajax({
type: 'GET',
url: 'http://127.0.0.1:8007/api/get',
data: {
name: 'ssy',
age: 23
},
success: function(e){
console.log(e);
}
})
})
$('#postbtn').on('click', function() {
$.ajax({
type: 'POST',
url: 'http://127.0.0.1:8007/api/post',
data: {
name: 'ssy',
age: 23
},
success: function(e){
console.log(e);
}
})
})
</script>
</body>
</html>
CORS注意事项:
7.4.1 CORS响应头部
①Access-Control-Allow-Origin
res.setHeader('Access-Control-Allow-Origin','*') // *代表可以允许任何域名访问
②Access-Control-Allow-Headers
③Access-Control-Allow-Methods
7.4.2 请求分类
1.简单请求
2.预检请求
3.区别
7.4.3 JSONP请求
const express = require('express')
const router = require('./21-apiRouter')
const cors = require('cors')
const app = express()
// 配置解析表单数据的中间件
app.use(express.urlencoded({ extended: false }))
// 必须在配置 CORS 中间件之前,配置JSONP的接口
app.get('/api/jsonp', (req, res) => {
// TODO: 定义 JSONP 接口的具体实现过程
// 获取客户端放过来的回调函数名字
const funcName = req.query.callback
// 得到通过 JSONP 形式发送给客户端的数据
const data = { name: 'ssy', age: 23 }
// 拼接一个函数调用的字符串
const scriptStr = `${funcName}(${JSON.stringify(data)})`
// 把字符串响应给客户端的<script>标签进行解析
res.send(scriptStr)
})
// 在路由之前,配置cors中间件,解决接口跨域问题
app.use(cors())
// 路由模块定义为全局中间件
app.use('/api', router)
app.listen(8007, () => {
console.log('http://127.0.0.1:8007');
})
<script>
$('#JSONPbtn').on('click', function() {
$.ajax({
url: 'http://127.0.0.1:8007/api/jsonp',
dataType: 'jsonp',
success: function(e){
console.log(e);
}
})
})
</script>
八、数据库与身份认证
8.1 数据库
8.2 数据库操作
-- 通过 * 把 users 表中的所有数据查询出来
-- select * from users
-- 从 users 表中吧 username 和 password 对应的数据查询出来
-- select username , password from users
-- 向 users 表中,插入新数据,username 的值为 ***,password 的值为 ***
-- insert into users (username,password) values ('***','***')
-- select * from users
-- 将 id 为 4 的用户密码,更新成 ***
-- update users set password='***' where id=4
-- select * from users
-- 更新 id 为 2 的用户,把用户密码更新为 lisi123 同时,把用户的状态更新为 1
-- update users set password='lisi123', status=1 where id=2
-- select * from users
-- 删除 users 表中,id 为 4 的用户
-- delete from users where id=4
-- select * from users
-- 演示 where 子句的使用
-- select * from users where status=1
-- select * from users where id>=2
-- select * from users where username<>'李四'
-- select * from users where username!='李四'
-- 使用 AND 来显示所有状态为 0 且 id 小于 3 的用户
-- select * from users where status=0 and id<3
-- 使用 OR 来显示所有状态为 1 或 username 为 张三 的用户
-- select * from users where status=1 or username='张三'
-- 对 users 表中的数据,按照 status 字段进行升序排序
-- select * from users order by status
-- select * from users order by status asc
-- 按照 id 对结果进行降序的排序,desc 表示降序排序,asc 表示升序排序(默认情况下,就是升序排序)
-- select * from users order by id desc
-- 对 users 表中的数据,先按照 status 进行降序排序,再按照 username 字母的·顺序,进行升序的排序
-- select * from users order by status desc,username asc
-- 使用 count(*) 来统计 users 表中,状态为 0 用户的总数量
-- select count(*)from users where status=0
-- 使用 AS 关键字给列起别名
-- select count(*) as total from users where status=0
-- select username as uname , password as upwd from users
8.3 在Express中操作MySQL
8.3.1 安装、配置、测试
// npm i mysql
// 安装完成后,导入mysql模块
const mysql = require('mysql')
// 建立与 mysql 数据库的连接
const db = mysql.createPool({
host: '127.0.0.1', // 数据库的 IP 地址
user: 'root', // 登录数据库的账号
password: '123456', // 登录数据库的密码
database: 'node_db_01' // 指定要操作的数据库
})
// 检测 mysql 模块能否正常工作
db.query('select 1', (err, results)=>{
// mysql 模块工作期间报错了
if(err) return console.log(err.message);
// 能够成功的执行 SQL 语句,打印出 [ RowDataPacket { '1': 1 } ]
console.log(results);
})
8.3.2 查询、插入、更新和删除
const sqlStr = 'select * from users'
db.query(sqlStr, (err, results) => {
// 查询失败
if (err) return console.log(err.message);
// 查询成功
// 注意:如果执行的是 select 查询语句,则执行的结果是 数组
console.log(results);
})
// let user1 = {username: 'ss', password: '666666'}
// 定义 SQL 语句, ? 表示占位符, 不能直接写数据
let sqlStr1 = 'INSERT INTO users (username, password) VALUES (?, ?)'
db.query(sqlStr1, [user1.username, user1.password], (err, results) => {
// 失败
if (err) return console.log(err.message);
// 成功
// 结果是对象,可以通过 affectedRows 来判断是否插入成功
if(results.affectedRows == 1) {
console.log('插入数据成功!');
}
})
// 插入数据的便捷方式 数据属性名要和数据库里的表名一一对应
let user2 = {username: 'ssy', password: 'admin'}
// 定义 SQL 语句,SET ?
let sqlStr2 = 'INSERT INTO users SET ?'
db.query(sqlStr2, user2, (err, results) => {
// 失败
if (err) return console.log(err.message);
// 成功
// 结果是对象,可以通过 affectedRows 来判断是否插入成功
if(results.affectedRows == 1) {
console.log('插入数据成功!');
}
})
// 更新数据对象
let user3 = { id: 7, username: 'hh', password: '000000' }
// 定义 SQL 语句, ? 表示占位符, 不能直接写数据
let sqlStr3 = 'UPDATE users SET username=?, password=? WHERE id=?'
db.query(sqlStr3, [user3.username, user3.password, user3.id], (err, results) => {
// 失败
if (err) return console.log(err.message);
// 成功
// 结果是对象,可以通过 affectedRows 来判断是否插入成功
if (results.affectedRows == 1) {
console.log('更新数据成功!');
}
})
// 更新数据对象的便捷方式 数据属性名要和数据库里的表名一一对应
let user4 = { id: 7, username: 'aaaa', password: '0000' }
// 定义 SQL 语句
let sqlStr4 = 'UPDATE users SET ? WHERE id=?'
db.query(sqlStr4, [user4, user4.id], (err, results) => {
// 失败
if (err) return console.log(err.message);
// 成功
// 结果是对象,可以通过 affectedRows 来判断是否插入成功
if (results.affectedRows == 1) {
console.log('更新数据成功!');
}
})
// 删除数据
// 定义 SQL 语句
let sqlStr5 = 'DELETE FROM users WHERE id=?'
// db.query() 执行 SQL 语句要为,占位符指定数据,只有一个占位符可以省略 数组写法
db.query(sqlStr5, 10, (err, results) => {
// 失败
if (err) return console.log(err.message);
// 成功
// 结果是对象,可以通过 affectedRows 来判断是否插入成功
if (results.affectedRows == 1) {
console.log('删除数据成功!');
}
})
// 标记删除--更新数据
// 定义 SQL 语句
let sqlStr6 = 'UPDATE users SET status=? WHERE id=?'
// db.query() 执行 SQL 语句要为,占位符指定数据,只有一个占位符可以省略 数组写法,多个必须写数组
db.query(sqlStr6, [1, 7], (err, results) => {
// 失败
if (err) return console.log(err.message);
// 成功
// 结果是对象,可以通过 affectedRows 来判断是否插入成功
if (results.affectedRows == 1) {
console.log('标记删除数据成功!');
}
})
8.4 前后端身份认证
8.4.1 Web开发模式
- 服务器端渲染
- 前后端分离
8.4.2 身份认证
通过一定的手段,完成用户身份的确认
对于服务器段渲染
和前后端分离
这两种开发模式来说,分别有着不同的身份认证方案:
① 服务器段渲染
推荐使用Session认证机制
② 前后端分离
推荐使用JWT认证机制
8.4.3 Session 认证机制
突破http无状态性,可以颁发 Cookie 身份认证
Cookie 是存储在用户浏览器中的一段不超过4kb的字符串,它由一个名称(Name)、一个值(Value)和其他几个用于控制Cookie有效期、安全性、使用范围的可选属性组成。
特性:
① 自动发送
② 域名独立
③ 过期时限
④ 4KB 限制
因为Cookie没有安全性,所以需要提高身份认证的安全性,可以添加一个认证机制
Session 工作原理
8.4.4 在 Express 中使用 Session 认证
const express = require('express')
// npm i express-session 安装
const session = require('express-session')
const app = express()
app.use(session({
secret: 'pluto_ssy', // 可以是任何字符串
resave: false, // 固定写法
saveUninitialized: false // 固定写法
}))
// 解析POST提交过来的数据
app.use(express.urlencoded({ extended: false }))
// // 托管静态资源
// app.use(express.static('./page'))
// 登录
app.post('/api/login', (req, res) => {
if (req.body.username != 'admin' || req.body.password != '000000') {
return res.send({ status: 1, msg: '登录失败!' })
}
// 将用户的登录信息保存到session中
// 只有配置中间件后,才有req.seesion
req.session.user = req.body // 将用户信心存到session中
req.session.islogin = true // 将用户登录状态,存储到 Session 中
res.send({ status: 0, msg: '登陆成功!' })
})
// 获取用户名
app.get('/api/username', (req, res) => {
if (!req.session.islogin) {
return res.render({ status: 1, msg: 'fail' })
}
// 从req.session上获取用户名
res.send({ status: 0, msg: 'success', username: req.session.user.username })
})
// 退出登录
app.post('/api/logout', (req, res) => {
// destroy() 清空当前客户端登录用户对应 session 信息
req.session.destroy()
res.send({
status: 0,
msg: '退出登录成功!'
})
})
app.listen(8008, () => {
console.log('http://127.0.0.1:8008');
})
8.4.5 JWT 认证机制
工作原理
组成部分: Header(头部)、Rayload(有效荷载)、Signature(签名)
使用方式:客户端通常把token储存在 localStorage 和 是、sessionStorage中,推荐把 JWT 放在 HTTP 请求头的 Authorization 字段中
8.4.6 在 Express 中使用 JWT
安装:
npm i jsonwebtoken express-jwt@6.1.0
- jsonwebtoken 用于生成 JWT 字符串
- express-jwt 用于将 JWT 字符串解析还原成 JSON 对象
const express = require('express')
const jwt = require('jsonwebtoken')
// 版本问题 express-jwt由于7.x.x版本导出方式改变所致,可以降低版本或者修改引入方式
// 注意 :入参option需要带上 algorithms: ["HS256"]
// 6.1.0 版本
const expressJWT = require('express-jwt')
const cors = require('cors')
const app = express()
// 定义 secret 密钥,建议命名为 secretKey
const secretKey = 'pluto_ssy'
// 解析 post 表单数据的中间件
app.use(express.urlencoded({ extended: false }))
// 在路由之前,配置cors中间件,解决接口跨域问题
app.use(cors())
// expressJWT({ secret: secretKey }) 就是用来解析 Token 的中间件, 成功后自动挂载 req.user(高版本没有)
// .unless({ path: [/^\/api\//] }) 用来指定哪些接口不需要访问权限
app.use(expressJWT({ secret: secretKey, algorithms: ['HS256'] }).unless({ path: [/^\/api\//] }))
// 登录接口
app.post('/api/login', (req, res) => {
if (req.body.username != 'admin' || req.body.password != '000000') {
return res.send({ status: 1, msg: '登录失败!' })
}
// 登陆成功
res.send({
status: 200,
message: '登陆成功',
// 调用 jwt.sign() 生成 JWT 字符串,三个参数分别是:用户信息对象(不要使用密码)、加密密钥、配置对象
token: jwt.sign({ username: req.body.username }, secretKey, { expiresIn: '1h' })
})
})
// 有权限的 API 接口 访问的时候 请求头需要携带 Authorization=Bearer token命令
app.get('/admin/getinfo', function (req, res) {
// req.user 是配置 express-jwt 成功后自动挂载的 用户信息(高版本没有)
console.log(req.user);
res.send({
status: 200,
message: '获取用户信息成功!',
data: req.user
})
})
// 定义错误全局中间件,捕获解析 jwt 错误
app.use((err, req, res, next) => {
// token 解析失败导出错误
if (err.name == 'UnauthorizedError') {
return res.send({ status: 401, message: '无效的token!' })
}
// 其他问题
res.send({ status: 500, message: '未知错误!'})
})
app.listen(8009, () => {
console.log('http://127.0.0.1:8009');
})
九、项目概述
9.1 大事件项目后台主代码
主文件:app.js
// 导入
const express = require('express')
// 导入cors 允许跨域访问
const cors = require('cors')
// 导入 Joi
const Joi = require('joi')
const expressJWT = require('express-jwt')
// 导入全局配置文件
const config = require('./config')
// 导入自定义路由模块
const userRouter = require('./router/user')
// 导入用户信息路由模块
const userinfoRouter = require('./router/userinfo')
// 导入文章分类路由模块
const artCateRouter = require('./router/artcate')
// 导入文章的路由魔铠
const articleRouter = require('./router/article')
// 创建实例
const app = express()
// 配置跨域中间件
app.use(cors())
// 配置解析表单数据的中间件 只能解析 application/x-www-form-urlencoded 格式
app.use(express.urlencoded({ extended: false }))
// 在路由之前封装 res.cc 函数,解决响应消息
app.use((req, res, next) => {
// status 默认为1,表示失败的情况
// err 的值,可能是一个错误对象,也可能是一个错误的描述字符串
res.cc = (err, status = 1) => {
res.send({
status,
message: err instanceof Error ? err.message : err
})
}
// 注意添加 next()
next()
})
// 在路由之前配置解析 Token 中间件,自动挂载 req.user
app.use(expressJWT({ secret: config.jwtSecretKey }).unless({ path: /^\/api/ }))
// 使用自定义路由模块
app.use('/api', userRouter)
// 使用用户信息路由
app.use('/my', userinfoRouter)
// 使用文章分类路由
app.use('/my/article', artCateRouter)
// 使用文章路由
app.use('/my/article', articleRouter)
// 托管静态资源
app.use('/upload', express.static('./upload'))
// 定义错误级别中间件
app.use(function (err, req, res, next) {
// Joi 参数校验失败
if (err instanceof Joi.ValidationError) {
return res.cc(err)
}
// token 身份认证失败的错误
if(err.name == 'UnauthorizedError') return res.cc('身份认证失败!')
// 未知错误
res.cc(err)
})
// 指定端口号,启动服务器
app.listen(8010, function () {
console.log('api server running at http://127.0.0.1:8010');
})
配置文件:config.js
// 全局配置文件
module.exports = {
// 配置加密解密的 token 密钥
jwtSecretKey: 'pluto_ssy ^_^',
expiresIn: '1h' // token 有效期
}
导入的包文件:package.json
{
"dependencies": {
"@escook/express-joi": "^1.1.1",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"express": "^4.17.1",
"express-jwt": "^5.3.3",
"joi": "^17.4.0",
"jsonwebtoken": "^8.5.1",
"multer": "^1.4.2",
"mysql": "^2.18.1"
}
}
数据库配置模块:db/index.js
// 导入模块
const mysql = require('mysql')
// 创建数据库连接对象
const db = mysql.createPool({
host: '127.0.0.1',
user: 'root',
password: '123456',
database: 'node_db_01'
})
// 向外共享 db 数据库连接对象
module.exports = db
9.2 用户注册登录模块
路由模块 router/user.js
// 路由模块
const express = require('express')
// 导入 @escook/express-joi
const expressJoi = require('@escook/express-joi')
// 导入用户路由处理函数对应模块
const user_handler = require('../router_handler/user')
// 导入验证规则对象,{ reg_login_schema } 结构赋值
const { reg_login_schema } = require('../schema/user')
const router = express.Router()
// 调用验证中间件 expressJoi
router.post('/reguser', expressJoi(reg_login_schema), user_handler.regUser)
// 调用验证中间件 expressJoi
router.post('/login', expressJoi(reg_login_schema), user_handler.login)
module.exports = router
路由模块操作函数 router_handler/user.js
// 导入bcryptjs包
// 优点:只加密不解密;同一字符串,加密结果不同
const bcrypt = require('bcryptjs')
// 导入 生成 token 的包
const JWT = require('jsonwebtoken')
// 导入全局配置文件
const config = require('../config')
// 导入数据库操作模块
const db = require('../db/index')
// 注册新用户的处理函数,exports是简写模式
exports.regUser = (req, res) => {
// 获取客户端提交到服务器的用户信息
const userinfo = req.body
// 不用if判断了
// // 对表单中的数据进行合法性校验
// if (!userinfo.username || !userinfo.password) {
// // return res.send({ status: 1, messaeg: '用户名或密码不合法!' })
// return res.cc('用户名或密码不合法!')
// }
// 定义 SQL 语句,查询用户名是否被占用
const sqlStr = 'SELECT * FROM ev_users WHERE username=?'
db.query(sqlStr, userinfo.username, (err, results) => {
// 执行 SQL 语句失败
if (err) {
// return res.end({ status: 1, message: err.message })
return res.cc(err)
}
if (results.length > 0) {
// return res.send({ status: 1, message: '用户名被占用,请更换其他用户名!' })
return res.cc('用户名被占用,请更换其他用户名!')
}
// 用户名可以使用
// 调用 bcrypt.hashSync() 对密码进行加密
// console.log(userinfo);
userinfo.password = bcrypt.hashSync(userinfo.password, 10)
// console.log(userinfo);
// 定义插入新用户的 SQL 语句
const sql = 'INSERT INTO ev_users SET ?'
db.query(sql, { username: userinfo.username, password: userinfo.password }, (err, results) => {
// 执行 SQL 语句失败
if (err) {
// return res.send({ status: 1, message: err.message })
return res.cc(err)
}
// 判断影响行数是否为1
if (results.affectedRows !== 1) {
// return res.send({ status: 1, message: '注册用户失败,请稍后再试。'})
return res.cc('注册用户失败,请稍后再试。')
}
// 注册成功
// res.send({ status: 0, message: '注册成功!' })
res.cc('注册成功!', 0)
})
})
}
// 登录的处理函数
exports.login = (req, res) => {
// 接受表单数据
const userinfo = req.body
// 定义 SQL 语句
const sql = 'SELECT * FROM ev_users where username=?'
db.query(sql, userinfo.username, (err, results) => {
// 执行 SQL 语句失败
if (err) {
return res.cc(err)
}
// 执行 SQL 语句成功,但是获取到的数据不等于1
if (results.length !== 1) {
return res.cc('登录失败!')
}
// 判断密码是否正确
const compareResult = bcrypt.compareSync(userinfo.password, results[0].password)
if (!compareResult) return res.cc('登录失败!')
// 验证成功,生成 JWT 的 token 字符串
// ES6 高级语法,展开和剔除敏感信息
const user = { ...results[0], password: '', user_pic: '' }
// 对用户信息进行加密,生成 token 字符串
const tokenStr = JWT.sign(user, config.jwtSecretKey, { expiresIn: config.expiresIn })
// console.log(tokenStr);
// 把 tokenStr 响应给客户端
res.send({
status: 0,
message: '登陆成功!',
token: 'Bearer ' + tokenStr // 方便客户端使用,直接拼接上 'Bearer '
})
})
}
验证规则模块 schema/user.js
// 前端验证为辅,后端验证为主
// 导入 Joi 来定义验证规则
const Joi = require('joi')
// 命名保持一致,ES6简写
const username = Joi.string().alphanum().min(2).max(10).required()
const password = Joi.string().pattern(/^[\S]{6,15}$/).required()
const id = Joi.number().integer().min(1).required()
const nickname = Joi.string().required()
const email = Joi.string().email().required()
// dataUri() 表示 data:image/png;base64,VE9PTUFOWVNFQ1JFVFM=
const avatar = Joi.string().dataUri().required()
// 定义验证注册和登录表单数据的规则对象
exports.reg_login_schema = {
body: {
username,
password
}
}
// 定义更新用户基本信息的验证规则对象
exports.update_userinfo_schema = {
body: {
id,
nickname,
email
}
}
// 定义更新用户密码的验证规则对象
exports.update_password_schema = {
body: {
oldPwd: password,
newPwd: Joi.not(Joi.ref('oldPwd')).concat(password) // 不能等于原密码,且规则与原密码相同
}
}
// 定义验证头像的规则
exports.update_avatar_schema = {
body: {
avatar
}
}
9.3 用户信息管理模块
路由模块 router/userinfo.js
const express = require('express')
// 导入 @escook/express-joi
const expressJoi = require('@escook/express-joi')
// 导入路由处理函数模块
const userinfo_handler = require('../router_handler/userinfo')
// 导入验证规则对象,{ update_userinfo_schema } 结构赋值
const { update_userinfo_schema, update_password_schema, update_avatar_schema } = require('../schema/user')
// 调用路由函数
const router = express.Router()
// 挂载路由
// 获取用户基本信息
router.get('/userinfo', userinfo_handler.getUserInfo)
// 更新用户基本信息
router.post('/userinfo', expressJoi(update_userinfo_schema), userinfo_handler.updateUserInfo)
// 更新密码的路由
router.post('/updatepwd', expressJoi(update_password_schema), userinfo_handler.updatePassword)
// 更换用户头像路由
router.post('/update/avatar', expressJoi(update_avatar_schema), userinfo_handler.updateAvatar)
module.exports = router
路由模块操作函数 router_handler/userinfo.js
// 导入数据库操作模块
const db = require('../db/index')
// 导入处理密码的模块
const bcrypt = require('bcryptjs')
// 获取用户基本信息的处理函数
exports.getUserInfo = (req, res) => {
// 定义 SQL 语句查询用户信息
const sql = 'SELECT id, username, nickname, email, user_pic FROM ev_users where id=?'
db.query(sql, req.user.id, (err, results) => {
// 执行 SQL 语句失败
if (err) return res.cc(err)
// 执行 SQL 语句成功,查询结果为空
if (results.length !== 1) return res.cc('获取用户信息失败!')
// 用户信息获取成功
res.send({
status: 0,
message: '获取用户信息成功!',
data: results[0]
})
})
}
// 获取用户基本信息的处理函数
exports.updateUserInfo = (req, res) => {
// 定义 SQL 语句
const sql = 'UPDATE ev_users SET ? WHERE id=?'
db.query(sql, [req.body, req.body.id], (err, results) => {
// 执行 SQL 语句失败
if (err) return res.cc(err)
// 执行 SQL 语句成功,但是影响行数不等于 1
if (results.effectedRows != 1) return res.cc('更新用户基本信息失败!')
// 更新成功!
res.cc('更新用户信息成功!', 0)
})
}
// 重置用户密码的处理函数
exports.updatePassword = (req, res) => {
// 根据id查询用户的信息
const sql = 'SELECT * FROM ev_users where id=?'
db.query(sql, req.user.id, (err, results) => {
// 执行 SQL 语句失败
if (err) return res.cc(err)
// 判断是否存在
if (results.length != 1) return res.cc('用户不存在!')
// 判断用户输入的旧密码是否正确
const compareResult = bcrypt.compareSync(req.body.password, results[0].password)
if (!compareResult) return res.cc('旧密码错误!')
// 更新数据库的密码
// 定义更新用户的 SQL 语句
const sqlStr = 'UPDATE ev_users SET password=? WHERE id=?'
// 对新密码进行加密
const newPwd = bcrypt.hashSync(req.body.newPwd, 10)
db.query(sqlStr, [newPwd, req.user.id], (err, results) => {
// 执行 SQL 语句失败
if (err) return res.cc(err)
// 执行 SQL 语句成功,但是影响行数不等于 1
if (results.effectedRows != 1) return res.cc('更新密码失败!')
// 更新密码成功
res.cc('更新密码成功!', 0)
})
})
}
// 更新用户头像的处理函数
exports.updateAvatar = (req, res) => {
// 定义更新用户头像的 SQL 语句
const sql = 'UPDATE ev_users SET user_pic=? WHERE id=?'
db.query(sql, [req.body.avatar, req.user.id], (err, results) => {
// 执行 SQL 语句失败
if (err) return res.cc(err)
// 影响的行数是否等于 1
if (results.affectedRows != 1) return res.cc('更换头像失败!')
// 成功
res.cc('更换头像成功!', 0)
})
}
验证规则模块 schema/userinfo.js
// 导入定义验证规则的模式
const Joi = require('joi')
// 定义 分类名称 和 分类别名 的校验规则
const name = Joi.string().required()
const alias = Joi.string().alphanum().required()
// 定义 分类Id 校验规则
const id = Joi.number().integer().min(1).required()
// 校验规则对象 - 添加分类,并共享出去
exports.add_cate_schema = {
body: {
name,
alias
}
}
// 校验规则对象 - 删除分类,并共享出去
exports.delete_cate_schema = {
params: {
id
}
}
// 校验规则对象 - 根据id查询分类,并共享出去
exports.get_cate_schema = {
params: {
id: id
}
}
// 校验规则对象 - 更新分类,并共享出去
exports.update_cate_schema = {
body: {
id: id,
name,
alias
}
}
9.4 文章分类管理模块
路由模块 router/artcate.js
// 导入 express
const express = require('express')
// 导入验证数据的中间件
const expressJoi = require('@escook/express-joi')
// 导入文章分类的路由处理模块
const artCate_handler = require('../router_handler/artcate')
// 导入验证规则的对象
const { add_cate_schema, delete_cate_schema, get_cate_schema, update_cate_schema } = require('../schema/artcate')
const router = express.Router()
// 获取文章分类列表的路由
router.get('/cates', artCate_handler.getArtCates)
// 新增文章分类的路由
router.post('/addcates', expressJoi(add_cate_schema), artCate_handler.addArtCates)
// 删除文章分类的路由
router.get('/deletecate/:id', expressJoi(delete_cate_schema), artCate_handler.deleteCateById)
// 根据id获取文章分类的路由
router.get('/cates.:id', expressJoi(get_cate_schema), artCate_handler.getArtCateById)
// 根据id更新文章分类的路由
router.post('/updatecate', expressJoi(update_cate_schema), artCate_handler.updateCateById)
module.exports = router
路由模块操作函数 router_handler/artcate.js
// 路由处理模块
// 导入数据库操作模块
const db = require('../db/index')
// 获取文章分类的处理函数
exports.getArtCates = (req, res) => {
// 定义查询文章分类列表数据的 SQL 语句
const sql = 'SELECT * FROM ev_article_cate WHERE is_delete=0 ORDER BY ASC'
db.query(sql, (err, results) => {
// 执行 SQL 错误
if (err) return res.cc(err)
res.send({
status: 0,
message: '获取文章分类成功!',
data: results
})
})
}
// 新增文章分类的处理函数
exports.addArtCates = (req, res) => {
// 定义查重 SQL 语句
const sql = 'SELECT * FROM ev_article_cate WHERE name=? or alias=?'
db.query(sql, [req.body.name, req.body.alias], (err, results) => {
// 执行 SQL 错误
if (err) return res.cc(err)
// 判断是否是标记删除的
// console.log(results[0].isdelete);
// 判断数据lenth
if (results.length == 2) return res.cc('分类名称与分类别名被占用,请更换后重试!')
// length 等于 1 的三种情况
if (results.length == 1 && results[0].name == req.body.name && results[0].alias == req.body.alias) return res.cc('分类名称与分类别名被占用,请更换后重试!')
if (results.length == 1 && results[0].name == req.body.name) return res.cc('分类名称被占用,请更换后重试!')
if (results.length == 1 && results[0].alias == req.body.alias) return res.cc('分类别名被占用,请更换后重试!')
// 分类名称和分类别名都可用,添加操作
const sql = 'INSERT INTO ev_article_cate SET ?'
db.query(sql, req.body, (err, results) => {
// 执行 SQL 错误
if (err) return res.cc(err)
if (results.affectedRows != 1) return res.cc('新增文章分类失败!')
// 成功
res.cc('新增文章分类成功!', 0)
})
})
}
// 删除文章分类
exports.deleteCateById = (req, res) => {
// 定义判断重复删除的 SQL 语句
const sql = 'SELECT * FROM ev_article_cate WHERE id=?'
db.query(sql, req.params.id, (err, results) => {
// 执行 SQL 错误
if (err) return res.cc(err)
// 查看当前删除状态,判断是否已经删除
if (results[0].is_delete == 1) return res.cc('不能重复删除!')
// 不是重复删除,定义标记删除 SQL 语句
const sql = 'UPDATE ev_article_cate SET is_delete=1 WHERE id=?'
db.query(sql, req.params.id, (err, results) => {
// 执行 SQL 错误
if (err) return res.cc(err)
// 判断影响行数
if (results.affectedRows != 1) return res.cc('删除文章分类失败!')
// 成功删除
res.cc('删除文章分类成功!', 0)
})
})
}
// 根据id获取文章分类
exports.getArtCateById = (req, res) => {
// 定义根据id 查询文章分类的 SQL 语句
const sql = 'SELECT * FROM ev_article_cate WHERE name=? or alias=?'
db.query(sql, req.params.id, (err, results) => {
// SQL 语句执行失败
if (err) return res.cc(err)
// 判断数据lenth
if (results.length != 1) return res.cc('获取文章分类失败!')
// 成功
res.send({
status: 0,
message: '获取文章分类数据成功!',
data: results[0]
})
})
}
// 根据id更新文章分类
exports.updateCateById = (res, req) => {
// 定义查重 SQL 语句, 将本id 排除,查询别的
const sql = 'SELECT * FROM ev_article_cate WHERE id<>? AND (name=? or alias=?)'
db.query(sql, [req.body.id, req.body.name, req.body.alias], (err, results) => {
// 执行 SQL 错误
if (err) return res.cc(err)
// 判断是否是标记删除的
// console.log(results[0].isdelete);
// 判断数据lenth
if (results.length == 2) return res.cc('分类名称与分类别名被占用,请更换后重试!')
// length 等于 1 的三种情况
if (results.length == 1 && results[0].name == req.body.name && results[0].alias == req.body.alias) return res.cc('分类名称与分类别名被占用,请更换后重试!')
if (results.length == 1 && results[0].name == req.body.name) return res.cc('分类名称被占用,请更换后重试!')
if (results.length == 1 && results[0].alias == req.body.alias) return res.cc('分类别名被占用,请更换后重试!')
// 更新操作
const sql = 'UPDATE ev_article_cate SET ? WHERE id=?'
db.query(sql, [req.body, req.body.id], (req, res) => {
// 执行 SQL 语句失败
if (err) return res.cc(err)
// 判断影响行数
if (results.affectedRows != 1) return res.cc('更新文章分类失败!')
// 成功
res.cc('更新文章分类成功!', 0)
})
})
}
验证规则模块 schema/artcate.js
// 导入定义验证规则的模式
const Joi = require('joi')
// 定义 分类名称 和 分类别名 的校验规则
const name = Joi.string().required()
const alias = Joi.string().alphanum().required()
// 定义 分类Id 校验规则
const id = Joi.number().integer().min(1).required()
// 校验规则对象 - 添加分类,并共享出去
exports.add_cate_schema = {
body: {
name,
alias
}
}
// 校验规则对象 - 删除分类,并共享出去
exports.delete_cate_schema = {
params: {
id
}
}
// 校验规则对象 - 根据id查询分类,并共享出去
exports.get_cate_schema = {
params: {
id: id
}
}
// 校验规则对象 - 更新分类,并共享出去
exports.update_cate_schema = {
body: {
id: id,
name,
alias
}
}
9.5 文章信息管理模块
路由模块 router/article.js
// 文章路由模块
const express = require('express')
// 导入处理路径内置模块 path
const path = require('path')
// 安装并导入 multer@1.4.2 解析 Formdata 格式表单数据
const multer = require('multer')
// 导入验证数据的中间件
const expressJoi = require('@escook/express-joi')
// 导入路由处理函数模块
const article_handler = require('../router_handler/article')
// 导入验证规则的对象
const { add_article_schema, list_article_schema, delete_article_schema, select_article_schema, update_article_schema } = require('../schema/article')
// 创建路由实例对象
const router = express.Router()
// 创建 multer 的实例对象,通过 dest 属性指定文件存放路径
const upload = multer({ dest: path.join(__dirname, '../upload') })
// 发布文章的路由
// upload.single() 是一个局部生效的中间件,用来解析 FormData 格式的表单数据
// 将文件类型的数据,解析并挂载 req.file 属性中
// 将文本类型的数据,解析并挂载 req.body 属性中
router.post('/add', upload.single('cover_img'), expressJoi(add_article_schema), article_handler.addArticle)
// 获取文章列表的路由
router.get('/list', expressJoi(list_article_schema), article_handler.getArticle)
// 根据文章id删除文章的路由
router.get('/delete/:id', expressJoi(delete_article_schema), article_handler.deleteArticleById)
// 根据文章id获取文章详情的路由
router.get('/:id', expressJoi(select_article_schema), article_handler.getArticleById)
// 根据文章id更新文章的路由
router.post('/edit', upload.single('cover_img'), expressJoi(update_article_schema), article_handler.updateArticleById)
module.exports = router
路由模块操作函数 router_handler/article.js
// 路由处理函数模块
// 导入 path 模块
const path = require('path')
// 导入数据库操作模块
const db = require('../db/index')
// 发布文章
exports.addArticle = (req, res) => {
// console.log(req.file);
if (!req.file || req.file.fieldname !== 'cover_img') return res.cc('文章封面是必选参数!')
// 数据合法,后续发布操作
const articleInfo = {
// 展开就是 标题、内容、发布状态、分类id
...req.body,
// 文章封面的存放路径
cover_img: path.join('/upload', req.file.filename),
// 文章的发布时间
pub_date: new Date(),
// 文章作者的id
author_id: req.user.id
}
// console.log(articleInfo);
// 定义发布文章的 SQL 语句
const sql = 'INSERT INTO ev_articles SET ?'
db.query(sql, articleInfo, (err, results) => {
if (err) return res.cc(err)
if (results.affectedRows != 1) return res.cc('发布新文章失败!')
res.cc('发布文章成功!', 0)
})
}
// 获取文章列表
exports.getArticle = (req, res) => {
// 接收所有的请求参数
// let pagenum = req.query.pagenum;
// let pagesize = req.query.pagesize;
// let cate_id = req.query.cate_id;
// let state = req.query.state;
// 使用解构的方式,获取请求参数
let { pagenum, pagesize, cate_id, state } = req.query;
// 生成where条件,完成筛选工作
let w = '';
if (cate_id) {
w += ' AND cate_id=' + cate_id // 注意前面有空格
}
if (state) {
w += ` AND state = '${state}'` // 注意前面有空格
}
// 查询总记录数
db.query('SELECT count(*) AS total FROM ev_articles WHERE author_id=?' + w, req.user.id, (err, results) => {
// 执行SQL语句失败
if (err) return res.cc(err)
if (results.length == 0) return res.cc('获取文章列表数目失败!')
// console.log(results);
// 成功
let total = results[0].total
// 下面查询数据,把结果响应给客户端
// 下面是SQL比较长,可以先在 Navicat中查询,没问题,复制过来
let sql = `SELECT a.Id, a.title, a.pub_date, a.state, c.name cate_name FROM ev_articles a
JOIN ev_article_cate c ON a.cate_id=c.id
JOIN ev_users u ON u.id=a.author_id
WHERE u.id = ? ${w}
LIMIT ${(pagenum - 1) * pagesize}, ${pagesize}`;
db.query(sql, req.user.id, (err, results) => {
// 执行SQL语句失败
if (err) return res.cc(err)
if (results.length == 0) return res.cc('获取文章列表失败!')
// 成功
res.send({
status: 0,
message: "获取文章列表成功!",
data: results,
total: total
})
})
})
}
// 根据文章id删除文章
exports.deleteArticleById = (req, res) => {
// 定义判断重复删除的 SQL 语句
const sql = 'SELECT * FROM ev_articles WHERE id=?'
db.query(sql, req.params.id, (err, results) => {
// 执行 SQL 错误
if (err) return res.cc(err)
// 查看当前删除状态,判断是否已经删除
if (results[0].is_delete == 1) return res.cc('不能重复删除!')
// 不是重复删除
// 根据文章id删除文章的 SQL 语句
let sql = 'UPDATE ev_articles SET is_delete=1 WHERE id=?'
db.query(sql, req.params.id, (err, results) => {
// 执行SQL语句失败
if (err) return res.cc(err)
if (results.affectedRows != 1) return res.cc('删除文章失败!')
// 成功
res.cc('删除成功!', 0)
})
})
}
// 根据文章id获取文章
exports.getArticleById = (res, req) => {
// SQL
const sql = 'SELECT * FROM ev_articles WHERE id=?'
db.query(sql, req.params.id, (err, results) => {
// sql 执行失败
if (err) return res.cc(err)
// 判断结果长度
if (results.length != 1) return res.cc('获取文章失败!')
// 成功
res.send({
status: 0,
message: '获取文章成功!',
data: results[0]
})
})
}
// 根据文章id更新文章
exports.updateArticleById = (req, res) => {
// console.log(req.file);
if (!req.file || req.file.fieldname !== 'cover_img') return res.cc('文章封面是必选参数!')
// 数据合法,后续发布操作
const articleInfo = {
// 展开
...req.body,
// 文章封面的存放路径
cover_img: req.file.fieldname == '/upload/cover_img' ? req.file.filename : path.join('/upload', req.file.filename)
}
// 定义查重 SQL 语句, 将本id 排除,查询别的
const sql = 'SELECT * FROM ev_articles WHERE id<>? AND title=?'
db.query(sql, [req.body.id, req.body.title], (err, results) => {
// 执行 SQL 错误
if (err) return res.cc(err)
// 判断是否是标记删除的
// console.log(results[0].isdelete);
// 判断数据lenth
if (results.length == 1) return res.cc('文章名称被占用,请更换后重试!')
// 更新操作
const sql = 'UPDATE ev_articles SET ? WHERE id=?'
db.query(sql, [articleInfo, req.body.id], (err, results) => {
// 执行 SQL 语句失败
if (err) return res.cc(err)
// 判断影响行数
if (results.affectedRows != 1) return res.cc('更新文章失败!')
// 成功
res.cc('更新文章成功!', 0)
})
})
}
验证规则模块 schema/article.js
// 导入定义验证规则的模式
const Joi = require('joi')
// 定义 标题、分类id、内容、发布状态 的校验规则
const title = Joi.string().required()
const cate_id = Joi.number().integer().min(1).required()
const content = Joi.string().required().allow('')
const state = Joi.string().valid('已发布', '草稿').required()
// 定义 页码、每页显示条数 的校验规则
const pagenum = Joi.number().integer().min(1).default(1).required()
const pagesize = Joi.number().integer().min(1).max(50).required()
// 定义 id 校验规则
const id = Joi.string().required()
// 校验规则对象 - 发布文章,并共享出去
exports.add_article_schema = {
body: {
title,
cate_id,
content,
state
}
}
// 校验规则对象 - 查问文章列表,并共享出去
exports.list_article_schema = {
query: {
pagenum,
pagesize,
cate_id: Joi.string(),
state: Joi.string().valid('已发布', '草稿')
}
}
// 校验规则对象 - 查询文章列表,并共享出去
exports.delete_article_schema = {
params: {
id: id
}
}
// 校验规则对象 - 根据id获取文章,并共享出去
exports.select_article_schema = {
params: {
id: id
}
}
// 校验规则对象 - 根据id更新文章,并共享出去
exports.update_article_schema = {
body: {
id: Joi.number().integer().min(1).required(),
title: title,
cate_id: cate_id,
content: content,
state: state
}
}