文章目录
Node.js基础
初识Node.js
javaScript可以做后端开发,但是要借助运行环境,node.js
安装
查看安装成功
node -v
node js运行js代码
fs 文件系统模块
用来操作文件的模块
获得fs系统模块对象
const fs = require('fs')
读取文件
fs.readFile()
示例代码
// 1.导入fs 模块,来操作文件
const fs = require('fs');
// 2.调用 fs.readFile() 方法读取文件
// 参数1:读取文件的存放路径
// 参数2:读取文件的时候采用的编码格式,一般默认指定utf8
// 参数3:回调函数,拿到读取失败和成功的结果 err dataStr
fs.readFile('./file/1.txt','utf8',function (err,dataStr){
// 2.1打印失败的结果
console.log(err)
console.log('--------------')
// 2.2打印成功的结果
console.log(dataStr)
})
运行测试
判断文件读取成功
// 1.导入fs 模块,来操作文件
const fs = require('fs');
// 2.调用 fs.readFile() 方法读取文件
// 参数1:读取文件的存放路径
// 参数2:读取文件的时候采用的编码格式,一般默认指定utf8
// 参数3:回调函数,拿到读取失败和成功的结果 err dataStr
fs.readFile('./file/1.txt','utf8',function (err,result){
if(err){
return console.log('文件读取失败' + err.message)
}
console.log('文件读取成功,内容是:' + result)
})
写入文件
fs.writeFile(file,data[,options],callback)
示例代码
//1.导入 fs 文件系统模块
const fs = require('fs')
// 2.调用 fs.writeFile()方法,写入文件的内容
// 参数1:表示文件的存放路径
// 参数2:表示要写入的内容
// 参数3:回调函数
fs.writeFile('./file/2.txt','adbc',function (err){
// 2.1 如果文件写入成功,则 err的值等于 null
// 2.2 如果文件写入失败,则 err的值等于一个错误对象
console.log(err)
})
判断文件是否写入成功
//1.导入 fs 文件系统模块
const fs = require('fs')
// 2.调用 fs.writeFile()方法,写入文件的内容
// 参数1:表示文件的存放路径
// 参数2:表示要写入的内容
// 参数3:回调函数
fs.writeFile('./file/2.txt','adbc',function (err){
// 2.1 如果文件写入成功,则 err的值等于 null
// 2.2 如果文件写入失败,则 err的值等于一个错误对象
// console.log(err)
if(err){
return console.log('文件写入失败!' + err.message)
}
console.log('文件写入成功!')
})
案例题目练习(1)
const fs = require('fs')
fs.readFile('./file/成绩.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('\n')
console.log(newStr)
})
演示路径问题
const fs = require('fs')
// 出现路径拼接错误的问题,是因为提供了 ./或 ../ 开头的相对路径
// 如果要解决这个问题,可以直接提供一个完整的文件存放路径就行
// fs.readFile('./file/1.txt','utf8',function (err,dataStr){
// if(err){
// return console.log('读取失败' + err.message)
// }
// console.log('读取文件成功' + dataStr)
// })
// ****************************************************************************************
// 移植性非常差,不利于维护
// fs.readFile('E:\\gitCode\\Node_JS_Study\\file\\1.txt','utf8',function (err,dataStr){
// if(err){
// return console.log('读取失败' + err.message)
// }
// console.log('读取文件成功' + dataStr)
// })
// ****************************************************************************************
//__dirname 表示当前文件所处的目录
console.log(__dirname) // E:\gitCode\Node_JS_Study
fs.readFile(__dirname + '/file/1.txt','utf8',function (err,dataStr){
if(err){
return console.log('读取失败' + err.message)
}
console.log('读取文件成功' + dataStr)
})
path 路径模块
获得path路径模块对象
const path = require('path)
path.join
const path = require('path')
const dir = '/usr/local';
const file = 'index.html';
const fullPath = path.join(dir, file);
console.log(fullPath) //\usr\local\index.html
//注意 ../ 会抵消前面的路径
const pathStr = path.join('/a','/b/c','../','./d','e')
console.log(pathStr) // \a\b\d\e
//path.join会在不同操作系统中自动替换分隔符,比如windows替换为‘\’,linux替换为'/'
path.basename()
//获取扩展名的文件名
const path = require('path')
const fpath = '/a/b/c/index.html'
// const fullName = path.basename(fpath)
// console.log(fullName)
const nameWithoutExt = path.basename(fpath,'.html')
console.log(nameWithoutExt) // index
path.extname()
// 获得扩展名部分
const path = require('path')
const fpath = '/a/b/c/index.html'
const fext = path.extname(fpath)
console.log(fext) // .html
综合案例——(拆分HTML文件)
可以将html文件拆分为html、js、css文件
// 1.1 导入 fs 模块
const fs = require('fs')
// 1.2 导入 path 模块
const path = require('path')
// 1.3 定义正则表达式,分别匹配 <style></style> 和 <script></script> 标签
const regStyle = /<style>[\s\S]*?<\/style>/
const regScript = /<script>[\s\S]*<\/script>/
// 2.1 调用 fs.readFile() 方法读取文件
fs.readFile(path.join(__dirname, './clock/index.html'), 'utf8', function(err, dataStr) {
// 2.2 读取 HTML 文件失败
if (err) return console.log('读取HTML文件失败!' + err.message)
// 2.3 读取文件成功后,调用对应的三个方法,分别拆解出 css, js, html 文件
resolveCSS(dataStr)
resolveJS(dataStr)
resolveHTML(dataStr)
})
// 3.1 定义处理 css 样式的方法
function resolveCSS(htmlStr) {
// 3.2 使用正则提取需要的内容
const regStyle = /<style>[\s\S]*<\/style>/
const r1 = regStyle.exec(htmlStr)
// 3.3 将提取出来的样式字符串,进行字符串的 replace 替换操作
const newCSS = r1[0].replace('<style>', '').replace('</style>', '')
// // 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 将提取出来的内容,做进一步的处理
const newJS = r2[0].replace('<script>', '').replace('</script>', '')
// 4.4 将处理的结果,写入到 clock 目录中的 index.js 文件里面
fs.writeFile(path.join(__dirname, './clock/index.js'), newJS, function(err) {
if (err) return console.log('写入 JavaScript 脚本失败!' + err.message)
console.log('写入 JS 脚本成功!')
})
}
// 5.1 定义处理 HTML 结构的方法
function resolveHTML(htmlStr) {
// 5.2 将字符串调用 replace 方法,把内嵌的 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 页面成功!')
})
}
http 模块
服务器和电脑的区别就在于——电脑中有没有安装web服务器软件
网络节点中,负责消费资源的电脑,叫做客户端;负责对外提供网络资源的电脑,叫做服务器
http 模块是 Node.js 官方提供的、用来创建 web 服务器的模块。通过 http 模块提供的 http.createServer0 方法,就能方便的把一台普通的电脑,变成一台 Web 服务器,从而对外提供 Web 资源服务。
如果要希望使用 http 模块创建 Web 服务器,则需要先导入它:
获取http模块对象
const http = require('http')
创建最基本的web服务器
//1. 导入 http 模块
const http = require('http')
//2. 创建 web 服务器实例
const server = http.createServer()
//3. 为服务器实例绑定 request 事件,监听客户端的请求
server.on('request',function (req,res){
console.log('Someone visit our web server')
})
//4. 启动服务器
server.listen(80,function(){
console.log('server running at http://127.0.0.1:80')
})
req请求对象
const http = require('http')
const server = http.createServer()
// req 是请求对象,包含了与客户端相关的数据和属性
server.on('request',req => {
// req.url 是客户端请求的URL地址
const url = req.url
// req.method 是客户端请求的method 类型
const method = req.method
const str = 'your request url is ' + url + ',and request method is ' + method
console.log(str)
})
server.listen(80,() => {
console.log('server running at http://127.0.0.1')
})
res响应对象
const http = require('http')
const server = http.createServer()
// req 是请求对象,包含了与客户端相关的数据和属性
server.on('request',(req,res) => {
// req.url 是客户端请求的URL地址
const url = req.url
// req.method 是客户端请求的method 类型
const method = req.method
const str = 'your request url is ' + url + ',and request method is ' + method
console.log(str)
// 调用res.end()方法,向客户端响应一些内容
res.end(str)
})
server.listen(80,() => {
console.log('server running at http://127.0.0.1')
})
解决中文乱码问题
const http = require('http')
const server = http.createServer()
server.on('request',(req,res)=>{
//定义一个字符串,包含中文的内容
const str = '您请求的 URL地址是 ' + req.url + '请求的method类型为' + req.method
// 为了防止中文乱码的问题,设置响应头
res.setHeader('Content-Type','text/html; charset=utf-8')
// res.end() 将内容响应给客户端
res.end(str)
})
server.listen(80,()=> {
console.log('server running at http://127.0.0.1')
})
根据不同的url响应不同的html内容
const http = require('http')
const server = http.createServer()
server.on('request',(req,res)=>{
// 获取请求的url地址
const url = req.url
// 设置默认的内容为 404 Not found
let content = '<h1>404 Not Found!</h1>'
if (url === '/' || url === '/index.html'){
//用户请求的是首页
content = '<h1>首页</h1>'
}else if(url === '/about.html'){
// 用户请求的是关于页面
content = '<h1>关于页面</h1>'
}
// 设置 Content-Type 响应头,防止中文乱码
res.setHeader('Content-Type','text/html; charset=utf-8')
// 把内容发送给客户端
res.end(content)
})
server.listen(80,()=> {
console.log('server running at http://127.0.0.1')
})
案例-实现clock时钟的web服务器
- 核心思路
把文件的实际存放路径,作为每个资源的请求url地址
服务器充当的角色,就是一个字符串的搬运工
服务器就当一个读文件的功能
const fs = require('fs');
const path = require('path')
const http = require('http')
const server = http.createServer()
server.on('request',(req,res)=>{
// 获取到客户端的url地址
// /clock/index.html
// /clock/index.css
// /clock/index.js
const url = req.url
// 把请求的url地址,映射为本地文件的存放路径
let fpath = ''
if(url === '/'){
fpath = path.join(__dirname,'./clock/index.html');
}else{
fpath = path.join(__dirname,'/clock',url)
}
// 根据“映射”过来的文件路径读取文件的内容
fs.readFile(fpath,'utf-8',(err,dataStr) => {
//读取失败,向客户端响应固定的“错误消息”
if(err) return res.end('404 Not Found')
//如果没有失败,则发送客户端
res.end(dataStr)
})
})
server.listen(80,()=> {
console.log('server running at http://127.0.0.1')
})
模块化
模块化的基本概念
比如小霸王游戏机的游戏机和游戏卡,分模块
比如vue中的组件
编程领域中的模块化,就是遵守固定的规则,把一个大文件拆成独立并互相依赖的多个小模块
例如:
- 使用什么样的语法格式来引用模块
- 在模块中使用什么样的语法格式向外暴露成员
Node.js中模块的分类
内置模块
内置模块是由Node.js官方提供的,例如 fs、path、http等
自定义模块
用户创建的每个.js文件,都是自定义模块
第三方模块
由第三方开发出来的模块,并非官方提供的内置模块,也不是用户创建的自定义模块
加载模块案例
// 加载内置 fs 模块
const fs = require('fs')
// 加载用户的自定义模块
const custom = require('./custom.js')
// 加载第三方模块(关于第三方模块的下载和使用,会在后面的课程中进行专门的讲解)
const moment = require('moment')
使用require()方法加载其他模块时,会执行被加载模块中的代码
注意:在使用 require 加载用户自定义模块期间,可以省略.js的后缀名
// 加载用户的自定义模块
const custom = require('./custom')
模块作用域
向外共享模块作用域成员
- module对象
console.log(module)
Module {
id: ‘.’,
path: ‘E:\gitCode\Study_Node.js\day02’,
exports: {},
filename: ‘E:\gitCode\Study_Node.js\day02\10.演示module对象.js’,
loaded: false,
children: [],
paths: [
‘E:\gitCode\Study_Node.js\day02\node_modules’,
‘E:\gitCode\Study_Node.js\node_modules’,
‘E:\gitCode\node_modules’,
‘E:\node_modules’
]
}
- moudle.exports对象
在自定义模块中,可以使用module.exports对象,将模块的成员共享出去,供外界使用
外界用require()方法导入自定义模块时,得到的就是module.exports所指的对象
共享成员时的注意点
使用require()方法导入模块时,导入的结果,永远以module.exports 指向的对象为准
exports == moudle.exports
由于 module.exports 单词写起来比较复杂,为了简化向外共享成员的代码,Node提供了exports对象,默认情况下,exports 和 module.exports 指向同一个对象,最终共享的结果,还是以 moudle.exports 指向的对象为准
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lGQfPazb-1680527715183)(C:\Users\guigui\AppData\Local\Temp\1680351528738.png)]
export和module.exports的使用误区
时刻谨记,require()模块时,得到的永远是module.exports 指向的对象
最终我们得到的依然是module.exports,但是module.exports == exports,所以,exports也指向也新的对象
模块化规范(commonJS)
commonJS规定
- 每个模块内部,module变量代表当前模块
- module变量是一个对象,他的exports属性(即module.exports)是对外的接口
- 加载某个模块,其实是加载该模块的module.exports属性。require()方法用于加载模块。
npm与包
包
概念
什么是包
Node.js中的第三方模块又叫做包
别人开发出来的东西,就像python中的包
包是基于内置模块封装出来的,提供了更高级,更方便的API,极大地提高了开发效率
包和内置模块之间的关系,类似于jQuery和浏览器内置API之间的关系
从哪里下载包
全球最大的包共享平台
如何下载包
npm,inc提供了一个包管理工具,这个包管理工具的名字叫做Node Package Manager(简称 npm 包管理工具),这个包管理工具随着Node.js的安装包一起被安装到了用户的电脑上。
查看包管理工具版本号:
C:\Users\guigui>npm -v
9.6.1
npm 初体验
格式化时间的传统做法
15.dateFormat.js
//1.定义格式化时间的方法
function dateFormat(dtStr){
const dt = new Date(dtStr)
const y = dt.getFullYear()
const m = padZero(dt.getMonth() + 1);
const d = padZero(dt.getDate());
const hh = padZero(dt.getHours());
const mm = padZero(dt.getMinutes());
const ss = padZero(dt.getSeconds());
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`
}
//定义补零的函数
function padZero(n){
return n > 9 ? n : '0' + n
}
module.exports = {
dateFormat
}
// 导入自定义的格式化时间的模块
const TIME = require('./15.dateFormat');
// 调用方法,进行时间的格式化
const dt = new Date();
const newDT = TIME.dateFormat(dt);
console.log(newDT)
格式化时间的高级做法(包)
使用npm包管理工具,在项目中安装格式化时间的包 moment
使用require()导入格式化时间的包
参考moment的官方API文档对时间进行格式化
安装包
npm install moment
// 导入 moment 包
const moment = require('moment')
// 参考 moment 官方 API文档,调用对应的方法,对时间进行格式化
// 通知 moment()方法得到当前的时间
// 针对当前的时间,调用format()方法,按照指定的格式进行时间的格式化
const dt = moment().format('YYYY-MM-DD HH:mm:ss')
console.log(dt) //输出2023-04-01 21:44:21
node_modules和package-lock.json
node_modules
node_modules用来存放所有已安装到项目中的包。require()导入第三方包时,就是从这个目录中查找并加载包
注意:
node_modules包含了项目中最大的体积容量,所以我们在上传git时,git会默认ignore此包
package-lock.json
用来记录node_modules目录下的每一个包的下载信息,例如包的名字、版本号、下载地址等
安装指定版本的包
默认情况下,使用npm install命令安装包的时候,会自动安装最新版本的包。如果需要安装指定版本的包,可以在包名之后,通过@符号指定具体的版本,例如
npm install moment@2.22.2
第一位数字:大版本
第二位数字:功能版本
第三位数字:Bug修复版本
package.json
运行npm install 命令安装包的时候,npm包管理工具会自动把包的名称和版本号,记录到package.json中
npm包管理工具提供了一个快捷命令,可以在执行命令时所处的目录中,快速创建package.json这个包管理配置文件:
npm init -y
dependencies节点
专门用来记录您使用npm install命令安装了哪些包
一次性安装所有包
当我们使用git拉取项目代码时,我们只有package.json文件,而没有node_modules文件,也就是丢失了安装包内容,只保留了安装包名称,所以我们就需要用package.json文件来安装所需的包
npm install
卸载包
例如,卸载moment包
npm uninstall moment
devDependencies节点安装
如果某些包只在项目开发阶段用到,上线后不会用到,那建议把这些包记录到devDependencies节点中
简写形式
npm i 包名 -D
完整语法
npm install 包名 --save-dev
解决下包慢的问题(镜像)
配置淘宝镜像
npm config set registry=https://registry.npm.taobao.org/
nrm镜像源管理
# 通过 npm 包管理器,将nrm 安装为全局可用的工具
npm i nrm -g
# 查看所有可用的镜像源
nrm ls
# 将下包的镜像源切换为 taobao 镜像
nrm use taobao
开发依赖包,核心依赖包
devDependencies
dependencies
全局包
只有工具性质的包,才有全局安装的必要性,因为他们提供了好用的终端命令
安装全局包
npm install 包名 -g
卸载全局包
npm uninstall 包名 -g
查看全局包位置
npm root -g
规范的包结构
一个规范的包,它的组成结构,必须符合以下3点要求
- 包必须以单独的目录而存在
- 包的顶级目录必须包含package.json这个包管理配置文件
- package.json中必须包含name,version,main这三个属性,分别代表包的名字,版本号,包的入口
开发属于自己的包
初始化包的基本结构
- 新建itheima-tools文件夹,作为包的根目录
- 在itheima-tools文件夹中,新建如下三个文件:
- package.json(包管理配置文件)
- index.js(包的入口文件)
- README.md(包的说明文档)
在index.js中定义转义HTML的方法
function htmlEscape(htmlstr){
return htmlstr.replace(/<|>|"|&/g,(match) =>{
switch (match){
case '<':
return '<'
case '>':
return '>'
case '"':
return '"'
case '&':
return '&'
}
})
}
在index.js中定义还原html字符串的函数
function htmlUnEscape(str){
return str.replace(/<|>|"|&/g,(match)=>{
switch (match){
case '<':
return '<'
case '>':
return '<'
case '"':
return '"'
case '&':
return '&'
}
})
}
将不同的功能进行模块化拆分
//定义格式化时间函数
function dateFormat(dateStr){
const dt = new Date(dateStr)
const y = dt.getFullYear()
const m = padZero(dt.getMonth() + 1)
const d = padZero(dt.getDate())
const hh = padZero(dt.getHours())
const mm = padZero(dt.getMinutes())
const ss = padZero(dt.getDate())
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`
}
//定义一个补零的函数
function padZero(n){
return n > 9 ? n : '0' + n
}
module.exports = {
dateFormat
}
// 定义转义html字符的函数
function htmlEscape(htmlstr){
return htmlstr.replace(/<|>|"|&/g,(match) =>{
switch (match){
case '<':
return '<'
case '>':
return '>'
case '"':
return '"'
case '&':
return '&'
}
})
}
// 定义还原html字符串的函数
function htmlUnEscape(str){
return str.replace(/<|>|"|&/g,(match)=>{
switch (match){
case '<':
return '<'
case '>':
return '<'
case '"':
return '"'
case '&':
return '&'
}
})
}
module.exports = {
htmlEscape,
htmlUnEscape
}
// 这是包的入口文件
const date = require('./src/dateFormat')
const escape = require('./src/htmlEscape')
module.exports = {
// es6语法、展开运算符
...date,
...escape
}
编写包的说明文档
## 安装
```
npm install itheima-tools
```
## 导入
```js
const itheima = require('itheima-tools')
```
## 格式化时间
```js
//调用dateFormat 对时间进行格式化
const dtStr = itheima.dateFormat(new Date());
// 结果 2023-04-02 21:17:02
console.log(dtStr)
```
## 转义 html 中的特殊字符
```js
// 待转换的html字符串
const htmlStr = '<h1 title="abc">这是h1标签<span>123 </span></h1>'
// 调用htmlEscape方法进行转换
const str = itheima.htmlEscape(htmlStr);
// 转换的结果 <h1 title="abc">这是h1标签<span>123&nbsp;</span></h1>
console.log(str)
```
## 还原 html 中的特殊字符
```js
// 待还原的html字符串
const str2 = itheima.htmlUnEscape(str);
// 输出结果 <h1 title="abc">这是h1标签<span>123undefinednbsp;</span></h1>
console.log(str2)
```
# 开源协议
ISC
发布包到官网
在官网注册
登录
注意:在使用这个命令之前,需要把下包的服务器的地址切换为npm的官方服务器。
npm login
# 下面就是一个错误案例,登录到淘宝的服务器了,所以需要使用nrm来切换服务器
PS E:\gitCode\Study_Node.js> npm login
npm notice Log in on https://registry.npm.taobao.org/
Username:
发布
注意事项
- 需要转到包的根目录
- 包名不能在官网重复
- 包名与package.json中的name名字对应
npm publish
删除已发布的包
npm unpublish 包名 --force
- npm unpublish 命令只能删除 72 小时以内发布的包
- npm unpublish 删除的包,在 24 小时内不允许重复发布
- 发布包的时候要慎重,尽量不要往 npm 上发布没有意义的包
模块的加载机制
模块在第一次加载后会被缓存。这也意味着多次调用require()不会导致模块的代码被执行多次
内置模块加载机制
内置模块加载优先级最高
自定义模块的加载机制
需要使用./或…/开头,不然会被node.js识别为内置模块,从而报错
// 正确
require('./03.自定义模块')
// 错误
require('03.自定义模块')
在使用require()导入自定义模块时,如果省略了文件的扩展名,则Node.js会按顺序分别尝试加载以下的文件:
- 按照确切的文件名进行加载
- 补全.js扩展名进行加载
- 补全.json扩展名进行加载
- 补全.Node扩展名进行加载加载失败,终端报错
- 加载失败,终端报错
express
初识Express
Express是基于Node.js平台,快速、开放、极简的Web开发框架
通俗的理解:Express的作用和Node.js内置的http模块类似,是专门用来创建Web服务器的
Express的本质:就是一个npm上的第三方包,提供了快速创建web服务器的便捷方法
中文官网
http://www.expressjs.com.cn/
安装
npm i express@4.17.1
基本应用案例,web服务器
//1.导入express
const express = require('express');
//2.创建web服务器
const app = express();
//3.启动web服务器,监听80,并启用回调
app.listen(80,() =>{
console.log('express server running at http://127.0.0.1:80')
})
监听与响应
//1.导入express
const express = require('express');
//2.创建web服务器
const app = express();
//4.监听客户端的get 和 post请求,并向客户端响应具体的内容
app.get('/user',(req,res) =>{
//调用 express 提供的 res.send()方法,向客户端响应一个 JSON对象
res.send({name:'zs',age:20,gender:'男'})
})
app.post('/user',(req,res) =>{
//调用 express 提供的 res.send()方法,向客户端响应一个 文本字符串
res.send('请求成功')
})
//3.启动web服务器,监听80,并启用回调
app.listen(80,() =>{
console.log('express server running at http://127.0.0.1:80')
})
监听GET请求
通过app.get()方法,可以监听客户端的GET请求,具体的语法格式如下
监听POST请求
通过app.post()方法,可以监听客户端的POST请求,具体的语法格式如下
把内容响应给客户端
通过res.send()方法,可以把处理好的内容,发送给客户端
获取URL中携带的查询参数
通过req.query对象,可以访问到客户端通过查询字符串的形式,发送到服务器的参数:
//1.导入express
const express = require('express');
//2.创建web服务器
const app = express();
app.get('/',(req,res) =>{
// 通过 req.query 可以获取到客户端发送过来的查询参数
// 注意:默认情况下,req.query 是一个空对象
console.log(req.query)
res.send(req.query)
})
//3.启动web服务器,监听80,并启用回调
app.listen(80,() =>{
console.log('express server running at http://127.0.0.1:80')
})
postMan测试
http://127.0.0.1/?user=123&password=123
返回
{
“user”: “123”,
“password”: “123”
}
获取URL中的动态参数
通过req.params对象,可以访问到URL中,通过:匹配到的动态参数:
//1.导入express
const express = require('express');
//2.创建web服务器
const app = express();
// 这里的:id是一个动态的参数
app.get('/user/:id/:name',(req,res) =>{
// req.params是动态匹配到的 URL 参数,默认也是一个空对象
console.log(req.params)
res.send(req.params)
})
//3.启动web服务器,监听80,并启用回调
app.listen(80,() =>{
console.log('express server running at http://127.0.0.1:80')
})
postMan测试
http://127.0.0.1/user/1/taotao
测试结果
{
“id”: “1”,
“name”: “taotao”
}
Express 路由
Express 中间件
使用 Express 写接口
编写get接口
// 导入express模块
const express = require('express')
// 创建服务器实例对象
const app = express()
//导入路由模块
const router = require('./16.apiRouter')
//把路由模块,注册到app上
app.use('/api',router)
app.listen(80, () => {
console.log('express server running at http://127.0.0.1')
})
const express = require('express')
const router= express.Router()
// 在这里挂载对应的路由
router.get('/get',(req,res)=>{
//通过 req.query 获取客户端通过查询字符串,发送到服务器的数据
const query = req.query
//调用 res.send()方法,向客户端响应处理的结果
res.send({
status:0, //状态,0 表示成功,1 表示失败
msg:'GET请求成功!', //状态描述
data:query //需要响应给客户端的具体数据
})
})
module.exports = router
postman测试
http://localhost/api/get?1=1
请求头带参数
请求响应
{
“status”: 0,
“msg”: “GET请求成功!”,
“data”: {
“1”: “1”
}
}
编写post接口
// 导入express模块
const express = require('express')
// 创建服务器实例对象
const app = express()
//配置解析表单的中间件
app.use(express.urlencoded({extended:false}))
//导入路由模块
const router = require('./16.apiRouter')
//把路由模块,注册到app上
app.use('/api',router)
app.listen(80, () => {
console.log('express server running at http://127.0.0.1')
})
// 定义Post接口
router.post('/post',(req,res)=>{
//通过 req.body 获取请求体中包含的 url-encoded 格式的数据
const body = req.body
//调用 res.send()方法,向客户端响应处理的结果
res.send({
status:0, //状态,0 表示成功,1 表示失败
msg:'Post请求成功!', //状态描述
data:body //需要响应给客户端的具体数据
})
})
postman发送数据为x-www.form-urlencoded
响应结果为
{
“status”: 0,
“msg”: “Post请求成功!”,
“data”: {
“1”: “2”,
“name”: “12”
}
}
接口的跨域问题
启动上面的案例服务器
前端写ajax请求
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="https://cdn.staticfile.org/jquery/3.6.4/jquery.min.js"></script>
</head>
<body>
<button id="btnGET">GET</button>
<button id="btnPOST">POST</button>
<script>
$(function () {
//测试GET接口
$('#btnGET').on('click', function () {
$.ajax({
type: 'GET',
url: 'http://127.0.0.1/api/get',
data: {name: 'zs', age: 20},
success: function (res) {
console.log(res)
}
})
})
//测试POST接口
$('#btnPOST').on('click', function () {
$.ajax({
type: 'POST',
url: 'http://127.0.0.1/api/post',
data: {bookname: '水浒传', author: '施耐庵'},
success: function (res) {
console.log(res)
}
})
})
})
</script>
</body>
</html>
跨域报错
只要协议、域名、端口号任意不同,就会导致跨域问题
Access to XMLHttpRequest at ‘http://127.0.0.1/api/get?name=zs&age=20’ from origin ‘http://localhost:63342’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
解决跨域
刚才编写的 GET和 POST接口,存在一个很严重的问题: 不支持跨域请求解决接口跨域问题的方案主要有两种:
- CORS(主流的解决方案,推荐使用)
- JSONP (有缺陷的解决方案: 只支持 GET 请求)
使用cors中间件解决跨域问题
cors是Express的一个第三方中间件。通过安装和配置cors中间件,可以很方便解决跨域问题
- 运行 npm install cors 安装中间件
- 使用 const cors = require(‘cors’) 导入中间件
- 在路由之前调用 app.use(cors()) 配置中间件
// 导入express模块
const express = require('express')
// 创建服务器实例对象
const app = express()
//配置解析表单的中间件
app.use(express.urlencoded({extended:false}))
// 一定要在路由之前,配置cors这个中间件,从而解决接口跨域的问题
const cors = require('cors')
app.use(cors())
//导入路由模块
const router = require('./16.apiRouter')
//把路由模块,注册到app上
app.use('/api',router)
app.listen(80, () => {
console.log('express server running at http://127.0.0.1')
})
cors的注意事项
- CORS 主要在服务器端进行配置。客户端浏览器无须做任何额外的配置,即可请求开启了 CORS 的接口.
- CORS 在浏览器中有兼容性。只有支持 XMLHttpRequest Level2的浏览器,才能正常访问开启了 CORS 的服务端接口 (例如: IE10+、Chrome4+、FireFox3.5+)。
cos跨域资源共享
CORS 响应头部 - Access-Control-Allow-Origin
允许所有域名访问
res.setHeader('Access-Control-Allow-Origin','*')
只允许百度访问这个页面
res.setHeader('Access-Control-Allow-Origin','www.baidu.com')
CORS 响应头部 - Access-Control-Allow-Headers
默认情况下,CORS 仅支持客户端向服务器发送如下的9个请求头:
Accept、Accept-Language、 Content-Language、DPR、Downlink, Save-Data、 Viewport-Width、 Width .Content-Type (值仅限于 text/plain、multipart/form-data、 application/x-www-form-urlencoded 三者之一
如果客户端向服务器发送了额外的请求头信息,则需要在服务器端,通过 Access-Contro-Allow-Headers 对额外的请求头进行声明,否则这次请求会失败!
// 允许客户端额外向服务器发送 Content-Type 请求头和 X-Custom-Header 请求头
// 注意: 多个请求头之间使用英文的逗号进行分割
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Custom-Header')
CORS 响应头部 - Access-Control-Allow-Methods
默认情况下,CORS 仅支持客户端发起 GET、POST、HEAD 请求。
如果客户端希望通过 PUT、DELETE 等方式请求服务器的资源,则需要在服务器端,通过 Access-Control-Alow-Methods来指明实际请求所允许使用的 HTTP 方法
// 只允许 POST、GET、DELETE、HEAD 请求方法
res.setHeader('Access-Control-Allow-Methods','pST,GET,DELETE,HEAD')
// 允许所有的 HTTP 请求方法
res.setHeader('Access-Control-Allow-Methods','*')
简单请求
同时满足以下两大条件的请求,就属于简单请求:
请求方式:GET、POST、HEAD 三者之一
HTTP 头部信息不超过以下几种字段: 无自定义头部字段、Accept、Accept-Language、Content-Language、DPR.Downlink、Save-Data Viewport-Width、 Width 、ContentType (只有三个值application/x-www-formurlencoded、multipart/form-data、text/plain)
预检请求
只要符合以下任何一个条件的请求,都需要进行预检请求:
- 请求方式为 GET、POST、HEAD 之外的请求 Method 类型
- 请求头中包含自定义头部字段
- 向服务器发送了 application/json 格式的数据
在浏览器与服务器正式通信之前,浏览器会先发送 OPTION 请求进行预检,以获知服务器是否允许该实际请求,所以这一次的 OPTION 请求称为“预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据。
简单请求和预检请求的区别
- 简单请求的特点:客户端与服务器之间只会发生一次请求
- 预检请求的特点: 客户端与服务器之间会发生两次请求,OPTION 预检请求成功之后,才会发起真正的请求
这里我们使用delete来做案例
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="https://cdn.staticfile.org/jquery/3.6.4/jquery.min.js"></script>
</head>
<body>
<button id="btnGET">GET</button>
<button id="btnPOST">POST</button>
<button id="btnDelete">DELETE</button>
<script>
$(function () {
//测试GET接口
$('#btnGET').on('click', function () {
$.ajax({
type: 'GET',
url: 'http://127.0.0.1/api/get',
data: {name: 'zs', age: 20},
success: function (res) {
console.log(res)
}
})
})
//测试POST接口
$('#btnPOST').on('click', function () {
$.ajax({
type: 'POST',
url: 'http://127.0.0.1/api/post',
data: {bookname: '水浒传', author: '施耐庵'},
success: function (res) {
console.log(res)
}
})
})
// 3.未删除按钮绑定点击事件处理函数
$('#btnDelete').on('click',function (){
$.ajax({
type:'DELETE',
url:'http://127.0.0.1/api/delete',
success:function(res){
console.log(res)
}
})
})
})
</script>
</body>
</html>
const express = require('express')
const router= express.Router()
// 在这里挂载对应的路由
router.get('/get',(req,res)=>{
//通过 req.query 获取客户端通过查询字符串,发送到服务器的数据
const query = req.query
//调用 res.send()方法,向客户端响应处理的结果
res.send({
status:0, //状态,0 表示成功,1 表示失败
msg:'GET请求成功!', //状态描述
data:query //需要响应给客户端的具体数据
})
})
// 定义Post接口
router.post('/post',(req,res)=>{
//通过 req.body 获取请求体中包含的 url-encoded 格式的数据
const body = req.body
//调用 res.send()方法,向客户端响应处理的结果
res.send({
status:0, //状态,0 表示成功,1 表示失败
msg:'Post请求成功!', //状态描述
data:body //需要响应给客户端的具体数据
})
})
// 定义Delete接口
router.delete('/delete',(req,res)=>{
//通过 req.body 获取请求体中包含的 url-encoded 格式的数据
const body = req.body
//调用 res.send()方法,向客户端响应处理的结果
res.send({
status:0, //状态,0 表示成功,1 表示失败
msg:'Delete请求成功!', //状态描述
})
})
module.exports = router
// 导入express模块
const express = require('express')
// 创建服务器实例对象
const app = express()
//配置解析表单的中间件
app.use(express.urlencoded({extended:false}))
// 一定要在路由之前,配置cors这个中间件,从而解决接口跨域的问题
const cors = require('cors')
app.use(cors())
//导入路由模块
const router = require('./16.apiRouter')
//把路由模块,注册到app上
app.use('/api',router)
app.listen(80, () => {
console.log('express server running at http://127.0.0.1')
})
运行服务器
测试delete请求后,network栏应该弹出两个请求,分别是option和delete请求
JSONP接口
回顾JSONP 的概念与特点
概念:浏览器端通过
特点:
- JSONP不属于真正的Ajax 请求,因为它没有使用XMLHttpRequest 这个对象
- JSONP 仅支持 GET 请求,不支持 POST、PUT、DELETE 等请求L
注意事项
创建JSONP 接口的注意事项
如果项目中已经配置了 CORS 跨域资源共享,为了防止冲突,必须在配置 CORS 中间件之前声明SONP 的接口。否则JSONP 接口会被处理成开启了 CORS 的接口。示例代码如下:
实现JSONP接口的步骤
- 获取客户端发送过来的回调函数的名字
- 得到要通过JSONP 形式发送给客户端的数据
- 根据前两步得到的数据,拼接出一个函数调用的字符串
- 把上一步拼接得到的字符串,响应给客户端的
// 导入express模块
const express = require('express')
// 创建服务器实例对象
const app = express()
//配置解析表单的中间件
app.use(express.urlencoded({extended:false}))
// 必须在配置cors中间件之前,配置JSONP的接口
app.get('/api/jsonp',(req,res)=>{
//TODO:定义JSONP接口具体的实现过程
// 1,获取客户端发送过来的回调函数的名字
const funcName = req.query.callback
// 2,得到要通过 JSONP 形式发送给客户端的数据
const data = { name: 'zs' , age: 22 }
// 3,根据前两步得到的数据,拼接出一个函数调用的字符串
const scriptStr ="${funcName}($(JSON.stringify(data)})"
// 4.把上一步拼接得到的字符串,响应给客户端的 <script> 标签进行解析执行
res .send(scriptStr)
})
// 一定要在路由之前,配置cors这个中间件,从而解决接口跨域的问题
const cors = require('cors')
app.use(cors())
//导入路由模块
const router = require('./16.apiRouter')
//把路由模块,注册到app上
app.use('/api',router)
app.listen(80, () => {
console.log('express server running at http://127.0.0.1')
})
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="https://cdn.staticfile.org/jquery/3.6.4/jquery.min.js"></script>
</head>
<body>
<button id="btnGET">GET</button>
<button id="btnPOST">POST</button>
<button id="btnDelete">DELETE</button>
<button id="btnJSONP">JSONP</button>
<script>
$(function () {
//测试GET接口
$('#btnGET').on('click', function () {
$.ajax({
type: 'GET',
url: 'http://127.0.0.1/api/get',
data: {name: 'zs', age: 20},
success: function (res) {
console.log(res)
}
})
})
//测试POST接口
$('#btnPOST').on('click', function () {
$.ajax({
type: 'POST',
url: 'http://127.0.0.1/api/post',
data: {bookname: '水浒传', author: '施耐庵'},
success: function (res) {
console.log(res)
}
})
})
// 3.未删除按钮绑定点击事件处理函数
$('#btnDelete').on('click',function (){
$.ajax({
type:'DELETE',
url:'http://127.0.0.1/api/delete',
success:function(res){
console.log(res)
}
})
})
// 4.为jsonp绑定点击事件
$('#btnJSONP').on('click',function (){
$.ajax({
type:'GET',
url:'http://127.0.0.1/api/jsonp',
dataType:'jsonp',
success:function(res){
console.log(res)
}
})
})
})
</script>
</body>
</html>
const express = require('express')
const router= express.Router()
// 在这里挂载对应的路由
router.get('/get',(req,res)=>{
//通过 req.query 获取客户端通过查询字符串,发送到服务器的数据
const query = req.query
//调用 res.send()方法,向客户端响应处理的结果
res.send({
status:0, //状态,0 表示成功,1 表示失败
msg:'GET请求成功!', //状态描述
data:query //需要响应给客户端的具体数据
})
})
// 定义Post接口
router.post('/post',(req,res)=>{
//通过 req.body 获取请求体中包含的 url-encoded 格式的数据
const body = req.body
//调用 res.send()方法,向客户端响应处理的结果
res.send({
status:0, //状态,0 表示成功,1 表示失败
msg:'Post请求成功!', //状态描述
data:body //需要响应给客户端的具体数据
})
})
// 定义Delete接口
router.delete('/delete',(req,res)=>{
//通过 req.body 获取请求体中包含的 url-encoded 格式的数据
const body = req.body
//调用 res.send()方法,向客户端响应处理的结果
res.send({
status:0, //状态,0 表示成功,1 表示失败
msg:'Delete请求成功!', //状态描述
})
})
module.exports = router
mysql数据库
在Express中操作mysql
在项目中操作数据库的步骤
- 安装操作MySQL数据库的第三方模块 (mysql)
- 通过 mysql 模块连接到 MySQL 数据库
- 通过 mysql 模块执行 SQL 语句
安装 mysql 模块
mysql 横块是托管于 npm 上的第三方横块。它提供了在 Nodejs 项目中连接和操作 MySQL 数据库的能力想要在项目中使用它,需要先运行如下命令,将 mysql 安装为项目的依赖包:
npm install mysql
配置mysql模块
在使用 mysql 模块操作 MySQL 数据库之前,必须先对 mysql 模块进行必要的配置,主要的配置步骤如下:
// 1.导入 mysql 模块
const mysql = require( 'mysql' )
// 2.建立与 MySQL 据库的连接
const db = mysql.createPool({
host:'127.0.0.1',//数据库的 IP 地址
user:'root', //登录数据库的账号
password:12345 ,// 登录数据库的密码
database: 'my_db_01' // 指定要操作哪个数据库'
})
####测试mysql模块能否正常工作
调用 db.query0函数,指定要执行的 SQL 语句,通过回调函数拿到执行的结果:
// 1.导入 mysql 模块
const mysql = require( 'mysql' )
// 2.建立与 MySQL 据库的连接
const db = mysql.createPool({
host:'127.0.0.1',//数据库的 IP 地址
user:'root', //登录数据库的账号
password:'12345' ,// 登录数据库的密码
database: 'test' // 指定要操作哪个数据库'
})
// 测试mysql模块否正工作
db .query('select 1', (err, results) => {
// mysql模块工作期间报错了
if (err) return console.log(err.message)
// 只要能打印出 [ RowDataPacket {1: 1 ] 的结果,就证明数据库连接正常
console.log(results)
})
正常返回
[ RowDataPacket { ‘1’: 1 } ]
查询数据
// 1.导入 mysql 模块
const mysql = require( 'mysql' )
// 2.建立与 MySQL 据库的连接
const db = mysql.createPool({
host:'127.0.0.1',//数据库的 IP 地址
user:'root', //登录数据库的账号
password:'12345' ,// 登录数据库的密码
database: 'test' // 指定要操作哪个数据库'
})
// 测试mysql模块否正工作
// db .query('select 1', (err, results) => {
// // mysql模块工作期间报错了
// if (err) return console.log(err.message)
// // 只要能打印出 [ RowDataPacket {1: 1 ] 的结果,就证明数据库连接正常
// console.log(results)
// })
// 查询user表中所有数据
const sqlStr = 'select * from user'
db.query(sqlStr,(err,results)=>{
//查询数据失败
if(err) return console.log(err.message)
//查询数据成功
// 注意:如果执行的是select查询语句,则执行的结果是数组
console.log(results)
})
返回结果
[
RowDataPacket { id: 1, email: ‘test@example.com’, password: ‘12345’ },
RowDataPacket {
id: 2,
email: ‘user@example.com’,
password: ‘$2y 10 10 10gtkodBkZfghR6I3UShGTRuV9S5fLptGTN60nUb83IUBr0rYTO49G.’
},
RowDataPacket {
id: 3,
email: ‘user@example.com’,
password: ‘$2y 10 10 10IhJKZZYsEjVxPAKyJjiO0.ZymiQCC1hxvmGEJFPj.UpLmc03ROjRu’
},
]
插入数据
向 user 表中新增数据,其中 email 为 Spider-Man,password 为 pcc321。示例代码如下:
// 1.导入 mysql 模块
const mysql = require('mysql')
// 2.建立与 MySQL 据库的连接
const db = mysql.createPool({
host: '127.0.0.1',//数据库的 IP 地址
user: 'root', //登录数据库的账号
password: '12345',// 登录数据库的密码
database: 'test' // 指定要操作哪个数据库'
})
// 测试mysql模块否正工作
// db .query('select 1', (err, results) => {
// // mysql模块工作期间报错了
// if (err) return console.log(err.message)
// // 只要能打印出 [ RowDataPacket {1: 1 ] 的结果,就证明数据库连接正常
// console.log(results)
// })
// 查询user表中所有数据
// const sqlStr = 'select * from user'
// db.query(sqlStr,(err,results)=>{
// //查询数据失败
// if(err) return console.log(err.message)
// //查询数据成功
// // 注意:如果执行的是select查询语句,则执行的结果是数组
// console.log(results)
// })
// 1.要插入到 user 表中的数据对象
const user = {email: 'Spider-Man', password: 'pcc321'}
// 2.待执行的 SQL 语句,其中英文的 ? 表示占位符
const sqlStr = 'INSERT INTO user (email, password) VALUES (?, ?)'
// 3.使用数组的形式,依次为 ? 占位符指定具体的值
db.query(sqlStr, [user.email, user.password], (err, results) => {
if (err) return console.log(err.message) //失败
if(results.affectedRows === 1) {console.log('插入数据成功') }//成功
})
返回结果
插入数据成功!
插入数据的便捷方式
向表中新增数据时,如果数据对象的每个属性和数据表的字段一一对应,则可以通过如下方式快速插入数据:
// 1.导入 mysql 模块
const mysql = require('mysql')
// 2.建立与 MySQL 据库的连接
const db = mysql.createPool({
host: '127.0.0.1',//数据库的 IP 地址
user: 'root', //登录数据库的账号
password: '12345',// 登录数据库的密码
database: 'test' // 指定要操作哪个数据库'
})
//,要插入到 user 表中的数据对象
const user = {email: 'Spider-Man2', password: 'pcc43212'}
// 2.待执行的 SQL 语句,其中英文的 ? 表示占位符
const sqlstr = 'INSERT INTO user SET ?'
// 3.直接将数据对象当作占位符的值
db.query(sqlstr, user, (err, results) => {
if (err) return console.log(err.message) // 失败
if (results.affectedRows === 1) {
console.log('插入数据成功')// 成功
}
})
返回内容
插入数据成功
更新数据
// 1.导入 mysql 模块
const mysql = require('mysql')
// 2.建立与 MySQL 据库的连接
const db = mysql.createPool({
host: '127.0.0.1',//数据库的 IP 地址
user: 'root', //登录数据库的账号
password: '12345',// 登录数据库的密码
database: 'test' // 指定要操作哪个数据库'
})
//1.要更新的数据对象
const user = {email: 'Spider-Man', password: '000'}
// 2.要执行的 SQL 语句
const sqlStr = 'UPDATE user SET password=? WHERE email=?'
// 3.调用 d.query() 执行 SQL 语的同时,使用数组依次为占位符指定具体的值
db.query(sqlStr, [user.password, user.email], (err, results) => {
if (err) return console.log(err.message) //失败
if (results.affectedRows === 1) {
console.log('更新数据成功!')
} // 成功
})
返回内容
更新数据成功!
更新数据快捷方式
更新表数据时,如果数据对象的每个属性和数据表的字段一一对应,则可以通过如下方式快速更新表数据
// 2.建立与 MySQL 据库的连接
const db = mysql.createPool({
host: '127.0.0.1',//数据库的 IP 地址
user: 'root', //登录数据库的账号
password: '12345',// 登录数据库的密码
database: 'test' // 指定要操作哪个数据库'
})
//1.要更新的数据对象
const user = {email: 'Spider-Man', password: '0001'}
// 2.要执行的 SQL 语句
const sqlStr = 'UPDATE user SET ? WHERE email=?'
// 3.调用 d.query() 执行 SQL 语的同时,使用数组依次为占位符指定具体的值
db.query(sqlStr, [user, user.email], (err, results) => {
if (err) return console.log(err.message) //失败
if (results.affectedRows === 1) {
console.log('更新数据成功!')
} // 成功
})
返回内容
更新数据成功!
删除数据
在删除数据时,推荐根据 id 这样的唯一标识,来删除对应的数据。示例如下:
// 1.导入 mysql 模块
const mysql = require('mysql')
// 2.建立与 MySQL 据库的连接
const db = mysql.createPool({
host: '127.0.0.1',//数据库的 IP 地址
user: 'root', //登录数据库的账号
password: '12345',// 登录数据库的密码
database: 'test' // 指定要操作哪个数据库'
})
// 1.要执行的 SQL 语句
const sqlStr = 'DELETE FROM user WHERE email=?'
// 2.调用 db.query() 执行 SQL 语句的同时,为占位符指定具体的值
// 注意: 如果 SQL 语句中有多个占位符,则必须使用数组为每个占位符指定具体的值
// 如果 SQL 语句中只有一个占位符,则可以省略数组
db.query(sqlStr, 'Spider-Man', (err, results) => {
if (err) return console.log(err.message) // 失败
if (results.affectedRows === 1) {
console.log('删除数据成功!')
} // 成功
})
删除数据成功!
标记删除
使用 DELETE 语句,会把真正的把数据从表中删除掉。为了保险起见,推荐使用标记删除的形式,来模拟删除的动作.
所谓的标记删除,就是在表中设置类似于 status 这样的状态字段,来标记当前这条数据是否被删除。
当用户执行了删除的动作时,我们并没有执行 DELETE 语句把数据删除掉,而是执行了 UPDATE 语句,将这条数据对应的status字段标记为删除即可
// 1.导入 mysql 模块
const mysql = require('mysql')
// 2.建立与 MySQL 据库的连接
const db = mysql.createPool({
host: '127.0.0.1',//数据库的 IP 地址
user: 'root', //登录数据库的账号
password: '12345',// 登录数据库的密码
database: 'test' // 指定要操作哪个数据库'
})
// 标记删除:使用 UPDATE 语句替代 DELETE 语句;只更新数据的状态,并没有真正删除
db.query('UPDATE USERS SET status=1 WHERE id=?', 6, (err, results) => {
if (err) return console.log(err.message) // 失败
if (results.affectedRows === 1) {
console.log('删除数据成功')
}
})// 成功
这里我没有相关status字段,所以不再演示
前后端的身份认证
web开发模式
目前主流的 Web开发模式有两种
分别是:基于服务端染的传统Web开发模式
基于前后端分离的新型Web开发模式
服务端渲染的 Web 开发模式
服务端渲染的概念:服务器发送给客户端的 HTML 页面,是在服务器通过字符串的拼接,动态生成的。因此,客户端不需要使用Ajax这样的技术额外请求页面的数据。代码示例如下:
app.get(' /index.html', (req, res) => {
// 1.要渲染的数据
const user = {name: 'zs', age: 20}
// 2.服务器端通过字符串的拼接,动态生成 HTML 内容
const html = '<h1>姓名: $(user.name},年龄: $(user.age}</h1>'
//3,把生成好的页面内容响应给客户端。因此,客户端拿到的是带有真实数据的 HTML 页面
res.send(htm1)
})
优点
前端耗时少。因为服务器端负责动态生成 HTML内容,浏览器只需要直接渲染页面即可。尤其是移动端,更省电。
有利于SEO。因为服务器端响应的是完整的 HTML页面内容,所以爬虫更容易爬取获得信息,更有利于 SEO.
缺点
占用服务器端资源。即服务器端完成 HTML页面内容的拼接,如果请求较多,会对服务器造成一定的访问压力
不利于前后端分离,开发效率低。使用服务器端渲染,则无法进行分工合作,尤其对于前端复杂度高的项目,不利于项目高效开发
前后端分离的 Web 开发模式
前后端分离的概念:前后端分离的开发模式,依赖于 Ajax 技术的广泛应用。简而言之,前后端分离的 Web 开发模式就是后端只负责提供 API 接口,前端使用 Ajax 调用接口的开发模式
前后端分离的优缺点
优点
开发体验好。前端专注于UI页面的开发,后端专注于api的开发,且前端有更多的选择性
用户体验好。Ajax 技术的广泛应用,极大的提高了用户的体验,可以轻松实现页面的局部刷新
减轻了服务器端的渲染压力。因为页面最终是在每个用户的浏览器中生成的
缺点
不利于 SEO。因为完整的 HTML页面需要在客户端动态拼接完成,所以爬虫对无法爬取页面的有效信息(解决方案:利用 VueReact等前端框架的 SSR (server side render) 技术能够很好的解决 SEO 问题!)
选择开发模式
不谈业务场景而盲目选择使用何种开发模式都是耍流氓
比如企业级网站,主要功能是展示而没有复杂的交互,并且需要良好的 SEO,则这时我们就需要使用服务器端渲染,。
而类似后台管理项目,交互性比较强,不需要考虑 SEO,那么就可以使用前后端分离的开发模式。
另外,具体使用何种开发模式并不是绝对的,为了同时兼顾了首页的渲染速度和前后端分离的开发效率,一些网站采用了首屏服务器端渲染+其他页面前后端分离的开发模式
什么是身份认证
身份认证 (Authentication)又称“身份验证”,“鉴权”,是指通过一定的手段,完成对用户身份的确认。
日常生活中的身份认证随处可见,例如: 高铁的验票乘车,手机的密码或指纹解锁,支付宝或微信的支付密码等。
在 Web 开发中,也涉及到用户身份的认证,例如: 各大网站的手机验证码登录、邮箱密码登录、二维码登录等
为什么需要身份认证
身份认证的目的,是为了确认当前所声称为某种身份的用户,确实是所声称的用户。例如,你去找快递员取快递,你要怎么证明这份快递是你的。
在互联网项目开发中,如何对用户的身份进行认证,是一个值得深入探讨的问题。例如,如何才能保证网站不会错误的将马云的存款数额”显示到“马化腾的账户”上。
不同开发模式下的身份认证
对于服务端渲染和前后端分离这两种开发模式来说,分别有着不同的身份认证方案
服务端渲染推荐使用 Session 认证机制
前后端分离推荐使用JWT 认证机制
Session 认证机制
HTTP协议的无状态性
了解HTTP协议的无状态性是进一步学习 Session 认证机制的必要前提HTTP协议的无状态性,指的是客户端的每次 HTTP 请求都是独立的,连续多个请求之间没有直接的关系,服务器不会主动保留每次 HTTP 请求的状态
如何突破 HTTP 无状态的限制
对于超市来说,为了方便收银员在进行结算时给 VIP 用户打折,超市可以为每个VIP 用户发放会员卡
注意: 现实生活中的会员卡身份认证方式,在 Web 开发中的专业术语叫做 Cookie。
什么是 Cookie
Cookie 是存储在用户浏览器中的一段不超过 4 KB 的字符串。它由一个名称(Name) 、一个值(Value)和其它几个用于控制 Cookie 有效期、安全性、使用范围的可选属性组成
不同域名下的 Cookie 各自独立,每当客户端发起请求时,会自动把当前域名下所有未过期的 ookie 一同发送到服务器。
Cookie的几大特性
- 自动发送
- 域名独立
- 过期时限
- 4kb限制
Cookie在身份认证中的作用
客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的 Cookie,客户端会自动将 Cookie 保存在浏览器中。
随后,当客户端浏览器每次请求服务器的时候,浏览器会自动将身份认证相关的 Cookie,通过请求头的形式发送给服务器,服务器即可验明客户端的身份
Cookie 不具有安全性
由于 Cookie 是存储在浏览器中的,而且浏览器也提供了读写 Cookie 的 AP1,因此 Cookie 很容易被伪造,不具有安全性。因此不建议服务器将重要的隐私数据,通过Cookie 的形式发送给浏览器。
注意:千万不要使用 Cookie 存储重要且隐私的数据!比如用户的身份信息、密码等
提高身份认证的安全性
为了防止客户伪造会员卡,收银员在拿到客户出示的会员卡之后,可以在收银机上进行刷卡认证。只有收银机确认存在的会员卡,才能被正常使用。
这种“会员卡 + 刷卡认证”的设计理念,就是Session 认证机制的精髓
在Express 中使用 Session 认证
安装express-session 中间件
在Express 项目中,只需要安装 express-session 中间件,即可在项目中使用 Session 认证
npm install express-session
配置 express-session 中间件
express-session 中间件安装成功后,需要通过 app.use0 来注册 session 中间件,示例代码如下:
const session = require('express-session')
app.use(
session({
secret: 'itheima',
resave: false,
saveUninitialized: true,
})
)
向 session 中存数据
当express-session 中间件配置成功后,即可通过 req.session 来访问和使用 session 对象,从而存储用户的关键信息
// 登录的 API 接口
app.post('/api/login', (req, res) => {
// 判断用户提交的登录信息是否正确
if (req.body.username !== 'admin' || req.body.password !== '000000') {
return res.send({ status: 1, msg: '登录失败' })
}
// TODO_02:请将登录成功后的用户信息,保存到 Session 中
// 注意:只有成功配置了 express-session 这个中间件之后,才能够通过 req 点出来 session 这个属性
req.session.user = req.body // 用户的信息
req.session.islogin = true // 用户的登录状态
res.send({ status: 0, msg: '登录成功' })
})
从session 中取数据
可以直接从 req.session 对象上获取之前存储的数据,示例代码如下
// 获取用户姓名的接口
app.get('/api/username', (req, res) => {
// TODO_03:请从 Session 中获取用户的名称,响应给客户端
if (!req.session.islogin) {
return res.send({ status: 1, msg: 'fail' })
}
res.send({
status: 0,
msg: 'success',
username: req.session.user.username,
})
})
清空 session
调用 req.session.destroy() 函数,即可清空服务器保存的 session 信息
// 退出登录的接口
app.post('/api/logout', (req, res) => {
// TODO_04:清空 Session 信息
req.session.destroy()
res.send({
status: 0,
msg: '退出登录成功',
})
})
只会清空当前用户的session
JWT认证机制
了解 Session 认证的局限性
Session 认证机制需要配合 Cokie 才能实现。由于 Cookie 默认不支持跨域访问,所以,当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域 Session 认证。
注意:
当前端请求后端接口不存在跨域问题的时候,推荐使用 Session 身份认证机制。
当前端需要跨域请求后端接口的时候,不推荐使用 Session 身份认证机制,推荐使用WT 认证机制
什么是JWT
WT (英文全称: JSON Web Token)是目前最流行的跨域认证解决方案
JWT工作原理
总结:用户的信息通过 Token 字符串的形式,保存在客户端浏览器中。服务器通过还原 Token 字符用的形式来认证用户的身份。
JWT 的组成部分
WT 通常由三部分组成,分别是 Header (头部)、Payload (有效荷载)、Signature (签名)三者之间使用英文的“”分隔,格式如下:
Header .Payload.Signature
下面是JWT 字符串的示例:
eyJhbGcioiJIUzI1NiIsInR5cCZCI6MSwidXNlcm5hbwu101JhZG1pbiIsInBhc3N3b3JkIo1I1wibm11a25hbwuioilms6xlt7TIt7Q1LCJ1J1C2VyX3BoYy1611sImIhdcI6MTU300AZNIY4Mi1ZXhwIjoxNTc4MDcyNjgIKdZ3359KBL3XeuBxuI
JWT 的三个部分各自代表的含义
JWT 的三个组成部分,从前到后分别是 Header、Payload、Signature。其中:
Payload 部分才是真正的用户信息,它是用户信息经过加密之后生成的字符串。
Header和 Signature 是安全性相关的部分,只是为了保证 Token 的安全性
JWT的使用方式
客户端收到服务器返回的 Jwt 之后,通常会将它储存在 localStorage 或 sessionStorage 中
此后,客户端每次与服务器通信,都要带上这个JWT 的字符串,从而进行身份认证。推荐的做法是把JWT 放在 HTTP请求头的 Authorization 字段中,格式如下:
Authorization: Bearer <token>
在 Express 中使用JWT
安装JWT 相关的包
运行如下命令,安装如下两个JWT 相关的包:
npm install jsonwebtoken express-jwt
- jsonwebtoken 用于生成 JWT 字符串
- express-jwt 用于将 JWT 字符串解析还原成 JSON 对象
在 Express 中使用JWT
导入JWT 相关的包
使用 require()函数,分别导入JWT 相关的两个包:
// TODO_01:安装并导入 JWT 相关的两个包,分别是 jsonwebtoken 和 express-jwt
const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')
定义 secret 密钥
为了保证MT 字符串的安全性,防止JWT 字符在网络传输过程中被别人破解,我们需要专门定义一个用于加密和解密的 secret 密钥:
- 当生成JWT 字符串的时候,需要使用 secret 密对用户的信息进行加密,最终得到加密好的 WT 字符串
- 当把JWT 字符串解析还原成JSON 对象的时候,需要使用 secret 钥进行解密
// TODO_02:定义 secret 密钥,建议将密钥命名为 secretKey
const secretKey = 'Yamaha No1 ^_^'
在登录成功后生成JWT 字符串
调用jsonwebtoken 包提供的 sign( 方法,将用户的信息加密成JWT 字符串,响应给客户端:
app.post('/api/login', function (req, res) {
// ...省略登录失败情况下的代码
// 用户登录成功之后,生成 JWT 字符串,通过 token 属性响应给客户端
const tokenStr = jwt.sign({ username: userinfo.username }, secretKey, { expiresIn: '30s' })
res.send({
status: 200,
message: '登录成功!',
token: tokenStr, // 要发送给客户端的 token 字符串
})
})
将JWT字符串还原为JSON对象
客户端每次在访问那些有权限接口的时候,都需要主动通过请求头中的 Authorization 字段,将 Token 字符串发送到服务器进行身份认证。
此时,服务器可以通过 express-jwt 这个中间件,自动将客户端发送过来的 Token 解析还原成JSON 对象:
// TODO_04:注册将 JWT 字符串解析还原成 JSON 对象的中间件
// 注意:只要配置成功了 express-jwt 这个中间件,就可以把解析出来的用户信息,挂载到 req.user 属性上
//只要是带“api”的访问接口都不需要访问权限,其他都需要访问权限
app.use(expressJWT({ secret: secretKey,algorithm:['HS256']}).unless({ path: [/^\/api\//] }))
使用 req.user 获取用户信息
当express-jwt 这个中间件配置成功之后,即可在那些有权限的接口中,使用 req.user 对象,来访问JWT 字符串中解析出来的用户信息了,示例代码如下:
// 登录接口
app.post('/api/login', function (req, res) {
// ...省略录失败情况下的代码
// 用户登录成功之后,生成 JWT 字符串,通过 token 属性响应给客户端
const userinfo = req.body
// 登录失败
if (userinfo.username !== 'admin' || userinfo.password !== '000000') {
return res.send({
status: 400,
message: '登录失败!',
})
}
// 登录成功
// TODO_03:在登录成功之后,调用 jwt.sign() 方法生成 JWT 字符串。并通过 token 属性发送给客户端
// 参数1:用户的信息对象
// 参数2:加密的秘钥
// 参数3:配置对象,可以配置当前 token 的有效期
// 记住:千万不要把密码加密到 token 字符中
const tokenStr = jwt.sign({ username: userinfo.username }, secretKey, { expiresIn: '30s' })
res.send({
status: 200,
message: '登录成功!',
token: tokenStr, // 要发送给客户端的 token 字符串
})
})
// 这是一个有权限的 API 接口
app.get('/admin/getinfo', function (req, res) {
// TODO_05:使用 req.user 获取用户信息,并使用 data 属性将用户信息发送给客户端
console.log(req.user)
res.send({
status: 200,
message: '获取用户信息成功!',
data: req.user, // 要发送给客户端的用户信息
})
})
postman测试
http://127.0.0.1:8888/api/login
携带头urlencoded
username:admin
password:000000
返回结果
{
“status”: 200,
“message”: “登录成功!”,
“token”: “eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNjgxODkxMjk5LCJleHAiOjE2ODE4OTEzMjl9.qgpLCIk_cFNIzasLZ9xaMmAb7k_l3enJoMf-nLNXSTc”
}
postman通过此token发送getinfo请求
http://127.0.0.1:8888/admin/getinfo
携带header
Authorization
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNjgxODkxMjk5LCJleHAiOjE2ODE4OTEzMjl9.qgpLCIk_cFNIzasLZ9xaMmAb7k_l3enJoMf-nLNXSTc
返回结果
{
“status”: 200,
“message”: “获取用户信息成功!”,
“data”: {
“username”: “admin”,
“iat”: 1681891299,
“exp”: 1681891329
}
}
捕获解析JWT失败后产生的错误
当使用express-jwt 解析 Token 字符串时,如果客户端发送过来的 Token 字符串过期或不合法,会产生一个解析失败的错误,影响项目的正常运行。我们可以通过 Express 的错误中间件,捕获这个错误并进行相关的处理,示例代码如下:
// TODO_06:使用全局错误处理中间件,捕获解析 JWT 失败后产生的错误
app.use((err, req, res, next) => {
// 这次错误是由 token 解析失败导致的
if (err.name === 'UnauthorizedError') {
return res.send({
status: 401,
message: '无效的token',
})
}
res.send({
status: 500,
message: '未知的错误',
})
})
登录业务项目实现
开发文档
http://escook.cn:8088/#/
个别npm包注意版本问题
@hapi/joi
替换为joi
npm uninstall @hapi/joi
npm install joi@17.4.0
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,
},
}
// 导入 express 模块
const express = require('express')
// 创建 express 的服务器实例
const app = express()
const joi = require('joi')
// 导入 cors 中间件
const cors = require('cors')
// 将 cors 注册为全局中间件
app.use(cors())
// 配置解析表单数据中间件,注意:这个中间件,只能处理application/x-www-form-urlencoded格式的表单
app.use(express.urlencoded({ extended: false }))
// 一定要在路由之前,封装 res.cc 函数
// 响应数据的中间件
app.use(function (req, res, next) {
// status 默认值为 1,表示失败的情况
// err 的值,可能是一个错误对象,也可能是一个错误的描述字符串
res.cc = function (err, status = 1) {
res.send({
// 状态
status,
// 状态描述,判断 err 是 错误对象 还是 字符串
message: err instanceof Error ? err.message : err,
})
}
next()
})
// 导入并使用用户路由模块
const userRouter = require('./router/user')
app.use('/api',userRouter) //使头路径带“api”
// 错误中间件
app.use(function (err, req, res, next) {
// 数据验证失败
if (err instanceof joi.ValidationError) return res.cc(err)
// 未知错误
res.cc(err)
})
// 调用 app.listen 方法,指定端口号并启动web服务器
app.listen(3007, function () {
console.log('api server running at http://127.0.0.1:3007')
})
案例自我总结
更改密码
他这个业务顺序挺有意思,先登录获得token,然后根据token查id,然后根据id,输入oldPwd用密码解码的方式解码oldPwd密码是否等于这个id的密码,然后再进行newPwd的密码更改
他这个加密方式采用的是bcryptjs,所以每次验证都要解密
postman测试
要先使用login接口登录,然后用updatePwd接口,header带上Authorization,body带上id,oldPwd,newPwd
更新头像
postman测试时xxx文件avatar参数中value值格式案例:
data:image/png;base64,VE9PTUFOWVNFQ1JFVFM=
发布文章form-data
form-data数据添加图片文件
前后端互通运行
后端
前端
建议使用vs打开“大事件”前端项目,使用live server直接运行index页面即可