Node.js
配套课程:黑马程序员Node.js全套入门教程,nodejs最新教程含es6模块化+npm+express+webpack+promise等_Nodejs实战案例详解_哔哩哔哩_bilibili
注意,Node.js 的前置知识很多,比如 JavaScript,ES6,Ajax.js 等内容。建议去自主搜索前端学习路线,网上都有。该文档只针对上面给到的 B 站视频教程,不会额外补充前置知识。前端的知识是非常杂乱繁多的,因为每年都在更新迭代,任何一个作者写出一个 好的.js 模块就可以生成一个新的知识被广大用户使用在日常开发中。学无止境,请读者谨记。
该 md 文档里面的所有图片都已经上传到了图床中,请读者放心阅读。图片会在文件打开来之后一段时间内加载成功。如果发现图片无法查看或者是材料编写有错误等情况可以直接加我的 QQ:2247512851。
该视频是 2020 年的视频,对于现在来说里面所使用的东西可能会存在落后的情况。但无论如何,整体并不会发生大的变化。
node.js 是一个可以帮助 js 编写后端的语言 (称不上语言),其功能还是比较强大的,你可以使用它来做很多的东西。比如我 Web 的大作业后端就是使用 node.js 来编写的,总结下来虽然写得有些困难,但还是挺不错的。
任何一门语言看过视频、读过文档、写过小段代码都不足以证明你学会了它,你必须使用它去完整地写一个项目,这样才能证明你初步学会了它。我在写 Web 大作业之前将配套视频看完、文档也编写的差不多了,但到了项目里还是觉得无从下手。我不得不去网上查找资料然后再编写大作业。所以,请读者在看完视频和文档之后一定一定要去使用 node.js 以及其它的语言写一个完整的项目来检验自己。
最后,望各位变得更强。
由月下八哥编写: 平平无奇的苍蓝星,天命人,偶尔写写代码和小说。
Ps. 一开始单词拼错了,将 test 拼成了 text,写到一半发现拼错就懒得改了。
本文档归东南大学成贤学院计算机协会所有,严禁转载,但请标明出处。所有解释权归东南大学成贤学院计算机协会。
一、初识 Node.js
1、什么是 Node.js
Node.js 是一个基于 Chrome V8 引擎运行的 JavaScript 运行环境。
Node.js 官网链接:Node.js (nodejs.org)
2、Node.js 中的 JavaScript 运行环境
注意:
- 浏览器是 JavaScript 的前端运行环境
- Node.js 是 JavaScript 的后端运行环境
- Node.js 中无法调用 DOM 和 BOM 等浏览器内置 API
3、Node.js 可以做什么
Node.js 作为一个 JavaScript 的运行环境,仅仅提供了基础的功能和 API。然而,基于 Node,js 提供的这些基础功能,很多强大的工具和框架如雨后春笋,层出不穷。所以学会了 Node.js,可以让前端程序员胜任更多的工作和岗位:
- 基于 Express 框架 Express - Node.js web application framework ,可以快速构建 Web 应用
- 基于 Electron 框架 Electron,可以构建跨平台桌面应用
- 基于 Restify 框架 Restify,可以快速构建 API 借口项目
4、Node.js 怎么学
JavaScript 基础语法 + Node.js 内置 API 模块(fs、path、http等)+ 第三方 API 模块(express、mysql等)
二、fs 文件系统模块
1、什么是 fs 文件系统模块
fs 模块是 Node.js 官方提供的、用来操作文件的模块。它提供了一系列的方法和属性,用来满足用户对文件的操作需求。
例如:
fs.readFile()
方法,用来读取指定文件中的内容fs.writeFile()
方法,用来向制定文件中写入内容
如果要在 JavaScript 中使用 fs 模块来操作文件,则需要使用如下的方式先导入它:
const fs = require('fs')
其实 require()
方法是用来加载模块的一个方法,在加载模块的同时会先执行一遍模块里的代码,关于模块会在之后讲解。
2、读取指定文件中的内容
2.1 fs.readFile() 的语法格式
使用 fs.readFile()
方法,可以读取制定文件中的内容,其语法格式如下:
fs.readFile(path[,options],callback)
- 参数1(path[路径]):必选参数,表示文件的路径
- 参数2(options): 可选参数,表示以什么编码格式来读取文件
- 参数3(callback[回调函数]):必选参数,文件读取完成后,通过回调函数拿到读取的结果
2.2 fs.readFile() 示例代码
成功读取:
//导入 fs 模块
const fs = require('fs')
//调用 fs.readFile() 方法来读取文件
//如果读取成功,则 err 的值为 null
//如果读取的失败, 则 err 的值为错误对象,dataStr 的值为 undefined
//成功
//1.txt里面写了111
fs.readFile('./files/1.txt','utf8',function(err,dataStr){
console.log(err)
console.log('--------')
console.log(dataStr)
})
运行结果:
失败读取:
//导入 fs 模块
const fs = require('fs')
//调用 fs.readFile() 方法来读取文件
//如果读取成功,则 err 的值为 null
//如果读取的失败, 则 err 的值为错误对象,dataStr 的值为undefined
//失败
//没有11.txt文件
fs.readFile('./files/11.txt','utf8',function(err,dataStr){
console.log(err)
console.log('--------')
console.log(dataStr)
})
运行结果:
2.3 判断文件是否读取成功
可以通过 err 对象是否为 null,从而知晓文件读取的结果:
const fs = require('fs')
fs.readFile('./files/1.txt','utf8',function(err,dataStr){
if(err){
return console.log('文件读取失败!'+err.message)
}
console.log('文件读取成功!内容是:'+dataStr)
})
运行结果:
3、向指定的文件中写入内容
3.1 fs.writeFile() 的语法格式
使用 fs.writeFile()
方法,可以向指定文件中写入内容,其语法格式如下:
fs.writeFile(file,data[,options],callback)
- 参数1(path[路径]):必选参数,表示文件的存放路径
- 参数2(data[内容数据]):必选参数,表示要写入的内容
- 参数3(options):可选参数,表示用什么格式将内容写入文件
- 参数4(callback[回调函数]):必选参数,文件写入完成后的回调函数
3.2 fs.writeFile() 示例代码
成功写入:
//导入 fs 模块
const fs = require('fs')
//调用 fs.writeFile() 方法来将内容写入文件
//如果写入成功,则 err 的值为 null
//如果写入失败,则 err 的值等于一个错误对象
//成功
fs.writeFile('./files/2.txt','abcd',function(err){
console.log(err)
})
运行结果:
注意:如果要写入的文件不存在,将会自动创建一个文件
失败写入:
//导入 fs 模块
const fs = require('fs')
//调用 fs.writeFile() 方法来将内容写入文件
//如果写入成功,则 err 的值为 null
//如果写入失败,则 err 的值等于一个错误对象
//失败
//我的电脑没有G盘
fs.writeFile('G:/files/2.txt','abcd',function(err){
console.log(err)
})
运行结果:
4、练习——考试成绩整理
使用 fs 模块将 成绩.txt 里的内容整理到 成绩-ok.txt 文件中
代码:
const fs = require('fs')
fs.readFile('./files/成绩.txt','utf8',function(err,dataStr){
if(err){
return console.log('文件读取失败!'+err.message)
}
console.log('文件读取成功!内容是:'+dataStr)
console.log('--------------------')
console.log('第一步将字符串在空格处进行切割')
//solit可以将一个字符串在指定元素的地方进行分割成字符串数组
const arrOld = dataStr.split(' ')
console.log(arrOld)
console.log('--------------------')
console.log('第二部将得到的arrOld加入到arrNew中')
const arrNew = []
arrOld.forEach(item => {
arrNew.push(item.replace('=',':'))
})
console.log(arrNew)
console.log('--------------------')
console.log('第三步将arrNew加入到新的字符串newStr中')
const newStr = arrNew.join('\r\n')
console.log(newStr)
console.log('--------------------')
console.log('第四步将newStr字符串写入成绩-ok.txt中')
fs.writeFile('./files/成绩-ok.txt',newStr,function(err){
if(err){
return console.log('文件读取失败!'+err.message)
}
console.log('成绩整理写入成功!')
})
})
运行结果:
5、fs 模块,路径动态拼接的问题
在使用 fs 模块操作文件时,如果提供的操作路径是以 ./
或者 ../
开头的相对路径时,很容易出现路径动态拼接错误的问题。
原因:代码在运行的时候,会以执行 node 命令时所处的目录,动态拼接出被操作文件的完整路径。
观察这两个方框框出来的地方,可以发现语句中的路径是刚好可以和终端中的位置对接完成的,如果终端中的操作路径和代码语句中的路径没办法正确对接,那么就会显示如下错误:
解决办法1:使用绝对路径
__dirname
可以显示当前文件所处的目录
console.log("文件的路径为:"+__dirname)
运行结果:
所以就可以引申出来一个解决办法2:使用 __dirname
来获得当前文件所处的目录,然后再使用相对路径
const fs = require('fs')
fs.readFile(__dirname+'/files/1.txt','utf8',function(err,dataStr){
if(err){
return console.log('文件读取失败!'+err.message)
}
console.log('文件读取成功!内容是:'+dataStr)
})
运行结果:
注意:在此处相对路径的前面不要再加 .
了。
三、path 路径模块
1、什么是 path 路径模块
path 模块是 Node.js 官方提供的、用来处理路径的模块,它提供了一系列的方法和属性,用来满足用户对路径的处理需求。
例如:
path.join()
方法,用来将多个路径片段拼接成一个完整的路径字符串path.basename()
方法,用来从路径字符串中,将文件名解析出来
如果要在 JavaScript 中使用 path 模块来处理路径,则需要使用如下的方式先导入它:
const path = require('path')
2、路径拼接
2.1 path.join() 的语法格式
使用 path.join()
方法,可以把多个路径片段拼接成一个完整的路径字符串,语法格式如下:
path.join([...paths])
- 参数1(..paths < string > )路径片段的序列
- 参数2(返回值 < string > )
2.2 path.join() 示例代码
const path = require('path')
//注意 ../ 会抵消前面的路径
const pathStr = path.join('/a','/b/c','../','./d','e')
console.log(pathStr)
运行结果:
\c
被 ../
给抵消掉了
2.3 更好地解决拼接问题
所以上面的路径问题还可以这么写:
const fs = require('fs')
const path = require('path')
fs.readFile(path.join(__dirname,'/files/1.txt'),'utf8',function(err,dataStr){
if(err){
return console.log('文件读取失败!'+err.message)
}
console.log('文件读取成功!内容是:'+dataStr)
})
运行结果:
此时与直接拼接不同的是, '.' 是可以写上去的。
3、获取路径中的文件名
3.1 path.basename() 的语法格式
使用 path.basename()
方法,可以获取路径中的最后一部分,经常通过这个方法获取路径中的文件名,语法格式如下:
path.basename(path[,ext])
- 参数1(path < string > )必选参数,表示一个路径的字符串
- 参数2(ext < string > )可选参数,表示文件的扩展名
- 参数3(返回 < string > )表示路径中的最后一个部分
3.2 path.basename() 示例代码
const path = require('path')
//文件存放路径
const fpath = '/a/b/c/index.html'
//输出index.html
let fullName = path.basename(fpath)
console.log(fullName)
console.log('---------')
//只输出index,而不输出后缀名
let nameWithoutExt = path.basename(fpath,'.html')
console.log(nameWithoutExt)
运行结果:
4、获取路径中的扩展名
4.1 path.extname() 的语法格式
path.extname(path)
- 参数1(path < string > )必选参数,表示一个路径的字符串
- 参数2(返回 < string > )返回得到的扩展名字符串
4.3 path.extname() 示例代码
const path = require('path')
//文件存放路径
const fpath = '/a/b/c/index.html'
const fext = path.extname(fpath)
console.log(fext)
运行结果:
5、练习——时钟案例
要求
将目录下的 index.html 页面,拆分成三个文件,分别是:
- index.css
- index.js
- index.html
并且将拆分出来的3个文件,存放到 clock 目录中。
所需的 index.html 代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>index首页</title>
<style>
html,
body {
margin: 0;
padding: 0;
height: 100%;
background-image: linear-gradient(to bottom right, red, gold);
}
.box {
width: 400px;
height: 250px;
background-color: rgba(255, 255, 255, 0.6);
border-radius: 6px;
position: absolute;
left: 50%;
top: 40%;
transform: translate(-50%, -50%);
box-shadow: 1px 1px 10px #fff;
text-shadow: 0px 1px 30px white;
display: flex;
justify-content: space-around;
align-items: center;
font-size: 70px;
user-select: none;
padding: 0 20px;
/* 盒子投影 */
-webkit-box-reflect: below 0px -webkit-gradient(linear, left top, left bottom, from(transparent), color-stop(0%, transparent), to(rgba(250, 250, 250, .2)));
}
</style>
</head>
<body>
<div class="box">
<div id="HH">00</div>
<div>:</div>
<div id="mm">00</div>
<div>:</div>
<div id="ss">00</div>
</div>
<script>
window.onload = function () {
// 定时器,每隔 1 秒执行 1 次
setInterval(() => {
var dt = new Date()
var HH = dt.getHours()
var mm = dt.getMinutes()
var ss = dt.getSeconds()
// 为页面上的元素赋值
document.querySelector('#HH').innerHTML = padZero(HH)
document.querySelector('#mm').innerHTML = padZero(mm)
document.querySelector('#ss').innerHTML = padZero(ss)
}, 1000)
}
// 补零函数
function padZero(n) {
return n > 9 ? n : '0' + n
}
</script>
</body>
</html>
案例代码:
以下案例使用到了部分正则表达式的知识,可以先去以下网址学习一下正则表达式
//导入两个模块
const fs = require('fs')
const path = require('path')
//匹配 style 标签的正则表达式
//其中 \s 表示空白字符,\S 表示非空白字符,* 表示匹配任意次
//将包括 <style> 和 </style>内的所有内容放到 regStyle中
const regStyle = /<style>[\s\S]*<\/style>/
//匹配 script 标签的正则
//将包括 <script> 和 </script>内的所有内容放到 regScript中
const regScript = /<script>[\s\S]*<\/script>/
//读取需要被处理的 html 文件
fs.readFile(path.join(__dirname,'./files/index.html'),'utf8',(err,dataStr)=>{
//读取失败
if(err){
return console.log('读取 html 文件失败!'+err.message)
}
//读取成功,调用相应的方法来解析 style,script,html
resolveCSS(dataStr)
resolveJS(dataStr)
resolveHTML(dataStr)
})
//自定义 CSS 样式
function resolveCSS(htmlStr){
//使用正则提取页面中的 <style></style> 标签
const r1 = regStyle.exec(htmlStr)
//将提取出来的样式字符串作进一步处理,将 <style> 和 </style> 用空格符替换掉
const newCSS = r1[0].replace('<style>','').replace('</style>','')
//将提取出来的 CSS 样式,写入到 index.css 中
fs.writeFile(path.join(__dirname,'./clock/index.css'),newCSS,err=>{
if(err){
return console.log('写入 CSS 样式失败!'+err.message)
}
console.log('写入 CSS 成功!')
})
}
//自定义 JS 脚本
function resolveJS(htmlStr){
//使用正则提取页面中的 <script></script> 标签
const r2 = regScript.exec(htmlStr)
//将提取出来的脚本字符串作进一步处理,将 <script> 和 </script> 用空格符替换掉
const newJS = r2[0].replace('<script>','').replace('</script>','')
//将提取出来的 JS 脚本,写入到 index.js 中
fs.writeFile(path.join(__dirname,'./clock/index.js'),newJS,err=>{
if(err){
return console.log('写入 JS 脚本失败!'+err.message)
}
console.log('写入 JS 成功!')
})
}
//处理 html 文件
function resolveHTML(htmlStr){
//使用字符串中的 replace 方法,把内嵌的 <style> 和 <script> 标签,替换为外联的 <link> 和 <script> 标签
const newHTML = htmlStr
.replace(regStyle,'<link rel="stylesheet" href="./index.css" />')
.replace(regScript,'<script src="./index.js"></script>')
//将替换完成后的的 html 代码,写入到 index.html 文件中
fs.writeFile(path.join(__dirname,'./clock/index.html'),newHTML,err=>{
if(err){
return console.log('写入 HTML 文件失败!',err.message)
}
console.log('写入 HTML 文件成功!')
})
}
运行结果:
并且在 clock 文件内已经有了 index.html、index.css 和 index.js 三个文件了
注意:
fs.writeFile()
方法只能用来创建文件,不能用来创建路径。也就是说,可以创建一个 .txt 文件,但不能创建一个用以存放这个文件的文件夹重复调用
fs.writeFile()
方法写入的新内容会覆盖就内容,而不是在后面添加
四、http 模块
1、什么是 http 模块
http 模块是 Node.js 官方提供的、用来创建 Web 服务器的模块。通过 http 模块提供的 http.createServer()
方法,写几行简单的代码,就能轻松地手写一个服务器软件,方便地把一台普通的电脑,变成一台 Web 服务器,从而对外提供 Web 资源服务。
如果希望使用 http 模块创建 Web 服务器,则需要先导入它:
const http = require('http')
2、和服务器有关的概念
(1)IP 地址
IP 地址就是互联网上每台计算机的唯一地址,因此 IP 地址具有唯一性。只有在知道对方 IP 地址的前提下,才能与对应的电脑之间进行数据通信。
- 互联网上,每台计算机都有自己的 IP 地址
- 在开发期间,自己的电脑既是一台服务器,也是一个客户端。为了方便测试,可以在自己的浏览器中输入 127.0.0.1 这个 IP 地址,就能把自己的电脑当做一台服务器进行访问
(2)域名和域名服务器
由于 IP 地址不方便记忆,所以人们又发明了一套字符型的地址方案,就是所谓的域名地址。
IP 地址和域名是一一对应的关系,这份对应关系存放在一种叫做域名服务器的电脑中。使用者只需通过好记的域名访问对应的服务器即可,对应的转换工作由域名服务器实现。因此,域名服务器就是提供 IP 地址和域名之间的转换服务的服务器。
- 单纯使用 IP 地址,也能进行访问
- 在开发测试期间,127.0.0.1 对应的域名是 localhost,它们都代表我们自己的这台电脑,在使用效果上没有任何区别。
(3)端口号
计算机中的端口号,就像是现实生活中的门牌号一样,通过门牌号可以在整栋大楼若干个房间中找到唯一的那个房间。
同样的道理,在一台电脑中可以运行成百上千个 Web 服务,每个 Web 服务都对应一个唯一的端口号。客户端发送过来的网络请求,通过端口号,可以准确地交给对应的 Web 服务进行处理。
- 每个端口号不能同事被多个 Web 服务占用
- 在实际应用中,URL 中的 80 端口可以被省略
3、创建最基本的 Web 服务器
3.1 创建 Web 服务器的基本步骤
(1)导入 http 模块
const http = require('http')
(2)创建 Web 服务器实例
const server = http.createServer()
(3)为服务器实例绑定 request 事件,监听客户端的请求
//使用服务器实例 .on() 方法,为服务器绑定一个 request 事件
server.on('request',(req,res)=>{
//只要有客户端来请求我们自己的服务器,就会触发 request 事件,从而调用这个事件处理函数
console.log('Someone visit our web server.')
})
(4)启动服务器
//调用 server.listen(端口号,cb回调)方法,即可启动 Web 服务器
server.listen(80,()=>{
console.log('http server running at http://127.0.01')
})
总代码:
const http = require('http')
const server = http.createServer()
//使用服务器实例 .on() 方法,为服务器绑定一个 request 事件
server.on('request',(req,res)=>{
//只要有客户端来请求我们自己的服务器,就会触发 request 事件,从而调用这个事件处理函数
console.log('Someone visit our web server.')
})
//调用 server.listen(端口号,cb回调)方法,即可启动 Web 服务器
server.listen(80,()=>{
console.log('http server running at http://127.0.0.1:8080')
})
运行结果:
按住 ctrl 然后单击 'http://127.0.01' 就可以进入页面
3.2 req 请求对象
只要服务器接收到了客户端的请求,就会调用通过 server.on()
为服务器绑定的 request 事件处理函数。如果想在事件处理函数中,访问与客户端相关的数据或属性,可以使用如下的方式:
const http = require('http')
const server = http.createServer()
server.on('request',()=>{
//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')
})
运行截图:
但当打开页面的时候,会发现我们并没有拿到东西,或者说客户端没有响应一些内容,这我们就要用到下面的方法。
3.3 res 响应对象
在服务器的 request 事件处理函数中,如果想访问与服务器相关的数据或属性,可以使用如下的方式:
const http = require('http')
const server = http.createServer()
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')
})
此时的运行结果:
当打开页面之后终端会显示
并且页面内会显示
我们拿到了东西了。
3.4 解决中文乱码问题
输入以下代码:
const http = require('http')
const server = http.createServer()
server.on('request',(req,res)=>{
//定义一个字符串,包含中文内容
const str = `您请求的 URL 地址是 ${req.url}, 请求的 method 类型为 ${req.method}`
//调用 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}`
//为了解决中文乱码问题,需要设置响应头 Content-Type 的值为 text/html;charset=utf-8
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')
})
此时的运行结果:
3.5 根据不同的 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>'
}
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')
})
此时,当我们打开页面的时候,会显示
当我们接着网址后输入 '/about.html' 时,会显示
我们可以发现,这时我们已经可以从服务器内切换不同的页面。
4、练习——实现 clock 时钟的 Web 服务器
4.1 基本完成需求
代码:
const fs = require('fs')
const path = require('path')
const http = require('http')
//创建服务器
const server = http.createServer()
server.on('request',(req,res)=>{
//获取请求的 url 地址
const url = req.url
//把请求的 URL 地址映射为具体具体文件的存放地址
const fpath = path.join(__dirname,url)
//根据映射过来的文件路径来读取文件的内容
fs.readFile(fpath,'utf8',(err,dataStr)=>{
//读取失败,向客户响应固定的错误消息
if(err){
return res.end('<h1>404 Not Found</h1>')
}
//如果读取成功,将读取成功的内容,响应给客户端
else{
res.setHeader('Content-Type','text/html;charset=utf-8')
res.end(dataStr)
}
})
})
server.listen(80,()=>{
console.log('server running at http://127.0.0.1')
})
当我们运行时,会显示
而当我们在地址后面输入 /clock/index.html
后,会显示我们的主界面
4.2 优化资源的请求路径
我们不难发现,当且仅当我们在地址栏输入 /clock/index.html
时我们才能访问时钟首页,这种输入还是太过于复杂。其根本原因是请求的 URL 地址是服务器的地址,其中并没有包含 /clock
。所以,我们需要重新编写有关路径的代码。
用
//重新编写
//预定义空白的文件来存放路径
let fpath = ''
//判断所请求的路径是否为 '/',如果是的话,则直接进行拼接 index.html
if(url === '/'){
fpath = path.join(__dirname,'./clock/index.html')
}
//如果不为 '/',那么动态拼接文件的存放路径,将中间的 /clock 去掉
else{
fpath = path.join(__dirname,'./clock',url)
}
代替原代码中的
//把请求的 URL 地址映射为具体具体文件的存放地址
const fpath = path.join(__dirname,url)
即可。(但是貌似会有样式和脚本文件丢失的情况,我也有这个问题,目前不知道该怎么解决)
五、模块化
1、什么是模块化
编程领域中的模块化,就是遵守固定的规则,把一个大文件拆成若干个独立并且相互依赖的小模块。
使用模块化的好处有:
- 提高代码的复用性
- 提高代码的可维护性
- 可以实现按需加载
2、模块的分类
Node.js 中根据模块来源的不同,将模块分为了三大类,分别是:
- 内置模块(由官方提供,比如 fs、http、path 等)
- 自定义模块(用户创建的每个 .js 文件都是一个自定义模块)
- 第三方模块(由第三方开发出来的模块,并非官方提供的内置模块,也不是用户自定义的模块,使用前需下载)
3、模块作用域
3.1 什么是模块作用域
和函数作用域类似,在自定义模块中定义的变量、方法等成员,只能在当前模块内被访问,这种模块级别的访问限制,叫做模块作用域。
3.2 模块作用域的好处
模块作用域的好处是防止全局变量污染的问题
3.3 向外共享模块作用域的成员
3.3.1 module 对象
在每个 .js 自定义模块中都有一个 module
对象,它里面存储了和当前模块有关的信息
console.log(module)
打印如下:
3.3.2 module.exports 对象
在自定义模块中,可以使用 module.exports
对象,讲模块内的成员共享出去,供外界使用。
外界用 require()
方法导入自定义模块时,得到的就是 module.exports
所指向的对象。
const m = require('./自定义模块.js')
console.log(m)
这里 '自定义模块.js' 里什么也没写,那么当我们 node text.js 时会显示
我们可以观察到这里显示了一个 '{}' 这是因为 module.exports
里默认指向的是一个空对象。那我们就可以在 '自定义模块.js' 文件中使用 module.export
方法来设置对象
注意:
使用 require()
方法导入模块的时候,导入的结果,永远以 module.exports
指向的对象为准,即新的会覆盖旧的,如下图
3.3.3 exports 对象
由于 module.exports
单词写起来比较复杂,为了简化向外共享成员的代码,Node 提供了exports
对象。默认情况下,exports
和 module.exports
指向同一个对象。最终共享的结果,还是以 module.exports
指向的对象为准。
console.log(exports)
console.log(module.exports)
console.log(exports === module.exports)
运行结果:
到此我们可以证明所谓的 module.exports
和 exports
指向的是同一个对象。
3.3.4 exports 和 module.exports 的使用注意点
时刻谨记, require()
模块时,得到的永远是 module.exports
指向的对象。
我们上面已经讲过 module.exports
指向的对象和 exports
指向的对象是同一个对象,但其实严格上来说是不准确的。我们来看看以下代码:
exports.username = 'zs'
module.exports = {
sex : '男',
age : 22
}
这段代码首先会创建一个拥有 " username = 'zs' " 的属性,exports
和 module.exports
都指向了这个属性。接着,又创建一个拥有 sex 和 age 的对象,此时 module.exports
指向了这个对象。而当我们调用 require()
方法的时候,其只会得到 module.exports
指向的对象。(建议去b站上看配套视频, P24,视频讲解的更清楚)
运行代码,其结果如下:
但当我们将 exports 和 module.exports 都指向属性的时候,则都可以调用,如下:
接着我们再来看看以下代码:
exports = {
username : 'zs',
sex : '男'
}
modules.exports = exports
modules.exports.age = '20'
这里就简单的讲解一下这段代码,不再运行。
首先,exports 指向了一个包含 username 和 sex 的对象,接着 module.exports 又指向了这个对象,然后 module.exports 又指向了一个属性,此时我们运行则会打印所有的东西。
为了防止混乱,在一个模块中不要同时使用 exports
和 module.exports
。
4、Node.js 中的模块化规范
Node.js 遵循了 CommonJS 模块化规范,CommonJS 规定了模块的特性和各模块之间如何相互依赖。
CommonJS 规定:
- 每个模块内部,module 变量代表当前模块
- module 变量是一个对象,它的 exports 属性(即 module.exports)是对外的接口
- 加载某个模块,其实是加载该模块的 module.exports 属性,
require()
方法用于加载模块
5、模块的加载机制
5.1 优先从缓存中加载
模块在第一次加载后会被缓存。这也意味着多次调用 require()
不会导致模块的代码被执行多次。
注意:不论是内置模块、用户自定义模块、还是第三方模块,他们都会优先从缓存中加载,从而提高模块的加载效率。
例如
尽管我们调用了四次这个模块,但是它只输出了一个 ok。
5.2 内置模块的加载机制
内置模块是由 Node.js 官方提供的模块,内置模块的加载优先级最高。比如说你自己再定义一个叫做 'fs' 的自定义个模块,但
require ('fs')
始终返回内置的 fs 模块
5.3 自定义模块的加载机制
使用 require()
加载自定义模块时,必须制定以 ./
或者 ../
开头的路径标识符。在加载自定义模块时,如果没有制定 ./
或者 ../
这样的路径标识符,则 node 会把它当做内置模块或第三方模块进行加载。这与上一节可以形成对应,当我们指定加载的模块的路径时,它就会加载我们指定的路径的模块。
同时,在使用 require()
导入自定义模块时,如果省略了文件的扩展名,则 Node.js 会按顺序分别尝试加载以下的文件:
- 按照确切的文件名进行加载
- 补全 .js 扩展名进行加载
- 补全 .json 扩展名进行加载
- 补全 .node 扩展名进行加载
- 加载失败,终端报错
这也是为什么我们在调用的时候不写 .js 也能调用成功。
5.4 第三方模块的加载机制
如果传递给 require()
的模块标识符不是一个内置模块,也没有以 ./ 或者 ../ 开头,则 Node.js 会从当前模块的父级目录开始,尝试从 /node_modules 文件夹中加载第三方模块。
如果没有找到对应的第三方模块,则移动到再上一层父目录中,进行加载,知道文件系统的根目录。例如,假设在 'C:\Users\YUEXIABUG\project\foo.js' 文件里调用了 require('tools')
,则 Node.js 会按一下顺序进行查找:
- C:\Users\YUEXIABUG\project\node_modules\tools
- C:\Users\YUEXIABUG\node_modules\tools
- C:\Users\node_modules\tools
- C:\node_modules\tools
- 加载失败,终端报错
5.5 目录作为模块
当把目标作为模块标识符,传递给 require()
进行加载的时候,有三种加载方式:
- 在被加载的目录下查找一个叫做 package.json 的文件,并寻找 main 属性,作为
require()
加载的入口 - 如果目录中没有 package.json 文件, 或者 main 入口不存在或者无法解析,则Node.js 将会试图加载目录下的 index.js 文件
- 如果以上两步都失败了,则 Node.js 会在终端打印错误消息,报告模块的缺失:Error:Cannot find module 'xxx'
六、npm 与包
1、什么是包
Node.js 中的第三方模块又叫做包,不同于 Node.js 中的内置模块与自定义模块,包是由第三方个人或者团队开发出来的,免费供所有人使用。因为 Node.js 的内置模块仅提供了一些底层的 API,导致了在基于内置模块进行项目开发的时候,效率很低。包是基于内置模块封装出来的,提供了更高级、更方便的 API,极大地提高了开发效率。包和内置模块之间的关系,类似于 jQuery 和浏览器内置 API 之间的关系。
npm,Inc. 是有一个著名的网站:npm (npmjs.com),它是全球最大的包共享平台,可以在这个网站上搜索到任何你需要的包。而使用地址为 https://registry.npmjs.org/ 的服务器,就可以下载自己所需要的包。
npm,Inc. 公司提供了一个包下载工具——npm,这个工具随着我们下载安装 Node.js 时一起安装到了用户的电脑上。
我们在终端内输入 npm -v
就可以来查看自己电脑上 npm 包管理工具的版本号了。
通过终端输入 npm i 或者 npm install 都可以在项目中安装包。
2、npm初体验——格式化时间
安装包可在终端输入 npm install 包的名字
或是 npm i 包的名字
来进行安装。
首先在项目文件夹中安装包
这样就显示安装完成了。
之后在 text.js 文件中输入
//导入需要的包
const moment = require('moment')
const dt = moment().format('YYYY-MM-DD HH:mm:ss')
console.log(dt)
运行结果:
这相比于之前硬敲会显得很方便。
在我们初次安装包之后,在项目文件夹下面会多出一个叫做 node_modules 的文件夹和 package-lock,json 的配置文件。
其中,node_modules 文件夹用来存放所有已经安装到项目中的包。require()
导入第三方包时,就是从这个项目中查找并加载包。
pack-lock.json 配置文件用来记录 node_modules 目录下的每一个包的下载信息,例如包的名字、版本号、下载地址等。
**注意:**尽量不要去手动修改这两个东西中的任何代码,npm 包管理工具会帮助我们自动维护它们。
3、包管理配置文件
npm 规定,在项目根目录中,必须提供一个叫做 package.json 的包管理配置文件。用来记录与项目有关的一些配置信息。比如说包的名称、版本号等·
3.1 多人协作的问题
由于在实际开发中我们肯定要引用模块,这时候我们就会发现对于整个项目的体积,第三方包的体积占了将近百分之 95%,而源代码的体积只占了不到 5%。而这么大的体积就会使得团队成员之间共享项目源代码不是那么方便。解决办法就是剔除 node_modules 这个文件夹。那么问题又来了,当你把 node_modules 这个文件夹剔除之后,你的团队成员要怎么知道你安装了哪些包呢?
3.2 如何记录项目中安装了哪些包
在项目根目录中,创建一个叫做 package.json 的配置文件,即可用来记录项目中安装了哪些包。从而方便剔除 node_modules 目录之后,在团队成员之间共享源代码。
注意:今后在项目中,一定要把 node_modules 文件夹添加到 .gitignore 忽略文件中。
3.3 如何快速的创建 package.json
npm 包管理工具提供了一个快捷命令,可以在执行命令时所处的目录中,快速创建 package.json 这个包管理配置文件:
npm init -y
该命令在项目创建之初就要执行,且整个项目开发阶段只执行一次。
当然,大部分项目开发的时候都是使用框架,在创建项目的时候会自动创建好 package.json 文件,无需手动创建。
注意:
- 上述命令只能在英文的目录下成功运行!所以,项目文件夹的名称一定要使用英文命名,不要使用中文,不要出现空格
- 运行
npm install 包的名字
命令安装包的时候,npm 包管理工具会自动把包的名称和版本号记录到 package.json 文件中
3.4 dependencies 节点
在创建 package.json 的时候是没有 dependencies 节点的,当我们第一次使用 npm install
来安装包的时候,会自动在 package.json 中创建这个节点。
dependencies 节点专门用来记录使用 npm install 命令安装了哪些包。
3.5 一次性安装的所有的包
可以在终端内输入 npm install
来一次性安装 package.json 中记录过的所有依赖包。
3.6 卸载包
在终端内输入 npm uninstall 包的名字
来卸载指定名字的包,并将其从 package.json 中移除掉。
3.7 devDependencies 节点
如果某些只在项目开发阶段会用到,在项目上线之后不会用到,则建议把这些包记录到 devDependencies 节点中。与之对应的,如果某些包在开发和项目上线之后都需要用到,则建议把这些包记录到 dependencies 节点中。
当我们在终端中输入 npm install 包的名字 --save-dev
或者 npm i 包的名字 -D
来将包安装到devDependencies 节点中。而是否要将包记录到 devDependencies 节点中,可以自己去 npm 官网中查询。
4、npm 在国内的镜像服务器(可提供更快的下载包的速度)
因为 npm 的服务器在国外,众所周知的原因我们在使用普通 npm 下载包的时候经常会发生下载缓慢甚至是下载失败的情况。而淘宝在国内搭建了一个服务器,专门吧国外官方服务器上的包同步到国内的服务器中,然后在国内提供下载包的服务,从而极大地提高了下载包的速度和稳定性。
在终端内输入
npm config set registry=https://registry.npmmirror.com/
来将下载包的镜像源切换为淘宝镜像源,并在终端内执行
npm config get registry
来查看镜像源是否转换成功。
如果显示:
则转换成功了。
5、nrm
nrm 是一个可以让用户自主切换下载包的镜像源的工具,在终端内输入 npm i nrm -g
来对 nrm 进行全局安装。
显示如下界面则表示安装完成
接着我们再输入 nrm ls
来查看可以使用的镜像源
接着我们就可以在这里面转换镜像源了,镜像源转换的语句为 nrm use 你想要转换的镜像源名字
如下:
6、包的分类
6.1 项目包
那些被安装到项目的 node_modules 目录中的包,仅限安装了该包的项目使用这个包。
6.2 全局包
全局包是可以在所有项目中使用的包。这种包通常只需要下载一次就可以在所有的项目中使用。其被安装在了C:\Users\YUEXIABUG\AppData\Roaming\npm\node_modules 目录下。
注意:
- 只有工具性质的包,才有全局安装的必要性。因为它们提供了好用的终端命令。
- 判断某个包是否要全局安装后才能使用,可以参考官方提供的使用说明。
7、包的结构规范
- 包必须以单独的目录而存在
- 包的顶级目录下必须要包含 package.json 这个包管理配置文件
- package.json 中必须要包含 name(包的名字),version(版本号),main(包的入口)这三个属性
*8、开发自己的包
8.1 实现功能
- 格式化日期
- 转义 html 中的特殊字符
- 还原 html 中的特殊字符
8.2 初始化包的基本结构
新建 package-test-yxbg 文件夹,作为包的根目录(先去 npm 官网上搜一下有没有同名的,有同名就要改)
在 package-test-yxbg 文件夹中,新建以下三个文件:
package.json (包管理配置文件)
index.js (包的入口文件)
README.md (包的说明文档)
8.3 初始化 package.json
package.json 里的元素要有
- name:包的名字
- version:版本号
- main:包的入口
- description:包的介绍
- keywords:关键字
- license:开源许可协议
我们在 package.json 中写入下方的代码即可
{
"name":"package-test-yxbg",
"version": "1.0.0",
"main": "index.html",
"description":"提供了格式化时间,HTMLEscape的功能",
"keywords": ["ityxbg","dataFormat","secape"],
"license": "ISC"
}
8.4 编写 index.js
//这是包的入口
//格式化时间
//定义格式化时间函数
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.getSeconds())
return `${y}-${m}-${d} ${hh}:${mm}${ss}`
}
//定义一个补零的函数
function padZero(n){
return n > 9 ? n:'0'+n
}
//转义 html 字符
//定义转义 html 字符的函数
function htmlEscape(htmlstr){
return htmlstr.replace(/<|>|"|&/g,(match)=>{
switch(match){
case '<':
return '<'
case '>':
return '>'
case '"':
return '"'
case '&':
return '&'
}
})
}
//还原 html 字符
//定义还原 html 字符的函数
function htmlUnEscape(str){
return str.replace(/<|>|"|&/g,(match)=>{
switch(match){
case '<':
return '<'
case '>':
return '>'
case '"':
return '"'
case '&':
return '&'
}
})
}
//向外暴露需要的成员
module.exports={
dateFormat,
htmlEscape,
htmlUnEscape
}
然后在 test.js 中进行测试
测试代码:
//先导入包
const ityxbg = require('./package-test-yxbg/index')
//格式化时间功能
console.log("格式化时间功能---------------")
const dtStr = ityxbg.dateFormat(new Date())
console.log(dtStr)
console.log("")
//转义 html 字符
console.log("转义 html 字符---------------")
const htmlstr = '<h1 title="abc">这是一个 h1 标签<span>1234 </span></h1>'
const str = ityxbg.htmlEscape(htmlstr)
console.log(str)
console.log("")
//还原 html 字符
console.log("还原 html 字符---------------")
const newstr = ityxbg.htmlUnEscape(str)
console.log(newstr)
运行结果:
8.5 根据不同的功能拆分模块
- 将格式化时间的功能 拆分到 src-> dateFormat.js 中
- 将处理 html 字符串的功能拆分到 src-> htmlEscape.js 中
- 在 index.js 中,导入两个模块,得到需要向外共享的方法
- 在 index.js 中,使用 module.exports 把对应的方法共享出去
(1) 首先我们先将格式化时间和处理 html 字符串的代码粘贴到 dateFormat.js 和 htmlEscape.js 中,如下:
那在这个时候我们就会发现原来 index.js 中 module.exports 中的元素不存在了,这是因为我们把不同功能拆分到不同模块之后原 index.js 中是没有这些模块功能的,所以我们就要在模块中进行暴露以及在 index.js 中进行导入。
(2) 在 dateFormat.js 和 htmlEscape.js 中进行暴露,其代码如下:
//暴露
module.exports={
htmlEscape,
htmlUnEscape
}
//暴露
module.exports={
dateFormat
}
在暴露完之后我们还要在 index.js 中进行导入。
(3) 在 index.js 中进行导入
首先使用 require
来进行导入响应的模块
const date = require('./src/dateFormat')
const escape = require('./src/htmlEscape')
但问题又来了,date
里面只有一个方法,但是 escape
中有两个方法,那我们就可以回顾以前学到的一个语法 '...' 可以进行展开。既然知道了方法,那我们直接向外暴露的时候展开即可,如下
//向外暴露需要的成员
module.exports={
...date,
...escape
}
接着再在 test.js 中进行测试,其测试代码和上面的一样,我们来看看测试结果
没问题。那我们已经成功将不同的功能进行拆分了。
README.md
8.6 编写说明文档包根目录中的 README.md 文件,是包的使用说明文档。通过它,我们可以事先把包的使用说明,以 markdown 的格式写出来,方便用户参考。
README.md 文件中具体写什么内容,没有强制性的要求,只要能够清晰地把包的作用、用法、注意事项等描述清楚即可。我们所创建的这个包的 README.md 文档中,会包含以下 6 项内容:
安装方式、导入方式、格式化时间、转义 html 中的特殊字符、还原 html 中的特殊字符、开源协议
编写如下(利用 md 语法编写,其实大家完全可以使用 Typora 来进行直接编写)
### 下载安装
```
npm install package-test-yxbg
```
### 导入
```js
const ityxbg = require('package-test-yxbg')
```
### 格式化时间
```js
//调用 dateFormat 对时间进行格式化
const dtStr = ityxbg.dateFormat(new Date())
console.log(dtStr)
```
### 转义 html 字符
```js
//待转换的 html 字符串
const htmlstr = '<h1 title="abc">这是一个 h1 标签<span>1234 </span></h1>'
//调用 htmlEscape 方法进行转换
const str = ityxbg.htmlEscape(htmlstr)
//转换结果为 <h1 title="abc">这是一个 h1 标签<span>1234&nbsp;</span></h1>
console.log(str)
```
### 还原 html 字符
```js
//待还原的 html 字符串
const newstr = ityxbg.htmlUnEscape(str)
//还原结果为 <h1 title="abc">这是一个 h1 标签<span>1234 </span></h1>
console.log(newstr)
```
### 开源协议
ISC
8.7 发布包
请读者自己去看npm包发布详细教程_醉逍遥neo的博客-CSDN博客_npm 发布 或者是去配套视频看P36
七、Express
1、什么是 Express
Express 是基于 Node.js 平台,快速、开放、极简的 Web 开发框架。其作用和 Nnode.js 内置 http 模块类似,是专门用来创建 Web 服务器的。Express 的本质就是一个 npm 上的第三方包,提供了快速创建 Web 服务器的便捷方法。相比于 http 内置模块,Express 模块能极大地提高开发效率。
官方网址:Express - 基于 Node.js 平台的 web 应用开发框架 - Express 中文文档 | Express 中文网 (expressjs.com.cn)
2、安装 Express
因为视频是使用的 4.17.1 版本的包,所以我们这里也将以 4.17.1 版本的包来作示范。同时,以下所有有关 Express 的操作使用的都是 4.17.1 版本的包。
npm i express@4.17.1
通过在终端内输入以上代码来安装相应版本的包。
3、Express 的基本使用
3.1 创建最基本的 Web 服务器
//导入 Express 包
const express = require('express')
//创建 Web 服务器
const app = express()
//启动 Web 服务器
//调用 app.listen(端口号,启动成功后的回调函数),启动服务器
app.listen(80,()=>{
console.log('express server running at http://127.0.0.1')
})
运行结果:
3.2 监听 GET 请求
什么是 GET 请求?
GET 请求是最常见的一种请求方式,当客户端要从服务器中读取文档时,当点击网页上的链接或者通过在浏览器的地址栏输入网址来浏览网页的,使用的都是GET方式。GET方法要求服务器将URL定位的资源放在响应报文的数据部分,回送给客户端。
通过 app.get()
方法,可以监听到客户端的 GET 请求,具体语法格式如下:
app.get('请求 URL', function(req,res){
//处理函数
})
- 参数1:客户端请求的 URL 地址
- 参数2:请求对应的处理函数
- req:请求对象(包含了与请求相关的属性与方法)
- res:响应对象(包含了与响应相关的属性与方法)
3.3 监听 POST 请求
什么是 POST 请求?
对于上面提到的不适合使用GET方式的情况,可以考虑使用POST方式,因为使用POST方法可以允许客户端给服务器提供信息较多。POST方法将请求参数封装在HTTP请求数据中,以名称/值的形式出现,可以传输大量数据,这样POST方式对传送的数据大小没有限制,而且也不会显示在URL中。
通过 app.post()
方法,可以监听客户端的 POST 请求,具体语法格式如下:
app.post('请求 URL', function(req,res){
//处理函数
})
参数1:客户端请求的 URL 地址
参数2:请求对应的处理函数
req:请求对象(包含了与请求相关的属性与方法)
res:响应对象(包含了与响应相关的属性与方法)
3.4 把内容响应给客户端
通过 res.send() 方法,可以把处理好的内容,发送给客户端,具体语法格式如下:
res.send(/*发送内容*/)
3.5 有关 GET 和 POST 和 SEND 方法的样例
//导入 Express 包
const express = require('express')
//创建 Web 服务器
const app = express()
//监听客户端的 GET 和 POST 请求
app.get('/user',(req,res)=>{
//调用 res.send() 方法,向客户端响应一个 JSON 对象
res.send({name :'zs',age:20,sex:'男'})
})
app.post('user',(req,res)=>{
//调用 res.,send() 方法,向客户端响应一个文本字符串
res.send('请求成功!')
})
//调用 app.listen(端口号,启动成功后的回调函数),启动服务器
app.listen(80,()=>{
console.log('express server running at http://127.0.0.1')
})
按住 ctrl 并单击地址,然后在打开的浏览器上的地址后输入 /user
,会有以下结果:
但此时我们无法看到 POST 的结果。
所以在这里我们可以使用一个叫做 Postman 的软件,在这个软件上可以进行服务器的操作。
下载地址:Download Postman | Get Started for Free
下载安装注册完账号之后就可以开始使用了。
我们打开 Postman 并登录账号,然后点击 Overview 旁边的 '+' 号创建新的页面:
在新的页面中有一个文本输入框,左边有 GET 的字样,在这个文本框中输入 http://127.0.0.1/user ,并单击右边的 Send 来得到客户端的请求:
然后我们再新建一个页面,这次我们将 GET 换成 POST 并再次重复之前的操作:
3.6 获取 URL 中携带的查询参数
通过 req.query()
对象,可以访问到客户端通过查询字符串的形式,发送到服务器的参数:
app.get('/',(req,res)=>{
//req.query 默认是一个空对象
//客户端使用 ?=name=zs&age=20 这种查询字符串形式,发送到服务器的参数
//可以通过 req.query 对象访问到,例如:
//req.query.name req.query.age
console.log(req.query)
})
样例代码:
const express = require('express')
const app = express()
app.get('/user',(req,res)=>{
res.send({name :'zs',age:20,sex:'男'})
})
app.post('/user',(req,res)=>{
res.send('请求成功!')
})
app.get('/',(req,res)=>{
//通过 req.query 可以获取到客户端发送过来的查询参数
//默认情况下是一个空对象
console.log(req.query)
res.send(req.query)
})
app.listen(80,()=>{
console.log('express server running at http://127.0.0.1')
})
运行结果:
我们重启服务器,然后再 Postman 上 GET 一下,会发现底下是一个空对象
这证明了默认情况下 req.query
是一个空对象。接着我们再在地址后面输入 ?name=zs&age=20
点击 send,会发现以下结果:
我们发现当我们在 ① 位置通过查询字符串形式输入 ?name=zs&age=20
这样的方式来设置参数的时候,在 ② 位置会实时同步进行更新。所以我们也可以在 ② 位置设置参数,而当我们在 ② 位置设置参数的时候,会发现 ① 位置也会实时同步更新,读者可以自行尝试。
3.7 获取 URL 中的动态参数
通过 req.params
对象,可以访问到 URL 中,通过 : 匹配到的动态参数:
//URL 地址中,可以通过 :参数名 的形式,匹配动态参数值
app.get('/user/:id',(req,res)=>{
//req.params 默认是一个空对象
//里面存放着通过 : 动态匹配到的参数值
console.log(req.params)
})
样例代码:
在上一节代码的后面插入以下语句
//注意这里的 :id 是一个动态的参数
app.get('/user/:id',(req,res)=>{
//req.params 是动态匹配到的 URL 参数,默认是一个空对象
console.log(req.params)
res.send(req.params)
})
运行结果:
注意:
我们在
user/
后面输入的:id
是一个动态参数的值,你输入 5 后面显示的就是 5;如果后面输入的 1 后面显示的就是 1代码中 : 后的 id 是参数的名字,是可以自行输入的,只要合法就行
user/
后可以加若干动态参数
4、托管静态资源
4.1 express.static()
express 提供了一个非常好用的函数,叫做 express.static()
,通过它我们可以非常方便地创建一个静态资源服务器。例如,通过以下代码可以将 public 目录下的图片、CSS 文件。JavaScript 文件对外开放访问了:
app.use(express.static('public'))
注意:Express 在指定的静态目录中查找文件,并对外提供资源的访问路径。因此存放静态文件的目录名不会出现在 URL 中。
样例代码:
const express = require('express')
const app = express()
//调用 express.static() 方法,快速的对外提供静态资源
app.use(express.static('./clock'))
app.listen(80,()=>{
console.log('express server running at http://127.0.0.1')
})
运行结果:
可以正常访问该网页。
如果要托管多个静态资源目录就需要多次调用 express.static()
函数,该函数会根据目录的添加顺序查找所需的文件。
4.2 挂载路径前缀
如果希望再拖管的静态资源访问路径之前,挂载路径前缀,则可以使用如下的方式:
app.use('./public',express.static('public'))
样例代码:
const express = require('express')
const app = express()
//挂载路径前缀 /abc
app.use('/abc',express.static('./clock'))
app.listen(80,()=>{
console.log('express server running at http://127.0.0.1')
})
运行结果:
5、nodemon
5.1 什么是 nodemon
在编写调试 Node.js 项目的时候,如果修改了项目的代码,则需要频繁的手动 close 掉,然后再重新启动,非常繁琐。现在,我们可以使用 nodemon 这个工具来监听项目文件的变动。当代码被修改之后,nodemon 会自动帮我们重启项目,极大方便开发和调试。
5.2 安装 nodemon
我们在终端内输入 npm i -g nodemon
来安装全局的 nodemon。
5.3 使用 nodemon
现在,我们可以用 nodemon
命令开替换 node
命令,使用 nodemon test.js
来启动项目。这样做的好处是:代码被修改之后,会被 nodemon 监听到,从而实现自动重启项目的效果。
样例代码 (这里使用第一章第一节的代码,readFile):
const fs = require('fs')
//成功,1.txt 文件中写了 '111'
fs.readFile('./files/1.txt','utf8',function(err,dataStr){
console.log(err)
console.log('--------')
console.log(dataStr)
})
运行结果:
会发现多了很多行。现在我们将再原代码的底下添加以下代码:
//失败,没有11.txt文件
fs.readFile('./files/11.txt','utf8',function(err,dataStr){
console.log(err)
console.log('--------')
console.log(dataStr)
})
然后,我们按下 Ctrl + S 来保存项目。我们会发现终端内瞬间刷新了,并且执行了新的代码
运行结果:
Ctrl + C 来结束。
6、Express 路由
6.1 什么是路由
广义上来讲,路由就是映射关系。在 Expresss 中,路由指的是客户端的请求与服务期处理函数之间的映射关系。
Express 中的路由分 3 部分组成,分别是请求的类型、请求的 URL 地址、处理函数,其具体格式如下:
app.METHOD(PATH,HANDLER)
- 参数1:METHOD 对应的是请求的类型,可以是 GET 或者是 POST
- 参数2:PATH 对应的是请求的 URL 地址
- 参数3:HANDLER 对应的是处理函数,通过前面的请求类型和请求的地址,然后来调用处理函数
6.2 Express 中的路由的例子
const express = require('express')
const app = express()
//匹配 GET 请求,并且请求 URL 为 /
app.get('/',(req,res)=>{
res.send('Hello World!')
})
//匹配 POST 请求,且请求为 URL 为 /
app.post('/',(req,res)=>{
res.send('Got a POST request')
})
//启动 Web 服务器
app.listen(80,()=>{
console.log('server running at http://127.0.0.1')
})
运行结果:
GET
POST
6.3 路由匹配过程
每当一个请求到达服务器之后,需要先经过路由的匹配,只有匹配成功之后,才会调用对应的处理函数。
在匹配时,会按照路由的顺序进行匹配,如果请求类型和请求 URL 同时匹配成功,则 Express 会将这次请求,转交给对应的 function 函数进行处理。
注意:
- 按照定义的先后顺序进行匹配
- 请求类型和请求的 URL 同时匹配成功,才会调用对应的处理函数
6.4 路由的使用
6.5.1 最简单的用法
在 Express 中使用路由最简单的方式,就是把路由挂载到 app 上,具体代码如下:
const express = require('express')
const app = express()
//挂载路由
app.get('/',(req,res)=>{
res.send('Hello World!')
})
app.post('/',(req,res)=>{
res.send('Post Request.')
})
//启动 Web 服务器
app.listen(80,()=>{
console.log('server running at http://127.0.0.1')
})
运行结果:
GET
POST
6.5.2 模块化路由
为了方便对路由进行模块化的管理,Express 不建议将路由直接挂载到 app 上,而是推荐将路由抽离为单独的模块。
将路由抽离为单独模块的步骤如下:
- 创建路由模块对应的 .js 文件
- 调用
express.Router()
函数创建路由对象 - 向路由对象上挂载具体路由
- 使用
module.exports
向外共享路由对象 - 使用
app.use()
函数注册路由模块
我们在文件夹下新建一个 router.js 的文件,在里面写入代码
var express = require('express')
//创建具体的路由
var router = express.Router
//挂载具体的路由
router.get('/user/list',(req,res)=>{
res.send('Get user list.')
})
router.post('/user/add',(req,res)=>{
res.send('Add new user.')
})
//向外导出路由
module.exports = router
在这之后我们就可以在 test.js 中注册路由了
const express = require('express')
const app = express()
//导入路由模块
const router = require('./router.js')
//注册路由模块
app.use(router)
app.listen(80,()=>{
console.log('server running at http://127.0.0.1')
})
运行结果:
GET
POST
注意:app.use()
函数的作用就是用来注册全局中间件
6.5 为路由模块添加前缀
与之前挂载路径前缀一样,我们也可以为路由添加前缀,其具体语法格式如下:
app.use('/api',router)
在这里我们就已经为 router 路由添加了 /api
的前缀。
7、Express 中间件
7.1 什么是中间件
中间件(Middleware),特指业务流程的中间处理环节。
这就好比是日常处理污水的时候一般都要经历三个环节,从而保证处理过后的污水能够达到排放标准。
而这中间处理污水的三个环节就可以称为**“中间件”**
7.1.1 Express 中间件的调用流程
当一个请求到达 Express 服务器之后,可以连续调用多个中间件,从而对这次请求进行预处理。
7.1.2 Express 中间件的格式
Express 中间件本质上就是一个 function 处理函数,其具体结构如下:
这个 next()
函数是实现多个中间件连续调用的关键,它表示把流转关系转交给下一个中间件或者路由。
7.2 定义一个最简单的中间件
根据上面的中间件的格式我们就可以来试着定义一个最简单的中间件,其代码如下:
//定义一个最简单的中间件
const mw = (req,res,next)=>{
console.log('这是最简单的一个中间件')
//把流转关系,转交给下一个中间件或者是路由
next()
}
7.3 全局中间件
7.3.1 定义全局中间件
客户端发起的任何请求到达服务器之后,都会触发的中间件叫做全局生效的中间件。在定义完一个中间件之后通过 app.use()
函数就可以注册一个全局生效的中间件,其代码如下:
//将 mw 注册成为全局生效的中间件
app.use(mw)
知道了这么多我们就可以来编写一个简单的需要用到中间件的代码了:
const express = require('express')
const app = express()
//定义一个最简单的中间件
const mw = (req,res,next)=>{
console.log('这是最简单的一个中间件')
//把流转关系,转交给下一个中间件或者是路由
next()
}
//将 mw 注册成为全局生效的中间件
app.use(mw)
app.get('/',(req,res)=>{
res.send('Home page')
})
app.listen(80,()=>{
console.log('server running at http://127.0.0.1')
})
运行结果:
7.3.2 全局生效的中间件的简化形式
app.use((req,res,next)=>{
console.log('这是一个简单的中间件')
next()
})
7.3.3 定义多个全局中间件
可以使用 app.use()
函数来连续定义多个中间件。客户端请求到达服务器之后,会按照中间件定义的先后顺序一次进行调用,示例代码如下:
const express = require('express')
const app = express()
const mw1 = (req,res,next)=>{
console.log('这是中间件1')
next()
}
const mw2 = (req,res,next)=>{
console.log('这是中间件2')
next()
}
app.use(mw1,mw2)
app.get('/',(req,res)=>{
res.send('Home page')
})
app.listen(80,()=>{
console.log('server running at http://127.0.0.1')
})
当我们点击 Send 之后控制台内会显示如下:
7.4 局部中间件
不适用 app.use()
定义的中间件就叫做局部生效的中间件,示例代码如下:
const express = require('express')
const app = express()
//定义中间件函数
const mw = (req,res,next)=>{
console.log('这是中间件函数')
next()
}
//创建路由
app.get('/',mw,(req,res)=>{
res.send('Home page')
})
app.get('/user',(req,res)=>{
res.send('User page')
})
app.listen(80,()=>{
console.log('server running at http://127.0.0.1')
})
运行的时候我们发现在 /
下 send
时,终端无任何反应,而当在 /user
下 send
时,终端会显示 '这是中间件函数'。
7.4.1 定义多个局部中间件
可以在路由中通过以下两种等价的方式,使用多个局部中间件:
app.get('/',mw1,mw2,(req,res)=>{
res.send('Home page')
})
app.get('/',[mw1,mw2],(req,res)=>{
res.send('Home page')
})
这里就不再进行多的代码演示,请自主完成。
7.5 中间件的作用
多个中间件之间共同享用一份 req 和 res 。基于这样的特性,我们可以在上游的中间件中,统一为 req 和 res 对象添加自定义的属性和方法,供下游的中间件或者是路由进行使用。
const express = require('express')
const app = express()
const mw = (req,res,next)=>{
//获取到请求到达服务器的时间
const time = Date.now()
//为 req 对象,挂载自定义属性,从而把时间共享给后面的所有路由
req.startTime = time
next()
}
app.use(mw)
app.get('/',(req,res)=>{
res.send('Home page, '+req.startTime)
})
app.listen(80,()=>{
console.log('server running at http://127.0.0.1')
})
运行结果:
7.6 中间件的五个使用注意事项
- 一定要在路由之前注册中间件
- 客户端发送过来的请求,可以连续调用多个中间件进行处理
- 执行完中间件的业务代码之后,不要忘记调用
next()
函数 - 为了防止代码逻辑混乱,调用
next()
函数后不要再写额外的代码 - 连续调用多个中间件时,多个中间件之间,共享
req
和res
对象
7.7 中间件的分类
7.7.1 应用级别的中间件
通过 app.use()
或者 app.get()
或者 app.post()
,绑定到 app 实例上的中间件,叫做应用级别的中间件,代码示例如下:
//应用级别的中间件(全局中间件)
app.use((req,res,next)=>{
next()
})
//应用级别的中间件(局部中间件)
app.get('/',mw,(req,res)=>{
res.send('Home page')
})
7.7.2 路由级别的中间件
绑定到 express.Router() 实例上的中间件,都可以叫做是路由级别的中间件。它的用法和应用级别中间没有任何区别。只不过,应用级别中间是绑定到 app 实例上,路由级别中间件绑定到 router 实例上,代码示例如下:
const express = require('express')
const app = express()
const router = express.Router()
//路由级别的中间件
router.use((req,res,next)=>{
console.log('Time:',Date.now())
next()
})
app.use('/',router)
7.7.3 错误级别的中间件
错误级别的中间件的作用:专门用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题。
在错误级别的中间件中的 function 处理函数中,必须包含 4 个形参,形参顺序从前到后,分别是 (err, req, res, next)。
首先我们来测试一下什么是崩溃
const express = require('express')
const app = express()
app.get('/',(req,res)=>{
//人为的制造错误
throw new Error('服务期内部发生错误!')
res.send('Home page')
})
app.listen(80,()=>{
console.log('server running at http://127.0.0.1')
})
在 vscode 中会发现 res.send('Home page')
这一句被隐去了,然后我们再在 Postman 中测试一下,会得到以下结果:
那么整个项目就已经崩溃了。
那接下来我们就可以使用错误级别的中间件来防止崩溃,代码如下:
const express = require('express')
const app = express()
app.get('/',(req,res)=>{
//人为的制造错误
throw new Error('服务期内部发生错误!')
res.send('Home page')
})
//定义错误级别的中间件,防止项目崩溃
app.use((err,req,res,next)=>{
console.log('发生了错误! '+err.message)
res.send('Error: '+err.message)
})
app.listen(80,()=>{
console.log('server running at http://127.0.0.1')
})
运行结果:
项目虽然有错误,但却没有崩溃。
注意:错误级别的中间件必须注册在所有路由之后。
7.7.4 Express 内置的中间件
自从 Express 4.16.0 版本开始,Express 内置了 3 个常用的中间件,极大的提高了 Express 项目的开发效率和体验:
express.static
快速托管静态资源的内置中间件,例如:HTML 文件、图片、CSS 样式等(无兼容性)express.json
解析 JSON 格式的请求体数据(有兼容性,尽在 4.16.0+ 版本中可用)express.urlencoded
解析 URL-encoded 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)
其使用方法很简单,如下:
//配置解析 application/json 格式数据的内置中间件
app.use(express.json())
//配置解析 application/x-www-urlencoded 格式数据的内置中间件
app.use(express.urlencoded({extended:false}))
接下来我们先来看看 express.json()
的有关使用:
const express = require('express')
const app = express()
//通过 express.json() 这个中间件,解析表单中的 JSON 格式的数据
app.use(express.json())
app.get('/user',(req,res)=>{
//在服务器,可以使用 req.body 这个属性,来接收客户端发送过来的请求体数据
//默认情况下,如果不配置解析表单数据的中间件,则 req.body 默认等于 undefined
console.log(req.body)
res.send('OK')
})
app.listen(80,()=>{
console.log('Express server running at http://127.0.0.1')
})
在 Postman 中运行之后我们进行如下操作:
之后我们在主体框里面编写代码:
{
"name":"zs",
"age":20
}
我们再次 send 一下,然后回到 vscode 里可以看到控制台显示:
接着再来看看 express.urlencoded() 的有关使用:
const express = require('express')
const app = express()
//通过 express.urlencoded() 这个中间件,来解析表单中的 url-encoded 格式的数据
app.use(express.urlencoded({extended:false}))
app.post('/book',(req,res)=>{
//在服务端内,可以通过 req.body 来获取 JSON 格式的表单数据和 url-encoded 格式的数据
console.log(req.body)
res.send('ok')
})
app.listen(80,()=>{
console.log('Express server running at http://127.0.0.1')
})
在 Postman 中进行如下操作:
我们回到 vscode 中,可以在控制台内看到:
7.7.5 第三方中间件
非 Express 官方内置的,而是由第三方开发出来的中间级看,叫做第三方中间件。在项目中,大家可以按需下载并配置第三方中间件,从而提高项目的开发效率。
其使用步骤如下:
- 运行
npm install body-parser
安装中间件 - 使用
require
导入中间件 - 调用
app.use()
注册并使用中间件
7.7.6自定义中间件
步骤:
- 定义中间件
- 监听 req 的 data 事件
- 监听 req 的 end 事件
- 使用 querystring 模块解析请求体数据
- 讲解洗出来的数据对象挂载为 req.body
- 将自定义中间件封装为模块
下面我们手动模拟一个类似于 express.urlencoded
这样的中间件,来解析 POST 提交到服务器的表单数据。
首先我们定义一个全局中间件
const express = require('express')
const app = express()
//这是解析表单数据的中间件
app.use((req,res,next)=>{
//定义中间件具体的业务逻辑
})
app.listen(80,()=>{
console.log('Express server running at http://127.0.0.1')
})
第二步我们来编写监听 req 的 data 事件的代码
在中间件中,需要监听 req 对象的 data 事件,来获取客户端发送到服务器的数据。
如果数据量比较大,无法一次性发送完毕,则客户端会把数据切割后,分批发送到服务器。所以 data 事件可能会触发多次,每一次触发 data 事件时,获取到数据只是完整数据的一部分,需要手动对接受到的数据进行拼接。
代码如下:
//1、定义一个 str 字符串,专门用来存储客户端发送过来的请求体数据
let str = ''
//2、监听 req 的 data 事件
req.on('data',(chunk)=>{
str += chunk
})
第三步我们来编写监听 req 的 end 事件的代码
当请求体数据接收完毕之后,会自动触发 req 的 end 事件。
因此,我们可以在 req 的 end 事件中,拿到并处理完整的请求体数据。
代码如下:
//3、监听 req 的 end 事件
req.on('end',()=>{
//在 str 中存储的就是完整的请求体数据
console.log(str)
//TODO: 把字符串格式的请求体数据,解析成对象格式
})
我们可以尝试着先来看看能不能打印出请求体的数据
第三步我们使用 querystring 模块解析请求体数据
Node.js 中内置了一个 querystring 模块,专门用来处理查询字符串。通过这个模块提供的 parse()
函数,可以轻松把查询字符串,解析成对象的格式。
先在开头导入 querystring 模块
//导入 querystring 模块
const qs = require('querystring')
然后在 end 中写进代码:
const body = qs.parse(str)
console.log(body)
并重复第二步的测试步骤会发现:
第四步将解析出来的数据对象挂载为 req.body
上游的中间件和下游的中间件以及路由之间,共享用一份 req 和 res。因此,我们可以将解析出来的数据挂在为 req 的自定义属性,命名为 req.body
,供下游使用。
在 end 中写进代码:
req.body = body
next()
并且将 res.send('ok')
更改成 res.send(req.body)
之后我们再进行测试:
服务器能够接受到返回的结果。
最后一步将自定义中间件封装为模块
为了优化代码的结构,我们可以把自定义的中间件函数,封装为独立的模块。
先定义一个常量将 res.use()
内的内容封装起来,然后再向外导入
代码如下:
const bodyParser = ((req,res,next)=>{
//...
})
module.exports = bodyParser
删去我们不需要的代码,其整个代码如下:
//导入 querystring 模块
const qs = require('querystring')
//这是解析表单数据的中间件
const bodyParser = ((req,res,next)=>{
//定义中间件具体的业务逻辑
//1、定义一个 str 字符串,专门用来存储客户端发送过来的请求体数据
let str = ''
//2、监听 req 的 data 事件
req.on('data',(chunk)=>{
str += chunk
})
//3、监听 req 的 end 事件
req.on('end',()=>{
//在 str 中存储的就是完整的请求体数据
//TODO: 把字符串格式的请求体数据,解析成对象格式
const body = qs.parse(str)
req.body = body
next()
})
})
module.exports = bodyParser
至此,自定义一个中间件已经全部完成,现在只需要在 test.js 中进行导入就可使用。
测试代码如下:
const express = require('express')
const app = express()
//导入自己封装好的中间件模块
const customBody = require('./类urlencoded的中间件')
//将自定义的2中间件函数,注册成为全局可用的中间件
app.use(customBody)
app.post('/book',(req,res)=>{
res.send(req.body)
})
app.listen(80,()=>{
console.log('Express server running at http://127.0.0.1')
})
运行结果:
8、Express 接口
8.1 什么是接口
API (Application Programming Interface,应用程序编程接口 ) 是一些预先定义的函数,目的是提供应用程序与开发人员基于某软件或硬件的以访问一组例程的能力,而又无需访问源码,或理解内部工作机制的细节。API除了有应用“应用程序接口”的意思外,还特指 API的说明文档,也称为帮助文档。
8.2 创建 API 路由模块
创建 API 路由模块的方法十分简单,和之前创建自定义模块的方法几乎一样,所以就不再赘述。
const express = require('express')
const apiRouter = express.Router()
module.exports = apiRouter
然后我们在 test.js 中注册路由:
const express = require('express')
const apiRouter = require('./apiRouter')
const app = express()
app.use('/api',apiRouter)
app.listen(80,()=>{
console.log('Express server running at http://127.0.0.1')
})
这样我们就可以直接在 API 路由模块中写相应的路由,然后在 test.js 中调用即可。
8.3 编写 GET 接口
我们直接在 API 路由模块中编写路由代码。
//定义 GET 接口
router.get('/get',(req,res)=>{
//调用 res.send() 方法,向客户端响应处理的结果
res.send({
status:0,//0 表示处理成功,1 表示处理失败
msg:'GET 请求成功!',//状态的描述
data:req.query//通过 req.query 来获取客户端通过查询字符串发送到服务器的数据
})
})
然后运行 nodemon test.js 来看看运行结果:
8.4 编写 POST 接口
和上面一样,我们直接在 API 接口中编写路由代码
//定义 POST 接口
router.post('/post',(req,res)=>{
//调用 res.send() 方法,向客户端响应结果
res.send({
status:0,
msg:'POST 请求成功',
data:req.body //通过 req.body 获取请求体包含的 url-encoded 格式的数据
})
})
然后还需要在 test.js 中配置一段代码
//配置解析表单数据的中间件
app.use(express.urlencoded({extended:false}))
完整代码:
apiRouter.js
const express = require('express')
const router = express.Router()
//定义 GET 接口
router.get('/get',(req,res)=>{
//调用 res.send() 方法,向客户端响应处理的结果
res.send({
status:0, //0 表示处理成功,1 表示处理失败
msg:'GET 请求成功!', //状态的描述
data:req.query //通过 req.query 来获取客户端通过查询字符串发送到服务器的数据
})
})
//定义 POST 接口
router.post('/post',(req,res)=>{
//调用 res.send() 方法,向客户端响应结果
res.send({
status:0,
msg:'POST 请求成功',
data:req.body //通过 req.body 获取请求体包含的 url-encoded 格式的数据
})
})
//向外暴露
module.exports = router
test.js
const express = require('express')
const apiRouter = require('./apiRouter')
const app = express()
//配置解析表单数据的中间件
app.use(express.urlencoded({extended:false}))
app.use('/api',apiRouter)
app.listen(80,()=>{
console.log('Express server running at http://127.0.0.1')
})
运行结果:
8.5 CORS 跨域资源共享
刚刚编写的 GET 和 POST 接口,存在一个很严重的问题:不支持跨域请求。我们来看看下面的效果(需要 jQuery 和 Ajax.js 前置知识)
先来简单介绍一下跨域请求:当前发起请求的域与该请求指向的资源所在的域不一样。这里的域指的是这样的一个概念:我们认为若协议 + 域名 + 端口号均相同,那么就是同域。
举个例子:假如一个域名为aaa.cn
的网站,它发起一个资源路径为aaa.cn/books/getBookInfo
的 Ajax 请求,那么这个请求是同域的,因为资源路径的协议、域名以及端口号与当前域一致(例子中协议名默认为http,端口号默认为80)。但是,如果发起一个资源路径为bbb.com/pay/purchase
的 Ajax 请求,那么这个请求就是跨域请求,因为域不一致,与此同时由于安全问题,这种请求会受到同源策略限制。
我们编写一个用以测试的 .html 文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.staticfile.org/jquery/1.10.0/jquery.min.js"></script>
</head>
<body>
<button id="btnGet">GET</button>
<br/>
<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>
结局接口跨域问题的方案主要有两种:
- CORS(主流的解决方案,推荐使用)
- JSONP(有缺陷的解决方案,只能支持 GET 请求)
8.5.1使用 CORS 中间件解决跨域问题
CORS 是 Express 的一个第三方中间件。通过安装和配置 CORS 中间件,可以很方便地解决跨域问题。使用步骤分为如下 3 步:
- 运行
npm install cors
安装中间件 - 使用
const cors = require('cors')
导入中间件 - 在路由之前调用
app.use(cors())
配置中间件
安装完 CORS 中间件之后,在 test.js 路由之前导入并配置 CORS 中间件,其代码如下:
//一定要在路由之前配置 cors 这个中间件,从而解决接口跨域的问题
const cors = require('cors')
app.use(cors())
接着我们再运行 test.js 并且回到页面中,刷新一下页面,获得以下结果:
8.5.2 什么是 CORS
通过第一小节的案例,我们已经初步使用了 CORS 中间件来进行跨域资源共享,那什么是 CORS 呢?
CORS(Cross-Origin Resource Sharing,跨域资源共享)由一系列 HTTP 响应头组成,这些 HTTP 响应头决定浏览请是否阻止前端 JS 代码跨域获取资源。
浏览器的同源安全策略默认会阻止网页 “跨域” 获取资源
但如果接口服务器配置了 CORS 相关的 HTTP 响应头,就可以接触浏览器端的跨域访问限制
注意:
- CORS 主要在服务器端进行配置,客户端浏览器无需做任何额外的配置,即可请求开启了 CORS 的接口。
- CORS 在浏览器中有兼容性。只有支持 XMLHttpRequest Level2 的浏览器,才能正常访问开启了 CORS 的服务端口。
8.5.3 CORS 响应头部 - Access-Control-Allow-Origin
响应头部中可以携带一个 Access-Control-Allow-Origin
字段,其语法格式如下:
Access-Control-Allow-Origin: <origin> | *
其中,origin 参数指定了允许访问该资源的外域 URL。
例如,下面的字段值将只允许来自 http://itcast.cn 的请求:
res.setHeader('Access-Control-Allow-Origin','http://itcast.cn')
如果指定了 Access-Control-Allow-Origin
字段的值为通配符 *,表示允许来自任何域的请求,示例代码如下:
res.setHeader('Access-Control-Allow-Origin','*')
CORS 响应头部 - Access-Control-Allow-Headers
默认情况下,CORS 仅支持客户端向服务器发送如下的九个请求头:
- 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-Control-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-Allow-Methods
来指明实际请求所允许使用的 HTTP 方法。
示例代码如下:
//只允许 POST、GET、DELETE、HEAD 请求方法
res.setHeader('Access-Control-Allow-Methods','POST,GET,DELETE,HEAD')
//允许所有的 HTTP 请求方法
res.setHeader('Access-Control-Allow-Methods','*')
CORS 请求的分类
客户端在请求 CORS 接口时,根据请求方式和请求头的不同,可以将 CORS 的请求分为两大类,分别是:
- 简单请求
- 预检请求
简单请求:
- 请求方式是 GET、POST、HEAD
- HTTP 头部信息不超过以下几种字段:物资定义头部字段、Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width、Content-Type (值仅限于 text/plain、multipart/form-data、application/x-www-form-urlencoded 三者之一)
预检请求(只要满足以下任何一个请求):
- 请求方式为 GET、POST、HEAD 之外的请求 Method 类型
- 请求投中包含自定义头部字段
- 向服务器发送了 application/json 格式的数据
在浏览器与服务器正式通信之前,浏览器会发送 OPTION 请求进行预检,已获知服务器是否允许该实际请求,所以一次的 OPTION 请求称为 “预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据。
简单请求的特点:客户端与服务器之间只会发送一次请求
预检请求的特点:客户端之间会发生两次请求,OPTION 预检请求成功之后,才会发起真正的请求
下面我们来实际的体验一下:
首先在测试接口的 .html 文件下添加测试 DELETE 的接口:
<body>
<button id="btnDELETE">DELETE</button>
<script>
//测试 DELETE 接口
$('#btnDELETE').on('click',function(){
$.ajax({
type:'DELETE',
url:'http://127.0.0.1/api/delete',
success:function(res){
console.log(res)
}
})
})
</script>
</body>
然后再在 apiRouter.js 文件里添加定义 DELETE 接口的代码:
//定义 DELEET 接口
router.delete('/delete',(req,res)=>{
res.send({status:0,message:'DELETE 请求成功'})
})
然后我们就可以试着运行了
我们打开控制台的网络页面,然后点击 GET 可以得到一下结果:
我们会发现 GET 发送了一次请求,然后我们再点击 DELETE:
发现 DELETE 向服务器发送了两次请求,其中有一次是被标记为预检。
8.6 JSONP 接口
8.6.1 什么是 JSONP 接口
浏览器端通过 <script>
标签的 src 属性,请求服务器上的数据。同时,服务器返回一个函数的调用。这种请求数据的方式叫做 JSONP。
特点:
- JSONP 不属于真正的 Ajax 请求,因为它没有使用 XMLHttpRequest 这个对象
- JSONP 仅支持 GET 请求,不支持 POST、PUT、DELETE 等请求
8.6.2 创建JSONP 接口的注意事项
如果项目中已经配置了 CORS 跨域资源共享,为了防止冲突,必须在配置 CORS 中间件之前声明 JSONP 的接口。否则 JSONP 接口会被处理成开了了 CORS 的接口,示例代码如下:
//必须在配置 cors 中间件之前,配置 JSONP 的接口
app.get('/api/jsonp',(req,res)=>{
//TODO:定义 JSONP 接口具体的实现过程
})
8.6.3 实现 JSONP 接口
步骤如下:
- 获取客户端发送过来的回调函数的名字
- 得到要通过 JSONP 形式发送给客户端的数据
- 根据前两步得到的数据,拼接出一个函数调用的字符串
- 把上一步拼接得到的字符串响应给客户端的
<script>
标签进行解析执行
那根据步骤我们就可以在 test.js 中实现 JSONP 接口,其代码如下:
//必须在配置 cors 中间件之前,配置 JSONP 的接口
app.get('/api/jsonp',(req,res)=>{
//TODO:定义 JSONP 接口具体的实现过程
//1、获取客户端发送过来的回调函数的名字
const funcName = req.query.callback
//2、得到要通过 JSONP 形式发送给客户端的数据
const data = {name:'zs',age:20}
//3、根据前两步得到的数据,拼接出一个函数调用的字符串
const scripStr = `${funcName}(${JSON.stringify(data)})`
//4、把上一步拼接得到的字符串响应给客户端
res.send(scripStr)
})
接下来我们就可以在 html 中调用 $.ajax()
,提供 JSONP 的配置选项来发起 JSONP 请求,其代码如下:
<body>
<button id="btnJSONP">JSONP</button>
<script>
//为 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>
运行结果:
八、数据库与身份认证
1、数据库的基本概念
1.1 什么是数据库
数据库是“按照数据结构来组织、存储和管理数据的仓库”。是一个长期存储在计算机内的、有组织的、可共享的、统一管理的大量数据的集合。
当今世界是一个充满着数据的互联网世界,比如说消费记录、浏览记录、出行记录等等。除了我们一般认为的文本类型的数据,图像、音乐、声音其实都是数据。
而面对这庞大的数据,我们就需要有一个工具去方便地管理管理这些数据,数据库管理系统 (简称:数据库) 这一概念也就应运而生。用户可以通过数据库来对数据进行增、删、改、查等操作。
数据库大致分为两种,一种是 传统型数据库, 包括:MySQL、sql server等;另一种是 新型数据库, 包括:Mongodb数据库。
而我们最常使用的就是 MySQL 数据库。其官网地址:MySQL
1.2 传统型数据库的数据组织结构
在传统型数据库中,数据的组织结构分为**数据库 (database)、数据表 (table)、数据行 (row)、字段 (filed)**这 4 大部分组成。
注意:
- 在实际项目开发中,一般情况下,每个项目都对应独立的数据库
- 不同的数据,要存储到数据库的不同的表中。
- 每个表中具体存储哪些信息有字段来决定。
- 表中的行代表每一条具体的数据
2、安装并配置 MySQL
这里不做过多的演示,请读者自行去阅读以下两个链接并安装好 MySQL。
win 系统安装:超级详细的mysql数据库安装指南 - 知乎 (zhihu.com)
mac 系统安装:安装并配置 MySQL
对于使用 win 系统的同学,我们这里有 navicat15 (一个可视化编辑数据库的软件,非常方便) 的破解包,大家可以自行下载安装 navicat15。
3、使用 navicat15 新建数据库和表
- 打开 navicat15
- 在右上角点击文件,并找到新建连接,选择第一个 MySQL
- 输入自己的用户名和密码,单击确定
- 双击连接数据库
- 右击连接,点击新建数据库
或者在查询中新建查询使用查询语句来新建数据库
CREATE DATABASE test;
- 双击 test 连接数据库,右键表来新建表,或者使用查询语句来新建表:
USE test;
CREATE TABLE stu(
name VARCHAR(20),
age INT(3),
id VARCHAR(20)
);
并先运行 USE test;
再运行底下的代码。在 navicat15 中,我们可以选择单条语句进行运行。
- 在查询中执行 sql 语句来插入数据:
INSERT INTO stu VALUES('张三',20,'112233');
- 在查询中执行 sql 语句来查询数据:
SELECT * FROM stu;
运行结果:
接下来有关数据库的基础语句等我们在这里都不再多展示,请各位自行去本文档的配套教程中观看 P58 - P67,或者单独学习有关数据库的知识,文档后面的内容会直接从配套教程的 P68 开始。
4、mysql 模块
以下所有数据库的操作都将建立在以下 sql 语句的基础上,请读者自行复制粘贴建议相应的数据库:
CREATE DATABASE test;
USE test;
CREATE TABLE stu(
name VARCHAR(20),
age INT(3),
sex VARCHAR(5),
id VARCHAR(20)
);
INSERT INTO stu
VALUES('陈二',20,'男','112233'),
('张三',20,'男','113344'),
('李四',21,'女','223344'),
('王五',22,'男','334455'),
('赵六',20,'女','114455'),
('孙七',21,'女','224455'),
('钱八',22,'男','335566');
4.1 安装 mysql 模块
Express 项目和 MySQL 数据库之间的关系如下图,我们通过安装和使用 mysql 模块来连接 MySQL 数据库以及对其进行操作。
那我们就先来安装 mysql 模块,在终端中执行 npm i mysql
语句来安装 mysql 模块。
4.2 连接数据库
在安装完 mysql 模块之后我们就可以尝试连接数据库了,连接数据库主要有两个步骤,一个是导入 mysql 模块,一个是连接数据库,代码如下:
//导入 mysql 模块
const mysql = require('mysql')
//建立与 mysql 数据库的连接关系
const db = mysql.createPool({
host:'127.0.0.1',//数据库的 IP 地址
user:'root',//登录数据库的账号
password:'123456',//数据库的密码
database:'test'//指定要操作那个数据库
})
连接完数据库之后我们就要去测试 mysql 模块是否能够正常的工作,或者说我们是否真的已经连接上了数据库。我们在这里调用 db.query()
函数,通过回调函数来拿到执行的结果,代码如下:
//检测 mysql 模块是否能够正常工作
db.query('SELECT 1',(err,results)=>{
if(err){
return console.log(err.message)
}
console.log(results)
})
运行结果:
当显示这样的信息时就表示已经连接成功。
注意,这里可能会有 Client does not support authentication protocol requested by server; consider upgrading MySQL client
这样的报错,这是因为 node 没有办法解析 mysql 数据库的密码。我们只需要在 cmd 中打开 MySQL 文件的 bin,然后输入代码即可:
然后输入
mysql> alter user 'root'@'localhost' identified with mysql_native_password by '123456';
mysql> flush privileges;
4.3 查询数据表中的数据
我们之前已经成功连接上了数据库,那我们就可以使用 sql 语法来操作数据库了。我们先来试试查询:
//查询数据库
db.query('SELECT * FROM stu',(err,results)=>{
if(err){
return console.log(err.message)
}
console.log(results)
})
运行结果:
通过这个例子我们可以知道,在调用 db.query()
函数的时候,前面的单引号的内容就是用来写要执行的 sql 语句。但一般来说我们并不会直接在单引号里面写 sql 语句,而是采用变量的形式来写,代码示例如下:
//定义一个 sqlStr 的常量用来写 sql 语句
const sqlStr = 'SELECT * FROM stu'
//直接调用即可
db.query(sqlStr,(err,results)=>{
if(err){
return console.log(err.message)
}
console.log(results)
})
其运行结果和上面的相同。这样书写的好处是大大增加的代码的可读性,能够更加方便了自己日后维护的成本以及他人阅读。
那接下来我们再来试试插入语句:
//向数据库中插入数据
const stuinfo = {name:'张大三',age:21,sex:'男',id:'225566'}
//?表示占位符
const sqlInsert = 'INSERT INTO stu (name,age,sex,id) VALUES (?,?,?,?)'
//向 sqlInsert 语句中插入数据,然后再通过语句插入到数据库
db.query(sqlInsert,[stuinfo.name,stuinfo.age,stuinfo.sex,stuinfo.id],(err,results)=>{
if(err){
return console.log(err.message)
}
//插入成功
//注意:如果执行的是 INSERT INTO 插入语句,则 results 是一个对象
//可以通过 affectedRows 属性,来判断是否插入数据成功
if(results.affectedRows === 1){
console.log('数据插入成功!')
}
})
运行结果:
我们再使用查询语句来查询一下数据库:
我们的插入语句成功运行了!
那我们再回头来看看我们的代码,我们会发现我们再 db.query()
中一个一个插入数据很是麻烦,而且显得代码很杂乱,所以我们可以使用简便的插入语句:
const stuinfo = {name:'张大三',age:21,sex:'男',id:'225566'}
//使用占位符
const sqlInsert = 'INSERT INTO stu SET ?'
db.query(sqlInsert,stuinfo,(err,results)=>{
if(err){
return console.log(err.message)
}
if(results.affectedRows === 1){
console.log('数据插入成功!')
}
})
这样,我们的代码可读性大大增加了。
5、身份认证
**注意:**身份认证后面所讲内容会牵涉部分后端内容,所以部分代码没有办法直接进行。笔者会在代码块相应的位置标注 *****,读者需要自行去看原视频获取代码资料自行尝试。同时由于这部分的内容过于抽象,并且脱离项目没有意义,所以建议读者在学完这张之后一定要写个项目来巩固知识。
5.1 什么是身份认证
身份认证 (Authentication) 又称为 “身份验证” 、“鉴权”,指的是通过一定的手段,完成对用户身份的确认。
- 日常生活中的身份认证随处可见,例如:高铁的验票乘车,手机的密码或指纹解锁,支付宝或微信的支付密码等。
- 在 Web 开发中,也涉及到用户身份的认证,例如:各大网站中的手机验证码登录、邮箱密码登录、二维码登录等。
5.2 不同开发模式下的身份认证
对于服务器渲染和前后端分离这两种开发模式来说,分别有着不同的身份认证方案:
- 服务器渲染推荐使用 Session 认证机制
- 前后端分离推荐使用 JWT 认证机制
5.3 Session 认证机制
5.3.1 HTTP 协议的无状态性
了解 HTTP 协议的无状态性是进一步学习 Session 认证机制的必要前提。
HTTP 协议的无状态性,指的是客户端的每次 HTTP 请求都是独立的,连续多个请求之后没有直接的关系,服务器不会主动保留每次 HTTP 请求的状态。
5.3.2 如何突破 HTTP 无状态的限制
对于超市来说,为了方便收银员的进行结算时给 VIP 用户打折,超时可以为每个 VIP 用户发放会员卡。
身份认证方法一个关键的东西Cookie
5.3.3 什么是 Cookie
Cookie 是存储在用户浏览器中的一段不超过 4KB 的字符串。它由一个名称 (Name)、一个值 (Value) 和其它几个用于控制 Cooke 有效期、安全性、使用范围的可选属性的组织。
不同域名下的 Cookie 各自独立,每当客户端发起请求的时候,会自助把当前域名下所有未过期的 Cookie 一同发送到服务器。
Cookie 几大特性:
- 自动发送
- 域名独立
- 过期时限
- 4KB 限制
而因为 Cookie 是存储在浏览器中,而非是本地的,所以不具备安全性是可以被伪造的。因此,不建议将用户的隐私数据使用 Cookie 进行存储。
5.3.4 Cookie 身份认证过程
客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的 Cookie,客户端会自动将 Cookie 保存在浏览器中。随后,当客户端浏览器每次请求服务器的时候,浏览器会自动将身份认证相关的 Cookie,通过请求头的形式发送给服务器,服务器即可验明客户端的身份
5.3.5 提高身份认证的安全性
为了提高 Cookie 的安全性,我们可以选择将一些数据放在本地 。这就类似于在之前的 VIP 例子,我们不仅仅要出示会员卡(因为会员卡的外观是可以伪造的),而且还要刷卡进行验证(会员卡磁条是没办法进行伪造的)。
而这种 “会员卡+刷卡” 的设计理念就是 Session 认证机智的精髓。
5.3.6 什么是 Session
在计算机中,尤其是在网络应用中,称为 “会话控制”。Session对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的Web页之间跳转时,存储在Session对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web页时,如果该用户还没有会话,则Web服务器将自动创建一个 Session对象。当会话过期或被放弃后,服务器将终止该会话。
5.3.7 Session 工作原理
5.4 Session 中间件
5.4.1 安装 express-session中间件
npm i express-session
5.4.2 配置 express-session 中间件
express-session 中间件安装成功后,需要通过 app.use() 来注册 session 中间件,其示例代码如下:
// 1、导入 express-session 中间件
var session = require('express-session')
const { append } = require('express/lib/response')
// 2、配置 Session 中间件
app.use(Sesson({
secret:'keyboard cat', // secret 属性的值可以为任意字符串
resave:false, // 固定写法
saveUninitialized:true // 固定写法
}))
5.4.3 向 Sesssion 中存储数据
当 express-session 中间件配置成功之后,即可通过 req.session 来访问和使用 session 对象,从而存储用户的关键信息,其示例代码如下:
*
// 3、向 Session 中存储数据
app.post('/api/login',(req,res)=>{
//判断用户提交的登录信息是否正确
if(req.body.username !== 'admin' || req.body.password !== '000000'){
return res.send({status:1,message:'登录失败'})
}
req.session.user = req.body // 将用户的信息存储在 Session 中
req.session.islogin = true // 将用户的登录状态存储在 Session 中
res.send({status:0,message:'登陆成功'})
})
5.4.4 从 Session 中取出数据
*
// 4、向 Session 中取出数据
app.get('/api/username',(req,res)=>{
// 判断用户是否登录
if(!req.session.islogin){
return res.send({status:1,message:'fail'})
}
res.send({status:0,message:'success',username:res.session.user.username})
})
5.4.5 清空 Session 中的信息
*
// 5、清空 Session 中的信息
app.post('/api/logout',(req,res)=>{
req.session.destroy()
res.send({status:0,message:'退出登录成功!'})
})
5.5 JWT 认证机制
5.5.1 了解 Session 认证的局限性
Session 认证机制需要配合 Cookie 才能实现,由于 Cookie 默认不支持跨域访问,所以当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域 Session 认证。
注意:
- 当前端请求后端接口不存在跨域问题的时候,推荐使用 Session 身份认证机制
- 当前端需要跨域请求后端接口,不推荐使用 Session 身份认证机制,推荐使用 JWT 认证机制
5.5.2 什么是 JWT 认证机制
JWT (JSON Web Token):是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
5.5.3 JWT 认证原理
总结:用户的信息通过 Token 字符串的形式保存在客户端浏览器中。服务器通过还原 Token 字符串的形式来认证用户的身份。
5.5.4 JWT 的组成部分
JWT 通常由三部分组成,分别是 Header(头部)、Payload(有效荷载)、Signature(签名)。
- Payload 部分才是真正的用户信息,它是用户信息经过加密之后生成的字符串
- Header 以及 Signature 是安全性相关的部分,只是为了保证 Token 的安全性
5.5.5 JWT 的使用方式
客户端收到服务器返回的 JWT 之后,通常会将它存储在 localStorage 或者 sessionStorage 中。此后,客户端每次与服务器通信,都要带上这个 JWT 的字符串,从而进行身份验证。推荐的做法是把 JWT 放在 HTTP 请求头的 Authorizatiom 字段中,格式如下:
Authorization:Bearer<token>
5.6 在 Express 中使用 JWT
5.6.1 安装 JWT 相关的包
在使用 JWT 之前,我们需要安装相关的两个包,分别是 jsonwebtoken 以及 express-jwt :
npm i jsonwebtoken express-jwt
其中:
- jsonwebtoken 用于生成 JWT 字符串
- express-jwt 用于将 JWT 字符串解析还原成 JSON 对象
5.6.2 导入 JWT 相关的包
// 导入用于生成 JWT 字符串的包
const jwt = require('jsonwebtoken')
// 导入用于将客户端发送给过来的 JWT 字符串解析还原成 JSON 对象的包
const expressJWT = require('express-jwt')
5.6.3 定义 secret 秘钥
为了保证 JWT 字符串的安全性,防止 JWT 字符串在网络传输过程中被别人破解,我们需要专门定义一个用于加密和解密的 secret 秘钥:
- 当生成 JWT 字符串的时候,需要使用 secret 秘钥对用户的信息进行加密,最终得到加密好的 JWT 字符串
- 当把 JWT 字符串解析还原成 JSON 对象的时候,需要使用 secret 秘钥进行解密
那接下来就先弄个 secret 字符串看看:
// 定义一个 secret 字符串
const secretKey = 'hello world'
// 通常将秘钥字符串的名字命名为 secretKey
// 现在还只是一个普通的字符串,并没有其他用途
5.6.4 生成 JWT 字符串
调用 jsonwebtoken 包提供的 sign()
方法将用户的信息加密成 JWT 字符串然后响应给客户端,其示例代码如下:
*
app.post('/api/login',(req,res)=>{
// 用户登录成功之后,生成 JWT 字符串,通过 token 属性响应给客户端
res.send({
status:200,
message:'登陆成功!',
token:jwt.sign({username:userinfo.username},secretKet,{expiresIn:'30s'})
// 从生成开始算 30s,在这时间内这个 token 是有效的(有效时间)
})
})
5.6.5 将 JWT 字符串还原为 JSON 对象
客户端每次在访问那些有权限接口的时候,都需要主动通过请求头中的 Authorization 字段,将 Token 字符串发送到服务器进行身份验证。
此时,服务器可以通过 express-jwt 这个中间件,自动将客户端发送过来的 Token 解析还原成 JSON 对象,其示例代码如下:
*
// 将 JWT 字符串还原成 JSON 对象
app.use(expressJWT({secret:secretKey}).unless({path:[/^\/api\//]}))
// 其中 .usless() 是说明哪些接口不需要访问权限
5.6.6 使用 req.user 获取用户信息
当 express-jwt 这个中间件配置成功之后,即可在那些有权限的接口中使用 req.user 对象,来访问从 JWT 字符串中解析出来的用户信息,示例代码如下:
*
// 这是一个有权限的接口
app.get('/admin/getinfo',(req,res)=>{
// 此处的 user 是在我们配置成功 express-jwt 这个中间件,就可以把解析出来的用户信息挂载到 req 上
console.log(req.user)
res.send({
status:200,
message:'获取用户信息成功!',
data:req.user
})
})
捕获解析 JWT 失败后产生的错误
当使用 express-jwt 解析 Token 字符串的时候,如果客户端发送过来的 Token 字符串过期或不合法,会产生一个解析失败的错误,影响项目的正常运行,我们可以通过 Express 的错误中间件来捕获这个错误并进行相关的处理,其示例代码如下:
// 使用全局错误处理中间件,来捕获 JWT 产生的错误
app.use((err,req,res,next)=>{
// token 解析失败导致的错误
if(err.name === 'UnauthorizedError'){
return res.send({status:401,message:'无效的 token'})
}
// 其他原因导致的错误
res.send({status:500,message:'未知错误'})
})
九、项目实战
此处的项目实战不再在文档中编写,请读者自行去原视频或是以下链接处学习编写:Headline - api_server_ev (escook.cn)
参考资料
- 黑马程序员Node.js全套入门教程,nodejs最新教程含es6模块化+npm+express+webpack+promise等_Nodejs实战案例详解_哔哩哔哩_bilibili
- npm包发布详细教程_醉逍遥neo的博客-CSDN博客_npm 发布
- 10分钟快速掌握正则表达式_哔哩哔哩_bilibili
- Http请求头和响应头_未来可期-2022的博客-CSDN博客_请求头和响应头
- 什么是跨域请求以及实现跨域的方案 - 简书 (jianshu.com)
- 数据库(电子化的文件柜)_百度百科 (baidu.com)
- 超级详细的mysql数据库安装指南 - 知乎 (zhihu.com)
- session(计算机术语)_百度百科 (baidu.com)
- 什么是 JWT -- JSON WEB TOKEN - 简书 (jianshu.com)
- Headline - api_server_ev (escook.cn)