初识Node
- 什么是Node.js?
Node.js是一个基于Chrome V8 引擎的JavaScript运行环境
- 注意:
浏览器是JavaScript的前端运行环境
Node.js是JavaScript的后端运行环境
Node.js中无法调用DOM和BOM等浏览器内置API
- Node.js可以做什么
①基于Express框架,可以快速构建web应用
②基于Electron框架,可以构建跨平台的桌面应用
③基于restify框架,可以快速构建API接口项目
④读写和操作数据库、创建实用的命令行工具辅助前端开发
fs文件系统模块:
fs模块是Node.js官方提供的、用来操作文件的模块。它提供了一系列的方法和属性,用来满足用户对文件的操作需求
- 例如:
fs.readFile()方法:用来读取指定文件中的内容
fs.writeFile()方法:用来向指定文件中写入内容
如果要在JavaScript代码中,使用fs模块来操作文件,则需要使用如下方式先导入它
const fs = require('fs');
1、fs.readFile()的语法格式:
fs.readFile(path[,options],callback)
参数一:必选参数,字符串,表示文件的路径
参数二:可选参数:表示以什么编码格式来读取
参数三:必选参数,文件读取完成后,通过回到函数拿到读取的结果
- 示例代码:
// 导入fs模块
const fs = require("fs");
// 调用
fs.readFile("./files/1.txt", "utf8", function (err, dataStr) {
console.log(err);
console.log(dataStr);
});
如果读取成功,err的值为null
如果读取失败,err的值为 错误对象,dataStr的值为undefined
- 判断文件是否读取成功
const fs = require("fs");
fs.readFile("./files/1.txt", "utf8", function (err, dataStr) {
if (err) {
return console.log("读取失败" + err.message);
}
console.log("读取成功" + dataStr);
});
2、向指定的文件写入内容:
fs.writeFile()的语法格式
fs.writeFile(file,data[,options],callback)
参数一:必选参数,需要指定一个文件路径的字符串,表示文件的存放路径
参数二:必选参数,表示要写入的内容
参数三:可选参数:表示以什么格式写入文件内容,默认值为utf8
参数四:必选参数:文件写入完成后的回调函数
- 示例代码:
const fs = require("fs");
fs.writeFile("./files/2.txt", "abcd", function (err) {
// 如果写入成功,err为null
// 如果写入失败,err为错误对象
console.log(err);
});
判断文件是否写入成功:
const fs = require("fs");
fs.writeFile("./files/3.txt", "ok", function (err) {
// 如果写入成功,err为null
// 如果写入失败,err为错误对象
console.log(err);
if (err) {
return console.log("文件写入失败" + err.message);
}
console.log("文件写入成功");
});
考试成绩整理:
const fs = require("fs");
fs.readFile("./files/成绩.txt", "utf8", function (err, dataStr) {
// 判断是否读取成功
if (err) {
return console.log("读取文件失败") + err.message;
}
console.log("读取文件成功" + dataStr);
// 把成绩的数据,按照空格进行分割
const arrOld = dataStr.split(" ");
// 训话分割后的数组,对每一项数据,进行字符串的替换操作
const arrNew = [];
arrOld.forEach((item) => {
arrNew.push(item.replace("=", ":"));
});
console.log(arrNew);
// 将新数组中的每一项,进行合并,得到一个新的字符串
const newStr = arrNew.join("\r\n");
console.log(newStr);
// 存
fs.writeFile("./files/成绩-ok.txt", newStr, function (err) {
if (err) {
console.log("写入文件失败" + err.message);
}
console.log("写入成功");
});
});
路径动态拼接的问题:
在使用fs模块操作文件时,如果提供的操作路径是以./或../开头的相对路径,很容易出现路径动态拼接错误的问题。
原因:代码在运行的时候,会以执行node命令的所处目录,动态拼接出被操作文件的完整路径
- __dirname 表示当前文件夹的目录
path路径模块:
path模块是Node.js官方提供的、用来处理路径的模块。它提供了一系列的方法和属性,用来满足用户对路径的处理需求
例如:
- path.join()方法,用来将多个路径片段拼接成一个完整的路径字符串
- path.basename()方法,用来将从路径字符串中,将文件名解析出来
如果要在JavaScript代码中,使用path模块来处理路径,则需要使用如下方式先导入它
const path = require('path')
路径拼接:
- path.join()语法格式
path.join([...paths])
- ...paths<string>路径片段序列
- 返回值:<string>
代码示例:
const path = require("path");
const pathStr = path.join("/a", "/b/c", "../", "/d", "/e");
console.log(pathStr);
const pathStr2 = path.join(__dirname, "./files/1.txt");
console.log(pathStr2);
注意:凡是涉及到路径拼接的操作,都要使用path.join()方法进行处理,不要直接使用+字符串的拼接
const path = require("path");
const fs = require("fs");
const pathStr = path.join("/a", "/b/c", "../", "/d", "/e");
console.log(pathStr);
const pathStr2 = path.join(__dirname, "./files/1.txt");
console.log(pathStr2);
fs.readFile(
path.join(__dirname, "/files/1.txt"),
"utf8",
function (err, dataStr) {
if (err) {
return console.log(err.message);
}
console.log(dataStr);
}
);
获取路径中的文件名:
- path.basename()的语法格式
使用path.basename()方法可以获取路径中的最后一部分,经常通过这个方法获取路径中的文件名,语法如下:
path.basename(path[,ext])
- path<string>必选参数,表示一个路径的字符串
- ext<string>可选参数,表示文件扩展名
- 返回:<string>表示路径中的最后一部分
const path = require("path");
const fpath = "/a/b/c/index.html";
// 带上文件扩展名
const fullName = path.basename(fpath);
console.log(fullName);
// 只拿文件名称
const nameWithoutExt = path.basename(fpath, ".html");
console.log(nameWithoutExt);
使用path.extname()方法可以获取路径中的扩展名部分,语法如下:
path.extname(path)
- path<string>必选参数,表示一个路径的字符串
- 返回:<string>返回得到的扩展名字符串
const fpath = "/a/b/c/index.html";
// 获取文件扩展名
const fext = path.extname(fpath);
console.log(fext);
拆分html、css、js合为一体的文件
实现步骤:
- 创建两个正则表达式,分别用来匹配<style>和<script>标签
- 使用fs模块,读取需要被处理的html文件
- 自定义resolveCSS方法,来写入index.css样式文件
- 自定义resolveJS方法,来写入index.js脚本文件
- 自定义resolveHTML方法,来写入index.html文件
匹配正则:
//匹配<style></style>标签的正则
//其中 \s 表示空白字符;\S 表示非空白字符; *表示撇配任意次
const regStyle = /<style>[\s\S]*<\/style>/
//匹配<script></script>标签的正则
const regScript = /<script>[\s\S]*<\/script>/
案例代码:
// 导入fs
const fs = require("fs");
// 导入path
const path = require("path");
const regStyle = /<style>[\s\S]*<\/style>/;
const regScript = /<script>[\s\S]*<\/script>/;
// 调用fs.readFile()
fs.readFile(
path.join(__dirname, "./files/index.html"),
"utf8",
function (err, dataStr) {
if (err) return console.log("读取失败" + err.message);
resolveCSS(dataStr);
resolveJS(dataStr);
resolveHTML(dataStr);
}
);
// 处理css
function resolveCSS(htmlStr) {
// 使用正则提取页面中<style></style>标签
const r1 = regStyle.exec(htmlStr);
// console.log(r1);
// 将提取出来的样式字符串,做进一步处理
const newCSS = r1[0].replace("<style>", "").replace("</style>", "");
// console.log(newCSS);
// // 将提取出来的css样式,写入到index.css文件中
fs.writeFile(path.join(__dirname, "./files/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);
// 将提取出来的脚本字符串,做进一步的处理
const newJS = r2[0].replace("<script>", "").replace("</script>", "");
// 将提取出来的js脚本,写入到index.js文件中
fs.writeFile(path.join(__dirname, "./files/index.js"), newJS, (err) => {
if (err) return console.log("写入JavaScript脚本失败" + err.message);
console.log("写入JS脚本");
});
}
// 处理html文件
function resolveHTML(htmlStr) {
// 使用字符串replace方法,把内嵌<style>和<script>标签,替换为外联的<link>和<script>标签
const newHTML = htmlStr
.replace(regStyle, '<link rel="stylesheet" href="./files/index.css">')
.replace(regScript, '<script src="./files/index.js"></script>');
// 替换完成后的html代码,写入到index.html文件中
fs.writeFile(
path.join(__dirname, "./files/Newindex.html"),
newHTML,
(err) => {
if (err) return console.log("写入html文件失败" + err.message);
console.log("写入HTML页面成功");
}
);
}
注意点:
- fs.writeFile()方法只能用来创建文件,不能用来创建路径
- 重复调用fs.writeFile()写入同一个文件,新写入的内容会覆盖之前的旧内容
HTTP模块:
http模块是Node.js官方提供的、用来创建web服务器的模块,通过http模块提供的http.createServer()方法,就能方便的把一台普通的电脑,变成一台web服务器,从而对外提供web资源服务
如果要希望使用http模块创建web服务器,则需要导入它
const http = require('http')
创建web服务器的基本步骤:
①导入http模块
②创建web服务器实例
③为服务器实例绑定request事件,监听客户端的请求
④启动服务器
- 导入http模块:
const http = require('http')
- 创建web服务器实例
const server = http.createServer()
- 为服务器实例绑定request事件
server.on('request',(req,res)=>{
//只要有客户端来请求我们自己的服务器,就会触发request事件,从而调用这个事件处理函数
console.log('Someone visit our web server')
})
- 启动服务器
//调用werver.listen(端口号,cb回调)方法,即可启动web服务器
server.listen(80,()=>{
console.log('http server running at http://127.0.0.1')
})
- req请求对象:
只要服务器接收到了客户端的请求,就会通过server.on()为服务器绑定的request事件处理函数
如果想在事件处理函数中,访问与客户端相关的数据或属性,可以使用如下方式:
server.on('request',(req)=>{
//req是请求对象,它包含了与客户端相关的数据和属性,例如:
//req.url 是客户端请求的URL地址
//req.method 是客户端的method请求类型
const str = `Your request url is ${req.url},and request method is ${req.method}`
console.log(str)
})
- 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)
})
解决中文乱码问题:
当调用res.end()方法,向客户端发送中文内容的时候,会出现乱码问题,此时,需要手动设置内容的编码格式:
res.setHeader('Content-Type','text/html;charset=utf-8')
根据不同的url响应不同的内容:
核心实现步骤:
- 获取请求的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首页
if (url == "/" || url == "/index.html") {
content = "<h1>首页</h1>";
}
// 判断用户请求的是否为/about.html关于页面
else if (url == "/about") {
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");
});
请求本地文件:
实现步骤:
- 导入需要的模块
- 创建基本的web服务器
- 将资源的请求url地址映射为文件的存放路径
- 读取文件内容并响应给客户端
- 优化资源的请求路径
// 导入http模块
const http = require("http");
// 导入fs模块
const fs = require("fs");
// 导入path文件
const path = require("path");
// 创建web服务器
const server = http.createServer();
// 监听web服务器
server.on("request", (req, res) => {
//获取到客户请求的url
const url = req.url;
console.log(url);
//把请求的url地址映射为具体的存放路径
// const fpath = path.join(__dirname, "../node/files", url);
let fpath = "";
if (url == "/") {
fpath = path.join(__dirname, "../node/files/index.html");
} else {
fpath = path.join(__dirname, "../", "/node/files", url);
}
console.log(fpath);
fs.readFile(fpath, "utf8", (err, dataStr) => {
if (err) return console.log("404 Not Fount");
res.end(dataStr);
});
});
// 启动服务器
server.listen(80, () => {
console.log("server running at http://127.0.0.1");
});
模块化:
模块化是指解决一个复杂问题时,自顶向下逐层把系统划分成若干模块的过程,对于整个系统来说,模块是可组合,分解和更换的单元
- 变成邻域中的模块化:
遵守固定的规则,把一个大文件拆分成独立并互相依赖的多个小模块
好处:
- 提高了代码的复用性
- 提高了代码的可维护性
- 可以实现按需加载
模块化规范:
就是对代码进行模块化的拆分与组合时,需要遵守的那些规则
例如:
- 使用什么样的语法格式来引用模块
- 在模块中使用什么样的语法格式向外暴露成员
好处:大家都遵循同样的模块化规范写代码,降低了沟通的成本,极大方便了各个模块之间的相互调用
Node.js中模块的分类:
Node.js中根据模块来源的不同,将模块分为了3大类,分别是:
- 内置模块:是Node.js官方提供的,例如fs、path、http等
- 自定义模块:用户创建的每个.js文件,都是自定义模块
- 第三方模块:并非官方提供的内置模块,也不是用户自定义的,使用需要下载
加载模块:
使用强大的require()方法,可以加载需要的内置模块、自定义模块、第三方模块进行使用
注意:在使用require()方法,可以省略后缀名
模块作用域:
和函数作用域类似,在自定义模块中定义的变量,方法等成员,只能在当前模块内被访问,这种模块级别的访问限制,叫做模块的作用域
好处:防止全局污染
module对象:
在每个.js自定义模块中有一个module对象,它里面存储了和当前模块有关的信息,打印如下:
Module {
id: '.',
path: 'C:\\Users\\admin\\Desktop\\前端知识点\\code\\node\\web服务器',
exports: {},
parent: null,
filename: 'C:\\Users\\admin\\Desktop\\前端知识点\\code\\node\\web服务器\\10.演
示module对象.js',
loaded: false,
children: [],
paths: [
'C:\\Users\\admin\\Desktop\\前端知识点\\code\\node\\web服务器\\node_modules', 'C:\\Users\\admin\\Desktop\\前端知识点\\code\\node\\node_modules',
'C:\\Users\\admin\\Desktop\\前端知识点\\code\\node_modules',
'C:\\Users\\admin\\Desktop\\前端知识点\\node_modules',
'C:\\Users\\admin\\Desktop\\node_modules',
'C:\\Users\\admin\\node_modules',
'C:\\Users\\node_modules',
'C:\\node_modules'
]
}
- module.exports对象:
在自定义模块中,可以使用module.exports对象,将模块内的成员共享出去,供外界使用
外界用require()方法,导入自定义模块时,得到的就是module.exports所指向的对象
- 共享成员时注意点
使用require()方法导入模块时,导入的结果,永远以module.exports指向的对象为准
exports对象
由于module.exports单词写起来比较复杂,为了简化,Node提供了exports对象,默认情况下exports和module.exports指向同一个对象,最终共享的结果,还是以module.exports指向的对象为准
注意:为了防治混乱,建议不要在同一个模块中同时使用exports和module.exports
Node.js中的模块化规范:
Node.js遵循了CommonJS模块化规范,CommonJS规定了模块的特性和各模块之间如何让相互依赖
CommonJS规定:
- 每个模块内部,module变量代表当前模块
- module变量是一个对象,它的exports属性(即module.exports)是对外的接口
- 加载某个模块,其实是加载该模块的module.exports属性,require()方法用于加载模块
包:
- 什么是包:
Node.js中的第三方模块又叫做包
- 包的来源:
不同于Node.js中的内置模块与自定义模块,包是由第三方个人或团队开发出来的,免费供所有人使用
- 为什么需要包:
由于Node.js的内置模块仅提供了一些底层的API,导致在基于内置模块进行项目开发时,效率低,包是基于内置模块封装出来的,提高了更高级、更方便的API,极大的提高了开发效率
npm初体验:
- 格式化时间的传统做法:
创建格式化时间的自定义模块
定义格式化时间的方法
创建补零函数
从自定义模块中导出格式化时间的函数
导入格式化时间的自定义模块
调用格式化时间的函数
- dateFormat.js:
// 定义格式化时间的方法
function dateFormat(dtStr) {
const dt = new Date(dtStr);
const y = dt.getFullYear();
const m = padZero(dt.getMonth() + 1);
const d = padZero(dt.getDate());
const hh = dt.getHours();
const mm = padZero(dt.getMinutes());
const ss = padZero(dt.getSeconds());
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`;
}
// 补零函数
function padZero(n) {
return n > 9 ? n : "0" + n;
}
module.exports = {
dateFormat,
};
- test.js:
// 导入自定义格式化时间的模块
const TIME = require("./dateFormat");
// 调用方法,进行时间的格式化
const dt = new Date();
const newDT = TIME.dateFormat(dt);
console.log(newDT);
- 格式化时间的高级做法:
使用npm包管理工具,在项目中安装格式化时间的包 moment
使用require()导入格式化时间的包
参考moment的官方API文档对时间进行格式化
- 在项目中安装包命令
npm install 包完整名称
或
npm i 包完整名称
- 初次装包后多了哪些文件
初次装包完成后,在项目文件夹下多了一个叫做node_modules的文件夹和package-lock.json的配置文件
其中,node_modules文件夹用来存放已安装到项目中的包,require()导入第三方包时,就是从这个目录中查找并加载包
package-lock.json配置文件用来记录node_modules目录下的每一个包的下载信息,例如包的名字、版本号、下载地址等
- 安装指定版本的包:
默认情况下,使用npm install命令安装包的时候,会自动安装最新版本的包。如果需要安装指定版本的包,可以在包名之后,通过@符号指定具体的版本号
- 包的语义化版本规范
包的版本号是以“点分十进制”形式进行定义的,总共有三位数字,例如:2.24.0
- 第一位数字:大版本
- 第二位数字:功能版本
- 第三位数字:Bug修复版本
包管理配置文件:
npm规定,在项目根目录,必须提供一个叫package.json的包管理配置文件。用来记录与项目有关的一些配置信息
- 项目的名称、版本号、描述等
- 项目中都用到了哪些包
- 哪些包只在开发期间会用到
- 哪些包在开发和部署时都需要用到
如何记录项目中安装了哪些包:
在项目根目录,创建一个叫package.json的配置文件,即可用来记录项目中安装了哪些包。从而方便剔除node_modules目录之后,在团队成员之间共享的源代码
注意:开发中,一定要把node_modules文件夹,添加到.gitigonre忽略文件中
快速创建package.json
npm init -y
- dependencies节点:
专门用来记录使用npm install 命令安装了哪些包
- 一次性安装所有的包:
npm install
读取到记录的所有依赖包名称和版本号之后,npm包管理工具会把这些包一次性下载到项目中
- 卸载包:
可以运行 npm uninstall 命令,来卸载包
npm uninstall moment
- devDependencies节点:
如果某些包只在项目开发阶段会用到,在项目上线之后就不会用到,则建议把这些包记录到devDependencies节点中
与之对应的,如果某些包在开发和项目上线之后都需要用到,则建议把这些包记录到dependencies节点中
npm i 包名 -D
或
npm install 包名 --save-dev
- 解决下包速度慢的问题:
淘宝NPM镜像服务器:
切换npm的下包镜像源
npm config set registry=https://registry.npm.taobao.org/
- nrm:
为了更方便的切换下包的镜像源
npm i nrm -g
//查看所有可用镜像源
nrm ls
//切换npm下载镜像
nrm use taobao
包的分类:
- 项目包:
哪些被安装到项目的node_modules 目录中的包,都是项目包
项目包又分为两类,分别是:
- 开发依赖包:记录到devDependencied节点中,只在开发时用
- 核心依赖包:记录到dependencie节点中的包,开发和上线都用
- 全局包:
在执行npm install命令时,如果提供了-g参数,则会把包安装为全局包
安装全局包
npm i 包名 -g
卸载
npm uninstall 包名 -g
注意:
- 只有工具性质的包,才有安装全局的必要性,因为他们提供了好用的终端命令
- 判断某个包是否需要全局安装后才能使用,可以参考官方提供的使用说明即可
规范的包结构:
一个规范的包,它的组成结构,必须符合以下3点要求
- 包必须以单独的目录存在
- 包的顶级目录下要必须包含package.json这个包管理配置文件
- package.json中必须包含name,version,main这三个属性,分别代表包的名字、版本号、包的入口
开发属于自己的包:
- 初始化包的基本脑结构:
- 新建itheima-tools文件夹,作为包的根目录
在itheima-tools文件夹中,新建如下三个文件
- package.json(包管理配置文件)
- index.js(包的入口文件)
- README.md(包的说明文档)
- 初始化package.json
{
"name":"itleima-tools",
"version": "1.0.0",
"main": "index.js",
"description": "提供了格式化时间、HTMLEscape相关的功能",
"keywords": ["itheima","detaFormat","escape"],
"license": "ISC"
}
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 = dt.getHours();
const mm = padZero(dt.getMinutes());
const ss = padZero(dt.getSeconds());
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`;
}
// 补零方法
function padZero(n) {
return n > 9 ? n : "0" + n;
}
module.exports = {
dateFormat,
};
5、在index.js中定义转义HTML的方法
// 定义转义HTML字符的函数
function htmlEscape(htmlstr) {
return htmlstr.replace(/<|>|"|&/g, (match) => {
switch (match) {
case "<":
return "<";
case ">":
return ">";
case '"':
return """;
case "&":
return "&";
}
});
}
6、在index.js中定义还原HTML字符串的函数
//定义还原HTML字符串的函数
function htmlUnEscape(str) {
return str.replace(/<|>|"|&/g, (match) => {
switch (match) {
case "<":
return "<";
case ">":
return ">";
case """:
return '"';
case "&":
return "&";
}
});
}
7、将不同的功能进行模块化拆分
- 将格式化时间的功能,拆分到src->dateFormat.js中
- 将处理HTML字符串的功能,拆分到src->htmlEscape.js中
- 在index.js中,导入两个模块,得到需要向外共享的方法
- 在index.js中,使用module.exports把对应的方法共享出去
8、编写包的说明文档
- 安装方式
- 导入方式
- 格式化时间、
- 转义HTML中的特殊字符
- 还原HTML中的特殊字符
- 开源协议
发布npm包
npm账号注册完成后,可以在终端中执行npm login命令,依次输入用户名、密码、邮箱后,即可登录成功
- 注意:在运行npm login 之前,必须先把下包的服务器地址切换为npm的官方服务器。否则会导致发布包失败
- 把包发布到npm上
将终端切换到包的跟目录之后,运行npm publish 命令,即可将包发布到npm上(包不能雷同)
- 删除已发布的包
运行npm unpublish 包名 --force 命令,即可从npm删除已发布的包
注意:
- npm unpublish命令只能删除72小时以内发布的包
- npm unpublish删除的包,在24小时内不允许重复发布
- 发包的时候要慎重,尽量不要往npm上发布没有意义的包
模块的加载机制:
优先从缓存中加载:
模块在第一次加载后会被缓存。这也意味着多次调用require()不会导致模块的代码被执行多次
注意:不论是内置模块、用户自定义模块、还是第三方模块,他们都会优先从缓存中加载,从而提高模块加载的效率
内置模块的加载机制:
内置模块是由Node.js官方提供的模块,内置模块的加载优先级最高
自定义模块的加载机制:
使用require()加载自定义模块时,必须指定以./或../开头的路径标识符,在加载自定义模块时,如果没有指定./或../这样的路径标识符,则node会把它当作内置模块或第三方模块进行加载
同时,在使用require()导入自定义模块时,如果省略了文件的扩展名,则Node.js会按顺序分别尝试以下文件:
- 按照确切的文件名进行加载
- 补全.js扩展名进行加载
- 补全.json扩展名进行加载
- 补全.node扩展名进行加载
- 加载失败,终端报错
第三方模块的加载机制:
如果没有找到对应的第三方模块,则移动到再上一层父目录中,进行加载,直到文件系统的根目录
目录作为模块:
当把目录作为模块标识符,传递给require()进行加载的时候,有三种加载模式
- 再被加载的目录下查找一个叫做package.json的文件,并寻找main属性
- 如果目录里没有package.json文件,或者main入口不存在,则Node.js会试图找index.js文件
- 如果以上两步都失败了,则Node.js会在终端打印错误消息,报告模块的缺失:Error:Cannot find module 'xxx'
Express:
- 什么是Express:
Express的作用和Node.js内置的http模块类似,是专门用来创建web服务器的
Express的本质:就是一个npm上的第三包,提供了快速创建服务器的便捷方法
- 有了htpp内置模块,为什么还要用Express
http内置模块用起来很复杂,开发效率低,Express是基于内置http模块进一步封装出来的,能够极大的提高开发效率
- http内置模块与Express是什么关系
类似与浏览器中的webAPI和jQuery的关系
- Express能做什么
对于前端程序员来说,最常见的两种服务器,分别是:
- web网站服务器:专门对外提供web网页资源的服务器
- API接口服务器:专门对外提供API接口的服务器
使用Express,我们可以方便,快速的创建web网站服务器或API接口服务器
安装:
- 在项目所处的目录中,运行如下命令:
npm i express @4.17.1
- 创建基本的web服务器
//导入express
const express = require('express')
//创建web服务器
const app = express()
//调用app.listen(端口号,启动成功后的回调函数),启动服务器
app.listen(80,()=>{
console.log('express server running at http://127.0.0..1')
})
监听GET请求:
- 通过app.get()方法,可以监听客户端的GET请求
//参数1:客户端请求的url地址
//参数2:请求对于的处理函数
//req:请求对象(包含了与请求相关的属性与方法)
//res:响应对象(包含了与响应相关的属性与方法)
app.get('请求的URL',function(req,res){处理函数})
监听POST请求:
- 通过app.post()方法,可以监听客户端的POST请求
//参数1:客户端请求的url地址
//参数2:请求对于的处理函数
//req:请求对象(包含了与请求相关的属性与方法)
//res:响应对象(包含了与响应相关的属性与方法)
app.post('请求的URL',function(req,res){处理函数})
把内容响应给客户端
通过res.send()方法,可以把处理好的内容,发送给客户端
app.get('/user',(req,res)=>{
res.send('name:'wss',age:18,gender:'男')
})
app.post('/user',(req,res)=>{
res.send('请求成功')
})
获取URL中携带的查询参数
通过req.query对象,可以访问到客户端通过查询字符串的形式,发送到服务器的参数
app.get("/", (req, res) => {
// 通过req.query可以获取到客户端发送来的参数 查询参数
// 默认情况下是空对象
console.log(req.query);
res.send(req.query);
});
获取URL中的动态参数
通过req.params对象,可以访问到URL中,通过:匹配到的动态参数
app.get("/user/:id/:name", (req, res) => {
// req.params是动态匹配到的URL参数,默认也是空对象
console.log(req.params);
res.send(req.params);
});
托管静态资源:
- express.static():
express提供了一个非常好用的函数,叫做express.static(),通过它,我们可以非常方便地创建一个静态资源服务器,例如:通过如下代码就可以将public目录下的图片、css文件、JavaScript文件对外开放访问了
app.use(express.static('public'))
- 托管多个静态资源:
使用多次express.static()函数
app.use(express.static('public'))
app.use(express.static('files'))
访问静态资源文件时,express.static()函数会根据目录的添加顺序查找所需的文件
挂载路径前缀:
app.use('/public',express.static('public'))
nodemon:
当代码修改后,nodemon会自动帮我们重启项目
npm install -g nodemon
- 使用nodemon
nodemon app.js
路由:
在Express中,路由指的是客户端的请求与服务器处理函数之间的映射关系
Express中的路由分3部分组成,分别是:请求的类型、请求的URL地址、处理函数,格式如下:
app.METHOD(PATH,MANDLER)
Express中路由的例子:
//匹配GET请求,且请求URL为 /
app.get('/',function(req,res)=>{
res.send('hello world')
})
//匹配POST请求,且请求URL为 /
app.post('/',function(req,res)=>{
res.send('Got a POST request')
})
路由的匹配过程:
每当一个请求到达服务器之后,需要先经过路由的匹配,只有匹配成功后,才会调用对应的处理函数
在匹配时,会按照路由的顺序进行匹配,如果请求类型和请求的URL同时匹配成功,则Express会将这次请求,转交给对应的function函数进行处理
- 路由匹配规则
- 按照定义的先后顺序进行匹配
- 请求类型和请求的URL同时匹配成功,才会调用对应的处理函数
const express = require("express");
const app = express();
// 挂载路由
app.get("/", function (req, res) {
res.send("hello world");
});
app.post("/", function (req, res) {
res.send("Post Request");
});
app.listen(80, () => {
console.log("http://127.0.0.1");
});
模块化路由:
为了方便对路由进行模块化管理,Express不建议将路由直接挂载到app上,而是推荐将路由抽离为单独的模块
- 步骤:
- 创建路由模块对应的.js文件
- 调用express.Router()函数创建路由对象
- 向路由对象上挂载具体的路由
- 使用module.exports向外共享路由对象
- 使用app.use()函数注册路由模块
- 创建路由模块:
- 使用路由模块:
//导入路由模块
const userRouter = require('./router/user.js')
//使用app.use()注册路由模块
app。use(userRouter)
注意:app.use() 函数的作用,就是用来注册全局中间件
为路由模块添加前缀:
//导入
const userRouter = require('./router/user.js')
//添加前缀
app.use('/api',userRouter)
中间件:
中间件:特指业务流程的中间处理环节
Express的中间件,本质上就是一个function处理函数,Express中间件的格式如下
注意:中间件函数的形参列表中,必须包含next参数,而路由处理函数中只包含req和res
next函数的作用:
next函数是实现多个中间件连续调用的关键,它表示把流转关系转交给下一个中间件或路由
Express中间件的初体验:
1、定义中间件函数:
可以通过如下方式,定义一个最简单的中间件函数:
//常量mw所指向的,就是一个中间件
const mw = function(req,res,next) {
console.log('这是一个最简单的中间件函数')
//注意:在当前中间件的业务处理完毕后,必须调用next()函数
//表示把流转关系交给下一个中间件或路由
}
2、全局生效的中间件
客户端发起的任何请求,到达服务器之后,都会触发的中间件,叫做全局生效的中间件
通过调用app.use(中间件函数),即可定义一个全局生效的中间件
//常量mw所指向的,就是一个中间件函数
const mw = function(req,res,next) {
console.log('这是一个最简单的中间件函数')
next()
}
//全局生效的中间件
app.use(mw)
3、定义全局中间件的简化方式
//全局生效的中间件
app.use(function (req,res,next)=>{
console.log('这是一个最简单的中间件函数')
next()
})
4、中间件的作用:
多个中间件之间,共享同一份req和res,基于这样的特性,我们可以在上游的中间件中,统一为req或res对象添加自定义的属性或方法,供下游的中间件或路由进行使用
5、定义多个全局中间件
可以使用app.use()连续定义多个全局中间件。客户端请求到达服务器之后,会按照中间件定义的先后顺序依次进行调用
app.use(funciton(req,res,) {//第1个中间件
console.log('调用了第1个全局中间件')
next()
})
app.use(funciton(req,res,) {//第2个中间件
console.log('调用了第2个全局中间件')
next()
})
app.get('/user',(req,res)=>{//请求这个路由,会依次触发上述两个全局中间件
res.send('Home page')
})
局部生效的中间件
不使用app.use()定义的中间件,叫做局部生效的中间件
//定义中间件函数mw1
const mw1 = function(req,res,next) {
console.log('这是中间件函数')
next()
}
//mw1这个中间件只在“当前路由中生效”,这种用法属于“局部生效的中间件”
app.get('/',mw1,function(req,res){
res.send('Home page')
})
//mw1这个中间件不会影响下面这个路由
app.get('/user',function(req,res){
res.send('User page')
})
- 定义多个局部中间件
可以在路由中,通过如下两种等价的方式,使用多个局部中间件
//以下两种写法是“完全等价”的,可根据自己的喜好,选择任意一种方式进行使用
app.get('/',mw1,mw2,(req,res)=>{res.send('Home page')})
app.get('/',[mw1,mw2],(req,res)=>{res.send('Home page')})
- 了解中间件的5个使用注意事项
- 一定要在路由之前注册中间件
- 客户端发过来的请求,可以连续调用多个中间件进行处理
- 执行完中间件的业务代码之后,不要忘记调用next()函数
- 为了防止代码逻辑的混乱,调用next()函数之后不要再写额外的代码
- 连续调用多个中间件时,多个中间件之间,共享req和res对象
中间件的分类:
为了方便大家理解和记忆中间件的使用,Express官方把常见的中间件用法,分成了5大类,分别是:
- 应用级别的中间件
- 路由级别的中间件
- 错误级别的中间件
- Express内置的中间件
- 第三方的中间件
- 应用级别的中间件
通过app.use()或app.get()或app.post(),绑定到app实例上的中间件,叫做应用级别的中间件
//应用级别的中间件(全局)
app.use((req,res,next)=>{
next()
})
//应用级别的中间件(局部中间件)
app.get('/',mw1,(req,res)=>{
res.send('Home page')
})
- 路由级别的中间件
绑定到express.Router()实例上的中间件,叫做路由级别的中间件。它的用法和应用级别中间件没有任何区别。只不过,应用级别中间件是绑定到app实例上,路由级别中间件绑定到router实例上
var app = express()
var router = express.Router()
//路由级别的中间件
router.use(function(req,res,next) {
console.log('Time',Date.now())
next()
})
app.use('/',router)
- 错误级别的中间件
错误级别中间件的作用:专门用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题
格式:错误级别中间件的function处理函数,必须有4个形参,形参顺序从前到后,分别是(err,req,res,next)
app.get('/',function(req,res) {//路由
throw new Error('服务器内部发生了错误')//抛出一个自定义错误
res.send('Home page')
})
app.use(function(err,req,res,next) {//错误级别的中间件
console.log('发生了错误:' + err.message)//在服务器打印错误消息
res.send('Error!' + err.message)//向客户端响应错误相关的内容
})
- 注意:错误级别中间件,必须注册在所有路由之后
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-form-urlencoded格式数据的内置中间件
app.use(express.urlencoded({extended:false}))
第三方中间件:
例如:在express@4..16.0之前版本中,经常使用body-parser 这个第三方中间件,来解析请求体数据
- 运行npm install body-parser 安装中间件
- 使用require 导入中间件
- 调用app.use() 注册并使用中间件
自定义中间件:
自己手动模拟一个类似于express.urlencoded这样的中间件,来解析POST提交到服务器的表单数据
- 定义中间件
- 监听req的data事件
- 监听res的end事件
- 使用querystring模块解析请求体数据
- 将解析出来的数据对象挂载为req.body
- 将自定义中间件封装
- 定义中间件:
使用app.use() 来定义全局生效的中间件
app.use(function(req,res,next){
//中间件业务逻辑
})
2.监听req的data事件:
在中间件中,需要监听req对象的data事件,来获取客户端发送到服务器的数据
如果数据量比较大,无法一次性发送完毕,则客户端会把数据切割后,分批发送到服务器。所以data事件可能会触发多次,每一次触发data事件时,获取到数据只是完整数据的一部分,需要手动对接收到的数据进行拼接
// 定义一个str字符串,专门用来存储客户端发来的请求体数据
let str = "";
// 监听req的data事件
req.on("data", (chunk) => {
str += chunk;
});
3.监听req的end事件:
当请求体数据接收完毕之后,会自动触发req的end事件
因此,我们可以在req的end事件中,拿到并处理完整的请求体数据
// 监听req的end事件
req.on("end", () => {
// 在str中存放的是完整的请求体数据
console.log(str);
// 把字符串格式解析为对象格式
});
4.使用querystring 模块解析请求体数据
Node.js内置了一个querystring 模块,专门用来处理查询字符串。通过这个模块提供的parse() 函数,可以轻松把查询字符串,解析成对象的格式
// 导入内置的querystring模块
const qs = require("querystring");
const body = qs.parse(str);
console.log(body);
5.将解析出来的数据挂载为req.body
const body = qs.parse(str);
req.body = body;
next();
6.将自定义中间件封装为模块
为了优化代码的结构,我们可以把自定义的中间件函数,封装为独立的模块
const qs = require("querystring");
// 这是解析表单数据的中间件
const bodyParser = (req, res, next) => {
// 定义中间件具体业务逻辑
// 定义一个str字符串,专门用来存储客户端发来的请求体数据
let str = "";
// 监听req的data事件
req.on("data", (chunk) => {
str += chunk;
});
// 监听req的end事件
req.on("end", () => {
// 在str中存放的是完整的请求体数据
// console.log(str);
// 把字符串格式解析为对象格式
const body = qs.parse(str);
req.body = body;
next();
});
};
module.exports = bodyParser;
const express = require("express");
const app = express();
// 导入内置的querystring模块
// 导入自己封装的中间件模块
const customBodyParser = require("./custom-body-parser");
app.use(customBodyParser);
app.post("/user", (req, res) => {
res.send(req.body);
});
app.listen(80, () => {
console.log("http://127.0.0.1");
});
使用Express写接口:
- 编写express接口:
// 导入express
const express = require("express");
// 创建express实例
const app = express();
// 启动
app.listen(80, () => {
console.log("http://127.0.0.1");
});
- 创建API路由:
const express = require("express");
const router = express.Router();
// 在这里挂载对应的路由
module.exports = router;
- 编写GET接口:
// 在这里挂载对应的路由
router.get("/get", (req, res) => {
// 通过req.query 获取客户端通过查询字符串,发送到服务器的数据
const query = req.query;
// 调用res.send()方法,向客户端响应处理的结果
res.send({
status: 0, //0处理成功,1表示处理失败
mag: "GET请求成功", //状态的描述
data: query, //需要响应给客户端的数据
});
});
- 编写POST接口:
// 配置解析表单的中间件
app.use(express.urlencoded({ extends: false }));
--------------------------------------
router.post("/post", (req, res) => {
// 获取客户端通过请求体,发送到服务器的URL-encoded数据
const body = req.body;
// 调用res.send()方法,把响应数据客户端
res.send({
status: 0,
msg: "POST请求成功!",
data: body,
});
});
CORS跨域资源共享:
解决接口跨域问题的方案主要有两种
- CORS(主流的解决方案,推荐使用)
- JSONP(有缺陷的解决方案,只支持GET请求)
使用cors中间件解决跨域问题:
- 运行npm install cors安装中间件
- 使用const cors = require('cors') 导入中间件
- 在路由之前调用app.use(cors()) 配置中间件
注意: 一定要在路由之前,配置cors这个中间件
CORS响应头:
- Access-Control-Allow-Origin
响应头部中可以携带一个AccessControl-Allow-Origin字段
Access-Control-Allow-Origin:<origin> | *
其中,origin参数的值指定了允许访问该资源的外域URL
例如:下面的字段值将只允许来自http://itcast.cn的请求
res.setHeader('Access-Control-Allow-Origin','http://itcast.cn')
星号表示所有网站都可以请求
- Access-Control-Allow-Headers
默认请求下,CORS 仅支持客户端向服务器发送如下的9个请求头
Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width、Content-Type(值仅限于text/plain、multipart/form-data、application/x-www-form-urlencoded三者之一)
如果客户端向服务器发送了额外的请求头信息,则需要在服务器端,通过Access-Control-Allow-Headers对额外的请求头进行声明,否则这次请求会失败
//允许客户端额外向服务器发送Content-Type 请求头和 X-Custom-Header请求头
//注意:多个请求头之间使用英文的逗号进行分割
res.setHeader('Access-Control-Allow-Headers','Content-Type, X-Custom-Header')
- 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(只有三个值application/x-www-form-urlencoded、multiparty/form-data、text/plain)
- 预检请求:
只要符合以下任何一个条件的请求,都需要进行预检请求:
- 请求方式为:GET、POST、HEAD之外的请求Methods类型
- 请求头中包含自定义头部字段
- 向服务器发送了application/json格式的数据
在浏览器与服务器正式通信之前,浏览器会先发送OPTION请求进行预检,以获知服务器是否允许该实际请求,所以这一次OPTION请求称为”预检请求“。服务器成功响应预检请求后,才会发送真正的请求,并携带真实数据
简单请求和预检请求的区别:
简单请求的特点:客户端与服务器之间只会发生一次请求
预检请求的特点:客户端与服务器之间会发生两次请求,OPTION预检请求之后,才会发起真正的请求
JSONP接口:
概念:浏览器端通过<script>标签的src属性,请求服务器上的数据,同时,服务器返回一个函数的调用。这种请求数据的方式叫做JSONP
特点:
- JSONP不属于真正的Ajax请求,因为它没有使用XMLHttpRequest这个对象
- JSONP仅支持GET请求,不支持POST、PUT、DELETE等请求
项目中操作MySQL:
- 安装操作MySQL数据库的第三方模块(mysql)
- 通过mysql模块连接到MySQL数据库
- 通过mysql模块执行SQL语句
在使用mysql模块操作MySQL数据库之前,必须先对mysql模块进行必要的配置
//导入mysql模块
const mysql = require('mysql')
//建立mysql数据库连接
cosnt db = mysql.createPool({
host:'127.0.0.1',//数据库的IP地址
user:'root',//登录数据库的账号
password:'123456',//登录数据库的密码
database:'my_db_01'//指定要操作哪个数据库
})
测试连接:
// 检测mysql模块是否正常工作
db.query("SELECT 1", (err, results) => {
if (err) return console.log(err.message);
// 只要能打印出[RowDataPacket {'1':1}]的结果,就证明数据库连接正常
console.log(results);
});
查询数据:
查询users表中的所有用户数据
// 查询users表中所有的数据
const sqlStr = "select * from users";
db.query(sqlStr, (err, results) => {
if (err) console.log(err.message);
console.log(results);
});
如果执行的是select 查询语句,则执行的结果是数组
插入数据:
向users表中新增数据,其中username为Spider-Man,password为pcc321
// 插入数据
const user = { username: "Spider-Man", password: "pcc321" };
const sqlStr = "insert into users (username,password) values(?,?)";
db.query(sqlStr, [user.username, user.password], (err, results) => {
if (err) return console.log(err.message);
if (results.affectedRows == 1) {
console.log("插入数据成功");
}
});
可以使用问号?占位符
可以使用results.affectedRows === 1判断是否插入成功
- 插入数据的便捷方式:
向表中新增数据时,如果数据对象的每个属性和数据表的字段一一对应,则可以通过如下操作:
// 插入数据
const user = { username: "Web-Wss", password: "123456" };
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("插入数据成功");
}
});
更新数据:
// 更新新的数据对象
const user = { id: 6, username: "aaa", password: "000" };
// 要执行的sql语句
const sqlStr = "update users set username=?,password=? where id=?";
// 调用db.query()执行sql语句的同时,使用数组依次为占位符指定具体的值
db.query(sqlStr, [user.username, user.password, user.id], (err, results) => {
if (err) return console.log(err.message);
if (results.affectedRows === 1) {
console.log("更新数据成功");
}
});
- 更新数据的便捷方式:
const user = { id: 6, username: "bbb", password: "000" };
// 要执行的sql语句
const sqlStr = "update users set ? where id=?";
// 调用db.query()执行sql语句的同时,使用数组依次为占位符指定具体的值
db.query(sqlStr, [user, user.id], (err, results) => {
if (err) return console.log(err.message);
if (results.affectedRows === 1) {
console.log("更新数据成功");
}
});
删除数据:
在删除数据时,推荐根据id这样的唯一标识,来删除对应的数据
//要执行的sql语句
const sqlStr = 'DELETE FROM users WHERE id = ?'
//调用db.query()执行sql语句的同时,为占位符指定具体的值
//注意:如果sql语句中有多个占位符,则必须使用数组为每个占位符指定具体的值
//如果sql语句中只有一个占位符,则可以省略
db.query(sqlStr,7,(err,results)=>{
if(err) return console.log(err.message)//失败
if(results.affectedRows === 1) {
console.log('删除数据成功')
}
})
- 标记删除
使用DELETE语句,会把真正的数据从表中删除,为了保险,推荐使用标记删除,来模拟删除的动作。
所谓的标记删除,就是在表中设置类似status这样的状态字段,来标记当前这条数据是否被删除
当用户执行了删除的动作时,我们并没有执行DELETE语句把数据删除掉,而是执行update语句,将这条数据对应的status字段标记删除即可
const sqlStr = "update users set status=? where id=?";
db.query(sqlStr, [1, 5], (err, results) => {
if (err) return console.log(err.message);
if (results.affectedRows === 1) {
console.log("删除成功");
}
});
web开发模式:
目前主流的web开发方式有两种,分别是:
- 基于服务器渲染的传统web开发模式
- 基于前后端分离的新型web开发模式
- 服务端渲染:
优点:
- 前端耗时少。因为服务器端负责动态生成HTML内容,浏览器只需直接渲染页面即可,尤其是移动端,更省电
- 有利于SEO。因为服务端响应的是完整的HTML页面内容,所以爬虫更容易爬取获得信息,更有利于SEO
缺点:
- 占用服务端资源
- 不利于前后端分离,开发效率低
- 前后端分离的web开发模式
前后端分离的概念:前后端分离的开发模式,依赖于Ajax技术的广泛应用。简而言之,前后端分离的web开发模式,就是后端只负责提供API接口,前端使用Ajax调用接口的开发模式
优点:
- 开发体验好
- 用户体验好
- 减轻了服务器端的渲染压力
缺点:
- 不利于SEO
身份认证:
又称“身份验证”、“鉴权”,通过一定的手段,完成对用户身份的确认
- 为什么需要身份认证:
为了确认当前所声称为某种身份的用户,确实是所声称的用户
不同开发模式下的身份认证:
- 服务端渲染推荐使用:Session认证机制
- 前后端分离推荐使用:JWT认证机制
Session认证机制:
- HTTP协议的无状态性
HTTP协议的无状态性,指的是客户端的每次HTTP请求都是独立的,连续多个请求之间没有直接关系,服务器不会主动保留每次HTTP请求的状态
- 如何突破HTTP无状态的限制
在web开发中的专业术语叫做Cookie
- 什么是Cookie
Cookie是存储在用户浏览器中的一般不超过4KB的字符串。它由一个名称、一个值和其他几个用于控制Cookie有效期、安全性、使用范围的可选属性组成
不同域名下的Cookie各自独立,每当客户端发起请求时,会自动把当前域名下所有未过期的Cookie一同发送到服务器
- Cookie的几大特性:
- 自动发送
- 域名独立
- 过期时限
- 4KB限制
- Cookie在身份认证中的作用:
客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的Cookie,客户端会自动将Cookie保存在浏览器中
随后,当客户端浏览器每次想请求服务器的时候,浏览器会自动将身份认证相关的Cookie,通过请求体的形式发送给服务器,服务器即可验明客户端的身份
- Cookie不具有安全:
由于Cookie是存储在浏览器中的,而且浏览器也提供了读写Cookie的API,因此Cookie很容易被伪造,不具有安全性,因此不建议服务器将重要的隐私数据,通过Cookie的形式发送给浏览器
- 提高身份认证的安全性:
会员卡+刷卡认证的设计理念,就是Session认证的精髓
- Session的工作原理:
在Express中使用Session:
- 安装express-session中间件:
在Express项目中,只需要安装express-session中间件,即可在项目中使用Session认证
- 配置express-session中间件:
通过app.use()来注册session中间件
//导入session中间件
var session = require('express-session')
//配置Session中间件
app.use(session({
secret:'keyboard cat',//secret属性值可以为任意字符串
resave:false,//固定写法
saveUninitialized:true//固定写法
}))
- 向session中存数据
通过express-session中间件配置成功后,即可通过req.session来访问和使用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//将用户信息,存储到session中
req.session.islogin = true//将用户的登录状态存到session中
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)=>{
//清空当前客户端对应的session信息
req.session.destroy()
req.send({
status:0,
msg:'退出登录成功'
})
})
JWT认证机制:
session认证机制需要配合cookie才能实现,由于cookie默认不支持跨域访问,所以,当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域session认证
注意:
- 当前端请求后端不存在跨域问题的时候,推荐使用session身份认证机制
- 当前端需要跨域请求后端接口的时候,不推荐使用session身份认证机制,推荐使用JWT认证机制
什么是JWT:
JWT(英文全称:JSON Web Token)是目前最流行的跨域认证解决方案
JWT的工作原理:
总结:用户的信息通过Token字符串的形式,保存在客户端浏览器中,服务器通过还原Token字符串的形式来认证用户的身份
JWT的组成部分:
JWT通常由三部分组成,分别是Header(头部)、Payload(有效荷载)、Signature(签名)
三者之间使用英文的"."分割,格式如下:
Header.Payload.Signature
其中:
- Payload部分才是真正的用户信息,它是用户信息经过加密之后生成的字符串
- Header和Signature是安全性相关的部分,只是为了保证Token的安全性
JWT的使用方式:
客户端收到服务器返回的JWT之后,通常会将它存储在localStorage活sessionStorage中
此后,客户端每次与服务器通信,都要带上这个JWT的字符串,从而进行身份认证。推荐的做法是把JWT放在HTTP请求头的Authorization字段中
Authorization:Bearer <token>
在Express中使用JWT:
安装JWT相关的包
npm install jsonwebtoken express-jwt
其中:
- jsonwebtoken用于生成JWT字符串
- express-jwt用于将JWT字符串解析还原成JSON对象
导入:
//导入用于生成JWT字符串的包
const jwt = require('jsonwebtoken')
//导入用于客户端发送过来的JWT字符串,解析还原成JSON对象的包
const expressJWT = require('express-jwt')
定义secret密钥:
为了保证JWT字符串的安全性,防止JWT字符串在网络传输过程中被别人破解,我们需要专门定义一个用于加密和解密的secret密钥
①当生成JWT字符串的时候,需要使用secret密钥对用户的信息进行加密,最终得到加密的JWT字符串
②当把JWT字符串解析还原成JSON对象的时候,需要使用secret密钥进行加密
const secretKey = 'itheima No1 ^_^'
在登录成功后生成JWT字符串
调用jsonwebtoken包提供的sign()方法,将用户的信息加密成JWT字符串,响应给客户端
//登录接口
app.post('/api/login',function(req,res) {
//...省略登录失败情况下的代码
//用户登录成功之后,生成JWT字符串,通过token属性响应给客户端
res.send({
status:200,
message:'登录成功',
//调用jwt.sign()生成JWT字符串,三个参数分别是:用户信息对象、加密密钥、配置对象
token: jwt.sign({username:userinfo.username},secretKey,{expiresIn:'30s'})
})
})
将JWT字符串还原为JSON对象
客户端每次在访问那些有权限接口的时候,都需要主动通过请求头中的Authorization字段,将Token字符串发送到服务器进行身份验证
此时,服务器可以通过express-jwt这个中间件,自动将客户端发送过过来的Token解析还原成JSON对象
//使用app.use() 来注册中间件
//expressJWT({secret:secretKey})就是用来解析Token的中间件
//.unless({path:[/^\/api\//]})用来指定哪些接口不需要访问权限
app.use(expressJWT({secret:secretKey}).unless({path:[/^\/api\//]}))
使用req.user获取用户信息
当express-jwt这个中间件配置成功之后,即可在那些有权限的接口中,使用req.user对象,来访问JWT字符串中解析出来的用户信息了
//这是一个有权限的API接口
app.get('/admin/getinfo',function(req,res) {
console.log(req.user)
res.send({
status:200,
message:'获取用户信息成功',
data:req.user
})
})
捕获解析JWT失败后产生的错误
当使用express-jwt解析Token字符串时,如果客户端发送过来的Token字符串过期或不合法,会产生一个解析失败的错误,影响项目的正常运行,我们可以通过Express的错误中间件,捕获这个错误并进行相关的处理
app.use((err,req,res,next)=>{
//token解析失败导致的错误
if(err.name === 'UnauthorizedError') {
return res.send({status:401,message:'无效的token'})
}
//其他原因导致的错误
res.send({status:500,message:'未知错误'})
})