视频学习
Nodejs 基础
前提条件
学习Node.js
之前你必须掌握的知识
- HTML
- CSS
- JavaScript
- DOM API
- BOM API
- Ajax API
如果你已经掌握或者学习了以上的知识,那么我们先来回顾一下以上的一些知识点
-
浏览器为什么能执行Java代码
- 不同的浏览器具有不同的JavaScript解析引擎,来执行JS代码
-
为什么JavaScript 可以操作 DOM 和 BOM
- 由浏览器内置了DOM 、BOM的API函数,因此JavaScript才可以调用这些函数
-
JavaScript 的运行环境(代码正常运行所需的必要环境)
比如Chrome 浏览器运行环境
- 浏览器引擎主要是进行JavaScript 代码的解析和运行
- 浏览器中的内置API是由浏览器运行环境提供,并被调用
-
JavaScript 能否用于后端开发
- 能用于后端开发,需借助
node.js
运行环境
- 能用于后端开发,需借助
初识 Nodejs
Node.js 是一个基于 Chrome V8 引擎 的 JavaScript 运行时环境
Node.js 中的JavaScript运行环境
学习Node.js 中主要学习的就是内置的API内容,如图所示:
- Node.js 是执行JavaScript的后端环境
- Node.js 无法调用DOM、BOM等浏览器内置的API
Node.js 作用
- 基于 Express 框架 (opens new window),可以快速构建 Web 应用
- 基于 Electron 框架 (opens new window),可以构建跨平台的桌面应用
- 基于 restify 框架 (opens new window),可以快速构建 API 接口项目
- 读写和操作数据库、创建实用的命令行工具辅助前端开发、etc…
Node.js 初始使用
Node.js安装
[官网传送门](opens new window)
安装参考教程
Node.js 在终端运行JS脚本
本人使用的WebStrome 软件,可以直接在下方当中的Terminal 运行 ,与 使用Win + R输入cmd 的终端功能的相同的
以下是我的文件目录
我在其中编写了 一个输入Hello Nodejs的内容
最终的运行执行结果如下:
Buffer 缓冲区
Buffer 缓冲区文档(opens new window)
- Buffer 的结构与数组类似,操作方法也与数组类似
- 数组不能存储二进制文件,Buffer 是专门存储二进制数据的
- Buffer 存储的是二进制数据,显示时以 16 进制的形式显示
- Buffer 每一个元素范围是 00 ~ ff,即 0 ~ 255、00000000 ~ 11111111
- 每一个元素占用一个字节内存
- Buffer 是对底层内存的直接操作,因此大小一旦确定就不能修改
Buffer 常用方法:
Buffer.from(str[, encoding])
:将一个字符串转换为 BufferBuffer.alloc(size)
:创建指定大小的 BufferBuffer.alloUnsafe(size)
:创建指定大小的 Buffer,可能包含敏感数据(分配内存时不会清除内存残留的数据)buf.toString()
:将 Buffer 数据转为字符串
var str = 'Hello前端'
var buf = Buffer.from(str)
// 占用内存的大小,一个汉字3字节 13
console.log(buf.length)
// 字符串的长度 7
console.log(str.length)
// 8进制输出第一个元素 145
console.log(buf[1].toString(8))
//创建一个10个字节的buffer
var buf2 = Buffer.alloc(10)
//通过索引,来操作buf中的元素
buf2[0] = 88
buf2[1] = 255
buf2[2] = 0xaa
buf2[3] = 255
var buf3 = Buffer.allocUnsafe(10)
console.log(buf3)
写入缓冲区
不做过多的介绍
可以参考 菜鸟教程
FS 文件系统模块
- fs 模块中所有的操作都有两种形式可供选择:同步和异步
- 同步文件系统会阻塞程序的执行,也就是除非操作完毕,否则不会向下执行代码
- 异步文件系统不会阻塞程序的执行,而是在操作完成时,通过回调函数将结果返回
- 实际开发很少用同步方式,因此只介绍异步方式
打开文件的方式:
模式 | 说明 |
---|---|
r | 读取文件,文件不存在抛异常 |
r+ | 读写文件,文件不存在抛异常 |
rs | 同步模式下打开文件用于读取 |
rs+ | 同步模式下打开文件用于读写 |
w | 写文件,不存在则创建,存在则覆盖原有内容 |
wx | 写文件,文件存在打开失败 |
w+ | 读写文件,不存在创建,存在截断 |
wx+ | 读写,存在打开失败 |
a | 追加,不存在创建 |
ax | 追加,存在失败 |
a+ | 追加和读取,不存在创建 |
ax+ | 追加和读取,存在失败 |
Node.js 目前支持的字符编码包括:
- ascii - 仅支持 7 位 ASCII 数据。如果设置去掉高位的话,这种编码是非常快的。
- utf8 - 多字节编码的 Unicode 字符。许多网页和其他文档格式都使用 UTF-8 。
- utf16le - 2 或 4 个字节,小字节序编码的 Unicode 字符。支持代理对(U+10000 至 U+10FFFF)。
- ucs2 - utf16le 的别名。
- base64 - Base64 编码。
- latin1 - 一种把 Buffer 编码成一字节编码的字符串的方式。
- binary - latin1 的别名。
- hex - 将每个字节编码为两个十六进制字符。
fs 文件的读取
语法格式:
fs.readFile(path[, options], callback)
参数说明:
-
path
:文件路径 -
options
:配置选项,给文件指定编码格式打开
encoding
:编码格式flag
:打开方式
callback
:回调函数
error
:错误信息data
:读取的数据,如果未指定编码格式则返回一个 Buffer
如下代码所示:
// 1. 导入模块
const fs1 = require('fs');
// 2. 使用readFile 方法读取文件
fs1.readFile('./files/demo.txt', 'UTF-8', function (error, data) {
if (error) {
// 打开失败
console.log('文件打开失败' + error.message); // 可能文件不存在
} else {
console.log('打开的内容为:\n' + data); // 读取成功就打印文件的内容
}
})
fs 流式方式读取文件
- 简单文件读取的方式会一次性读取文件内容到内存中,若文件较大,会占用过多内存影响系统性能,且读取速度慢
- 大文件适合用流式文件读取,它会分多次将文件读取到内存中
var fs = require('fs')
// 创建一个可读流
var rs = fs.createReadStream('C:/Users/笔记.mp3')
// 创建一个可写流
var ws = fs.createWriteStream('a.mp3')
// 监听流的开启和关闭
// 这几个监听不是必须的
rs.once('open', function () {
console.log('可读流打开了~~')
})
rs.once('close', function () {
console.log('可读流关闭了~~')
//数据读取完毕,关闭可写流
ws.end()
})
ws.once('open', function () {
console.log('可写流打开了~~')
})
ws.once('close', function () {
console.log('可写流关闭了~~')
})
//要读取一个可读流中的数据,要为可读流绑定一个data事件,data事件绑定完毕自动开始读取数据
rs.on('data', function (data) {
console.log(data)
//将读取到的数据写入到可写流中
ws.write(data)
})
简便方式:
var fs = require('fs')
var rs = fs.createReadStream('C:/Users/lilichao/Desktop/笔记.mp3')
var ws = fs.createWriteStream('b.mp3')
// pipe()可以将可读流中的内容,直接输出到可写流中
rs.pipe(ws)
fs 文件的写入
语法:
fs.writeFile(file, data[, options], callback)
参数说明:
path
:文件路径(字符串格式)data
: 要写入到文件的内容options
:配置选项,给文件指定编码格式打开encoding
:编码格式flag
:打开方式
callback
:回调函数error
:错误信息data
:读取的数据,如果未指定编码格式则返回一个 Buffer
如下代码所示:
// 1. 导入模块
const fs1 = require('fs');
// writeFile写入文件
fs1.writeFile('./files/demoPast.txt', '写入的内容','utf8' , function (error) {
if (error) {
// 写入失败
console.log('文件打开失败' + error.message);
} else {
console.log('内容成功写入文件');
}
})
流式文件写入
// 同步、异步、简单文件的写入都不适合大文件的写入,性能较差,容易导致内存溢出
var fs = require('fs')
// 创建一个可写流
var ws = fs.createWriteStream('hello3.txt')
ws.once('open', function () {
console.log('流打开了~~')
})
ws.once('close', function () {
console.log('流关闭了~~')
})
// 通过ws向文件中输出内容
ws.write('通过可写流写入文件的内容')
ws.write('1')
ws.write('2')
ws.write('3')
ws.write('4')
// 关闭流
ws.end()
文件读取和写入综合案例(复制文件内容)
// 1. 导入fs模块
const fs = require('fs')
// 复制文件内容到指定文件,并指定格式显示的新的文件下
fs.readFile('./files/demoCopy.txt', 'utf8', function (error, data) {
if (error) {
// 读取文件失败
console.log('文件读取失败!!' + error.message);
} else {
// 对读取的字符进行操作
// 1.分割字符串
let spiltArr = data.split(' ');
// 2. 替换字符串的符号
let reArr = [];
spiltArr.forEach(item => {
reArr.push(item.replace('=', '----'));
})
// 将数组中的每一项合并成一个字符串 \r:表示拼接成字符串,\n:换行
let joStr = reArr.join('\r\n');
// 将这个字符串写入到指定的文件
fs.writeFile('./files/demoPast.txt', joStr, {encoding: 'utf8', flag: 'a+'}, function (err) {
if (err) {
//写入失败
console.log('文件复制失败!!' + err.message);
} else {
console.log('文件复制成功!!')
}
}
)
}
})
fs 中路径拼接问题
- 在使用 fs 模块操作文件时,如果提供的操作路径是以
./
或../
开头的相对路径时,容易出现路径动态拼接错误的问题 - 原因:代码在运行的时候,会以执行 node 命令时所处的目录,动态拼接出被操作文件的完整路径
- 解决方案:
- 在使用 fs 模块操作文件时,直接提供完整的路径,从而防止路径动态拼接的问题(弊端:移植性差,不利于维护)
__dirname
获取当前文件所处的目录(包括绝对路径)__filename
获取当前操作的文件名(包括绝对路径)
fs.readFile(__dirname + '/files/1.txt', 'utf8', function(err, data) {
...
})
// 所以就可以对刚刚进行编写的修改 成 __dirname + '文件名'
fs 更多文件操作
验证路径是否存在:
fs.exists(path, callback)
fs.existsSync(path)
获取文件信息:
fs.stat(path, callback)
fs.stat(path)
删除文件:
fs.unlink(path, callback)
fs.unlinkSync(path)
列出文件:
fs.readdir(path[,options], callback)
fs.readdirSync(path[, options])
截断文件:
fs.truncate(path, len, callback)
fs.truncateSync(path, len)
建立目录:
fs.mkdir(path[, mode], callback)
fs.mkdirSync(path[, mode])
删除目录:
fs.rmdir(path, callback)
fs.rmdirSync(path)
重命名文件和目录:
fs.rename(oldPath, newPath, callback)
fs.renameSync(oldPath, newPath)
监视文件更改:
fs.watchFile(filename[, options], listener)
path 路径模块
path 模块是 Node.js 官方提供的、用来处理路径的模块。它提供了一系列的方法和属性,用来满足用户对路径的处理需求。
path.join() 路径拼接
语法格式:
path.join([多个路径字符串])
- 返回值:
<string>
代码实例如下:
// 导入需要的模块
const path = require('path')
const fs = require('fs')
const pathStr = path.join('/a', '/b/c', '../', './d', 'e')
const pathStr2 = path.join('/a', '/b/c', '../../', './d', 'e')
/*
../ 会抵消前面一个的路径 如果有两层 ../ 则会抵消两层目录路径
./ 会被忽略
*/
console.log(pathStr) // \a\b\d\e
console.log(pathStr2) // \a\d\e
// 使用path.join 拼接 读取文件
fs.readFile(path.join(__dirname, '/files/demo.txt'), 'utf8', function (err, dataStr) {
if (err) {
return console.log(err.message)
}
console.log(dataStr)
})
涉及路径的都尽量使用path.join
path.basename() 获取路径中文件名
使用 path.basename()
方法,可以获取路径中的最后一部分,常通过该方法获取路径中的文件名
path.basename(path[, ext])
参数:
path
: 文件路径ext
: 文件扩展名
示例代码如下:
// 导入需要的模块
const path = require('path')
// 获取本文件的路径
const filenamePath = __filename;
// 得到文件名 包括扩展名
console.log(path.basename(filenamePath)) // index.html
// 得到文件名 不包括扩展名
console.log(path.basename(filenamePath, '.js')) // 只有文件名,没有js这个扩展名
path.extname() 获取路径中文件扩展名
// 导入需要的模块
const path = require('path')
const filenamePath = __filename;
// 得到文件的扩展名
console.log(path.extname(filenamePath)) // 只有扩展名
http 模块
http 模块是 Node.js 官方提供的、用来创建 web 服务器的模块。
使用http.createServer()
创建服务器。
http 模块主要用于搭建 HTTP 服务端和客户端,使用 HTTP 服务器或客户端功能必须调用 http 模块。
创建基本 Web 服务器
- 导入 http 模块
var http = require('http');
-
实例化web服务
-
为服务器绑定request事件
监听客户端发送过来的请求
参数说明:
1. 事件的名称
2. 触发事件的回调函数
- 启动服务器
调用服务器的实例 server.listen()
进行启动服务器
完整代码实现如下:
// 1. 导入模块
let http = require('http');
// 2.服务器初始化
let server = http.createServer();
// 3.给服务器绑定事件
server.on('request',function (req,res) {
console.log('服务器被访问');
})
// 4.启动服务器
server.listen(9090,function (){
console.log('127.0.0.1/9090 启动成功~');
})
req请求对象
只要服务器接收到客户端的请求,就会执行server.on
并绑定 request 事件处理函数
访问客户端的数据或属性
req.url
: 客户端的请求URL地址req.method
: 客户端请求的方式(GET、POST)
server.on('request', function (req, res) {
console.log('服务器被访问');
// 获取客户端请求的地址
console.log('url=' + req.url);
// 获取客户端请求的方式
console.log('method=' + req.method);
})
req常见的属性
req.app
:当callback为外部文件时,用req.app访问express的实例req.baseUrl
:获取路由当前安装的URL路径req.body / req.cookies
:获得「请求主体」/ Cookiesreq.fresh / req.stale
:判断请求是否还「新鲜」req.hostname / req.ip
:获取主机名和IP地址req.originalUrl
:获取原始请求URLreq.params
:获取路由的parametersreq.path
:获取请求路径req.protocol
:获取协议类型req.query
:获取URL的查询参数串req.route
:获取当前匹配的路由req.subdomains
:获取子域名req.accepts()
:检查可接受的请求的文档类型req.acceptsCharsets / req.acceptsEncodings / req.acceptsLanguages
:返回指定字符集的第一个可接受字符编码req.get()
:获取指定的HTTP请求头req.is()
:判断请求头Content-Type的MIME类型
res 效应对象
访问服务器的数据或者属性
res常见属性
res.app
:同req.app一样res.append()
:追加指定HTTP头res.set()
在res.append()
后将重置之前设置的头res.cookie(name,value [,option])
:设置Cookieopition: domain / expires / httpOnly / maxAge / path / secure / signed
res.clearCookie()
:清除Cookieres.download()
:传送指定路径的文件res.get()
:返回指定的HTTP头res.json()
:传送JSON响应res.jsonp()
:传送JSONP响应res.location()
:只设置响应的Location HTTP头,不设置状态码或者close responseres.redirect()
:设置响应的Location HTTP头,并且设置状态码302res.render(view,[locals],callback)
:渲染一个view,同时向callback传递渲染后的字符串,如果在渲染过程中有错误发生next(err)将会被自动调用。callback将会被传入一个可能发生的错误以及渲染后的页面,这样就不会自动输出了。res.send()
:传送HTTP响应res.sendFile(path [,options] [,fn])
:传送指定路径的文件 -会自动根据文件extension设定Content-Typeres.set()
:设置HTTP头,传入object可以一次设置多个头res.status()
:设置HTTP状态码res.type()
:设置Content-Type的MIME类型
解决中文乱码
通过设置响应头来解决
res.setHeader('Content-Type','text/html; charset=utf-8');
路由效果实现:
解释一下:路由
- 在进行页面切换的时候,浏览器发送不同的
url
请求;服务器接收到浏览器的请求时,通过解析不同的url
去拼接需要的html
或者模板,然后将结果返回给浏览器端进行渲染。 - 根据不同 url 地址渲染不同的 html 内容。
路由分类:
- 页面路由(刷新)
- hash路由(不刷新)
- history路由(不刷新)
const http = require('http')
const server = http.createServer()
server.on('request', (req, res) => {
const url = req.url
// 设置默认的响应内容为 404 Not found
let content = '<h1>404 Not found!</h1>'
// 判断用户请求的是否为 / 或 /index.html 首页
// 判断用户请求的是否为 /about.html 关于页面
if (url === '/' || url === '/index.html') {
content = '<h1>首页</h1>'
} else if (url === '/about.html') {
content = '<h1>关于页面</h1>'
}
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.end(content)
})
server.listen(8088, () => {
console.log('server running at http://127.0.0.1')
})
模块化
模块化介绍
- 模块化是指解决一个复杂问题时,自顶向下逐层把系统划分为若干模块的过程,模块是可组合、分解和更换的单元。
- 模块化可提高代码的复用性和可维护性,实现按需加载。
- 模块化规范是对代码进行模块化拆分和组合时需要遵守的规则,如使用何种语法格式引用模块和向外暴露成员。
模块化的特点
- 提高代码的复用性
- 提高代码的维护性
- 实现按需加载
Node.js 中模块的分类
- 内置模块(Nodejs提供)
- 自定义模块 (用户自定义JS文件)
- 第三方模块(第三方开发的模块,先下载再使用)
模块加载机制
模块第一次加载后会被缓存,即多次调用 require()
不会导致模块的代码被执行多次,提高模块加载效率。
1. 内置模块加载
内置模块加载优先级最高。
2. 自定义模块加载
加载自定义模块时,路径要以 ./
或 ../
开头,否则会作为内置模块或第三方模块加载。
导入自定义模块时,若省略文件扩展名,则 Node.js 会按顺序尝试加载文件:
- 按确切的文件名加载
- 补全
.js
扩展名加载 - 补全
.json
扩展名加载 - 补全
.node
扩展名加载 - 报错
3. 第三方模块加载
- 若导入第三方模块, Node.js 会从当前模块的父目录开始,尝试从
/node_modules
文件夹中加载第三方模块。 - 如果没有找到对应的第三方模块,则移动到再上一层父目录中,进行加载,直到文件系统的根目录。
例如,假设在 C:\Users\bruce\project\foo.js
文件里调用了 require('tools')
,则 Node.js 会按以下顺序查找:
C:\Users\bruce\project\node_modules\tools
C:\Users\bruce\node_modules\tools
C:\Users\node_modules\tools
C:\node_modules\tools
4.目录作为模块加载
当把目录作为模块标识符进行加载的时候,有三种加载方式:
- 在被加载的目录下查找
package.json
的文件,并寻找main
属性,作为require()
加载的入口 - 如果没有
package.json
文件,或者main
入口不存在或无法解析,则 Node.js 将会试图加载目录下的index.js
文件。 - 若失败则报错
Node.js 中的模块作用域
- 在自定义模块中定义的变量、方法等成员,只能在当前模块内被访问,这种模块级别的访问限制,叫做模块作用域
- 防止全局变量污染
模块作用域的成员
- 自定义模块中都有一个
module
对象,存储了和当前模块有关的信息 - 在自定义模块中,可以使用
module.exports
对象,将模块内的成员共享出去,供外界使用。导入自定义模块时,得到的就是module.exports
指向的对象。 - 默认情况下,
exports
和module.exports
指向同一个对象。最终共享的结果,以module.exports
指向的对象为准。
demo.js
// 导入模块
const stu1 = require('./module1.js')
stu1.address = 'gx'
console.log(stu1)
module.js
// 定义一个 stu对象
module.exports.stu = {
name: '张三',
age: '19'
}
// say方法
module.exports.say = function () {
console.log('我是:' + stu.name + '年龄为:' + stu.age)
}
const address = '';
module.exports.address = address;
最后的输出结果:
注意
- 如果是将
module.exports
指向全新的对象,则会替换掉前面的属性的内容
// 定义一个 stu对象
module.exports.stu = {
name: '张三',
age: '19'
}
// say方法
module.exports.say = function () {
console.log('我是:' + stu.name + '年龄为:' + stu.age)
}
const address = '';
module.exports.address = address;
module.exports = {
tname: 'lisi',
age: '89'
}
结果就会变成最下面的内容:
- 最终的结果,以
module.exports
指向的对象为准。
Node.js 遵循 CommonJS 模块化规范
- 每个模块内部,
module
变量代表当前模块 module
变量是一个对象,module.exports(exports)
是对外的接口- 加载某个模块即加载该模块的
module.exports
属性
npm 和 包
包:由第三方提供或开发
包特点
- 包有内置模块封装出来的
- 提高代码开发效率
npm(Node Package Manager)
初始化 npm 的时候会多出 node_modules
、packge.json
和 packge-lock.json
配置文件
node_modules
:主要就是已经安装到项目中的包packge.json
:当前安装包的版本信息packge-lock.json
:存放node_modules 中的包的各种信息。
安装指定版本的包
npm install 包名称@版本号
- 注意
@
符号不可以省略
格式化时间
安装包
npm install moment // moment包
// 1. 安装包 moment
// 2.导入包
const moment = require('moment');
// 时间格式化
let s = moment().format('YYYY-MM-DD HH:mm:ss');
console.log(s);
卸载包
npm uninstall
Express
官网传送门
基于 Node.js 平台,快速、开放、极简的 Web 开发框架
Express 是用于快速创建服务器的第三方包。
基本使用
基本步骤:
-
安装express
npm install express
-
导入express模块
-
启动服务端口
GET请求
使用 app.get()
方法监听客户端的GET请求
POST请求
使用 app.post()
方法监听客户端的 POST 请求
以上两个请求中都具有两个参数
- 请求的类型
- 触发的函数:具有两个形参
- req:请求对象
- res:响应事件
内容响应给客户端
通过 res.send()
方法,将处理好的内容返回给客户端
获取URL中携带的参数
通过 req.query 可以获取到客户端发送过来的查询参数
req.params 是动态匹配到的 URL 参数,默认是一个空对象
创建服务器,监听客户端请求,并返回内容
// 1. 安装express:npm install express
// 2. 导入express
const express = require('express')
// 创建 web 服务器
const app = express()
// 监听客户端的 GET 和 POST 请求,并向客户端响应具体的内容
app.get('/user', (req, res) => {
res.send('服务端获得GET请求~~~')
})
app.post('/user', (req, res) => {
res.send('服务端获得POST请求成功~~~~')
})
// 获取参数
app.get('/', (req, res) => {
// 通过 req.query 可以获取到客户端发送过来的查询参数
console.log(req.query)
res.send(req.query)
})
// :ids 和 :username 是一个动态的参数,请求时必须写完动态参数
app.get('/user/:ids/:username', (req, res) => {
// req.params 是动态匹配到的 URL 参数,默认是一个空对象
console.log(req.params)
res.send(req.params)
})
// 启动服务器
app.listen(8099, () => {
console.log('express server running at http://127.0.0.1/8099')
})
托管静态资源
- 通过
express.static()
方法可创建静态资源服务器,向外开放访问静态资源。 - Express 在指定的静态目录中查找文件,并对外提供资源的访问路径,存放静态文件的目录名不会出现在 URL 中
- 访问静态资源时,会根据托管顺序查找文件
- 可为静态资源访问路径添加前缀
// 1. 导入express
let express = require('express');
const app = express();
// 2.调用外层的一个/多个静态资源
app.use(express.static('../FS'))//将FS的文件对外提供
app.use(express.static('../http'))//将http的文件对外提供
app.use(express.static('../path'))//将path的文件对外提供
// 3.启动服务
app.listen(8099, () => {
console.log('Server running is http://127.0.0.1:8099');
})
挂载路径前缀
对静态资源进度托管时,必须在前面加入挂载路径前缀,如下代码所示,前一个参数就是路径前缀
app.use('/FS', express.static('../FS'))
Express 路由
在Express中,路由指的是客户端的请求与服务器处理函数之间的映射关系。
Express 中的路由分3部分组成,分别是请求的类型、请求的URL地址、处理函数,格式如下:
app.method(url,function(){})
Express 路由实例代码
// 1. 导入express
var express = require('express');
// 2.调用express函数
var app = express();
// 根目录 主页输出 "Hello World"
app.get('/', function (req, res) {
console.log("根页面 GET 请求");
res.send('Hello GET');
})
// POST 请求
app.post('/', function (req, res) {
console.log("根页面 POST 请求");
res.send('Hello POST');
})
var server = app.listen(8099, function () {
console.log('应用实例,访问地址为 http://127.0.0.1/' + server.address().port)
})
Express 路由实现过程
- 客户端发送的请求到达服务器之后,首先要经过路由的匹配 ,匹配成功,才会进行函数调用,
- 按照顺序进行匹配
- 在路由匹配时,会进行顺序的匹配,若是请求类型和请求的URL同时匹配成功,则Express将请求交给对应的 function 函数处理。
Express 路由模块化
实现过程:
- 创建路由模块对应的.js 文件
- 调用express.Router()函数创建路
- 由对象向路由对象上挂载具体的路由
- 使用
module.exports
向外共享路由对象 - 使用app.use()函数注册路由模块
router.js
// 1. 导入express
var express = require('express');
// 2.调用Router函数
var router = express.Router();
// 3. 挂载路由
router.get('/user/add', function (req, res) {
console.log("/user/add GET 请求");
res.send('/user/add');
})
router.post('/user/list', function (req, res) {
console.log("/user/list POST 请求");
res.send('/user/list');
})
// 4.向外抛出路由
module.exports = router;
router2.js
// 1. 导入express
var express = require('express');
// 2.调用express函数
var app = express();
// 3.导入路由模块
const router = require('./router');
// 4.注册路由模块 app.use 注册全局中间件
// 注册路由模块,添加访问前缀
app.use('/routerApi',router);
// 启动服务
app.listen(8099, (req, res) => {
console.log('http://127.0.0.1:8099')
})
启动router2.js 即可实现路由的模块化管理
Express 中间件
中间件介绍
- 中间件是指流程的中间处理环节
- 服务器收到请求后,可先调用多个中间件对这个请求进行预处理
- 中间件是一个函数,包含
req, res, next
三个参数next()
函数作为多个中间件之间的链接关键,会参数把流转关系交给下一个中间件或路由
中间件处理环节:
中间件注意事项;
- 在注册路由之前注册中间件(错误级别中间件除外)
- 中间件可连续调用多个
- 别忘记调用
next()
函数 next()
函数后别写代码- 多个中间件共享
req
、res
对象
简单的中间件函数
let express = require('express');
const app = express();
// 定义中间件函数
const midfunction = function (req, res, next) {
console.log('这是一个中间件函数');
next();
}
app.listen(8099, () => {
console.log('http://127.0.0.1:8099');
})
中间件作用
多个中间件之间,共享同一份req和res。基于这样的特性,我们可以在上游的中间件中,统一为req或res对象添加自定义的属性或方法,供下游的中间件或路由进行使用。
- 简化代码书写
用于记录到达路由时间来演示中间件作用:
let express = require('express');
const app = express();
// 定义中间件函数
const midfunction = function (req, res, next) {
const time = Data.now();
// req自定义挂载属性
req.starTime = time;
next();
}
// 全局生效中间件
app.use(midfunction);
// 挂载路由
app.get('/user/add', function (req, res) {
// 调用上游的自定义属性
res.send('/user/add' + req.startTime);
})
app.post('/user/list', function (req, res) {
res.send('/user/list' + req.startTime);
})
app.listen(8099, () => {
console.log('http://127.0.0.1:8099');
})
全局生效的中间件
全局中间件:客户端发送过来的请求,在服务器端接收之后,都会触发的中间件
使用的是 app.use() ,进行定义全局中间件
let express = require('express');
const app = express();
// 定义中间件函数
const midfunction = function (req, res, next) {
console.log('这是一个中间件函数');
next();
}
// 全局生效中间件
app.use(midfunction);
// 挂载路由
app.get('/user/add', function (req, res) {
console.log("/user/add GET 请求");
res.send('/user/add');
})
app.post('/user/list', function (req, res) {
console.log("/user/list POST 请求");
res.send('/user/list');
})
app.listen(8099, () => {
console.log('http://127.0.0.1:8099');
})
简化全局中间件
// 定义全局生效中间件
app.use(function (req, res, next) {
console.log('这是简化中间件函数');
next();
});
连续定义多个全局中间件
多个中间件之间会按照从上到下的顺序执行
let express = require('express');
const app = express();
// 定义全局生效中间件
app.use(function (req, res, next) {
console.log('这是第一个中间件函数');
next();
})
app.use((req, res, next)=> {
console.log('这是第二个中间件函数');
next();
})
// 挂载路由
app.get('/user', function (req, res) {
res.send('/user/add');
})
app.listen(8099, () => {
console.log('http://127.0.0.1:8099');
})
局部中间件
不使用 app.use() 进行创建中间件就是局部中间件
使用的语法格式:
app.mthod(URL,中间件对象,处理喊出);
代码实例如下:
let express = require('express');
const app = express();
// 定义中间件函数
const midfunction = function (req, res, next) {
console.log('这是一个局部中间件函数');
next();
}
// 挂载路由
app.get('/user',midfunction, function (req, res) {
console.log("/user/add GET 请求");
res.send('/user/add');
})
app.listen(8099, () => {
console.log('http://127.0.0.1:8099');
})
定义多个局部中间件
格式:
app.mthod(URL,中间件对象1,中间件对象2,...,中间件对象n,处理喊出);
实例代码如下:
let express = require('express');
const app = express();
// 定义中间件函数
const midfunction = function (req, res, next) {
console.log('这是一个局部中间件函数');
next();
}
// 定义中间件函数
const mid2 = function (req, res, next) {
console.log('这是二个局部中间件函数');
next();
}
// 定义中间件函数
const mid3 = function (req, res, next) {
console.log('这是三个局部中间件函数');
next();
}
// 挂载路由
app.get('/user', midfunction, mid2, mid3, function (req, res) {
console.log("/user/add GET 请求");
res.send('/user/add');
})
app.listen(8099, () => {
console.log('http://127.0.0.1:8099');
})
中间件注意事项
- 一定要在路由之前注册中间件
- 客户端发送过来的请求,可以连续调用多个中间件进行处理
- 执行完中间件的业务代码之后,不要忘记调用next()函数
- 为了防止代码逻辑混乱,调用next()函数后不要再写额外的代码
- 连续调用多个中间件时,多个中间件之间,共享req和res对象
中间件分类
1. 应用级别的中间件
- 通过
app.use()
或app.get()
或app.post()
,绑定到app
实例上的中间件- 全局中间件
- 局部中间件
// 定义全局生效中间件
app.use(function (req, res, next) {
console.log('这是简化中间件函数');
next();
});
// 定义局部中间件函数
const midfunction = function (req, res, next) {
console.log('这是一个局部中间件函数');
next();
}
// 挂载路由
app.get('/user',midfunction, function (req, res) {
console.log("/user/add GET 请求");
res.send('/user/add');
})
2. 路由级别的中间件
绑定到
express.Router()
实例上的中间件,叫做路由级别的中间件。用法和应用级别中间件没有区别。应用级别中间件是绑定到
app
实例上,路由级别中间件绑定到router
实例上。
const app = express()
const router = express.Router()
router.use(function (req, res, next) {
console.log(1)
next()
})
app.use('/', router)
3. 错误级别的中间件
- 用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题
- 错误级别中间件的处理函数中,必须有 4 个形参,形参顺序从前到后分别是
(err, req, res, next)
。 - 错误级别的中间件必须注册在所有路由之后
const express = require('express')
const app = express()
// 路由
app.get('/user', (req, res) => {
throw new Error('服务器内部发生了错误!') // 执行了这句之后的程序就不会在执行
res.send('user')
})
// 错误级别中间件
// 定义错误级别的中间件,捕获整个项目的异常错误,从而防止程序的崩溃
app.use((err, req, res, next) => {
console.log('发生了错误!' + err.message)
res.send('Error:' + err.message)
})
app.listen(8099, function () {
console.log('Running http://127.0.0.1:8099')
})
4. Express 内置中间件
Express 内置了 3 个常用的中间件,极大的提高了 Express 项目的开发效率和体验:
express.static
快速托管静态资源的内置中间件,例如: HTML 文件、图片、CSS 样式等(无兼容性)express.json
解析 JSON 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)express.urlencoded
解析 URL-encoded 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)
// 1. 导入express
const express = require('express');
// 2.调用express函数
const app = express();
//内置中间件
app.use(express.json()) //JSON数据解析
// 解析URL-encoded数据
app.use(express.urlencoded({
extended: false
}))
// 接收JSON数据
app.get('/user', function (req, res) {
// 获取到服务器的数据
console.log(req.body);
res.send('user');
})
// 接收URL
app.post('/list', function (req, res) {
console.log(req.body);
res.send('list');
})
var server = app.listen(8099, function () {
console.log('服务器启动成功)
})
5. 第三方中间件
- 安装中间件
npm install 包名
- 导入中间件 require()
- app.use() 注册中间件
自定义中间件
- 定义中间件
- 监听req的 data 事件
- 监听req的 end 事件
- 使用querystring 模块解析请求体数据
- 将解析出来的数据对象挂载为 req.body
- 将自定义中间件封装为模块
custom-body-parse.js
const express = require('express');
const app = express();
// 1.导入中间件模块js文件
let customBodyParse = require('./custom-body-parse');
// 2.注册中间件函数
app.use(customBodyParse)
app.post('/user', (req, res) => {
// 下游使用req.body
res.send(req.body);
})
var server = app.listen(8099, function () {
console.log('服务器地址为 http://127.0.0.1:' + server.address().port)
})
boot-custom-body-parse.js
const express = require('express');
const app = express();
// 1.导入中间件模块js文件
let customBodyParse = require('./custom-body-parse');
// 2.注册中间件函数
app.use(customBodyParse)
app.post('/user', (req, res) => {
// 下游使用req.body
res.send(req.body);
})
var server = app.listen(8099, function () {
console.log('服务器地址为 http://127.0.0.1:' + server.address().port)
})
Express 写接口
apiWeb.js
// 1. 导入express
var express = require('express');
// 2.调用express函数
var app = express();
// 解析数据
app.use(express.urlencoded({extended:false}))
// 3.导入api路由模块
let router = require('./apirouter');
// 4.注册路由 指定前缀
app.use('/api',router)
var server = app.listen(8099, function () {
console.log('应用实例,访问地址为 http://127.0.0.1/' + server.address().port)
})
})
apirouter.js
// 1. 导入express
var express = require('express');
// 2.调用Router函数
var router = express.Router();
// 3. 挂载路由
// GET路由
router.get('/get', function (req, res) {
// 获取客户端的请求数据req.query
let query = req.query;
// res.send向客户端响应数据
res.send({
status:1, //0:失败 1:成功
msg: "GET请求成功",
data:query
});
})
router.post('/post', function (req, res) {
// 获取客户端的请求数据req.query
let body = req.body;
// res.send向客户端响应数据
res.send({
status:1, //0:失败 1:成功
msg: "POST请求成功",
data: body
});
})
// 4.向外抛出路由
module.exports = router;
跨域问题
见 Ajax笔记 第6部分
CORS(跨域资源共享)解决跨域
在路由之前引入cors,并注册
- 安装中间件:
npm install cors
- 导入中间件:
const cors = require('cors')
- 配置中间件:
app.use(cors())
配置CORS的响应头 解除浏览器的跨域访问限制
数据库与身份认证
数据库介绍
看视频介绍吧
配置 mysql 模块
- 安装 mysql 模块
npm install mysql
- 建立连接
// 1. 导入mysql模块
const mysql = require('mysql')
// 2. 与MySQL建立连接
const db = mysql.createPool({
host: '127.0.0.1', // 数据库IP地址
user: 'root', // 数据库用户名
password: '123456', // 数据库密码
database: 'nodejs', // 数据库名称
})
- 测试链接数据库是否成功
// 3. 测试数据库链接
db.query('select 1',(err,results)=>{
if (err){
// 链接失败的信息
return console.log(err.message)
}
console.log(results) // 链接成功
})
可能出现的错误,解决方案参考
Express 操作 mysql 数据库
- 查询数据
// 查询操作
db.query('select * from user',(err,results)=>{
if (err){
// 链接失败的信息
return console.log(err.message)
}
console.log(results) // 链接成功
})
- 插入数据
// 插入操作
// 插入的对象内容
const user = {
username: '小明',
password: 'xiaoming',
}
// ? 占位符 对应数组中的两个值
db.query('insert into user(username,password) values(?,?)',[user.username,user.password], (err, results) => {
if (err) {
// 插入失败的信息
return console.log(err.message)
}
if (results.affectedRows ===1 ){
console.log('插入成功') //插入成功
}
})
插入的快捷方式:向表中新增数据时,如果数据对象的每个属性和数据表的字段一一对应,则可以通过如下方式快速插入数据:
const user = {
username: '小明',
password: 'xiaoming',
}
// 注意sql 语句的格式
const sql = 'insert into users set ?'
db.query(sql, user, (err, results) => {
if (err) {
// 插入失败的信息
return console.log(err.message)
}
if (results.affectedRows ===1 ){
console.log('插入成功') //插入成功
}
})
- 更新数据
// 更新操作
// 更新的对象内容
const user = {
id: 3,
username: '张三',
password: 'zhangsan',
}
// sql 语句
const sql = 'update user set username=?,password=? where id = ?'
// ? 占位符 对应数组中的值
db.query(sql, [user.username, user.password, user.id], (err, results) => {
if (err) {
// 插入失败的信息
return console.log(err.message)
}
if (results.affectedRows === 1) {
console.log('UPDATE successfully!') //插入成功
}
})
更新的快捷方式:
const user = {id:3,username:'Bruce',password:'11111'}
const sql = 'update users set ? where id=?'
db.query(sql, [user, user.id], (err, results) => {
...
})
- 删除数据
// 删除操作
// sql 语句
const sql = 'delete from user where id = ?'
// ? 占位符 对应数组中的值
db.query(sql, 4, (err, results) => {
if (err) {
// 插入失败的信息
return console.log(err.message)
}
if (results.affectedRows === 1) {
console.log('DELETE successfully!') //插入成功
}
})
使用 delete 语句会真正删除数据,保险起见,使用标记删除的形式,模拟删除的动作。即在表中设置状态字段,标记当前的数据是否被删除。
// 标记删除操作
// sql 语句 将用户变成禁用状态
const sql = 'update users set status=1 where id=?'
// ? 占位符 对应数组中的值
db.query(sql, 3, (err, results) => {
if (err) {
// 插入失败的信息
return console.log(err.message)
}
if (results.affectedRows === 1) {
console.log('DELETE successfully!') //插入成功
}
})
Web 开发模式
1. 服务端渲染
服务器发送给客户端的 HTML 页面,是在服务器通过字符串的拼接动态生成的。因此客户端不需要使用 Ajax 额外请求页面的数据。
app.get('/index.html', (req, res) => {
const user = { name: 'Bruce', age: 29 }
const html = `<h1>username:${user.name}, age:${user.age}</h1>`
res.send(html)
})
优点:
- 前端耗时短。浏览器只需直接渲染页面,无需额外请求数据。
- 有利于 SEO。服务器响应的是完整的 HTML 页面内容,有利于爬虫爬取信息。
缺点:
- 占用服务器资源。服务器需要完成页面内容的拼接,若请求比较多,会对服务器造成一定访问压力。
- 不利于前后端分离,开发效率低。前后端无法进行分工合作。
2. 前后端分离
前后端分离的开发模式,依赖于 Ajax 技术的广泛应用。后端只负责提供 API 接口,前端使用 Ajax 调用接口。
优点:
- 开发体验好。前端专业页面开发,后端专注接口开发。
- 用户体验好。页面局部刷新,无需重新请求页面。
- 减轻服务器的渲染压力。页面最终在浏览器里生成。
缺点:
- 不利于 SEO。完整的 HTML 页面在浏览器拼接完成,因此爬虫无法爬取页面的有效信息。Vue、React 等框架的 SSR(server side render)技术能解决 SEO 问题。
3. 如何选择开发模式
- 企业级网站,主要功能是展示,没有复杂交互,且需要良好的 SEO,可考虑服务端渲染
- 后台管理项目,交互性强,无需考虑 SEO,可使用前后端分离
- 为同时兼顾首页渲染速度和前后端分离开发效率,可采用首屏服务器端渲染+其他页面前后端分离的开发模式
身份认证(Authentication)
不同的用户,具有不同的身份
Session 认证机制
服务端渲染推荐使用 Session 认证机制
HTTP协议的无状态性
HTTP协议的无状态性:客户端每次请求都是独立,连续多个请求之间没有直接的关系,服务器不会主动保留每次HTTP请求的状态。
接触HTTP无状态性
通过Cookie 解除
Cookie :是存储在用户浏览器中的一段不超过4KB的字符串。它由一个名称(Name)、一个值(Value)和其它几个用于控制Cookie有效期、安全性、使用范围的可选属性组成。
不同域名下的Cookie各自独立,每当客户端发起请求时,会自动把当前域名下所有未过期的Cookie一同发送到服务器。
Cookie特点
- 自动发送
- 域名独立
- 过期时限
- 大小 4KB限制
Cookie 身份认证
客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的Cookie,客户端会自动将Cookie 保存在浏览器中。
随后,当客户端浏览器每次请求服务器的时候,浏览器会自动将身份认证相关的Cookie,通过请求头的形式发送给服务器,服务器即可验明客户端的身份。
百度的cookie
Cookie 不安全性
Cookie 很容易会被伪造
不要将重要的信息存储到cookie
提高身份认证的安全性:
用户和cookie 认证
Session 工作原理图
Express 中使用 Session 认证
- 安装 express-session 中间件
npm install express-session
- 配置中间件
const session = require('express-session')
app.use(
session({
secret: 'shier',
resave: false,
saveUninitialized: true,
})
)
- 向 session 中存数据
中间件配置成功后,可通过 req.session
访问 session 对象,存储用户信息
// 托管静态页面
app.use(express.static('./pages'))
// 解析 POST 提交过来的表单数据
app.use(express.urlencoded({ extended: false }))
// 登录的 API 接口
app.post('/api/login', (req, res) => {
// 判断用户提交的登录信息是否正确
if (req.body.username !== 'shier' || req.body.password !== '000000') {
return res.send({ status: 1, msg: '登录失败' })
}
// 请将登录成功后的用户信息,保存到 Session 中
req.session.user = req.body // 用户的信息
req.session.islogin = true // 用户的登录状态
res.send({ status: 0, msg: '登录成功' })
})
- 从 session 取数据
// 获取用户姓名的接口
app.get('/api/username', (req, res) => {
// 从 Session 中获取用户的名称,响应给客户端
if (!req.session.islogin) {
// 没有登录
return res.send({status: 1, msg: 'fail'})
}else {
res.send({
status: 0,
msg: 'successfully',
username: req.session.user.username,
})
}
})
- 清空 session
// 退出登录的接口
app.post('/api/logout', (req, res) => {
// 清空 Session 信息
req.session.destroy()
res.send({
status: 0,
msg: '退出登录成功',
})
})
JWT 认证机制
前后端分离推荐使用 JWT(JSON Web Token)认证机制,是目前最流行的跨域认证解决方案
JWT 工作原理
Session 认证的局限性:
- Session 认证机制需要配合 Cookie 才能实现。由于 Cookie 默认不支持跨域访问,所以,当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域 Session 认证。
- 当前端请求后端接口不存在跨域问题的时候,推荐使用 Session 身份认证机制。
- 当前端需要跨域请求后端接口的时候,不推荐使用 Session 身份认证机制,推荐使用 JWT 认证机制
JWT 工作原理图:
用户的信息通过 Token 字符串的形式,保存在客户端浏览器中。服务器通过还原 Token 字符串的形式来认证用户的身份。
JWT 组成部分:
- Header(头部)、Payload(有效荷载)、Signature(签名)
- Payload 是真正的用户信息,加密后的字符串
- Header 和 Signature 是安全性相关部分,保证 Token 安全性
- 三者使用
.
分隔
Header.Payload.Signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTcsInVzZXJuYW1lIjoiQnJ1Y2UiLCJwYXNzd29yZCI6IiIsIm5pY2tuYW1lIjoiaGVsbG8iLCJlbWFpbCI6InNjdXRAcXEuY29tIiwidXNlcl9waWMiOiIiLCJpYXQiOjE2NDE4NjU3MzEsImV4cCI6MTY0MTkwMTczMX0.bmqzAkNSZgD8IZxRGGyVlVwGl7EGMtWitvjGD-a5U5c
JWT 使用方式:
- 客户端会把 JWT 存储在 localStorage 或 sessionStorage 中
- 此后客户端与服务端通信需要携带 JWT 进行身份认证,将 JWT 存在 HTTP 请求头 Authorization 字段中
- 加上 Bearer 前缀
Authorization: Bearer <token>
Express 使用 JWT
-
安装
-
jsonwebtoken 用于生成 JWT 字符串
-
express-jwt 用于将 JWT 字符串解析还原成 JSON 对象
-
npm install jsonwebtoken express-jwt
-
定义 secret 密钥
-
为保证 JWT 字符串的安全性,防止其在网络传输过程中被破解,需定义用于加密和解密的 secret 密钥
-
生成 JWT 字符串时,使用密钥加密信息,得到加密好的 JWT 字符串
-
把 JWT 字符串解析还原成 JSON 对象时,使用密钥解密
-
const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')
// 密钥为任意字符串
const secretKey = 'Shier 😎'
- 生成 JWT 字符串
// 登录接口
app.post('/api/login', function (req, res) {
// 将 req.body 请求体中的数据,转存为 userinfo 常量
const userinfo = req.body
// 登录失败
if (userinfo.username !== 'shier' || userinfo.password !== '000000') {
return res.send({
status: 400,
message: '登录失败!',
})
}
// 登录成功
// 在登录成功之后,调用 jwt.sign() 方法生成 JWT 字符串。并通过 token 属性发送给客户端
// 参数1:用户的信息对象
// 参数2:加密的秘钥
// 参数3:配置对象,可以配置当前 token 的有效期
// 记住:千万不要把密码加密到 token 字符中
const tokenStr = jwt.sign({username: userinfo.username}, secretKey, {expiresIn: '60s'})
res.send({
status: 200,
message: '登录成功!',
token: tokenStr, // 要发送给客户端的 token 字符串
})
})
-
JWT 字符串还原为 JSON 对象
-
客户端访问有权限的接口时,需通过请求头的
Authorization
字段,将 Token 字符串发送到服务器进行身份认证 -
服务器可以通过 express-jwt 中间件将客户端发送过来的 Token 解析还原成 JSON 对象
-
// unless({ path: [/^\/api\//] }) 指定哪些接口无需访问权限
app.use(expressJWT({ secret: secretKey }).unless({ path: [/^\/api\//] }))
- 获取用户信息
- 当 express-jwt 中间件配置成功后,即可在那些有权限的接口中,使用
req.user
对象,来访问从 JWT 字符串中解析出来的用户信息
- 当 express-jwt 中间件配置成功后,即可在那些有权限的接口中,使用
// 有权限的 API 接口
app.get('/admin/getinfo', function (req, res) {
// 使用 req.user 获取用户信息,并使用 data 属性将用户信息发送给客户端
console.log(req.user)
res.send({
status: 200,
message: '获取用户信息成功!',
data: req.user, // 要发送给客户端的用户信息
})
})
-
捕获解析 JWT 失败后产生的错误
-
当使用 express-jwt 解析 Token 字符串时,如果客户端发送过来的 Token 字符串过期或不合法,会产生一个解析失败的错误,影响项目的正常运行
-
通过 Express 的错误中间件,捕获这个错误并进行相关的处理
-
app.use((err, req, res, next) => {
if (err.name === 'UnauthorizedError') {
return res.send({ status: 401, message: 'Invalid token' })
}
res.send({ status: 500, message: 'Unknown error' })
})
大事件后台 API 项目
API 接口文档
1. 项目初始化
1.1 创建项目
- 新建
apiServer
文件夹作为项目根目录,并在项目根目录中运行如下的命令,初始化包管理配置文件:
npm init -y
- 运行如下的命令,安装特定版本的
express
:
npm i express@4.17.1
- 在项目根目录中新建
app.js
作为整个项目的入口文件,并初始化如下的代码:
const express = require('express')
// 创建 express 的服务器实例
const app = express()
// write your code here...
// 调用 app.listen 方法,指定端口号并启动web服务器
app.listen(3007, function () {
console.log('api server running at http://127.0.0.1:3007')
})
1.2 配置 cors 跨域
- 运行如下的命令,安装
cors
中间件:
npm i cors@2.8.5
- 在
app.js
中导入并配置cors
中间件:
const cors = require('cors')
app.use(cors())
1.3 配置解析表单数据的中间件
- 通过如下的代码,配置解析
application/x-www-form-urlencoded
格式的表单数据的中间件:
app.use(express.urlencoded({ extended: false }))
1.4 初始化路由相关的文件夹
- 在项目根目录中,新建
router
文件夹,用来存放所有的路由
模块
路由模块中,只存放客户端的请求与处理函数之间的映射关系
- 在项目根目录中,新建
router_handler
文件夹,用来存放所有的路由处理函数模块
路由处理函数模块中,专门负责存放每个路由对应的处理函数
1.5 初始化用户路由模块
- 在
router
文件夹中,新建user.js
文件,作为用户的路由模块,并初始化代码:
const express = require('express')
// 创建路由对象
const router = express.Router()
// 注册新用户
router.post('/reguser', (req, res) => {
res.send('reguser OK')
})
// 登录
router.post('/login', (req, res) => {
res.send('login OK')
})
module.exports = router
- 在
app.js
中,导入注册用户路由模块 :
const userRouter = require('./router/user')
app.use('/api', userRouter)
1.6 抽离用户路由模块中的处理函数
目的:为了保证
路由模块
的纯粹性,所有的路由处理函数
,必须抽离到对应的路由处理函数模块
中
- 在
/router_handler/user.js
中,使用exports
对象,分别向外共享如下两个路由处理函数
- 在这里定义和用户相关的路由处理函数,供 /router/user.js 模块进行调用
// 注册用户的处理函数
exports.regUser = (req, res) => {
res.send('reguser OK')
}
// 登录的处理函数
exports.login = (req, res) => {
res.send('login OK')
}
- 将
/router/user.js
中的代码修改为如下结构:
const express = require('express')
const router = express.Router()
// 导入用户路由处理函数模块
const userHandler = require('../router_handler/user')
// 注册新用户
router.post('/reguser', userHandler.regUser)
// 登录
router.post('/login', userHandler.login)
module.exports = router
2. 登录注册
2.1 新建 ev_users 表
- 在
test
数据库中,新建ev_users
表如下:
2.2 安装并配置 mysql 模块
在 API 接口项目中,需要安装并配置
mysql
这个第三方模块,来连接和操作 MySQL 数据库
- 运行如下命令,安装
mysql
模块:
npm i mysql@2.18.1
- 在项目根目录中新建
/db/index.js
文件,在此自定义模块中创建数据库的连接对象:
const mysql = require('mysql')
// 创建数据库连接对象
const db = mysql.createPool({
host: '127.0.0.1',
user: 'root',
password: 'admin123',
database: 'my_db_01',
})
// 向外共享 db 数据库连接对象
module.exports = db
2.3 注册
2.3.0 实现步骤
- 检测表单数据是否合法
- 检测用户名是否被占用
- 对密码进行加密处理
- 插入新用户
2.3.1 检测表单数据是否合法
- 判断用户名和密码是否为空
// 接收表单数据
const userinfo = req.body
// 判断数据是否合法
if (!userinfo.username || !userinfo.password) {
return res.send({ status: 1, message: '用户名或密码不能为空!' })
}
2.3.2 检测用户名是否被占用
- 导入数据库操作模块:
const db = require('../db/index')
- 定义 SQL 语句:
const sql = `select * from ev_users where username=?`
- 执行 SQL 语句并根据结果判断用户名是否被占用:
db.query(sql, [userinfo.username], function (err, results) {
// 执行 SQL 语句失败
if (err) {
return res.send({ status: 1, message: err.message })
}
// 用户名被占用
if (results.length > 0) {
return res.send({ status: 1, message: '用户名被占用,请更换其他用户名!' })
}
// TODO: 用户名可用,继续后续流程...
})
2.3.3 对密码进行加密处理
为了保证密码的安全性,不建议在数据库以
明文
的形式保存用户密码,推荐对密码进行加密存储
在当前项目中,使用 bcryptjs
对用户密码进行加密,优点:
- 加密之后的密码,无法被逆向破解
- 同一明文密码多次加密,得到的加密结果各不相同,保证了安全性
- 运行如下命令,安装指定版本的
bcryptjs
:
npm i bcryptjs@2.4.3
- 在
/router_handler/user.js
中,导入bcryptjs
:
const bcrypt = require('bcryptjs')
- 在注册用户的处理函数中,确认用户名可用之后,调用
bcrypt.hashSync(明文密码, 随机盐的长度)
方法,对用户的密码进行加密处理:
// 对用户的密码,进行 bcrype 加密,返回值是加密之后的密码字符串
userinfo.password = bcrypt.hashSync(userinfo.password, 10)
2.3.4 插入新用户
- 定义插入用户的 SQL 语句:
const sql = 'insert into ev_users set ?'
- 调用
db.query()
执行 SQL 语句,插入新用户:
db.query(sql, { username: userinfo.username, password: userinfo.password }, function (err, results) {
// 执行 SQL 语句失败
if (err) return res.send({ status: 1, message: err.message })
// SQL 语句执行成功,但影响行数不为 1
if (results.affectedRows !== 1) {
return res.send({ status: 1, message: '注册用户失败,请稍后再试!' })
}
// 注册成功
res.send({ status: 0, message: '注册成功!' })
})
2.4 优化 res.send() 代码
在处理函数中,需要多次调用
res.send()
向客户端响应处理失败
的结果,为了简化代码,可以手动封装一个 res.cc() 函数
- 在
app.js
中,所有路由之前,声明一个全局中间件,为 res 对象挂载一个res.cc()
函数 :
// 响应数据的中间件
app.use(function (req, res, next) {
// status = 0 为成功; status = 1 为失败; 默认将 status 的值设置为 1,方便处理失败的情况
res.cc = function (err, status = 1) {
res.send({
// 状态
status,
// 状态描述,判断 err 是 错误对象 还是 字符串
message: err instanceof Error ? err.message : err,
})
}
next()
})
2.5 优化表单数据验证
表单验证的原则:前端验证为辅,后端验证为主,后端永远不要相信前端提交过来的任何内容
在实际开发中,前后端都需要对表单的数据进行合法性的验证,而且,后端做为数据合法性验证的最后一个关口,在拦截非法数据方面,起到了至关重要的作用。
单纯的使用 if...else...
的形式对数据合法性进行验证,效率低下、出错率高、维护性差。因此,推荐使用第三方数据验证模块,来降低出错率、提高验证的效率与可维护性,让后端程序员把更多的精力放在核心业务逻辑的处理上。
- 安装
joi
包,为表单中携带的每个数据项,定义验证规则:
npm install joi
- 安装
@escook/express-joi
中间件,来实现自动对表单数据进行验证的功能:
npm i @escook/express-joi
- 新建
/schema/user.js
用户信息验证规则模块,并初始化代码如下:
const joi = require('joi')
/**
* string() 值必须是字符串
* alphanum() 值只能是包含 a-zA-Z0-9 的字符串
* min(length) 最小长度
* max(length) 最大长度
* required() 值是必填项,不能为 undefined
* pattern(正则表达式) 值必须符合正则表达式的规则
*/
// 用户名的验证规则
const username = joi.string().alphanum().min(1).max(10).required()
// 密码的验证规则
const password = joi
.string()
.pattern(/^[\S]{6,12}$/)
.required()
// 注册和登录表单的验证规则对象
exports.reg_login_schema = {
// 表示需要对 req.body 中的数据进行验证
body: {
username,
password,
},
}
- 修改
/router/user.js
中的代码如下:
const express = require('express')
const router = express.Router()
// 导入用户路由处理函数模块
const userHandler = require('../router_handler/user')
// 1. 导入验证表单数据的中间件
const expressJoi = require('@escook/express-joi')
// 2. 导入需要的验证规则对象
const { reg_login_schema } = require('../schema/user')
// 注册新用户
// 3. 在注册新用户的路由中,声明局部中间件,对当前请求中携带的数据进行验证
// 3.1 数据验证通过后,会把这次请求流转给后面的路由处理函数
// 3.2 数据验证失败后,终止后续代码的执行,并抛出一个全局的 Error 错误,进入全局错误级别中间件中进行处理
router.post('/reguser', expressJoi(reg_login_schema), userHandler.regUser)
// 登录
router.post('/login', userHandler.login)
module.exports = router
- 在
app.js
的全局错误级别中间件中,捕获验证失败的错误,并把验证失败的结果响应给客户端:
const joi = require('joi')
// 错误中间件
app.use(function (err, req, res, next) {
// 数据验证失败
if (err instanceof joi.ValidationError) return res.cc(err)
// 未知错误
res.cc(err)
})
2.6 登录
2.6.0 实现步骤
- 检测表单数据是否合法
- 根据用户名查询用户的数据
- 判断用户输入的密码是否正确
- 生成 JWT 的 Token 字符串
2.6.1 检测登录表单的数据是否合法
- 将
/router/user.js
中登录
的路由代码修改如下:
// 登录的路由
router.post('/login', expressJoi(reg_login_schema), userHandler.login)
2.6.2 根据用户名查询用户的数据
- 接收表单数据:
const userinfo = req.body
- 定义 SQL 语句:
const sql = `select * from ev_users where username=?`
- 执行 SQL 语句,查询用户的数据:
db.query(sql, userinfo.username, function (err, results) {
// 执行 SQL 语句失败
if (err) return res.cc(err)
// 执行 SQL 语句成功,但是查询到数据条数不等于 1
if (results.length !== 1) return res.cc('登录失败!')
// TODO:判断用户输入的登录密码是否和数据库中的密码一致
})
2.6.3 判断用户输入的密码是否正确
核心实现思路:调用
bcrypt.compareSync(用户提交的密码, 数据库中的密码)
方法比较密码是否一致
返回值是布尔值(true 一致、false 不一致)
具体的实现代码如下:
// 拿着用户输入的密码,和数据库中存储的密码进行对比
const compareResult = bcrypt.compareSync(userinfo.password, results[0].password)
// 如果对比的结果等于 false, 则证明用户输入的密码错误
if (!compareResult) {
return res.cc('登录失败!')
}
// TODO:登录成功,生成 Token 字符串
#2.6.4 生成 JWT 的 Token 字符串
核心注意点:在生成 Token 字符串的时候,一定要剔除 密码 和 头像 的值
- 通过 ES6 的高级语法,快速剔除
密码
和头像
的值:
// 剔除完毕之后,user 中只保留了用户的 id, username, nickname, email 这四个属性的值
const user = { ...results[0], password: '', user_pic: '' }
- 运行如下的命令,安装生成 Token 字符串的包:
npm i jsonwebtoken@8.5.1
- 在
/router_handler/user.js
模块的头部区域,导入jsonwebtoken
包:
// 用这个包来生成 Token 字符串
const jwt = require('jsonwebtoken')
- 创建
config.js
文件,并向外共享 加密 和 还原 Token 的jwtSecretKey
字符串:
module.exports = {
jwtSecretKey: 'Bruce',
}
- 将用户信息对象加密成 Token 字符串:
// 导入配置文件
const config = require('../config')
// 生成 Token 字符串
const tokenStr = jwt.sign(user, config.jwtSecretKey, {
expiresIn: '10h', // token 有效期为 10 个小时
})
- 将生成的 Token 字符串响应给客户端:
res.send({
status: 0,
message: '登录成功!',
// 为了方便客户端使用 Token,在服务器端直接拼接上 Bearer 的前缀
token: 'Bearer ' + tokenStr,
})
2.7 配置解析 Token 的中间件
- 运行如下的命令,安装解析 Token 的中间件:
npm i express-jwt@5.3.3
- 在
app.js
中注册路由之前,配置解析 Token 的中间件:
// 导入配置文件
const config = require('./config')
// 解析 token 的中间件
const expressJWT = require('express-jwt')
// 使用 .unless({ path: [/^\/api\//] }) 指定哪些接口不需要进行 Token 的身份认证
app.use(expressJWT({ secret: config.jwtSecretKey }).unless({ path: [/^\/api\//] }))
- 在
app.js
中的错误级别中间件
里面,捕获并处理 Token 认证失败后的错误:
// 错误中间件
app.use(function (err, req, res, next) {
// 省略其它代码...
// 捕获身份认证失败的错误
if (err.name === 'UnauthorizedError') return res.cc('身份认证失败!')
// 未知错误...
})
3. 个人中心
3.1 获取用户的基本信息
3.1.0 实现步骤
- 初始化 路由 模块
- 初始化 路由处理函数 模块
- 获取用户的基本信息
3.1.1 初始化路由模块
- 创建
/router/userinfo.js
路由模块,并初始化如下的代码结构:
// 导入 express
const express = require('express')
// 创建路由对象
const router = express.Router()
// 获取用户的基本信息
router.get('/userinfo', (req, res) => {
res.send('ok')
})
// 向外共享路由对象
module.exports = router
- 在
app.js
中导入并使用个人中心的路由模块:
// 导入并使用用户信息路由模块
const userinfoRouter = require('./router/userinfo')
// 注意:以 /my 开头的接口,都是有权限的接口,需要进行 Token 身份认证
app.use('/my', userinfoRouter)
3.1.2 初始化路由处理函数模块
- 创建
/router_handler/userinfo.js
路由处理函数模块,并初始化如下的代码结构:
// 获取用户基本信息的处理函数
exports.getUserInfo = (req, res) => {
res.send('ok')
}
- 修改
/router/userinfo.js
中的代码如下:
const express = require('express')
const router = express.Router()
// 导入用户信息的处理函数模块
const userinfo_handler = require('../router_handler/userinfo')
// 获取用户的基本信息
router.get('/userinfo', userinfo_handler.getUserInfo)
module.exports = router
3.1.3 获取用户的基本信息
- 在
/router_handler/userinfo.js
头部导入数据库操作模块:
// 导入数据库操作模块
const db = require('../db/index')
- 定义 SQL 语句:
// 根据用户的 id,查询用户的基本信息
// 注意:为了防止用户的密码泄露,需要排除 password 字段
const sql = `select id, username, nickname, email, user_pic from ev_users where id=?`
- 调用
db.query()
执行 SQL 语句:
// 注意:req 对象上的 user 属性,是 Token 解析成功,express-jwt 中间件帮我们挂载上去的
db.query(sql, req.user.id, (err, results) => {
// 1. 执行 SQL 语句失败
if (err) return res.cc(err)
// 2. 执行 SQL 语句成功,但是查询到的数据条数不等于 1
if (results.length !== 1) return res.cc('获取用户信息失败!')
// 3. 将用户信息响应给客户端
res.send({
status: 0,
message: '获取用户基本信息成功!',
data: results[0],
})
})
3.2 更新用户的基本信息
3.2.0 实现步骤
- 定义路由和处理函数
- 验证表单数据
- 实现更新用户基本信息的功能
3.2.1 定义路由和处理函数
- 在
/router/userinfo.js
模块中,新增更新用户基本信息
的路由:
// 更新用户的基本信息
router.post('/userinfo', userinfo_handler.updateUserInfo)
- 在
/router_handler/userinfo.js
模块中,定义并向外共享更新用户基本信息
的路由处理函数:
// 更新用户基本信息的处理函数
exports.updateUserInfo = (req, res) => {
res.send('ok')
}
3.2.2 验证表单数据
- 在
/schema/user.js
验证规则模块中,定义id
,nickname
,email
的验证规则如下:
// 定义 id, nickname, emial 的验证规则
const id = joi.number().integer().min(1).required()
const nickname = joi.string().required()
const email = joi.string().email().required()
- 并使用
exports
向外共享如下的验证规则对象
:
// 验证规则对象 - 更新用户基本信息
exports.update_userinfo_schema = {
body: {
id,
nickname,
email,
},
}
- 在
/router/userinfo.js
模块中,导入验证数据合法性的中间件:
// 导入验证数据合法性的中间件
const expressJoi = require('@escook/express-joi')
- 在
/router/userinfo.js
模块中,导入需要的验证规则对象:
// 导入需要的验证规则对象
const { update_userinfo_schema } = require('../schema/user')
- 在
/router/userinfo.js
模块中,修改更新用户的基本信息
的路由如下:
// 更新用户的基本信息
router.post('/userinfo', expressJoi(update_userinfo_schema), userinfo_handler.updateUserInfo)
3.2.3 实现更新用户基本信息的功能
- 定义待执行的 SQL 语句:
const sql = `update ev_users set ? where id=?`
- 调用
db.query()
执行 SQL 语句并传参:
db.query(sql, [req.body, req.body.id], (err, results) => {
// 执行 SQL 语句失败
if (err) return res.cc(err)
// 执行 SQL 语句成功,但影响行数不为 1
if (results.affectedRows !== 1) return res.cc('修改用户基本信息失败!')
// 修改用户信息成功
return res.cc('修改用户基本信息成功!', 0)
})
3.3 重置密码
3.3.0 实现步骤
- 定义路由和处理函数
- 验证表单数据
- 实现重置密码的功能
3.3.1 定义路由和处理函数
- 在
/router/userinfo.js
模块中,新增重置密码
的路由:
// 重置密码的路由
router.post('/updatepwd', userinfo_handler.updatePassword)
- 在
/router_handler/userinfo.js
模块中,定义并向外共享重置密码
的路由处理函数:
// 重置密码的处理函数
exports.updatePassword = (req, res) => {
res.send('ok')
}
3.3.2 验证表单数据
核心验证思路:旧密码与新密码,必须符合密码的验证规则,并且新密码不能与旧密码一致!
- 在
/schema/user.js
模块中,使用exports
向外共享如下的验证规则对象
:
// 验证规则对象 - 重置密码
exports.update_password_schema = {
body: {
// 使用 password 这个规则,验证 req.body.oldPwd 的值
oldPwd: password,
// 使用 joi.not(joi.ref('oldPwd')).concat(password) 规则,验证 req.body.newPwd 的值
// 解读:
// 1. joi.ref('oldPwd') 表示 newPwd 的值必须和 oldPwd 的值保持一致
// 2. joi.not(joi.ref('oldPwd')) 表示 newPwd 的值不能等于 oldPwd 的值
// 3. .concat() 用于合并 joi.not(joi.ref('oldPwd')) 和 password 这两条验证规则
newPwd: joi.not(joi.ref('oldPwd')).concat(password),
},
}
- 在
/router/userinfo.js
模块中,导入需要的验证规则对象:
// 导入需要的验证规则对象
const { update_userinfo_schema, update_password_schema } = require('../schema/user')
- 并在
重置密码的路由
中,使用update_password_schema
规则验证表单的数据,示例代码如下:
router.post('/updatepwd', expressJoi(update_password_schema), userinfo_handler.updatePassword)
3.3.3 实现重置密码的功能
- 根据
id
查询用户是否存在:
// 定义根据 id 查询用户数据的 SQL 语句
const sql = `select * from ev_users where id=?`
// 执行 SQL 语句查询用户是否存在
db.query(sql, req.user.id, (err, results) => {
// 执行 SQL 语句失败
if (err) return res.cc(err)
// 检查指定 id 的用户是否存在
if (results.length !== 1) return res.cc('用户不存在!')
// TODO:判断提交的旧密码是否正确
})
- 判断提交的 旧密码 是否正确:
// 在头部区域导入 bcryptjs 后,
// 即可使用 bcrypt.compareSync(提交的密码,数据库中的密码) 方法验证密码是否正确
// compareSync() 函数的返回值为布尔值,true 表示密码正确,false 表示密码错误
const bcrypt = require('bcryptjs')
// 判断提交的旧密码是否正确
const compareResult = bcrypt.compareSync(req.body.oldPwd, results[0].password)
if (!compareResult) return res.cc('原密码错误!')
- 对新密码进行
bcrypt
加密之后,更新到数据库中:
// 定义更新用户密码的 SQL 语句
const sql = `update ev_users set password=? where id=?`
// 对新密码进行 bcrypt 加密处理
const newPwd = bcrypt.hashSync(req.body.newPwd, 10)
// 执行 SQL 语句,根据 id 更新用户的密码
db.query(sql, [newPwd, req.user.id], (err, results) => {
// SQL 语句执行失败
if (err) return res.cc(err)
// SQL 语句执行成功,但是影响行数不等于 1
if (results.affectedRows !== 1) return res.cc('更新密码失败!')
// 更新密码成功
res.cc('更新密码成功!', 0)
})
3.4 更新用户头像
3.4.0 实现步骤
- 定义路由和处理函数
- 验证表单数据
- 实现更新用户头像的功能
3.4.1 定义路由和处理函数
- 在
/router/userinfo.js
模块中,新增更新用户头像
的路由:
// 更新用户头像的路由
router.post('/update/avatar', userinfo_handler.updateAvatar)
- 在
/router_handler/userinfo.js
模块中,定义并向外共享更新用户头像
的路由处理函数:
// 更新用户头像的处理函数
exports.updateAvatar = (req, res) => {
res.send('ok')
}
3.4.2 验证表单数据
- 在
/schema/user.js
验证规则模块中,定义avatar
的验证规则如下:
// dataUri() 指的是如下格式的字符串数据:
// data:image/png;base64,VE9PTUFOWVNFQ1JFVFM=
const avatar = joi.string().dataUri().required()
- 并使用
exports
向外共享如下的验证规则对象
:
// 验证规则对象 - 更新头像
exports.update_avatar_schema = {
body: {
avatar,
},
}
- 在
/router/userinfo.js
模块中,导入需要的验证规则对象:
const { update_avatar_schema } = require('../schema/user')
- 在
/router/userinfo.js
模块中,修改更新用户头像
的路由如下:
router.post('/update/avatar', expressJoi(update_avatar_schema), userinfo_handler.updateAvatar)
3.4.3 实现更新用户头像的功能
- 定义更新用户头像的 SQL 语句:
const sql = 'update ev_users set user_pic=? where id=?'
- 调用
db.query()
执行 SQL 语句,更新对应用户的头像:
db.query(sql, [req.body.avatar, req.user.id], (err, results) => {
// 执行 SQL 语句失败
if (err) return res.cc(err)
// 执行 SQL 语句成功,但是影响行数不等于 1
if (results.affectedRows !== 1) return res.cc('更新头像失败!')
// 更新用户头像成功
return res.cc('更新头像成功!', 0)
})
文章分类管理
4.1 新建 ev_article_cate 表
4.2 获取文章分类列表
4.2.0 实现步骤
- 初始化路由模块
- 初始化路由处理函数模块
- 获取文章分类列表数据
4.2.1 初始化路由模块
- 创建
/router/artcate.js
路由模块,并初始化如下的代码结构:
// 导入 express
const express = require('express')
// 创建路由对象
const router = express.Router()
// 获取文章分类的列表数据
router.get('/cates', (req, res) => {
res.send('ok')
})
// 向外共享路由对象
module.exports = router
- 在
app.js
中导入并使用文章分类的路由模块:
// 导入并使用文章分类路由模块
const artCateRouter = require('./router/artcate')
// 为文章分类的路由挂载统一的访问前缀 /my/article
app.use('/my/article', artCateRouter)
4.2.2 初始化路由处理函数模块
- 创建
/router_handler/artcate.js
路由处理函数模块,并初始化如下的代码结构:
// 获取文章分类列表数据的处理函数
exports.getArticleCates = (req, res) => {
res.send('ok')
}
- 修改
/router/artcate.js
中的代码如下:
const express = require('express')
const router = express.Router()
// 导入文章分类的路由处理函数模块
const artcate_handler = require('../router_handler/artcate')
// 获取文章分类的列表数据
router.get('/cates', artcate_handler.getArticleCates)
module.exports = router
4.2.3 获取文章分类列表数据
- 在
/router_handler/artcate.js
头部导入数据库操作模块:
// 导入数据库操作模块
const db = require('../db/index')
- 定义 SQL 语句:
// 根据分类的状态,获取所有未被删除的分类列表数据
// is_delete 为 0 表示没有被 标记为删除 的数据
const sql = 'select * from ev_article_cate where is_delete=0 order by id asc'
- 调用
db.query()
执行 SQL 语句:
db.query(sql, (err, results) => {
// 1. 执行 SQL 语句失败
if (err) return res.cc(err)
// 2. 执行 SQL 语句成功
res.send({
status: 0,
message: '获取文章分类列表成功!',
data: results,
})
})
4.3 新增文章分类
4.3.0 实现步骤
- 定义路由和处理函数
- 验证表单数据
- 查询
分类名称
与分类别名
是否被占用 - 实现新增文章分类的功能
4.3.1 定义路由和处理函数
- 在
/router/artcate.js
模块中,添加新增文章分类
的路由:
// 新增文章分类的路由
router.post('/addcates', artcate_handler.addArticleCates)
- 在
/router_handler/artcate.js
模块中,定义并向外共享新增文章分类
的路由处理函数:
// 新增文章分类的处理函数
exports.addArticleCates = (req, res) => {
res.send('ok')
}
4.3.2 验证表单数据
- 创建
/schema/artcate.js
文章分类数据验证模块,并定义如下的验证规则:
// 导入定义验证规则的模块
const joi = require('joi')
// 定义 分类名称 和 分类别名 的校验规则
const name = joi.string().required()
const alias = joi.string().alphanum().required()
// 校验规则对象 - 添加分类
exports.add_cate_schema = {
body: {
name,
alias,
},
}
- 在
/router/artcate.js
模块中,使用add_cate_schema
对数据进行验证:
// 导入验证数据的中间件
const expressJoi = require('@escook/express-joi')
// 导入文章分类的验证模块
const { add_cate_schema } = require('../schema/artcate')
// 新增文章分类的路由
router.post('/addcates', expressJoi(add_cate_schema), artcate_handler.addArticleCates)
4.3.3 查询分类名称与别名是否被占用
- 定义查重的 SQL 语句:
// 定义查询 分类名称 与 分类别名 是否被占用的 SQL 语句
const sql = `select * from ev_article_cate where name=? or alias=?`
- 调用
db.query()
执行查重的操作:
// 执行查重操作
db.query(sql, [req.body.name, req.body.alias], (err, results) => {
// 执行 SQL 语句失败
if (err) return res.cc(err)
// 判断 分类名称 和 分类别名 是否被占用
if (results.length === 2) 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('分类别名被占用,请更换后重试!')
// TODO:新增文章分类
})
4.3.4 实现新增文章分类的功能
- 定义新增文章分类的 SQL 语句:
const sql = `insert into ev_article_cate set ?`
- 调用
db.query()
执行新增文章分类的 SQL 语句:
db.query(sql, req.body, (err, results) => {
// SQL 语句执行失败
if (err) return res.cc(err)
// SQL 语句执行成功,但是影响行数不等于 1
if (results.affectedRows !== 1) return res.cc('新增文章分类失败!')
// 新增文章分类成功
res.cc('新增文章分类成功!', 0)
})
4.4 根据 Id 删除文章分类
4.4.0 实现步骤
- 定义路由和处理函数
- 验证表单数据
- 实现删除文章分类的功能
4.4.1 定义路由和处理函数
- 在
/router/artcate.js
模块中,添加删除文章分类
的路由:
// 删除文章分类的路由
router.get('/deletecate/:id', artcate_handler.deleteCateById)
- 在
/router_handler/artcate.js
模块中,定义并向外共享删除文章分类
的路由处理函数:
// 删除文章分类的处理函数
exports.deleteCateById = (req, res) => {
res.send('ok')
}
4.4.2 验证表单数据
- 在
/schema/artcate.js
验证规则模块中,定义 id 的验证规则如下:
// 定义 分类Id 的校验规则
const id = joi.number().integer().min(1).required()
- 并使用
exports
向外共享如下的验证规则对象
:
// 校验规则对象 - 删除分类
exports.delete_cate_schema = {
params: {
id,
},
}
- 在
/router/artcate.js
模块中,导入需要的验证规则对象,并在路由中使用:
// 导入删除分类的验证规则对象
const { delete_cate_schema } = require('../schema/artcate')
// 删除文章分类的路由
router.get('/deletecate/:id', expressJoi(delete_cate_schema), artcate_handler.deleteCateById)
4.4.3 实现删除文章分类的功能
- 定义删除文章分类的 SQL 语句:
const sql = `update ev_article_cate set is_delete=1 where id=?`
- 调用
db.query()
执行删除文章分类的 SQL 语句:
db.query(sql, req.params.id, (err, results) => {
// 执行 SQL 语句失败
if (err) return res.cc(err)
// SQL 语句执行成功,但是影响行数不等于 1
if (results.affectedRows !== 1) return res.cc('删除文章分类失败!')
// 删除文章分类成功
res.cc('删除文章分类成功!', 0)
})
4.5 根据 Id 获取文章分类数据
4.5.0 实现步骤
- 定义路由和处理函数
- 验证表单数据
- 实现获取文章分类的功能
4.5.1 定义路由和处理函数
- 在
/router/artcate.js
模块中,添加根据 Id 获取文章分类
的路由:
router.get('/cates/:id', artcate_handler.getArticleById)
- 在
/router_handler/artcate.js
模块中,定义并向外共享根据 Id 获取文章分类
的路由处理函数:
// 根据 Id 获取文章分类的处理函数
exports.getArticleById = (req, res) => {
res.send('ok')
}
4.5.2 验证表单数据
- 在
/schema/artcate.js
验证规则模块中,使用exports
向外共享如下的验证规则对象
:
// 校验规则对象 - 根据 Id 获取分类
exports.get_cate_schema = {
params: {
id,
},
}
- 在
/router/artcate.js
模块中,导入需要的验证规则对象,并在路由中使用:
// 导入根据 Id 获取分类的验证规则对象
const { get_cate_schema } = require('../schema/artcate')
// 根据 Id 获取文章分类的路由
router.get('/cates/:id', expressJoi(get_cate_schema), artcate_handler.getArticleById)
4.5.3 实现获取文章分类的功能
- 定义根据 Id 获取文章分类的 SQL 语句:
const sql = `select * from ev_article_cate where id=?`
- 调用
db.query()
执行 SQL 语句:
db.query(sql, req.params.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],
})
})
4.6 根据 Id 更新文章分类数据
4.6.0]() 实现步骤
- 定义路由和处理函数
- 验证表单数据
- 查询
分类名称
与分类别名
是否被占用 - 实现更新文章分类的功能
4.6.1 定义路由和处理函数
- 在
/router/artcate.js
模块中,添加更新文章分类
的路由:
// 更新文章分类的路由
router.post('/updatecate', artcate_handler.updateCateById)
- 在
/router_handler/artcate.js
模块中,定义并向外共享更新文章分类
的路由处理函数:
// 更新文章分类的处理函数
exports.updateCateById = (req, res) => {
res.send('ok')
}
4.6.2 验证表单数据
- 在
/schema/artcate.js
验证规则模块中,使用exports
向外共享如下的验证规则对象
:
// 校验规则对象 - 更新分类
exports.update_cate_schema = {
body: {
Id: id,
name,
alias,
},
}
- 在
/router/artcate.js
模块中,导入需要的验证规则对象,并在路由中使用:
// 导入更新文章分类的验证规则对象
const { update_cate_schema } = require('../schema/artcate')
// 更新文章分类的路由
router.post('/updatecate', expressJoi(update_cate_schema), artcate_handler.updateCateById)
4.5.4 查询分类名称与别名是否被占用
- 定义查重的 SQL 语句:
// 定义查询 分类名称 与 分类别名 是否被占用的 SQL 语句
const sql = `select * from ev_article_cate where Id<>? and (name=? or alias=?)`
- 调用
db.query()
执行查重的操作:
// 执行查重操作
db.query(sql, [req.body.Id, req.body.name, req.body.alias], (err, results) => {
// 执行 SQL 语句失败
if (err) return res.cc(err)
// 判断 分类名称 和 分类别名 是否被占用
if (results.length === 2) 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('分类别名被占用,请更换后重试!')
// TODO:更新文章分类
})
4.5.5 实现更新文章分类的功能
- 定义更新文章分类的 SQL 语句:
const sql = `update ev_article_cate set ? where Id=?`
- 调用
db.query()
执行 SQL 语句:
db.query(sql, [req.body, req.body.Id], (err, results) => {
// 执行 SQL 语句失败
if (err) return res.cc(err)
// SQL 语句执行成功,但是影响行数不等于 1
if (results.affectedRows !== 1) return res.cc('更新文章分类失败!')
// 更新文章分类成功
res.cc('更新文章分类成功!', 0)
})
5. 文章管理
5.1 新建 ev_articles 表
5.2 发布新文章
5.2.0 实现步骤
- 初始化路由模块
- 初始化路由处理函数模块
- 使用 multer 解析表单数据
- 验证表单数据
- 实现发布文章的功能
5.2.1 初始化路由模块
- 创建
/router/article.js
路由模块,并初始化如下的代码结构:
// 导入 express
const express = require('express')
// 创建路由对象
const router = express.Router()
// 发布新文章
router.post('/add', (req, res) => {
res.send('ok')
})
// 向外共享路由对象
module.exports = router
- 在
app.js
中导入并使用文章的路由模块:
// 导入并使用文章路由模块
const articleRouter = require('./router/article')
// 为文章的路由挂载统一的访问前缀 /my/article
app.use('/my/article', articleRouter)
5.2.2 初始化路由处理函数模块
- 创建
/router_handler/article.js
路由处理函数模块,并初始化如下的代码结构:
// 发布新文章的处理函数
exports.addArticle = (req, res) => {
res.send('ok')
}
- 修改
/router/article.js
中的代码如下:
const express = require('express')
const router = express.Router()
// 导入文章的路由处理函数模块
const article_handler = require('../router_handler/article')
// 发布新文章
router.post('/add', article_handler.addArticle)
module.exports = router
5.2.3 使用 multer 解析表单数据
注意:使用
express.urlencoded()
中间件无法解析multipart/form-data
格式的请求体数据。
当前项目,推荐使用 multer 来解析
multipart/form-data
格式的表单数据。https://www.npmjs.com/package/multer
- 运行如下的终端命令,在项目中安装
multer
:
npm i multer@1.4.2
- 在
/router_handler/article.js
模块中导入并配置multer
:
// 导入解析 formdata 格式表单数据的包
const multer = require('multer')
// 导入处理路径的核心模块
const path = require('path')
// 创建 multer 的实例对象,通过 dest 属性指定文件的存放路径
const upload = multer({ dest: path.join(__dirname, '../uploads') })
- 修改
发布新文章
的路由如下:
// 发布新文章的路由
// upload.single() 是一个局部生效的中间件,用来解析 FormData 格式的表单数据
// 将文件类型的数据,解析并挂载到 req.file 属性中
// 将文本类型的数据,解析并挂载到 req.body 属性中
router.post('/add', upload.single('cover_img'), article_handler.addArticle)
- 在
/router_handler/article.js
模块中的addArticle
处理函数中,将multer
解析出来的数据进行打印:
// 发布新文章的处理函数
exports.addArticle = (req, res) => {
console.log(req.body) // 文本类型的数据
console.log('--------分割线----------')
console.log(req.file) // 文件类型的数据
res.send('ok')
})
5.2.4 验证表单数据
实现思路:通过 express-joi 自动验证 req.body 中的文本数据;通过 if 判断手动验证 req.file 中的文件数据;
- 创建
/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()
// 验证规则对象 - 发布文章
exports.add_article_schema = {
body: {
title,
cate_id,
content,
state,
},
}
- 在
/router/article.js
模块中,导入需要的验证规则对象,并在路由中使用:
// 导入验证数据的中间件
const expressJoi = require('@escook/express-joi')
// 导入文章的验证模块
const { add_article_schema } = require('../schema/article')
// 发布新文章的路由
// 注意:在当前的路由中,先后使用了两个中间件:
// 先使用 multer 解析表单数据
// 再使用 expressJoi 对解析的表单数据进行验证
router.post('/add', upload.single('cover_img'), expressJoi(add_article_schema), article_handler.addArticle)
- 在
/router_handler/article.js
模块中的addArticle
处理函数中,通过if
判断客户端是否提交了封面图片
:
// 发布新文章的处理函数
exports.addArticle = (req, res) => {
// 手动判断是否上传了文章封面
if (!req.file || req.file.fieldname !== 'cover_img') return res.cc('文章封面是必选参数!')
// TODO:表单数据合法,继续后面的处理流程...
})
5.2.5 实现发布文章的功能
- 整理要插入数据库的文章信息对象:
// 导入处理路径的 path 核心模块
const path = require('path')
const articleInfo = {
// 标题、内容、状态、所属的分类Id
...req.body,
// 文章封面在服务器端的存放路径
cover_img: path.join('/uploads', req.file.filename),
// 文章发布时间
pub_date: new Date(),
// 文章作者的Id
author_id: req.user.id,
}
- 定义发布文章的 SQL 语句:
const sql = `insert into ev_articles set ?`
- 调用
db.query()
执行发布文章的 SQL 语句:
// 导入数据库操作模块
const db = require('../db/index')
// 执行 SQL 语句
db.query(sql, articleInfo, (err, results) => {
// 执行 SQL 语句失败
if (err) return res.cc(err)
// 执行 SQL 语句成功,但是影响行数不等于 1
if (results.affectedRows !== 1) return res.cc('发布文章失败!')
// 发布文章成功
res.cc('发布文章成功', 0)
})
- 在
app.js
中,使用express.static()
中间件,将uploads
目录中的图片托管为静态资源:
// 托管静态资源文件
app.use('/uploads', express.static('./uploads'))
es) => {
res.send(‘ok’)
})
// 向外共享路由对象
module.exports = router
2. 在 `app.js` 中导入并使用文章的路由模块:
```js
// 导入并使用文章路由模块
const articleRouter = require('./router/article')
// 为文章的路由挂载统一的访问前缀 /my/article
app.use('/my/article', articleRouter)
5.2.2 初始化路由处理函数模块
- 创建
/router_handler/article.js
路由处理函数模块,并初始化如下的代码结构:
// 发布新文章的处理函数
exports.addArticle = (req, res) => {
res.send('ok')
}
- 修改
/router/article.js
中的代码如下:
const express = require('express')
const router = express.Router()
// 导入文章的路由处理函数模块
const article_handler = require('../router_handler/article')
// 发布新文章
router.post('/add', article_handler.addArticle)
module.exports = router
5.2.3 使用 multer 解析表单数据
注意:使用
express.urlencoded()
中间件无法解析multipart/form-data
格式的请求体数据。
当前项目,推荐使用 multer 来解析
multipart/form-data
格式的表单数据。https://www.npmjs.com/package/multer
- 运行如下的终端命令,在项目中安装
multer
:
npm i multer@1.4.2
- 在
/router_handler/article.js
模块中导入并配置multer
:
// 导入解析 formdata 格式表单数据的包
const multer = require('multer')
// 导入处理路径的核心模块
const path = require('path')
// 创建 multer 的实例对象,通过 dest 属性指定文件的存放路径
const upload = multer({ dest: path.join(__dirname, '../uploads') })
- 修改
发布新文章
的路由如下:
// 发布新文章的路由
// upload.single() 是一个局部生效的中间件,用来解析 FormData 格式的表单数据
// 将文件类型的数据,解析并挂载到 req.file 属性中
// 将文本类型的数据,解析并挂载到 req.body 属性中
router.post('/add', upload.single('cover_img'), article_handler.addArticle)
- 在
/router_handler/article.js
模块中的addArticle
处理函数中,将multer
解析出来的数据进行打印:
// 发布新文章的处理函数
exports.addArticle = (req, res) => {
console.log(req.body) // 文本类型的数据
console.log('--------分割线----------')
console.log(req.file) // 文件类型的数据
res.send('ok')
})
5.2.4 验证表单数据
实现思路:通过 express-joi 自动验证 req.body 中的文本数据;通过 if 判断手动验证 req.file 中的文件数据;
- 创建
/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()
// 验证规则对象 - 发布文章
exports.add_article_schema = {
body: {
title,
cate_id,
content,
state,
},
}
- 在
/router/article.js
模块中,导入需要的验证规则对象,并在路由中使用:
// 导入验证数据的中间件
const expressJoi = require('@escook/express-joi')
// 导入文章的验证模块
const { add_article_schema } = require('../schema/article')
// 发布新文章的路由
// 注意:在当前的路由中,先后使用了两个中间件:
// 先使用 multer 解析表单数据
// 再使用 expressJoi 对解析的表单数据进行验证
router.post('/add', upload.single('cover_img'), expressJoi(add_article_schema), article_handler.addArticle)
- 在
/router_handler/article.js
模块中的addArticle
处理函数中,通过if
判断客户端是否提交了封面图片
:
// 发布新文章的处理函数
exports.addArticle = (req, res) => {
// 手动判断是否上传了文章封面
if (!req.file || req.file.fieldname !== 'cover_img') return res.cc('文章封面是必选参数!')
// TODO:表单数据合法,继续后面的处理流程...
})
5.2.5 实现发布文章的功能
- 整理要插入数据库的文章信息对象:
// 导入处理路径的 path 核心模块
const path = require('path')
const articleInfo = {
// 标题、内容、状态、所属的分类Id
...req.body,
// 文章封面在服务器端的存放路径
cover_img: path.join('/uploads', req.file.filename),
// 文章发布时间
pub_date: new Date(),
// 文章作者的Id
author_id: req.user.id,
}
- 定义发布文章的 SQL 语句:
const sql = `insert into ev_articles set ?`
- 调用
db.query()
执行发布文章的 SQL 语句:
// 导入数据库操作模块
const db = require('../db/index')
// 执行 SQL 语句
db.query(sql, articleInfo, (err, results) => {
// 执行 SQL 语句失败
if (err) return res.cc(err)
// 执行 SQL 语句成功,但是影响行数不等于 1
if (results.affectedRows !== 1) return res.cc('发布文章失败!')
// 发布文章成功
res.cc('发布文章成功', 0)
})
- 在
app.js
中,使用express.static()
中间件,将uploads
目录中的图片托管为静态资源:
// 托管静态资源文件
app.use('/uploads', express.static('./uploads'))