1.1 什么是运行环境?
运行环境是指代码正常运行所需的必要环境!!!!!
- V8引擎负责解析和执行JavaScript代码。
- 内置API是由运行环境提供的特殊接口,只能在所属的运行环境中被调用
1.2 JavaScript能否做后端开发
可以,但是要借助node.js
浏览器
是JavaScript的前端运行环境Nodejs
是JavaScript的后端运行环境- ⚠️⚠️⚠️
Node.js
中无法调用DOM
和BOM
等浏览器内置API⚠️⚠️⚠️
会javascript,就要学会node.js
1. 认识Node.js内置模块
1.1 初识Node.js
1.1.1 那到底什么是Node.js?
Node.js
是一个基于ChromeV8引擎的JavaScript运行环境
。
1.1.2 Node.js的作用
Node.js 作为 JavaScript 的运行环境,仅提供了基础功能和 API
。但基于这些基础能力,众多强大的工具和框架不断涌现,使前端开发者能够承担更多工作,拓宽职业发展路径:
- 基于 Express 框架,可快速构建 Web 应用。
- 基于 Electron 框架,可开发跨平台桌面应用。
- 基于 Restify,可以快速构建API接口项目。
- 读写和操作数据库、创建实用的命令行工具辅助前端开发、etc…
1.1.3 Node.js环境的安装
建议:去csdn随便找个教程安装nvm,通过Nvm安装node更方便
打开终端,在终端输入命令node -v
后,按下回车键,即可查看已安装的Node.js的版本号。
1.1.4 常见的终端操作命令
- 终端(英文:Terminal)是专门为开发人员设计的,用于实现人机交互的一种方式
- 作为一名合格的程序员,我们有必要识记一些常用的终端命令,来辅助我们更好的操作与使用计算机。
- 在Node.js环境中执行JavaScript代码——打开终端——输入node要执行的js文件的路径
- 使用
↑
键,可以快速定位到上一次执行的命令 - 使用
tab
键,能够快速补全路径 - 使用
esc
键,能够快速清空当前已输入的命令 - 输入
cls
命令,可以清空终端
1.2 fs文件系统模块
fs 模块
是Node.js官方提供的、用来操作文件的模块。它提供了一系列的方法和属性,用来满足用户对文件的操作需求。例如:
fs.readFile()方法
,用来读取
指定文件中的内容fs.writeFile()方法
,用来向指定的文件中写入
内容
如果要在JavaScript代码中,使用fs模块
来操作文件,则需要使用如下的方式先导入它:
const fs = require('fs')
1.2.1 readFile()读取文件
/* fs.readFile()的语法格式*/
fs.readFile(path[.options],callback)
/*
调用fs.readFile()
参数1:读取文件存放的路径
参数2:读取文件时采用的编码格式
参数3:回调函数,拿到失败(err)和成功的结果
*/
/*以utf8的编码格式,读取文件的内容,并打印err和dataStr的值*/
const fs = require('fs')
fs.readFile('./2.js','utf8', function(err, dataStr){
/*读取成功,err为null*/
/*读取失败,err为 错误对象 , dataStr 为 undefined*//*打印读取失败的结果*/
console.log(err)
console.log('-------')
/*打印读取成功的结果*/
console.log(dataStr)
})
1.2.2 wirteFile()写入文件
/*语法格式*/
fs.writeFile(file, data[.options], callback)/*
参数1:必选,写入文件的存放路径
参数2:必选,写入的内容
参数3:可选,编码格式,如utf8
参数4:必选,文件写入后的回调函数
*/
const fs = require('fs')fs.writeFile('./2.js','Hello Node.js',function(err){
/*读取成功,err为null*/
/*读取失败,err为 错误对象*/
console.log(err)
})
1.2.3 整理成绩案例
const fs = require('fs')fs.readFile('./成绩1.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('=', ': '))})console.log(arrNew)const newStr = arrNew.join('\r\n') console.log(newStr)fs.writeFile('./成绩-ok.txt',newStr,function(err){if(err){return console.log('写入文件失败!' + err.message)}console.log('成绩写入成功')})
})
1.2.4 fs模块-路径动态拼接的问题
在使用fs
模块操作文件时,如果提供的操作路径是以./
或.//
开头的相对路径时,很容易出现路径动态拼接错误的问题。
原因:代码在运行的时候,会以执行node命令时所处的目录,动态拼接出被操作文件的完整路径。
解决方案:在使用fs模块操作文件时,直接提供完整的路径,不要提供./
或.//
开头的相对路径,从而防止路径动态拼接的问题。
__dirname
表示当前文件所处的目录
fs.readFile((__dirname, './成绩1.txt'), 'utf8', function (err, dataStr) {if (err) {return console.log('读取失败' + err.message);}console.log('读取文件成功!' + dataStr);
1.3 path路径模块
path
模块是Node.js官方提供的、用来处理路径的模块。它提供了一系列的方法和属性,用来满足用户对路径的处理
需求。例如:
path.join()
方法,用来将多个路径片段拼接成一个完整的路径字符串;path.basename()
方法,用来从路径字符串中,将文件名解析出来;
如果要在JavaScript代码中,使用path模块来处理路径,则需要使用如下的方式先导入它:
const path = require('path');
1.3.1 path.join()拼接路径
/*导入path模块*/
const path = require('path')/*注意../会抵消前面的路径*/
const pathStr = path.join('/a', '/b/c', '../', './d', 'e') // ../有抵消作用
console.log(pathStr)
✅ 情况1:如果 成绩1.txt
就在 222
文件夹下,必须加 222
,否则 Node.js 找不到文件。
项目根目录
├── 222
│ └── 成绩1.txt
└── index.js
path.join(__dirname, '222', '成绩1.txt') //必须加222
✅ 情况2:如果 成绩1.txt
和 index.js
在同一个目录下,不需要加 222
,直接写文件名即可。
项目根目录
├── 成绩1.txt
└── index.js
path.join(__dirname, '成绩1.txt')
✅ 情况3:如果 222
是 index.js
的父目录
项目根目录
├── 222
│ └── index.js
│ └── 成绩1.txt
此时也不需要加 222
,直接写:
path.join(__dirname, '成绩1.txt')
1.3.2 path.basename()解析文件名
使用path.basename方法,可以获取路径中的最后一部分,经常通过这个方法获取路径中的文件名,语法格式如下:
path.basename(path[,ext])
- path必选参数,表示一个路径的字符串
- ext可选参数,表示文件扩展名
- 返回:表示路径中的最后一部分
/*导入path模块*/
const path = require('path');/*定义文件的存放路径*/
const fpath = './2.js';const fullName = path.basename(fpath);
console.log(fullName); //输出2.jsconst nameWithoutExit = path.basename(fpath, '.js');
/*打印出index,移除了.js*/
console.log(nameWithoutExit); // 输出2
1.3.3 path.extname()获取扩展名
使用path.extname()方法,可以获取路径中的扩展名部分,语法格式如下:
path.extname(path)
参数解读:
- path必选参数,表示一个路径的字符串
- 返回:返回得到的扩展名字符串
/*导入path模块*/
const path = require('path')/*定义文件的存放路径*/
const fpath = '/a/b/c/index.html'const fext =path.extname(fpath)
/*打印出.html, 即为文件的扩展名*/
console.log(fext)//输出 .html
1.4 http模块
1.4.1 服务器的基础概念
回顾:什么是客户端、什么是服务器?
在网络节点中,负责消费资源的电脑,叫做客户端;负责对外提供网络资源的电脑,叫做服务器。
http
模块是Node.js
官方提供的、用来创建web服务器的模块。通过http
模块提供的http.createServer()
方法,就能方便的把一台普通的电脑,变成一台Web服务器,从而对外提供Web资源服务。
服务器和普通电脑的区别在于,服务器上安装了web服务器软件,例如:lIS、Apache等。通过安装这些服务器软件,就能把一台普通的电脑变成一台web服务器。
在 Node.js 中,我们不需要使用 lIS、Apache 等这些第三方 web 服务器软件。因为我们可以基于 Nodejs 提供的http模块,通过几行简单的代码,就能轻松的手写一个服务器软件,从而对外提供web服务。
1.4.2 基本服务器创建
分四步如下
① 导入http模块
const http = require('http')
② 创建web服务器实例
const server = http.createServer()
③ 为服务器实例绑定request事件,监听客户的请求
为服务器实例绑定request事件,即可监听客户端发送过来的网络请求:
server.on('request', function(req,res){console.log('Someone visit our web serevr')
})
④ 启动服务器
server.listen(80, function(){console.log('server running at http://127.0.0.1:8080')
})
1.4.2.1 在第③步中的——req请求对象
只要服务器接收到了客户端的请求,就会调用通过server.on()
为服务器绑定的request事件处理函数。
如果想在事件处理函数中,访问与客户端相关的数据或属性,可以使用如下的方式:
server.on('request', (req) => {//req是请求对象,它包含了与客户端相关的数据和属性,例如://req.url是客户端请求的URL地址//req.method是客户端的method请求类型(GET、POST)const str = `Your request url is ${req.url}, and request method is ${req.method}`;console.log(str);
});
1.4.2.2 在第③步中的——res响应服务对象
在服务器的request事件处理函数中,如果想访问与服务器相关的数据或属性,可以使用如下的方式:
server.on('request', (req, res) => {//res是响应对象,它包含了与服务器相关的数据和属性,例如://要发送到客户端的字符串const str = `Your request url is ${req.url}, and request method is ${req.method}`;//res.end() 方法的作用://向客户端发送指定的内容,并结束这次请求的处理过程res.end(str);
});
1.4.2.3 解决中文乱码的问题
当调用res.end()
方法,向客户端发送中文内容的时候,会出现乱码问题,此时,需要手动设置内容的编码格式:
1.4.2.4 根据不同的url响应不同的html内容
核心实现步骤
- 获取请求的
url
地址 - 设置默认的响应内容为
404 Not found
- 判断用户请求的是否为
/
或/index.html
首页 - 判断用户请求的是否为
/about.html
关于页面 - 设置
Content-Type
响应头,防止中文乱码 - 使用
res.end()
把内容响应给客户端
const http = require('http');const server = http.createServer();server.on('request', (req, res) => {/*获取请求的URL地址*/const url = req.url;/*设置默认的响应内容404 Not found*/let content = '404 Not found!';/*判断用户请求的是否为/或/index.html首页*//*判断用户请求的是否为/about.html关于页面*/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()把内容响应给客户端*/res.end(content);
});
server.listen(80, () => {console.log('server running at http://127.0.0.1');
});
1.4.3 时钟web服务器案例
根据用户发送过来的url地址去读取数据,并将数据返回给用户,起到一个传递作用
优化路径,用户在网址栏什么也不输,就fpath赋值为./clock/index.html
如果用户输入了,但是直接在网址后缀index.html,就进入了else执行体,手动path.join上./clock
2 模块化
2.1 模块化的基本概念
模块化是指解决一个复杂问题时,自顶向下逐层把系统划分成若干模块的过程。对于整个系统来说,模块是可组合、分解和更换的单元。
编程领域中的模块化,就是遵守固定的规则,把一个大文件拆成独立并互相依赖的多个小模块。把代码进行模块化拆分的好处:
- 提高了代码的复用性
- 提高了代码的可维护性
- 可以实现按需加载
那么就引出了两个问题!!
- 使用什么样的语法格式来引用模块
- 在模块中使用什么样的语法格式向外暴露成员
2.2 Node.js中模块的分类
Node.js 中根据模块来源的不同,将模块分为了3大类,分别是:
- 内置模块(内置模块是由
Node.js
官方提供的,例如fs
、path
、http
等) - 自定义模块(用户创建的每个
.js
文件,都是自定义模块) - 第三方模块(由第三方开发出来的模块,并非官方提供的内置模块,也不是用户创建的自定义模块,使用前需要先下载)
2.2.1 加载模块
使用强大的require()
方法,可以加载需要的内置模块、用户自定义模块、第三方模块进行使用。例如:
//1.加载内置的fs模块
const fs =require('fs')
//2.加载用户的自定义模块
const custom =require('./custom.js')
//3.加载第三方模块(关于第三方模块的下载和使用,会在后面的课程中进行专门的讲解)
const moment=require('moment')
注意:使用require()方法加载其他模块时,会执行被加载模块中的代码,加载用户自定义模块,可以省略.js的后缀名
2.2.2 模块作用域
和函数作用域类似,在自定义模块中定义的变量、方法等成员,只能在当前模块内被访问,这种模块级别的访问限制,叫做模块作用域。好处是防止全局变量的污染。
2.2.3 向外共享模块作用域中的成员
2.2.3.1 module对象
在每个js
自定义模块中都有一个module对象,它里面存储了和当前模块有关的信息,打印如下:
2.2.3.2 module.exports对象
在自定义模块中,可以使用module.exports
对象,将模块内的成员共享出去,供外界使用。外界用require()
方法导入自定义模块时,得到的就是module.exports
所指向的对象。
/*
在外界使用require导入一个自定义模块的时候,得到的成员,就是那个模块中,通过module.exports指向的那个对象
*/
const m = require('./自定义模块.js')console.log(m)
自定义模块
/*在一个自定义模块中,默认情况下,module.exports = {}*/
//对module.exports挂上username属性
module.exports.username = 'itheima'
//挂上sayHello的方法
module.exports.sayHello = function(){console.log('Hello')
}
注意:
- 使用
require()
方法导入模块时,导入的结果,永远以module.exports
指向的对象为准。 module.exports.age = 12
和module.exports ={ age = 21}
中后者输出- 由于
module.exports
单词写起来比较复杂,为了简化向外共享成员的代码,Node 提供了exports
对象。默认情况下,exports
和module.exports
指向同一个对象。最终共享的结果,还是以module.exports
指向的对象为准。
2.2.4 CommonJS规定
Node.js遵循了CommonJS模块化规范,CommonJS
规定了模块的特性和各模块之间如何相互依赖。
2.3 npm与包(第三方模块)的搜索与下载
Node.js中的第三方模块有叫做包。包由第三方团队或个人开发的免费的方便我们写代码的工具。
国外有一家IT公司,叫做npm, Inc.这家公司旗下有一个非常著名的网站: npm | Home,它是全球最大的包共享平台,你可以从这个网站上搜索到任何你需要的包,只要你有足够的耐心!
npm, Inc.公司提供了一个地址为https://registry.npmjs.org/的服务器,来对外共享所有的包,我们可以从这个服务器上下载自己所需要的包。该公司也提供包的管理工具帮助我们的使用,这个工具不需要额外的下载,在我们下载node.js时就已经有了
格式化时间(不适用包的版本)
temp.js
function dateFormat(dtStr){const dt = new Date(dtStr)const y = dt.getFullYear()const m = padZero(dt.getMonth()+1)const d = padZero(dt.getDay())const hh = padZero(dt.getHours())const mm = padZero(dt.getMinutes())const ss = padZero(dt.getSeconds())y.innerHTML=y;m.innerHTML=m;d.innerHTML=d;hh.innerHTML=hh;mm.innerHTML=mm;ss.innerHTML=ss;return y+'-'+m+'-'+d+' '+hh+':'+mm+":"+ss
}//定义补0的操作
function padZero(n){return n>9 ? n: '0'+n
}module.exports = {dateFormat
}
temp1.js
//导入自定义的格式化时间模块
const TIME = require('./temp.js')//请调用方法,进行时间的格式化
const dt = new Date()const newDT = TIME.dateFormat(dt)console.log(newDT)
使用包的版本
m
如果想在项目中安装指定名称的包,需要运行如下的命令:
npm install 包的完整名称
上述的装包命令,可以简写成如下格式:
npm i 完整的包名称
const moment = require('moment')const dt = moment().format('YYYY-MM-DD HH:mm:ss')console.log(dt)
2.3.1 注意
可以在npm的第一个链接中找到包的使用说明书。
初次装包完成后,在项目文件夹下多一个叫做node_modules
的文件夹和package-lock.json
的配置文件。
其中:
node_modules
文件夹用来存放所有已安装到项目中的包。require
导入第三方包时,就是从这个目录中查找并加载包。package-lock.json
配置文件用来记录node_modules目录下的每一个包的下载信息,例如包的名字、版本号、下载地址等。
注意:程序员不要手动修改node_modules
或package-lock.json
文件中的任何代码,npm包管理工具会自动维护它们。
2.3.2 包管理配置文件
npm规定,在项目根目录中,必须提供一个叫做package.json的包管理配置文件。用来记录与项目有关的一些配置
信息。例如:
- 项目的名称、版本号、描述等
- 项目中都用到了哪些包
- 哪些包只在开发期间会用到
- 那些包在开发和部署时都需要用到
‼️ 如何记录项目中安装了哪些包
在项目根目录中,创建一个叫做package.json的配置文件,即可用来记录项目中安装了哪些包。从而方便剔除
node_modules目录之后,在团队成员之间共享项目的源代码。
注意:今后在项目开发中,一定要把node_modules
文件夹,添加到.gitignore
忽略文件中。
‼️ 快速创建package.json
npm包管理工具提供了一个快捷命令,可以在执行命令时所处的目录中,快速创建package.json这个包管理
配置文件:
//作用:在执行命令所处的目录中,快速新建package.json文件
npm init -y
注意:
上述命令只能在英文的目录下成功运行!所以,项目文件夹的名称一定要使用英文命名,不要使用中文,不能出现空格。运行npm口版本号,npm包管理工具会自动把包的名称和版本号,记录到package.json中
‼️ dependencies节点
package.json文件中,有一个dependencies节点,专门用来记录您使用npm install 命令安装了哪些包
。
‼️ 一次性安装所有的包
当我们拿到一个剔除了node_modules的项目之后,需要先把所有的包下载到项目中,才能将项目运行起来。
可以运行npm install
命令(或npm i
)一次性安装所有的依赖包
‼️ 卸载包
npm uninstall 具体的包名
命令执行成功后,会把卸载的包,自动从package.json
的dependencies中移除掉
‼️ devDependencies节点
如果某些包只在项目开发阶段会用到,在项目上线之后不会用到,则建议把这些包记录到devDependencies
节点中。
与之对应的,如果某些包在开发和项目上线之后都需要用到,则建议把这些包记录到dependencies
节点中。
//安装指定的包,并记录到devDependencies节点中
npm i 包名 -D
//注意:上述命令是简写形式,等价于下面完整的写法:
npm install 包名 --save-dev
2.3.3 包的分类
那些被安装到项目的node_modules
目录中的包,都是项目包。项目包又分为两类,分别是:
- 开发依赖包(被记录到
devDependencies
节点中的包,只在开发期间会用到) - 核心依赖包(被记录到
dependencies
节点中的包,在开发期间和项目上线之后都会用到)
全局包
- 在执行
npm install
命令时,如果提供了-g
参数,则会把包安装为全局包
。 - 全局包会被安装到
C:\Users\用户目录\AppData\Roaming\npm\node_modules
目录下。
只有工具性质的包,才有全局安装的必要性。因为它们提供了好用的终端命令。判断某个包是否需要全局安装后才能使用,可以参考官方提供的使用说明即可
npm i 包名 -g # 全局安装指定的包
npm uninstall 包名 -g # 卸载全局安装的包
⚠️i5ting_toc
是一个可以把md文档转为html页面的小工具,使用步骤如下:
# 将i5ting_toc安装为全局包
npm install -g i5ting_toc
#调用i5ting_toc,轻松实现md转html的功能
i5ting_toc -f 要转换的md文件路径 -o //在终端里面写
2.3.4 规范的包结构
在清楚了包的概念、以及如何下载和使用包之后,接下来,我们深入了解一下包的内部结构。一个规范的包,它的组成结构,必须符合以下3点要求:
- 包必须以单独的目录而存在
- 包的顶级目录下要必须包含
package.json
这个包管理配置文件 package.json
中必须包含name
,version
,main
这三个属性,分别代表包的名字、版本号、包的入口。
中间部分是开发属于自己的包并发布,有兴趣可以去看一下,我没有这个想法所以没看
2.4 模块的加载机制
2.4.1 优先从模块中缓存
模块在第一次加载后会被缓存。这也意味着多次调用require()
不会导致模块的代码被执行多次。
注意:不论是内置模块、用户自定义模块、还是第三方模块,它们都会优先从缓存中加载,从而提高模块的加载效率。
2.4.2 内置模块的加载机制
内置模块是由Node.js
官方提供的模块,内置模块的加载优先级最高。
例如,require('fs')
始终返回内置的fs
模块,即使在node_modules
目录下有名字相同的包也叫做fs
。
2.4.3 自定义模块的加载机制
使用require()
加载自定义模块时,必须指定以./
或.//
开头的路径标识符。在加载自定义模块时,如果没有指定./
或.//
这样的路径标识符,则node会把它当作内置模块或第三方模块进行加载。
同时,在使用require()
导入自定义模块时,如果省略了文件的扩展名,则Node.js
会按顺序分别尝试加载以下的文件:
①按照确切的文件名进行加载
②补全js
扩展名进行加载
③补全json
扩展名进行加载
④补全.node
扩展名进行加载
⑤加载失败,终端报错
const m = require('./test')
console.log(m)
2.4.4 第三方模块的加载机制
如果传递给require()的模块标识符不是一个内置模块,也没有以‘/或‘./”开头,则Node.js会从当前模块的父目录开始,尝试从/node_modules文件夹中加载第三方模块。
如果没有找到对应的第三方模块,则移动到再上一层父目录中,进行加载,直到文件系统的根目录。
例如,假设在'C:\Users\itheima\project\foo.js'
文件里调用了require(tools),则Node.js会按以下顺序查找:
①C:\Users\itheima\project\node_modules\tools
②C:\Users\itheima\node_modules\tools
③C:\Users\node_modules\tools
④C:\node_modules\tools
2.4.5 目录作为模块加载机制
当把目录作为模块标识符,传递给require()进行加载的时候,有三种加载方式:
① 在被加载的目录下查找一个叫做package.json
的文件,并寻找main属性,作为require
加载的入口
② 如果目录里没有package.json
文件,或者main入口不存在或无法解析,则Node.js
将会试图加载目录下的index.js
文件。
③ 如果以上两步都失败了,则Node.js
会在终端打印错误消息,报告模块的缺失:Error:Can not find module xxx
3. express的使用
这里是express中文使用说明书 ,在使用学习时可以参考一下。
3.1 什么是Express
- 官方给出的概念:Express是基于Node.js平台,快速、开放、极简的Web 开发框架。
- 通俗的理解:Express的作用和Node.js内置的http模块类似,是专门用来创建Web服务器的。
- Express的本质:就是一个npm上的第三方包,提供了快速创建Web服务器的便捷方法。
思考:不使用Express能否创建Web服务器?
答案:能,使用Node.js
提供的原生http
模块即可。
思考:有了http内置模块,为什么还有用Express?
答案:http内置模块用起来很复杂,开发效率低;Express
是基于内置的http
模块进一步封装出来的,能够极大的提高开发效率。
思考:http内置模块与Express是什么关系?
答案:类似于浏览器中WebAPI
和jQuery
的关系。后者是基于前者进一步封装出来的。
Express能做什么
对于前端程序员来说,最常见的两种服务器,分别是:
- Web网站服务器:专门对外提供Web网页资源的服务器。
- API接口服务器:专门对外提供API接口的服务器。
使用Express,我们可以方便、快速的创建Web网站的服务器或API接口的服务器
3.1.1 Express的安装
在项目所处的目录中,运行如下的终端命令,即可将express安装到项目中使用:
npm i express@4.17.1
3.1.2 基本使用
3.1.2.1 创建基本的Web服务器
//1.导入express
const express = require('express')
//2.创建web服务器
const app = express()//2.1 get post等等//3.调用app.listen(端口号,启动成功后的回调函数),启动服务器
app.listen(80,()=>{console.log('express server running at http://127.0.0.1')
})
3.1.2.1.1 监听GET请求
//参数1:客户端请求的URL地址
//参数2:请求对应的处理函数
// req:请求对象(包含了与请求相关的属性与方法)
// res:响应对象(包含了与响应相关的属性与方法)
app.get ('请求URL',function(req,res){/*处理函数*/})
可以用apipost进行接口测试
const express = require('express');
const app = express();
app.get('/user', (req, res) => {res.send('Hello World!get');
});
app.post('/', (req, res) => {res.send('Hello World!post');
});
app.listen(80, () => console.log('Server listening on port 80'));
3.1.2.1.2 监听POST请求
//参数1:客户端请求的URL地址
//参数2:请求对应的处理函数
// req:请求对象(包含了与请求相关的属性与方法)
// res:响应对象(包含了与响应相关的属性与方法)
app.post ('请求URL',function(req,res){/*处理函数*/})
3.1.2.1.3 获取URL中携带的参数
app.get('/',(req,res)=>{console.log(req.query)
})
通过req.query
对象,可以访问到客户端通过查询字符串的形式,发送到服务器的参数:
app·get('/',(req, res) =>{//req.query默认是一个空对象//客户端使用?name=zs&age=20这种查询字符串形式,发送到服务器的参数,//可以通过req.query对象访问到,例如://req.query.namereq.query.ageconsole.log(req.query)
})
3.1.2.1.4 获取URL中的动态参数
通过req.params
对象,可以访问到URL中,通过:
匹配到的动态参数:
//URL地址中,可以通过:参数名的形式,匹配动态参数值
app.get('/user/:id/:name', (req, res) => {//req.params默认是一个空对象//里面存放着通过:动态匹配到的参数值console.log(req.params)
})
3.1.2.2 托管静态资源
express提供了一个非常好用的函数,叫做express.static()
,通过它,我们可以非常方便地创建一个静态资源服务器,例如,通过如下代码就可以将public目录下的图片、CSS文件、JavaScript文件对外开放访问了:
app.use(express.static('public'))
注意:Express在指定的静态目录中查找文件,并对外提供资源的访问路径。因此,存放静态文件的目录名不会出现在URL中(比如说上面的public就不会出现在下面的url里)。
托管多个静态资源目录
如果要托管多个静态资源目录,请多次调用express.static()
函数:
app.use(express.static('public'))
app.use(express.static('files' ))
访问静态资源文件时,express.static()
函数会根据目录的添加顺序查找所需的文件。
3.1.2.3 nodemon的安装并使用
在编写调试Nodejs
项目的时候,如果修改了项目的代码,则需要频繁的手动close
掉,然后再重新启动,非常繁琐。
现在,我们可以使用nodemon
安装nodemon
在终端中,运行如下命令,即可将nodemon安装为全局可用的工具:
npm install -g nodemon
- 现在,我们可以将node命令替换为
nodemon
命令,使用nodemon app.js
来启动项目。这样做的好处是:代码被修改之后,会被nodemon
监听到,从而实现自动重启项目的效果。
node app.js
#将上面的终端命令,替换为下面的终端命令,即可实现自动重启项目的效果
nodemon app.js
3.2 Express路由
路由即为映射关系。举例如下
在Express中,路由指的是客户端的请求与服务器处理函数之间的映射关系。
Express中的路由分3部分组成,分别是请求的类型、请求的URL地址、处理函数,格式如下:
app.METHOD(PATH,HANDLER)
// METHOD === GET POST
// PATH URL地址
// HANDLER 处理函数
Express中的路由的例子
// GET /user - 获取用户信息
app.get('/', (req, res) => {res.send('Hello World');
});// POST /user - 创建用户
app.post('/', (req, res) => {res.send('Got a POST req');
});
3.2.1 路由的匹配过程
- 每当一个请求到达服务器之后,需要先经过路由的匹配,只有匹配成功之后,才会调用对应的处理函数。
- 在匹配时,会按照路由的顺序进行匹配,如果[请求类型] 和 [请求的URL] 同时匹配成功,则Express会将这次请求,转交给对应的function函数进行处理。
路由匹配的注意点:
- 按照定义的先后顺序进行匹配
- 请求类型和请求的URL同时匹配成功才会调用对应的处理函数
3.2.2 路由的挂载使用
在Express中使用路由最简单的方式,就是把路由挂载到app上,示例代码如下
const express = require('express');const app = express()
//挂载路由
app.get('/',(req,res)=>{res.send('Hello World')
})
app.post('/',(req,res)=>{res.send('Post Resqust')
})
app.listen(8080,()=>{console.log('Express服务器已启动,运行在 http://127.0.0.1:8080')
})
但是这个做法是初始,实际中 根本不可以这么做!!!! 因为如果体系越来越大,app挂在路由会越来越多,所以不会直接挂在app上
3.2.3 模块化路由
为了方便对路由进行模块化的管理,Express不建议将路由直接挂载到app上,而是推荐将路由抽离为单独的模块将路由抽离为单独模块的步骤如下:
①创建路由模块对应的js文件
②调用express.Router()
函数创建路由对象
③向路由对象上挂载具体的路由
④使用module.exports
向外共享路由对象
⑤使用app.use()
函数注册路由模块
路由模块
const express=require('express');
const router=express.Router();router.get('/',function(req,res){
res.send('Hello World');
});module.exports=router;
注册路由模块
// 1. 导入express模块
const express = require('express');// 2. 创建Express应用实例
const app = express();// 3. 定义路由
//1.导入路由模块
const router = require('./router.js')
//2.使用app.use()注册路由模块
app.use(router)/*注意:app.use()函数的作用,就是用来注册全局中间件*/// 4. 启动服务器
const PORT = 8080;
app.listen(PORT, () => {console.log(`Express服务器已启动,运行在 http://127.0.0.1:${PORT}`);
});
3.2.4 为路由模块添加前缀
类似于托管静态资源时,为静态资源统一挂载访问前缀一样,路由模块添加前缀的方式也非常简单:
//1.导入路由模块
const router = require('./router.js')
//2.使用app.use()注册路由模块,并添加统一的访问前缀/api
app.use('/api',router)
加了统一前缀之后访问时候也要在前面加上api才能访问到
3.3 Express中间件
中间件(Middleware) ,特指业务流程的中间处理环节。
多个中间件之间,共享同一份req
和res
。基于这样的特性,我们可以在上游的中间件中,统一为req
或res
对象添加自定义的属性或方法,供下游的中间件或路由进行使用。
3.3.1 Express中间件的调用流程
当一个请求到达Express的服务器之后,可以连续调用多个中间件,从而对这次请求进行预处理。
上一个中间件的输出是下一个中间件的输入
3.3.2 next函数的作用
Express的中间件,本质上就是一个function处理函数,Express中间件的格式如下:
注意:中间件函数的形参列表中,必须包含next参数。而路由处理函数中只包含req
和res
。
next函数是实现多个中间件连续调用的关键,它表示把流转关系转交给下一个中间件或路由。
3.3.3 定义中间件函数
可以通过以下方式定义一个最简单的中间件函数:
const mw = function (req, res, next) {console.log('这是一个最简单的中间件函数');next();
};
3.3.4 全局中间件
全局中间件是指所有客户端请求到达服务器时都会触发的中间件。
注意:既然要执行中间件函数,所以app.use(中间件函数一定是第一个被执行的,在app.use里面要放在第一个)
使用 app.use(中间件函数)
可以定义全局生效的中间件,如下所示:
const mw = function (req, res, next) {console.log('这是一个最简单的中间件函数');next();
};app.use(mw);
简化写法
可以直接在 app.use()
内部定义中间件:
app.use(function (req, res, next) {console.log('这是一个最简单的中间件函数');next();
});
定义多个全局中间件
可以使用 app.use()
依次定义多个全局中间件,服务器收到请求后会按照定义顺序依次执行:
app.use(function (req, res, next) {console.log('调用了第1个全局中间件');next();
});app.use(function (req, res, next) {console.log('调用了第2个全局中间件');next();
});app.get('/user', (req, res) => {res.send('User page.');
});
3.3.5 局部中间件
不使用app.use()
定义的中间件,叫做局部生效的中间件,局部中间件指的是仅在特定路由生效的中间件,示例代码如下:
// 定义中间件函数 mw1
const mw1 = function (req, res, next) {console.log('这是中间件函数');next();
};// 该中间件仅在当前路由生效
app.get('/', mw1, function (req, res) {res.send('Home Page.');
});// 这个路由不会受到 mw1 的影响
app.get('/user', function (req, res) {res.send('User page.');
});
3.3.6 多个局部中间件的使用方式
在路由中可以同时使用多个局部中间件,写法有以下两种:
// 方式 1:直接传递多个中间件
app.get('/', mw1, mw2, (req, res) => {res.send('Home page.');
});// 方式 2:使用数组传递多个中间件
app.get('/', [mw1, mw2], (req, res) => {res.send('Home page.');
});
3.3.7 使用中间件的注意事项
- 必须在路由之前注册中间件,否则不会生效。
- 客户端请求可以连续调用多个中间件进行处理。
- 执行完中间件代码后,必须调用
next()
,否则请求会卡住。 - 调用
next()
后,不要再编写额外代码,否则可能导致逻辑混乱。 - 多个中间件之间共享
req
和res
对象,可以在不同中间件中修改它们的值。
这样调整后,代码更清晰,同时避免了一些拼写和格式错误,例如 funtion(req,res)
应该是 function(req,res)
,app-get()
应该是 app.get()
等。
3.4 中间件的分类
在 Express 中,中间件可以分为以下几类:
3.4.1 应用级中间件
应用级中间件是绑定到 app
实例上的中间件,可以通过 app.use()
、app.get()
或 app.post()
进行定义。例如:
app.use((req, res, next) => {next();
});app.get('/', mw1, (req, res) => {res.send('Home Page.');
});
3.4.2 路由级中间件
路由级中间件的使用方式与应用级中间件类似,区别在于它绑定在 express.Router()
实例上,而不是 app
实例。适用于模块化管理路由。在全局中间件之前执行(如果路由匹配成功)
方式1:通过router.use()
注册(对路由组生效)
const express = require('express');
const router = express.Router();// 路由中间件:仅对/users/*生效
router.use('/users/:id', (req, res, next) => {console.log('User ID:', req.params.id);next();
});// 实际路由
router.get('/users/:id', (req, res) => {res.send(`User ${req.params.id}`);
});module.exports = router;
方式2:通过router.METHOD()
内联注册(对单一路由生效)
const validateId = (req, res, next) => {if (isNaN(req.params.id)) return res.status(400).send('Invalid ID');next();
};// 只对GET /users/:id生效
router.get('/users/:id', validateId, (req, res) => {res.send(`User ${req.params.id}`);
});
方式3:通过app.METHOD()
注册(直接绑定到路径)
const app = express();// 仅对GET /admin生效
app.get('/admin', (req, res, next) => {if (!req.query.token) return res.status(401).send('Unauthorized');next();
}, (req, res) => {res.send('Admin Panel');
});
3.4.3 错误处理中间件
错误处理中间件用于集中处理程序中的错误,它的回调函数有四个参数:err, req, res, next
。
const express = require('express');
const router = express.Router();router.get('/',(req, res) => {throw new Error('Something went wrong'); //手动抛出错误
});
module.exports = router;
const express = require('express');
const req = require('express/lib/request');
const app = express();
const port = 3000;
const router=require('./router');// 定义错误处理中间件
const errormiddleware = (err, req, res, next) => {console.error(err.stack);res.status(500).send('11111111111111111111111111');
};
app.use('/api',router);
app.use(errormiddleware);app.listen(port, () => {console.log(`Server running on port ${port}2`);});
⚠️ 注意:错误处理中间件必须有 err
参数,否则 Express 不会将其识别为错误处理函数。
错误处理中间件必须注册在所有路由之后,必须是全局的
3.4.4 Express 内置中间件
从 Express 4.16.0
开始,框架内置了 3 个常用的中间件,大幅提升开发效率:
内置中间件 | 作用 |
| 快速托管静态资源(HTML、CSS、JS、图片等) |
| 解析 |
| 解析 |
示例:
app.use(express.static('public')); // 托管静态资源
app.use(express.json()); // 解析 JSON 格式数据(对客户端返回的body进行解析,获取body->req.body)
app.use(express.urlencoded({ extended: true })); // 解析 URL-encoded 数据
上面三个中间件在路由之前配置
3.4.5 自定义 express.urlencoded
解析表单数据
实现一个自定义中间件,解析 POST
提交的表单数据:
实现步骤
- 监听
req
的data
事件,接收请求数据 - 监听
req
的end
事件,完成数据接收 - 使用
querystring
模块解析POST
数据 - 将解析后的数据挂载到
req.body
- 调用
next()
让请求继续执行
示例代码
3.4.5.1 服务器端代码
const express = require('express');
const customBodyParser = require('./custom-body-parser'); // 导入自定义中间件const app = express();// 注册自定义中间件
app.use(customBodyParser);app.post('/user', (req, res) => {res.send(req.body);
});// 启动服务器
app.listen(80, () => {console.log('Server running at http://127.0.0.1:80');
});
先在apipost里面写好body
3.4.5.2 自定义 body-parser
模块
const qs = require('querystring'); // Node.js 内置模块const bodyParser = (req, res, next) => {let str = '';// 监听 data 事件,接收数据(拼接一下,有可能数据过大,是被分割传递过来的)req.on('data', (chunk) => {str += chunk;});// 监听 end 事件,数据接收完毕req.on('end', () => {req.body = qs.parse(str); // 解析表单数据next();});
};module.exports = bodyParser;
3.5 使用Express写接口
使用Express写接口
//导入express模块
const express = require('express')//创建express的服务器实例
const app = express()//write your code here
//配置表单解析的中间件
app.use(express.urlencoded({extended:false}))//一定要在路由之前配置跨域cors这个中间件,从而解决接口跨域的问题
const cors = require('cors')app.use(cors())//导入路由模块
const router =require('./apiRouter')
//把路由模块注册到app上
app.use('/api',router)//调用app.listen方法,指定端口号并启动web服务器
app.listen(80,function(){console.log('express server running at http://127.0.0.1:80')
})
apiRouter.json
get请求是query,post请求是body
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 //需要响应给客户端的数据})
})router.post('/post', (req, res)=>{//通过req.body获取请求体中包含的url-encoded的格式数据const body = req.body//调用res.send()方法,向客户端响应结束res.send({status:0, //0为处理成功,1为处理失败msg:'POST 请求成功!', //状态描述data:query //需要响应给客户端的数据})
})module.exports = router
3.5.1 跨域问题及解决方案
在刚才编写的 GET 和 POST 接口中,存在一个严重问题:不支持跨域请求。常见的跨域解决方案有以下两种:
- CORS(主流解决方案,推荐使用)
- JSONP(仅支持 GET 请求,局限性较大)
3.6 使用 CORS 解决跨域问题
CORS
(Cross-Origin Resource Sharing,跨域资源共享)是一组 HTTP 响应头,用于决定浏览器是否允许跨域访问资源。现代浏览器基于同源策略,默认会阻止跨域请求,但服务器可以通过配置 CORS
相关响应头来解除此限制。
3.6.1 CORS 解决方案(推荐)
在 Express 中,可以使用 cors
中间件快速启用 CORS
:
3.6.2 配置 CORS 的步骤
- 安装
cors
:
npm install cors
- 在项目中引入:
const cors = require('cors')
- 在路由之前调用
app.use(cors())
:
const express = require('express')
const app = express()
const cors = require('cors')app.use(cors()) // 允许跨域app.get('/api/data', (req, res) => {res.json({ message: 'Hello World!' })
})app.listen(3000, () => console.log('Server running on port 3000'))
3.7 CORS 响应头配置
3.7.1 允许特定域访问
可以使用 Access-Control-Allow-Origin
头部来指定允许访问的外部域:
res.setHeader('Access-Control-Allow-Origin', 'http://example.com')
如果允许任何域访问:
res.setHeader('Access-Control-Allow-Origin', '*')
3.7.2 允许的请求头
默认情况下,CORS 只允许一些基本请求头。如果客户端需要额外的请求头,则需在服务器端进行声明:
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Custom-Header')
3.7.3 允许的请求方法
默认情况下,仅允许 GET
、POST
、HEAD
请求。如果要允许其他方法,如 PUT
、DELETE
,需额外配置:
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
3.7.4 简单请求 vs 预检请求
3.7.4.1 简单请求
满足以下两个条件的请求,属于简单请求:
- 请求方式:
GET
、POST
、HEAD
- 请求头仅包含以下字段:
-
Accept
Accept-Language
Content-Language
Content-Type
(仅限text/plain
、multipart/form-data
、application/x-www-form-urlencoded
)
3.7.4.2 预检请求
如果请求满足以下任意条件,浏览器会先发送 OPTIONS 预检请求:
- 请求方法是
PUT
、DELETE
、PATCH
- 请求头中包含自定义字段
- 发送
application/json
格式数据
示例:预检请求的处理
app.options('*', (req, res) => {res.setHeader('Access-Control-Allow-Origin', '*')res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')res.setHeader('Access-Control-Allow-Headers', 'Content-Type')res.sendStatus(204) // No Content
})
3.8 JSONP 方案(仅支持 GET)
3.8.1 JSONP 介绍
JSONP
通过 <script>
标签的 src
属性请求服务器数据,服务器返回一个 JavaScript 函数调用,前端执行该函数来获取数据。
3.8.2 JSONP 的特点
✅ 兼容老旧浏览器
✅ 不受同源策略限制
不属于真正的ajax请求,没有使用xmlhttprequest这个对象
❌ 仅支持 GET 请求,无法使用 POST
、PUT
、DELETE
3.8.3 JSONP 实现
- 优先声明 JSONP 接口
app.get('/api/jsonp', (req, res) => {const callback = req.query.callback //获取回调函数const data = { name: 'John', age: 30 } //要返回的数据const scriptData=`${callback}(${JSON.stringify(data)})`res.send(scriptData) //拼接
})
- 前端发起 JSONP 请求 :这里使用cdn引入 jQuery 发起 JSONP 请求
$.ajax({type: 'get',url: 'http://127.0.0.1:3000/api/jsonp',dataType: 'jsonp',success: function (data) {console.log(data)}})
总结
方案 | 是否推荐 | 适用场景 | 兼容性 | 支持的请求方法 |
CORS | ✅ 推荐 | 现代浏览器 | IE10+ | GET、POST、PUT、DELETE 等 |
JSONP | ❌ 有局限 | 仅支持 GET 请求 | 兼容老旧浏览器 | 仅支持 GET |
👉 CORS 是目前最主流的跨域解决方案,建议使用 CORS!
4. 在项目中操作 MySQL 数据库
安装MySQL Server还有MySQL Workbench(csdn有教程)
MySQL8.0版安装教程 + Workbench可视化配置教程(史上最细、一步一图解)_mysql workbench8.0安装教程-CSDN博客
node-mysql数据库的下载与安装_node安装mysql-CSDN博客
点击此处进行sql编写
小闪电就是运行
-- 是注释的意思
4.1 安装 MySQL 模块
在 Node.js 项目中操作 MySQL 数据库,需要使用 mysql
模块,该模块托管于 npm 上。安装方式如下:
npm install mysql
4.2 配置 MySQL 连接
在操作 MySQL 数据库之前,需要先配置 mysql
模块,具体步骤如下:
4.2.1 导入 mysql
模块
const mysql = require('mysql');
4.2.2 创建数据库连接池
const db = mysql.createPool({host: '127.0.0.1', // 数据库 IP 地址user: 'root', // 登录数据库的账号password: 'admin123', // 登录数据库的密码database: 'my_db_01' // 指定要操作的数据库
});
4.3 测试数据库连接
可以使用 db.query()
方法执行简单的 SQL 语句,测试 MySQL 连接是否正常:
db.query('SELECT * FROM users', (err, results) => {if (err) throw err;console.log(results);
});
5. 查询与插入数据
5.1 查询 users
表的所有数据
db.query('SELECT * FROM users', (err, results) => {if (err) return console.log(err.message); // 查询失败console.log(results); // 查询成功,输出所有用户数据
});
5.2 插入数据
5.2.1 方式 1:使用占位符插入数据
// 1. 要插入的数据对象
const a = { username: 'aa', password: '123321' }// 2. SQL 语句,? 作为占位符
const sqlStr = 'INSERT INTO users (username, password) VALUES (?, ?)';// 3. 传递数据
db.query(sqlStr, [a.username, a.password], (err, results) => {if (err) return console.log(err.message);if (results.affectedRows === 1) console.log('插入数据成功!');
});
5.2.2 方式 2:便捷插入
如果 a 对象的键名与表字段名一致,可以使用 SET
语法直接插入:
const a = { username: 'Spider-Man2', password: 'pcc4321' };
const sqlStr = 'INSERT INTO users SET ?';
db.query(sqlStr, user, (err, results) => {if (err) return console.log(err.message);if (results.affectedRows === 1) console.log('插入数据成功!');
});
6. 更新与删除数据
6.1 更新数据
6.1.1 方式 1:使用占位符更新数据
const user = { id: 7, username: 'aaa', password: 'ooo' };
const sqlStr = 'UPDATE users SET username=?, password=? WHERE id=?';
db.query(sqlStr, [user.username, user.password, user.id], (err, results) => {if (err) return console.log(err.message);if (results.affectedRows === 1) console.log('更新数据成功!');
});
6.1.2 方式 2:便捷更新
如果 user
对象的键名与表字段名一致,可以使用 SET
语法直接更新:
const user = { id: 7, username: 'aaaa', password: '0ooo' };
const sqlStr = 'UPDATE users SET ? WHERE id=?';
db.query(sqlStr, [user, user.id], (err, results) => {if (err) return console.log(err.message);if (results.affectedRows === 1) console.log('更新数据成功!');
});
6.2 删除数据
6.2.1 方式 1:通过 id
删除数据
const sqlStr = 'DELETE FROM users WHERE id=?';
db.query(sqlStr, 7, (err, results) => {if (err) return console.log(err.message);if (results.affectedRows === 1) console.log('删除数据成功!');
});
6.2.2 方式 2:标记删除(推荐)
直接删除数据可能会导致数据丢失,通常可以使用标记删除(即通过 status
字段表示是否删除):
const sqlStr = 'UPDATE users SET status=0 WHERE id=?';
db.query(sqlStr, 7, (err, results) => {if (err) return console.log(err.message);if (results.affectedRows === 1) console.log('标记删除成功!');
});
6.3 实战——Web前后端身份验证
6.3.1 Web开发模式
6.3.1.1 服务端渲染
- 服务端渲染的Web开发模式
服务端渲染(Server-Side Rendering,简称SSR)是指服务器在响应客户端请求时,通过字符串拼接等手段动态生成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. 将生成好的页面内容响应给客户端res.send(html);
});
通过这种方式,客户端直接拿到带有真实数据的HTML页面。
- 服务端渲染的优缺点
优点:
- 前端耗时少:服务器负责生成HTML内容,浏览器只需渲染页面,减少客户端计算负担,尤其在移动端更省电。
- 有利于SEO:由于服务器返回完整的HTML页面,爬虫能轻松抓取内容,对搜索引擎优化(SEO)更有利。
缺点: - 占用服务器资源:服务器需要动态拼接HTML,若请求量大,会增加服务器压力。
- 不利于前后端分离:前后端代码耦合度高,无法分工协作,尤其对于前端复杂度高的项目,开发效率较低。
6.3.1.2 前后端分离
- 前后端分离的Web开发模式
前后端分离(Frontend-Backend Separation)依赖于Ajax技术的广泛应用。后端仅负责提供API接口,前端通过Ajax调用这些接口获取数据并渲染页面。这种模式将前端和后端的职责清晰划分,提升了开发灵活性。
优点:
- 开发体验好:前端专注于UI开发,后端专注于API设计,双方可并行工作,且前端技术选型更自由。
- 用户体验好:Ajax支持局部刷新,用户无需等待整个页面加载,交互体验更流畅。
- 减轻服务器压力:页面渲染任务转移到客户端浏览器,服务器只需提供数据。
缺点: - 不利于SEO:页面内容需在客户端动态生成,爬虫难以抓取完整信息。
解决方法:借助Vue、React等框架的SSR(Server-Side Rendering)技术,可在服务端预渲染页面解决SEO问题。
- 如何选择Web开发模式
选择开发模式需结合具体业务场景:
- 企业官网:以展示为主,无复杂交互且需SEO优化,适合服务端渲染。
- 后台管理系统:交互性强,无SEO需求,适合前后端分离。
此外,部分网站采用混合模式,例如首屏使用服务端渲染提升加载速度,其余页面采用前后端分离以提高开发效率。这种折中方案兼顾了性能和开发体验。
6.3.2 身份认证
身份认证(Authentication),又称“身份验证”或“鉴权”,是通过一定手段确认用户身份的过程。
- 现实例子:高铁验票、手机指纹解锁、支付宝支付密码等。
- Web开发中:手机验证码登录、邮箱密码登录、二维码登录等。
目的:确保声称某种身份的用户确实是其所声称的身份。例如,取快递时需证明包裹归属;在互联网中,需确保“马云的存款”不会错误显示到“马化腾的账户”上。
- 不同开发模式下的身份认证
- 服务端渲染:推荐使用Session认证机制。
- 前后端分离:推荐使用JWT认证机制。
6.3.2.1 Session认证机制
- HTTP协议的无状态性
HTTP是无状态协议,意味着每次请求之间相互独立,服务器不会主动保留请求状态。理解这一点是学习Session认证的基础。 - 什么是Cookie
Cookie是存储在浏览器中的小型数据(不超过4KB),由名称(Name)、值(Value)及若干属性(如有效期、安全性、域名范围)组成。
特性:
- 自动发送:客户端请求时,浏览器自动携带当前域名下的未过期Cookie。
- 域名独立:不同域名下的Cookie互不干扰。
- 过期时限:Cookie有过期时间,过期后失效。
- 4KB限制:存储容量有限。
- Cookie在身份认证中的作用
- 客户端首次请求时,服务器通过响应头发送身份认证相关的Cookie,浏览器自动保存。
- 后续请求中,浏览器通过请求头自动携带Cookie,服务器据此验证身份。
- Cookie的安全性问题
Cookie存储在浏览器中,且可通过API读写,易被伪造。因此,不建议通过Cookie直接传输敏感数据。 - 提高身份认证的安全性
为避免伪造,需结合服务端验证机制。类似现实中刷会员卡认证,只有服务端确认合法性后才生效。 - Session的工作原理
Session基于Cookie实现:
- 用户登录时,服务器生成唯一Session ID,存储在Cookie中,并将用户数据保存在服务端。
- 后续请求携带Session ID,服务器根据ID查找对应数据,验证身份。
6.3.2.1.1 在Express中使用Session认证
- 安装express-session中间件
npm install express-session
- 配置express-session中间件
const session = require('express-session');
app.use(session({secret: 'keyboardcat', // 任意字符串,用于加密Session IDresave: false, // 是否强制保存未修改的Session,固定写法saveUninitialized: true // 是否为未初始化的Session分配存储空间,固定写法
}));
- 向Session存数据
配置完成后,可通过req.session
存储用户数据:
app.post('/api/login', (req, res) => {
//判断用户提交信息是否正确if (req.body.username !== 'admin' || req.body.password !== '000000') { return res.send({ status: 1, msg: '登录失败' });}req.session.user = req.body; // 存储用户信息req.session.islogin = true; // 存储登录状态res.send({ status: 0, msg: '登录成功' });
});
- 从Session取数据
通过req.session
读取数据:
app.get('/api/username', (req, res) => {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) => {req.session.destroy();res.send({ status: 0, msg: '退出登录成功' });
});
6.3.3 JWT认证机制
6.3.3.1 Session认证的局限性
- Session认证机制需要配合Cookie实现。
- Cookie默认不支持跨域访问,因此在前端跨域请求后端接口时,需要额外配置才能实现跨域Session认证。
- 注意:
-
- 当不存在跨域问题时,推荐使用Session认证。
- 当需要跨域请求时,不推荐Session认证,推荐使用JWT认证。
6.3.3.2 JWT简介
- JWT(JSON Web Token) 是目前最流行的跨域认证解决方案。
6.3.3.3.JWT的组成部分
- JWT由三部分组成,用英文“.”分隔:
-
- Header(头部)
- Payload(有效荷载)
- Signature(签名)
- 格式:
Header.Payload.Signature
6.3.3.4 JWT三个部分的含义
- Header:安全性相关,用于保证Token的安全性。
- Payload:真正的用户信息,经过加密生成的字符串。
- Signature:安全性相关,用于验证Token的完整性。
6.3.3.5 JWT的使用方式
- 客户端收到服务器返回的JWT后,通常存储在
localStorage
或sessionStorage
中。 - 客户端每次请求时,需携带JWT字符串进行身份认证。
- 推荐做法:将JWT放入HTTP请求头的
Authorization
字段,格式为:
Authorization: Bearer <token>
// 设置请求头req.headers['Authorization'] = `Bearer ${token}`;
6.3.4 在Express中使用JWT
6.3.4.1 安装JWT相关的包
运行以下命令安装:
npm install jsonwebtoken express-jwt
jsonwebtoken
:用于生成JWT字符串。express-jwt
:用于解析JWT字符串还原为JSON对象。
6.3.4.2 导入JWT相关的包
const jwt = require('jsonwebtoken'); // 生成JWT字符串
const { expressjwt: jwtMiddleware } = require('express-jwt'); // 解析JWT字符串,jwtMiddleware作为解析出来的expressjwt的别名
6.3.4.3 定义secret密钥
- 密钥用于加密和解密JWT字符串,确保安全性。
- 示例:
const secretKey = 'my_secret_key';// 密钥本质是一个字符串
6.3.4.4 在登录成功后生成JWT字符串
- 使用
jsonwebtoken
的sign()
方法生成JWT:
app.post('/api/login', function(req, res) {// 省略登录失败逻辑res.send({status: 200,message: '登录成功!',token: jwt.sign({ username: userinfo.username }, secretKey, { expiresIn: '30s' })// 参数:用户信息对象、密钥、配置对象(如过期时间)});
});
6.3.4.5 将JWT字符串还原为JSON对象
- 使用
express-jwt
中间件解析客户端发送的Token:
app.use(jwtMiddleware({ secret: secretKey, algorithms: ['HS256'] }).unless({ path: [/^\/api/] }));
// unless指定无需权限的接口,如/api/开头的接口,algorithms是指定的算法
6.3.4.6 使用req.user
获取用户信息
- 在使用
express-jwt
中间件后,req.user
是一个固定的属性名,它用于存储解析出的用户信息 - 在某些版本的
express-jwt
中,用户信息可能被挂载到req.auth
而不是req.user
。如果你遇到这种情况,可以通过设置requestProperty
属性来指定使用req.user
app.get('/admin/getinfo', function(req, res) {console.log(req.user);res.send({status: 200,message: '获取用户信息成功',data: req.user});
});
- 注意:解析出的用户信息会挂载到
req.user
属性上。
6.3.4.7 捕获解析JWT失败的错误
- 使用错误中间件处理Token过期或不合法的情况:
app.use((err, req, res, next) => {
//token解析失败的错误if (err.name === 'UnauthorizedError') {return res.send({ status: 401, message: '无效的token' });}res.send({ status: 500, message: '未知错误' });
});