目录
目录
初识Node.js
回顾
浏览器中的JavaScript
-JS核心语法
-WebAPI
为什么JavaScript在浏览器中会被执行:JavaScript解析引擎
不同浏览器使用不同的解析引擎:Chrome => V8 ; Firebox => OdinMonkey ......
为什么JavaScript可以操作DOM和BOM:每个浏览器都内置了DOM、BOM这样的API函数。因此浏览器中的JavaScript才可以调用他们
Node.js简介
Node.js是一个基于Chrome V8引擎的JavaScript运行环境
浏览器是JS的前端运行环境
Node.js是JS的后端运行环境
Node.js中无法调用DOM和BOM等浏览器内置API
什么是终端
用于实现人机交互的一种方式
终端中的快捷键
tab键自动补全路径
esc键快速清空当前输入命令
cls命令清除终端当前内容
fs文件系统模块
const fs = require('fs')
操作文件的模块
读取指定文件中的内容
fs.readFile()语法格式
fs.readFile(path [,options],callback)
参数1:必选参数,字符串,表示文件路径
参数2:可选,表示以什么编码格式来读取文件
参数3:必选参数,文件读取完成后,通过回调函数拿到读取的结果
示例代码
以utf8的编码格式,读取指定文件内容,并打印err和dataStr的值
//导入fs模块,来操作文件
const fs = require('fs')
//调用fs.readFile()方法读取文件
fs.readFile('./11.txt','utf8',function(err,dataStr){
console.log(err)
console.log('-----s');
console.log(dataStr);
} )
如果成功:err的结果为null
如果失败:err的值为错误对象,dataStr的值为undefined
判断文件是否读取成功
可以判断err对象是否为null
const fs = require('fs')
fs.readFile('./1.txt','utf8',function(err,dataStr){
if(err) {
return console.log('错误信息:'+err.message);
}
console.log(dataStr);
} )
向指定文件中写入内容
fs.writeFile()语法格式
fs.writeFile(file,data [,options],callback)
参数1:必选参数,文件路径的字符串
参数2:必选参数,写入的内容
参数3:可选参数,表示以什么格式写入文件内容
参数4:必选参数,文件写入完成后的回调函数
示例代码
fs.writeFile('./111.txt','hello world&node','utf8',function(err) {
console.log(err);
})
判断文件是否写入成功
fs.writeFile('./111.txt','hello world&node','utf8',function(err) {
if(err) {
return console.log('失败!'+err.message);
}
console.log('成功');
})
考试成绩整理练习
实现步骤:
1.导入需要的fs文件系统模块
2.使用fs.readFile()方法,读取成绩.txt文件
3.判断文件是否读取失败
4.文件读取成功后,处理成绩数据
5.将处理完成的成绩数据,调用fs.writeFile()方法写入新文件中
//1.导入fs模块
const fs = require('fs')
//2.读取文件
fs.readFile('./成绩.txt','utf8',function(err,dataStr) {
//3.判断读取是否成功
if(err) {
console.log('读取失败'+err.message);
}
//console.log('success',dataStr);
//4.1先把成绩的数据按空格进行分割
const arrOld = dataStr.split(' ')
//4.2循环分割后的数组进行字符串替换操作
const arrNew = []
arrOld.forEach(item => {
arrNew.push(item.replace('=',':'))
})
//4.3把新数组每一项合并
//\r\n表示回车换行
const newStr = arrNew.join('\r\n')
//4.4得到新的字符串
//console.log(newStr);
//5.写文件
fs.writeFile('./成绩ok.txt',newStr,function(err) {
if(err) {
console.log('error'+err.message);
}
console.log('success');
})
})
路径动态拼接问题
使用fs模块操作文件时,如果提供的操作路径是以 ./ 或者 ../ 开头的相对路径时,很容易出现路径动态拼接错误问题
原因:代码在运行的时候会以执行node命令时所处的目录,动态拼接出被操作文件的完整路径
解决1:提供一个完整的文件存放路径 => 主要要两个斜线,一个(\)代表转义
但是移植性差不利于维护
解决2:__dirname 表示当前文件所处的目录,利用__dirname拼接
path路径模块
处理路径
const path = require('path')
路径拼接
path.join()
可以传任意多的片段,以逗号隔开
const pathStr = path.join('/a','/b/c','../','./d','e') // ../可以抵消路径(./不行)
console.log(pathStr) //输出\a\b\d\e
const pathStr2 = path.join(__dirname,'./files/1.txt') //当前文件目录下的file/1.txt
凡是涉及到路径的拼接尽量都适用join不要使用+
获取路径中的文件名
path.basename(path[,ext]) //可以获取路径中的最后一部分,经常用来获取路径中的文件名
参数1:path <string> 必选参数,表示一个路径的字符串
参数2:ext <string> 可选参数,表示文件扩展名(写上就表示要移除)
返回:<string>表示路径中的最后一部分
const path = require('path')
//定义文件的存放路径
const fPath = '/a/b/c/index.html'
// const fullName = path.basename(fPath)
// console.log(fullName); //index.html
const nameWithoutExt = path.basename(fPath,'.html')
console.log(nameWithoutExt); //index
获取路径中的文件扩展名
path.extname(path)
path是必选参数,表示一个路径的字符串
返回值是得到的扩展字符串
const path = require('path')
//定义文件的存放路径
const fPath = '/a/b/c/index.html'
// const fullName = path.basename(fPath)
// console.log(fullName); //index.html
const nameWithoutExt = path.basename(fPath,path.extname(fPath))
console.log(nameWithoutExt); //index
// const fext = path.extname(fPath)
// console.log(fext);
时钟案例
将index.html页面拆分成css,js,html三个文件,并将三个文件都放到clock目录中
实现步骤
1.创建正则表达式匹配<style>和<script>标签
2.使用fs模块,读取需要被处理的HTML文件
3.自定义resolveCSS方法来写入css文件
4.js
5.html
//1.1 导入模块
const fs = require('fs')
const path = require('path')
//1.2 匹配<style></style>标签的正则
// 其中\s表示空白字符,\S表示非空白字符,*表示匹配任意次
const regStyle = /<style>[\s\S]*<\/style>/
const regScript = /<script>[\s\S]*<\/script>/
//2 调用读文件方法
fs.readFile(path.join(__dirname, './clock.html'), (err, data) => {
if (err) {
console.log('error' + err.message);
}
resolveCSS(data)
resolveJS(data)
resolveHTML(data)
})
//3.定义处理CSS样式的方法
function resolveCSS(htmlStr) {
//使用正则提取
const r1 = regStyle.exec(htmlStr)
//将提取出来的样式字符串进行替换操作
const newCSS = r1[0].replace('<style>', '').replace('</style>', '')
//写操作
fs.writeFile(path.join(__dirname, './clock/index.css'), newCSS, (err, data) => {
if (err) {
console.log('写入css样式失败' + err.message);
}
console.log('写入CSS文件成功');
})
}
//4.定义处理js内容方法
function resolveJS(htmlStr) {
const r2 = regScript.exec(htmlStr)
const newJS = r2[0].replace('<script>', '').replace('</script>', '')
fs.writeFile(path.join(__dirname, './clock/index.js'), newJS, (err, data) => {
if (err) console.log('JS文件写入失败', err.message);
console.log('写入JS文件成功');
})
}
// 5.1 定义处理 HTML 结构的方法
function resolveHTML(htmlStr) {
// 5.2 将字符串调用 replace 方法,把内嵌的 style 和 script 标签,替换为外联的 link 和 script 标签
const newHTML = htmlStr.toString().replace(regStyle, '<link rel="stylesheet" href="./index.css" />').replace(regScript, '<script src="./index.js"></script>')
// 5.3 写入 index.html 这个文件
fs.writeFile(path.join(__dirname, './clock/index.html'), newHTML, function(err) {
if (err) return console.log('写入 HTML 文件失败!' + err.message)
console.log('写入 HTML 页面成功!')
})
}
注意
1.fs.writeFile() 方法只能创建文件不能创建路径,所以在这个案例下是先创建了文件夹
2.重复调用fs.writeFile()写入同一个文件,新写入的内容好覆盖之前的旧内容
http模块
了解http模块
什么是http模块
客户端:网络节点中负责消费资源的电脑
服务器:网络节点中负责对外提供网络资源的电脑
http模块是用来创建web服务器的模块,通过http模块提供的http.createServer()方法,能把一台普通的电脑变成一台web服务器,从而对外提供web资源服务
const http = require('http')
http模块的作用
服务器和普通电脑的区别在于,服务器上安装了web服务器软件,例如:lIS、Apache等。通过安装这些服务器软件,就能把一台普通的电脑变成一台web 服务器。
在Node.js 中,我们不需要使用lIS、Apache等这些第三方web服务器软件。因为我们可以基于Node.js 提供的http模块,通过几行简单的代码,就能轻松的手写一个服务器软件,从而对外提供web服务。
服务器相关概念
IP地址
I地址就是互联网上每台计算机的唯一地址,因此IP地址具有唯一性。如果把“个人电脑”比作“一台电话”,那么“IP地址”就相当于“电话号码”,只有在知道对方IP地址的前提下,才能与对应的电脑之间进行数据通信。
IP地址的格式:通常用“点分十进制”表示成(a.b.c.d)的形式,其中, a,b,c,d都是0~255之间的十进制整数。例如:用点分十进表示的IP地址(192.168.1.1)
注意:
1.互联网中每台Web服务器都有自己的IP地址
2.在开发期间,自己的电脑既是服务器,也是客户端,为了方便测试,可以在自己的浏览器中输入127.0.0.1这个IP地址,就能把自己的电脑当做一台服务器进行访问了。
域名和域名服务器
尽管IP地址能够唯一地标记网络上的计算机,但IP地址是一长串数字,不查观,而且不便于记忆,于是人们又发明了另一套字符型的地址方案,即所谓的域名地址。
IP地址和域名是一一对应的关系,这份对应关系存放在一种叫做域名服务器(DNS)的电脑中。使用者只需通过好记的域名访问对应的服务器即可,对应的转换工作由域名服务器实现。因此,域名服务器就是提供IР地址和域名之间的转换服务的服务器。
注意:
1.单纯使用IP地址,互联网中的电脑也能够正常工作,但有了域名加持能让互联网变得更加方便
2.在开发测试期间,127.0.0.1对应的域名是localhost,他们都代表我们自己的这台电脑,在使用时没有任何区别
端口号
类似显示生活中的门牌号
同样的道理,在一台电脑中,可以运行成百上千个web服务。每个web服务都对应一个唯一的端口号。客户端发送过来的网络请求,通过端口号,可以被准确地交给对应的web 服务进行处理。
注意:
1.每个端口号不能同时被多个web服务占用
2.实际应用中,URL中的80端口可以被省略
创建最基本的web服务器
基本步骤
1.导入http模块
2.创建web服务器实例
3.为服务器实例绑定request事件,监听客户端的请求
4.启动服务器
//1.导入http模块
const http = require('http')
//2.创建web服务器实例
const server = http.createServer()
//3.为服务器实例绑定request事件,监听客户端的请求
server.on('request',(req,res) => {
console.log('someone visit our web server');
})
//4.启动服务器
server.listen(8080,() => {
console.log('server running at http://127.0.0.1:8080');
})
req请求对象
只要服务器接收到了客户端的请求,就会调用通过server.on()为服务器绑定的 request事件处理函数。如果想在事件处理函数中,访问与客户端相关的数据或属性
res相应对象
在服务器的request事件处理函数中,如果想访问与服务器相关的数据或属性
//1.导入http模块
const http = require('http')
//2.创建web服务器实例
const server = http.createServer()
//3.为服务器实例绑定request事件,监听客户端的请求
//req是请求对象,包含了与客户端相关的数据和属性
server.on('request', (req, res) => {
console.log('someone visit our web server');
//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)
})
//4.启动服务器
server.listen(80, () => {
console.log('server running at http://127.0.0.1');
})
解决中文乱码问题
当调用res.end()方法,向客户端发送中文内容的时候,会出现乱码问题,此时,需要手动设置内容的编码格式
需要设置响应头:Content-type 的值为 text/html;charset=utf-8
res.setHeader('Content-Type','text/html;charset=utf-8')
//1.导入http模块
const http = require('http');
const { setUncaughtExceptionCaptureCallback } = require('process');
//2.创建web服务器实例
const server = http.createServer()
//3.为服务器实例绑定request事件,监听客户端的请求
//req是请求对象,包含了与客户端相关的数据和属性
server.on('request', (req, res) => {
console.log('someone visit our web server');
//req.url是客户端请求的url地址(从端口号开始的地址)
const url = req.url
//req.method是客户端请求的method类型
const method = req.method
const str = `您请求的url地址是${url},请求的method类型是${method} `
//解决中文乱码问题
res.setHeader('Content-type','text/html;charset=utf-8')
//调用res.end()方法,向客户端响应一些内容
res.end(str)
})
//4.启动服务器
server.listen(80, () => {
console.log('server running at http://127.0.0.1');
})
根据不同的url响应不同的html内容
1.获取请求的url地址
2.设置默认的响应内容为404 Not Found
3.判断用户请求是否为 / 或 /index.html 首页
4.判断用户请求的是否为 /about.html 关于页面
5.设置Content-Type响应头,防止中文乱码
6.使用res.end()把内容响应给客户端
const http = require('http')
const server = http.createServer()
server.on('request', (req, res) => {
//1.获取url地址
const url = req.url
//2.设置默认相应内容为404
let content = '<h1>404 Not fount</h1>'
//3.判断用户请求是否为 / 或 /index.html 首页
// 4.判断用户请求的是否为 /about.html 关于页面
if (url === '/' || url === '/index.html') {
content = '<h1>首页</h1>'
} else if (url === '/about.html') {
content = '<h1>关于页面</h1>'
}
// 5.设置Content-Type响应头,防止中文乱码
res.setHeader('Content-Type','text/html;charset=utf-8')
// 6.使用res.end()把内容响应给客户端
res.end(content)
})
server.listen(80, () => {
console.log('server running at http://127.0.0.1');
})
实现clock时钟的web服务器
核心思路:把文件的实际存放路径作为每个资源的请求url地址
实现步骤:
1.导入需要的模块
2.创建基本的web服务器
3.将资源的请求url地址映射为文件的存放路径
4.读取文件内容并响应给客户端
5.优化资源的请求路径
//1.导入模块
const http = require('http')
const fs = require('fs')
const path = require('path')
//2.1 创建web服务器
const server = http.createServer()
//2.2 监听web服务器的request事件
server.on('request',(req,res) => {
//3.1 获取到客户端请求的url地址
const url = req.url
//3.2 把请求的url地址映射为具体文件的存放路径
//const fpath = path.join(__dirname,url)
//5 优化资源的请求路径
let fpath = ''
if(url === '/') fpath = path.join(__dirname,'/clock/index.html')
else fpath = path.join(__dirname,'/clock'+url)
//4.1 根据映射过来的文件路径来读取文件内容
fs.readFile(fpath,'utf-8',(err,dataStr) => {
//4.2 读取失败,向客户端响应固定的错误消息
if(err) return res.end('404')
//4.3 读取成功,将读取成功的内容响应给客户端
res.end(dataStr)
})
})
//2.3 启动服务器
server.listen('80',() => {
console.log('server running at http://127.0.0.1');
})
模块化
基本概念
模块化是指解决一个复杂问题时,自顶向下逐层把系统划分成若干模块的过程。对于整个系统来说,模块是可组合、分解和更换的单元。
Node.js中的模块化
三大模块
内置模块 :内置模块是由Node.js 官方提供的,例如fs、path、http等
自定义模块 :用户创建的每个.js 文件,都是自定义模块
第三方模块 :由第三方开发出来的模块,并非官方提供的内置模块,也不是用户创建的自定义模块,使用前需要先下载
加载模块
使用require()方法,可以加载需要的三种模块进行使用
使用require()方法加载其他模块时,会执行被加载模块中的代码
模块作用域
好处:防止全局变量污染的问题
对外共享模块作用域中的成员
在每个.js自定义模块中都有一个module对象,它里面存储了和当前模块有关的信息
在自定义模块中,可以使用module.exports对象将模块内的成员共享出去,供外界使用
外界用require()方法导入自定义模块时,得到的就是module.exports所指向的对象
module.exports是一个空对象
共享成员的注意点:使用require()方法导入模块时,导入的结果,永远以module.exports指向的对象为准。
以最后所指向的结果为准
exports对象
为了简化module.exports,Node提供了exports对象,默认情况下二者指向同一个对象,最终共享的结果还是以module.exports指向的对象为准
exports和module.exports的使用误区
时刻谨记,require()模块时,得到的永远是module.exports指向的对象
Node.js中的模块化规范
CommonJS规定:
1.每个模块内部,module变量代表当前模块
2.module变量是一个对象,它的exports属性(即module.exports)是对外的接口
3.加载某个模块,其实是加载该模块的module.exports属性,require()方法用于加载模块
npm体验
自定义时间
moment包
初次装包完成后,在项目文件夹下多一个叫做node_modules 的文件夹和package-lockjson的配置文件。
node_modules,文件夹用来存放所有已安装到项目中的包。require()导入第三方包时,就是从这个目录中查找并加载包。
package-lock.json配置文件用来记录node modules目录下的每一个包的下载信息,例如包的名字、版本号、下载地址等。
包管理配置问题
问题:第三方包的体积过大,不方便团队成员之间共享项目源代码
解决方案:共享时剔除node_modules
如何记录项目中安装了哪些包:在项目根目录中创建一个叫package.json的配置文件,即可用来记录项目中安装了哪些包,从而方便提出node_modules目录之后,在团队成员之间共享项目的源代码。
注意:在开发中,一定要把node_moduls文件夹,添加到.gitignore忽略文件中
快速创建package.json
npm包管理工具提供了一个快捷命令,可以在执行命令时所处的目录中,快速创建package.json这个包管理
npm init -y
注意:
1.只能在英文目录下成功运行,也不能有空格
2.运行npm install命令安装包的时候,npm包管理工具会自动把包的名称和版本号记录到package.json中
dependencies节点
package.json文件中,有一个dependencies 节点,专门用来记录您使用npm install命令安装了哪些包。
devDependencies节点
如果某些包只在项目开发阶段会用到,在项目上线之后不会用到,则建议把这些包记录到devDependencies 节点中。
npm i 包名 -D //简写
npm install 包名 --save-dev
与之对应的,如果某些包在开发和项目上线之后都需要用到,则建议把这些包记录到dependencies 节点中。
一些操作
卸载包
npm uninstall
查看当前的下包镜像源
npm config get registry
将下包的镜像源切换为淘宝镜像源
npm config set registry=https://registry.npm.taobao.org/
模块的加载机制
优先从缓存中加载:模块在第一次加载后会被缓存,这也就意味着多次调用require()不会导致模块的代码被执行多次
内置模块的加载机制:内置模块的加载优先级最高
如果此时node_modules目录下也有叫fs的模块,require('fs')始终返回的是内置的fs模块
自定义模块加载机制:在加载自定义模块时,如果没有指定 ./ 或 ../ 这样的标识符,则node会把它当做内置模块或第三方模块进行加载
在使用require()导入自定义模块时,如果省略了文件的扩展名,则Node,js 会按顺序分别尝试加载以下的文件:
- 按照确切的文件名进行加载
- 补全.js扩展名进行加载
- 补全.json扩展名进行加载
- 补全.node扩展名进行加载
- 加载失败,终端报错
第三方模块的加载机制:如果传递给require()的模块标识符不是一个内置模块,也没有以 ./ 或 ../ 开头,则Node.js 会从当前模块的父目录开始,尝试从/node_modules文件夹中加载第三方模块。如果没有找到对应的第三方模块,则移动到再上一层父目录中进行加载,直到文件系统的根目录。
目录作为模块:
当把目录作为模块标识符,传递给require()进行加载的时候,有三种加载方式:
- 在被加载的目录下查找一个叫做package.json的文件,并寻找 main属性,作为require()加载的入口
- 如果目录里没有package.json文件,或者main入口不存在或无法解析,则 Node.js将会试图加载目录下的 index.js文件。
- 如果以上两步都失败了,则Node.js会在终端打印错误消息,报告模块的缺失: Error: Cannot find module 'xxx'
Express
Express是基于Node.js平台,快速、开放、极简的Web开发框架
通俗理解:Express的作用和Node.js内置的http模块类似,是专门用来创建Web服务器的
本质上是一个第三方包
Express是基于内置的 http模块进一步封装出来的,能够极大的提高开发效率。
对于前端程序员来说,最常见的两种服务器,分别是:
- Web网站服务器:专门对外提供 Web 网页资源的服务器。
- API接口服务器:专门对外提供API接口的服务器。
使用Express可以方便快速的创建Web网站服务器和API接口服务器
基本使用
监听GET请求
通过app.get()方法,可以监听客户端的GET请求
监听POST请求
通过app.post()方法,可以监听客户端的POST请求
把内容响应给客户端
通过res.send()方法,可以把处理好的内容发送给客户端
获取URL中携带的查询参数
通过req.query对象,可以访问到客户端通过查询字符串的形式(?),发送到服务器的参数
req.query默认是一个空对象
获取URL中的动态参数
通过req.params对象,可以访问到URL中,通过:匹配到的动态参数
//导入express
const express = require('express')
//创建web服务器
const app = express()
//监听客户端的GET和POST请求,并向客户端相应具体的内容
app.get('/user',(req,res) => {
//调用express提供的res.send()方法向客户端响应一个JSON对象
res.send({name:'zs',age:20,gender:'男'})
})
app.post('/user',(req,res) => {
//调用express提供的res.send()方法向客户端响应一个文本字符串
res.send('请求成功')
})
app.get('/',(req,res) => {
//通过req.query 可以获取到客户端发送过来的查询参数
//注意:默认情况下,req.query是一个空对象
console.log(req.query);
res.send(req.query)
})
//这里的:id是一个动态参数
app.get('/user/:id',(req,res) => {
//req.params是动态匹配到的URL参数,默认也是一个空对象
console.log(req.params);
res.send(req.params)
})
//启动web服务器
app.listen(80,() => {
console.log('express server running at http://127.0.0.1');
})
托管静态资源
express.static()
通过它可以非常方便地创建一个静态资源服务器
app.use(express.static('public'))
通过以上代码可以将public目录下的图片、css文件、JavaScript文件对外开放访问
注意:express在指定的静态目录中查找文件,并对外提供资源的访问路径。因此存放静态文件的目录名不会出现在URL中。
const express = require('express')
const app = express()
//在这里调用static方法,快速对外提供静态资源
app.use(express.static('./clock'))
app.listen(80,() => {
console.log('express server running at http://127.0.0.1');
})
托管多个静态资源目录
如果要托管多个静态资源目录,请多次调用express.static()函数
挂载路径前缀
如果希望在托管的静态资源访问路径之前挂载路径前缀
app.use('/public',express.static('public'))
const express = require('express')
const app = express()
//在这里调用static方法,快速对外提供静态资源
app.use(express.static('./clock'))
app.listen(80,() => {
console.log('express server running at http://127.0.0.1');
})
nodemon
在编写调试Node,js项目的时候,如果修改了项目的代码,则需要频繁的手动close掉,然后再重新启动,非常繁琐。
nodemon能够监听项目文件的变动,当代码被修改后,会自动帮我们重启项目,极大方便了开发和调试。
当基于Nodejs编写了一个网站应用的时候,传统的方式,是运行node app.js 命令,来启动项目。这样做的坏处是:代码被修改之后,需要手动重启项目。
将node命令替换为nodemon命令,使用nodemon app.js 来启动项目。这样做的好处是:代码被修改之后,会被nodemon监听到,从而实现自动重启项目的效果。
Express路由
Express中路由指的是客户端的请求与服务器处理函数之间的映射关系
Express中的路由由3部分组成,分别是请求的类型、请求的URL地址、处理函数
app.METHOD(PATH,HANDLER) //method指的是get or post
路由的匹配过程
每当一个请求到达服务器之后,需要先经过路由的匹配,只有匹配成功之后,才会调用对应的处理函数。
在匹配时,会按照路由的顺序进行匹配,如果请求类型和请求的URL同时匹配成功,则 Express 会将这次请求,转交给对应的 function函数进行处理
路由的使用
最简单的方法:在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')
})
app.listen(80,() => {
console.log('express server running at http://127.0.0.1');
})
模块化路由:为了方便对路由进行模块化的管理,Express不建议将路由直接挂载到 app 上,而是推荐将路由抽离为单独的模块。
1.创建路由模块对应的 .js文件
2.调用express.Router()函数创建路由对象
3.向路由对象上挂载具体的路由
4.使用module.exports向外共享路由对象
5.使用app.use()函数注册路由模块
注册路由
app.use(router)
app.use():注册全局中间件
为路由模块添加前缀
app.use('/api',userRouter)
Express中间件
Express中间件的调用流程
当一个请求到达Express的服务器之后,可以连续调用多个中间件,从而对这次请求进行预处理
Express中间件的格式
Express的中间件,本质上就是一个function处理函数
中间件函数的形参列表中,必须包含next参数,而路由处理函数中只包含req和res
next函数的作用:是实现多个中间件连续调用的关键,它表示把流转关系转交给下一个中间件或路由
定义最简单的中间件函数
const express = require('express')
const app = express()
//定义一个中间件函数
const mv = (req,res,next) => {
console.log('最简单的中间件函数');
//把流转关系转交给下一个中间件或路由
next()
}
app.listen(80,() => {
console.log('express server running at http://127.0.0.1')
})
全局生效的中间件
客户端发起的任何请求,到达服务器之后,都会触发的中间件,叫做全局生效的中间件。
通过调用app.use(中间件函数),即可定义一个全局生效的中间件
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.get('/user',(req,res) => {
res.send('user page')
})
app.listen(80,() => {
console.log('express server running at http://127.0.0.1')
})
定义全局中间件的简化形式
//全局中间件的简化形式
app.use((req,res,next) => {
console.log('这是最简单的中间件函数');
next()
})
中间件的作用
多个中间件之间,共享同一份req和res。基于这样的特性,我们可以在上游的中间件中,统一为req或 res 对象添加自定义的属性或方法,供下游的中间件或路由进行使用。
const express = require('express')
const app = express()
//全局中间件
app.use((req,res,next) => {
//获取到请求到达服务器的时间
const time = Date.now()
//为req对象挂载自定义属性,从而把时间共享给后面所有的路由
req.startTime = time
next()
})
app.get('/',(req,res) => {
res.send('home page'+req.startTime)
})
app.get('/user',(req,res) => {
res.send('user page'+req.startTime)
})
app.listen(80,() => {
console.log('express server running at http://127.0.0.1')
})
定义多个全局中间件
使用app.use()连续定义多个全局中间件,客户端请求到达服务器之后,会按照中间件定义的先后顺序依次进行调用
局部生效的中间件
不使用app.use()定义的中间件叫局部生效的中间件
const express = require('express')
const app = express()
//局部中间件
const mw1 = (req,res,next) => {
console.log('调用了局部生效的中间件');
}
app.get('/',mw1,(req,res) => {
console.log('home');
})
app.get('/user',(req,res) => {
console.log('user');
})
app.listen(80,() => {
console.log('express server running at http://127.0.0.1')
})
定义多个局部中间件
app.get('/',mw1,mw2,(req,res) => {res.send('xxx')})
app.get('/',[mw1,mw2],(req,res) => {res.send('xxx')})
中间件的注意事项
1.一定要在路由之前注册中间件
2.客户端发送过来的请求可以连续调用多个中间件进行处理
3.执行完中间件的业务代码之后,不要忘记调用next()函数
4.为了防止代码逻辑混乱,调用next()函数后不要再写额外的代码
5.连续调用多个中间件时,多个中间件之间共享req和res对象
中间件的分类
1.应用级别
通过app.use() 或 app.get() 或 app.post() ,绑定到app实例上的中间件,叫做应用级别的中间件
2.路由级别
绑定到express.Router()实例上的中间件,叫做路由级别的中间件。
它的用法和应用级别中间件没有任何区别,只不过应用级别中间件时绑定到app实例上,路由级别中间件绑定到router实例上
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.use((err,req,res,next) => {
console.log('发生了错误' + err.message);
res.send('Error:' + err.message)
})
app.listen(80,() => {
console.log('express server running at http://127.0.0.1');
})
正常的中间件要注册在路由之前
错误级别的中间件必须注册在所有路由之后
4.Express内置
express.static:没有兼容性,在任何版本中都可以使用。用来快速托管静态资源的内置中间件。例如:HTML文件、图片、CSS样式等
express.json:解析JSON格式的请求体数据(有兼容性,仅在4.16.0+版本中可用)
app.use(express.json())
express.urlencoded:解析URL-encoded格式的请求体数据(有兼容性,仅在4.16.0+版本中可用)
app.use(express.urlencoded({extended:false}))
在服务器可以使用req.body这个属性来接收客户端发来的请求体数据。默认情况下,如果不配置解析表单数据的中间件,则req.body默认等于undefined
5.第三方
可以按需下载并配置
body-parse,用法与express.urlencoded一样,express.urlencoded就是基于body-parse进一步封装出来的。
自定义中间件
模拟一个类似于express.urlencoded这样的中间件,来解析POST提交到服务器的表单数据
实现步骤:
- 定义中间件
- 监听req的data事件
在中间件中,需要监听req对象的 data事件,来获取客户端发送到服务器的数据。
如果数据量比较大,无法一次性发送完毕,则客户端会把数据切割后,分批发送到服务器。所以data事件可能会触发多次,每一次触发data事件时,获取到数据只是完整数据的一部分,需要手动对接收到的数据进行拼接 - 监听req的end事件
当请求体数据接收完毕之后,会自动触发req的end事件。我们可以在req的end事件中拿到并处理完整的请求体数据 - 使用node的内置querystring模块解析请求体数据
Node.js内置了一个querystring模块,专门用来处理查询字符串,通过这个模块提供的parse函数,可以轻松把查询字符串解析成对象的格式 - 将解析出来的数据对象挂载为req.body
- 将自定义中间件封装为模块
整体代码
const express = require('express')
const app = express()
//导入node内置模块
const qs = require('querystring')
//解析表单数据的中间件
app.use((req, res, next) => {
//定义中间件具体的业务逻辑
//1. 定义一个str字符串,专门用来存储客户端发送过来的请求体数据
let str = ''
//2. 监听req的data事件
req.on('data', (chunk) => {
str += chunk
})
//3. 监听end事件
req.on('end',() => {
//在str中存放的是完整的请求体数据
//console.log(str);
//TODO:把字符串格式的请求体数据,解析成对象格式
const body = qs.parse(str)
//console.log(body);
//将解析出来的数据对象挂载为req.body
req.body = body
next()
})
})
app.post('/user',(req,res) => {
res.send(req.body)
})
app.listen(80, () => {
console.log('express server running at http://127.0.0.1');
})
模块化
model.js
//导入node内置模块
const qs = require('querystring')
const bodyParser = (req, res, next) => {
//定义中间件具体的业务逻辑
//1. 定义一个str字符串,专门用来存储客户端发送过来的请求体数据
let str = ''
//2. 监听req的data事件
req.on('data', (chunk) => {
str += chunk
})
//3. 监听end事件
req.on('end', () => {
//在str中存放的是完整的请求体数据
//console.log(str);
//TODO:把字符串格式的请求体数据,解析成对象格式
const body = qs.parse(str)
//console.log(body);
//将解析出来的数据对象挂载为req.body
req.body = body
next()
})
}
module.exports = bodyParser;
主.js
const express = require('express')
const app = express()
//导入自己封装的中间件模块
const customBodyParse = require('./08model')
//解析表单数据的中间件
app.use(customBodyParse)
app.post('/user',(req,res) => {
res.send(req.body)
})
app.listen(80, () => {
console.log('express server running at http://127.0.0.1');
})
使用Express写接口
实现步骤
- 创建express服务器
- 创建路由模块
- 编写GET接口
- 编写POST接口
主.js
const express = require('express')
const app = express()
//配置解析表单数据的中间件
app.use(express.urlencoded({ extended: false }))
//导入路由模块
const router = require('./09apiRouter')
//把路由模块注册到app上
app.use('/api', router)
app.listen(80, () => {
console.log('express server running at http://127.0.0.1')
})
apiRouter.js
const express = require('express')
const router = express()
//在这里挂载对应的路由
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,
msg: 'POST请求成功',
data: body
})
})
module.exports = router
CORS跨域资源共享
GET和POST接口存在跨域问题
解决端口跨域问题的方案:CORS、JSONP(只支持GET请求)
使用CORS中间件解决跨域问题
cors是Express的一个第三方中间件,通过安装和配置cors中间件可以解决跨域问题
主.js
//要在路由之前配置cors中间件
const cors = require('cors')
app.use(cors())
什么是CORS
CORS(Cross-Origin Resource Sharing,跨域资源共享)由一系列HTTP响应头组成,这些HTTP响应头决定浏览器是否组织前端JS代码跨域获取资源
浏览器的同源安全策略默认会组织网页“跨域”获取资源。但如果接口服务器配置了CORS相关的HTTP响应头,就可以解除浏览器端的跨域访问限制。
CORS注意事项
CORS主要在服务器端进行配置,客户端浏览器无需做任何额外的配置,即可请求开启了CORS接口
CORS在浏览器中有兼容性,只有支持XMLHttpRequest Level2的浏览器,才能正常访问开启了CORS的服务端接口
CORS响应头
Access-Control-Allow-Origin
Access-Control-Allow-Origin : <origin> | *
origin参数的值指定了允许访问该资源的外域URL,*是通配符
Access-Control-Allow-Headers
默认情况下,CORS仅支持客户端向服务器发送如下的9个请求头:
Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、WidthContent-Type (值仅限于text/plain、multipart/form-data、application/x-www-form-urlencoded三者之一
如果客户端向服务器发送了额外的请求头信息,则需要在服务器端,通过Access-Control-Allow-Headers 对额外的请求头进行声明,否则这次请求会失败
Access-Control-Allow-Methods
默认情况下,CORS仅支持客户端发起GET、POST、HEAD请求
如果客户端希望通过PUT、DELETE等方式请求服务器的资源,则需要在服务器端,通过Access-Control-Alow-Methods来指明实际请求所允许使用的HTTP方法。
CORS请求的分类
客户端在请求CORS接口时,根据请求方式和请求头的不同,可以将CORS的请求分为两大类
简单请求
同时满足以下两大条件的请求,就属于简单请求:请求方式:
1. GET、POST、HEAD三者之一
2. HTTP头部信息不超过以下几种字段:无自定义头部字段、Accept、Accept-Language、Content-Language、DPR,Downlink、Save-Data、Viewport-Width、Width 、Content-Type (只有三个值application/x-www-form-urlencoded、multipart/form-data、text/plain)
预检请求
简单请求的对立面
只要符合以下任何一个条件的请求,都需要进行预检请求:
1. 请求方式为GET、POST、HEAD之外的请求Method类型
2. 请求头中包含自定义头部字段
3. 向服务器发送了application/json格式的数据
在浏览器与服务器正式通信之前,浏览器会先发送ОPTION请求进行预检,以获知服务器是否允许该实际请求,所以这一次的OPTION请求称为“预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据。
二者区别
简单请求的特点:客户端与服务器之间只会发生一次请求
预检请求的特点:客户端与服务器之间会发生两次请求,OPTION预检请求成功之后,才会发起真正的请求
JSONP接口
概念:浏览器端通过<script>标签的src属性,请求服务器上的数据,同时服务器返回一个函数的调用。这种请求数据的方式叫做JSONP
特点:
- JSONP不属于真正的Ajax请求,因为它没有使用XMLHttpRequest 这个对象。
- JSONP仅支持GET请求,不支持 POST、PUT、DELETE 等请求。
注意事项:如果项目中已经配置了CORS跨域资源共享,为了防止冲突,必须在配置CORS中间件之前声明JSONP的接口。否则JSONP接口会被处理成开启了CORS的接口。
实现JSONP接口的步骤
- 获取客户端发送过来的回调函数的名字
- 得到要通过JSONP形式发送给客户端的数据
- 根据前两步得到的数据,拼接出—个函数调用的字符串
- 把上一步拼接得到的字符串,响应给客户端的<script>标签进行解析执行
主.js
const express = require('express')
const app = express()
//配置解析表单数据的中间件
app.use(express.urlencoded({ extended: false }))
//必须在配置cors中间件之前,配置JSONP接口
app.get('/api/jsonp',(req,res) => {
//TODO:定义JSONP接口具体的实现过程
//1. 得到函数的名称
const funcName = req.query.callback
//2. 定义要发送给客户端的数据对象
const data = {name:'zs'}
//3. 拼接处一个函数的调用
const srciptStr = `${funcName}(${JSON.stringify(data)})`
//4. 把拼接的字符串响应给客户端
res.send(srciptStr)
})
//要在路由之前配置cors中间件
const cors = require('cors')
app.use(cors())
//导入路由模块
const router = require('./09apiRouter')
//把路由模块注册到app上
app.use('/api', router)
app.listen(80, () => {
console.log('express server running at http://127.0.0.1')
})
.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/3.4.1/jquery.min.js"></script>
</head>
<body>
<button id="btnGET">GET</button>
<button id="btnPOST">POST</button>
<button id="btnJSONP">JSONP</button>
<script>
$(function () {
//1.测试GET接口
$("#btnGET").on('click',function() {
$.ajax({
type:'GET',
url:'http://127.0.0.1/api/get',
data:{name:'zs'},
success: (res) => {
console.log(res);
}
})
})
//2.测试POST接口
$(() => {
$("#btnPOST").on('click',() => {
$.ajax({
type:'POST',
url:'http://127.0.0.1/api/post',
data:{age:16},
success:(res) => {
console.log(res);
}
})
})
})
//3.测试JSONP接口
$(() => {
$("#btnJSONP").on('click',() => {
$.ajax({
type:'GET',
url:'http://127.0.0.1/api/jsonp',
dataType:'jsonp',
success:(res) => {
console.log(res);
}
})
})
})
})
</script>
</body>
</html>
apiRouter.js跟上边的一样
MySQL
数据库基本使用
常见的数据库
- MySQL数据库(Community + Enterprise)
- Oracle数据库
- SQL Server数据库
- Mongodb数据库(Community + Enterprise)
传统型数据库的数据组织结构
指的是数据以什么样的结构进行存储。传统型数据库的数据组织结构与Excel中数据的组织结构类似。
组织结构分为数据库、数据表、数据行、字段这4大部分组成
实际开发中库、表、行、字段的关系
在实际项目开发中,一般情况下,每个项目都对应独立的数据库
不同的数据要存储到数据库的不同表中。例如用户数据存储到users表中,图书数据存储到books表中。
每个表中具体存储哪些信息,由字段决定。例如可以为users表设计id、username、password这3个字段。
表中的行代表每一条具体的数据
创建数据库
字段的特殊标识:
PK(Primary Key):主键、唯一标识
NN(Not Null):值不允许为空
UQ(Unique):值唯一
AI(Auto Increment):值自动增长
SQL语言
SQL语言只能在关系型数据库中使用,非关系型数据库不支持SQL语言。
基于SQL增删改查
SELECT语句
用于从表中查询数据,执行结果被存储在一个结果表中。
--从FROM指定的表中查询出所有的数据,*表示所有列
SELECT * FROM 表名称
--从FROM指定的表中查询出指定列名称(字段)的数据
SELECT 列名称 FROM 表名称
注意:SQL语句对大小写不敏感
INSERT INTO语句
用于向数据表中插入新的数据行
INSERT INTO table_name (列1,列2,......) VALUES (值1,值2,......)
insert into users (username,password) values ('tony','123usersusers')
UPDATE语句
用于修改表中的数据
UPDATE 表名称 SET 列名称 = 新值 WHERE 列名称 = 某值
update users set password='666666' where id = 1
DELETE语句
用于删除表中的行
DELETE FROM users WHERE id = 4
WHERE子句
WHERE子句用于限定选择的标准。在SELECT、UPDATE、DELETE语句中,暂可使用WHERE子句来限定选择的标准
AND和OR运算符
AND和OR可在WHERE子语句中把两个或多个条件结合起来
AND相当于&&,OR相当于||
ORDER BY子句
用于根据指定的列对结果集进行排序
默认情况是按照升序对记录进行排序,如果希望降序可以使用DESC关键字
-- 升序
select * from users order by status
-- 降序
select * from users order by id desc
进行多重排序
对于表中的数据,先按照status字段进行降序排序,再按照username的字母顺序进行升序排序
select * fro users order by status DESC ,username ASC
COUNT(*)
用于返回查询结果的总数据条数
select count(*) from 表名称
使用AS为列设置别名
SELECT COUNT(*) AS total FROM users WHERE status=0
在项目中操作MySQL数据库
步骤
- 安装MySQL数据库的第三方模块
- 通过mysql模块连接到MySQL数据库
- 通过mysql模块执行SQL语句
安装mysql模块
npm i mysql
配置sql模块
在使用mysql模块操作MySQL数据库之前,必须先对mysql模块进行必要的配
测试mysql模块能否正常工作
调用db.query()函数,指定要执行的SQL语句,通过回调函数拿到执行的结果
const mysql = require('mysql')
const db = mysql.createPool({
host:'127.0.0.1', //数据库的IP地址
user:'root', //登录数据库的账号
password:'admin123', //登录数据库的密码
database:'my_db_01', //指定要操作哪个数据库
})
//测试mysql模块能否正常工作
//这个sql语句没有任何实质上的作用,就是用来测试是否可以正常工作的
db.query('SELECT 1',(err,results) => {
//mysql模块工作期间报错了
if(err) return console.log(err.message)
//能够成功的执行sql语句
console.log(results);
})
使用mysql模块操作MySQL数据库
查询数据
//查询users表中所有的用户数据
const sqlStr = 'select * from users'
db.query(sqlStr,(err,results) => {
if(err) return console.log(err.message);
console.log(results);
})
注意:如果执行的是select查询语句,则执行的结果是数组
插入数据
const user = {username:'spider-man1',password:'wow123'}
//sql中可以使用?来进行占位
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('插入数据成功');
})
可以使用?来占位
insert语句执行的结果是对象
插入数据的便捷方式
向表中新增数据时,如果数据对象的每个属性和数据表的字段一一对应,则可以通过如下方式快速插入数据
//演示插入数据的便捷方式
const user = {username:'sun',password:'666666'}
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('success');
})
更新数据
const user = {id:4,username:'aaa',password:'aaa'}
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('yes');
})
update执行的结果也是对象
更新数据的便捷方式
//便捷方式
const user = {id:4,username:'aaaa',password:'aaaa'}
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('yes');
})
删除数据
const sqlStr = 'delete from users where id=?'
db.query(sqlStr,4,(err,results) => {
if(err) return console.log(err.message);
if(results.affectedRows === 1) console.log('删除数据成功');
})
执行delete语句之后得到的也是对象
标记删除
使用DELETE语句,会真正的把数据从表中删除掉,为了保险起见,推荐使用标记删除的形式,来模拟删除的动作
所谓的标记删除就是在表中设置类似于status这样的状态字段,来标记当前的这条数据是否被删除
当用户执行了删除的动作时,并没有执行DELETE语句把数据删除掉,而是执行了UPDATE语句,将这条数据对应的status字段标记为删除即可。
前后端的身份认证
身份认证(Authentication)又称“身份验证”、“鉴权”,是指通过一定的手段,完成对用户身份的确认。
不同开发模式下的身份认证
对于服务器渲染和前后端分离这两种开发模式来说,分别有着不同的身份认证方案
1.服务端渲染推荐使用session认证机制
2.前后端分离推荐使用JWT认证机制
session认证机制
HTTP协议的无状态性
HTTP协议的无状态性,指的是客户端的每次HTTP请求都是独立的,连续多个请求之间没有直接的关系,服务器不会主动保留每次HTTP请求的状态。
如何突破HTTP无状态的限制:Cookie
Cookie
Cookie是存储在用户浏览器中的一段不超过4KB的字符串,它由一个名称、一个值和其他几个用于控制Cookie有效期、安全性、使用范围的可选属性组成。
不同域名下的Cookie 各自独立,每当客户端发起请求时,会自动把当前域名下所有未过期的Cookie一同发送到服务器。
Cookie几大特性
- 自动发送
- 域名独立
- 过期时限
- 4KB限制
Cookie在身份认证中的作用
客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的Cookie,客户端会自动将Cookie保存在浏览器中。
随后,当客户端浏览器每次请求服务器的时候,浏览器会自动将身份认证相关的Cookie,通过请求头的形式发送给服务器,服务器即可验明客户端的身份。
Cookie不具有安全性:由于Cookie 是存储在浏览器中的,而且浏览器也提供了读写Cookie的API,因此Cookie很容易被伪造,不具有安全性。因此不建议服务器将重要的隐私数据,通过Cookie的形式发送给浏览器。
千万不要使用Cookie存储重要且隐私的数据
Session的工作原理
使用session
配置session中间件
const session = require('express-session')
app.use(session({
secret:'sun',
resave:false,
saveUninitialized:true
}))
向session中存数据
当express-session中间件配置成功后,即可通过req.session来访问和使用session对象,从而存储用户的关键信息
从session中取数据
可以直接 从req.session对象上获取之前存储的数据
清空session数据
调用req.session.destroy()函数,即可清空服务器保存的session信息
// 导入 express 模块
const express = require('express')
// 创建 express 的服务器实例
const app = express()
// TODO_01:请配置 Session 中间件
const session = require('express-session')
app.use(session({
secret:'sun',
resave:false,
saveUninitialized:true
}))
// 托管静态页面
app.use(express.static('./pages'))
// 解析 POST 提交过来的表单数据
app.use(express.urlencoded({ extended: false }))
// 登录的 API 接口
app.post('/api/login', (req, res) => {
// 判断用户提交的登录信息是否正确
if (req.body.username !== 'admin' || req.body.password !== '000000') {
return res.send({ status: 1, msg: '登录失败' })
}
// TODO_02:请将登录成功后的用户信息,保存到 Session 中
req.session.user = req.body //用户的信息
req.session.isLogin = true //用户的状态
res.send({ status: 0, msg: '登录成功' })
})
// 获取用户姓名的接口
app.get('/api/username', (req, res) => {
// TODO_03:请从 Session 中获取用户的名称,响应给客户端
if(!req.session.isLogin) {
return res.send({status:1,msg:'fail'})
}
res.send({
status:0,
msg:'su',
username:req.session.user.username
})
})
// 退出登录的接口
app.post('/api/logout', (req, res) => {
// TODO_04:清空 Session 信息
req.session.destroy()
res.send({
status:0,
msg:'退出登录成功'
})
})
// 调用 app.listen 方法,指定端口号并启动web服务器
app.listen(80, function () {
console.log('Express server running at http://127.0.0.1:80')
})
JWT认证机制
了解Session认证的局限性
Session认证机制需要配合Cookie 才能实现。由于Cookie默认不支持跨域访问,所以,当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域Session认证。
注意:
- 当前端请求后端接口不存在跨域问题的时候,推荐使用Session身份认证机制
- 当前端需要跨域请求后端接口的时候,不推荐使用Session身份认证机制,推荐使用JWT认证机制
什么是JWT
JWT(JSON Web Token)是目前最流行的跨域认证解决方案
JWT工作原理
用户的信息通过token字符串的形式,保存在客户端的浏览器中。服务器通过还原Token字符串的形式来认证用户的身份
JWT组成部分
Header(头部)、Payload(有效荷载)、Signature(签名)。三者之间用英文“.”分隔
Header.Payload.Signature
Payload部分才是真正的用户信息,它是用户信息经过加密之后生成的字符串
Header和Signature是安全性相关的部分,只是为了保证Token的安全性
JWT的使用方式
客户端收到服务器返回的JWT之后,通常会将它储存在localStorage或 sessionStorage 中。
此后,客户端每次与服务器通信,都要带上这个JWT的字符串,从而进行身份认证。推荐的做法是把JWT放在 HTTP请求头的Authorization字段中,格式如下:
Authorization:Bearer <token>
安装相关包:npm i jsonwebtoken express-jwt
jsonwebtoken:用于生成JWT字符串
express-jwt:用于将JWT字符串解析还原成JSON对象
导入安装包
定义secret密钥
为了保证JWT字符串的安全性,防止JWT字符串在网络传输过程中被别人破解,我们需要专门定义一个用于加密和解密的secret密钥
- 当生成JWT字符串的时候,需要使用secret密钥对用户的信息进行加密,最终得到加密好的JWT字符串
- 当把JWT字符串解析还原成JSON对象的时候,需要使用secret密钥进行解密
在登陆成功后生成JWT字符串
调用jsonwebtoken包提供的sign()方法,将用户的信息加密成JWT字符串,响应给客户端
将JWT字符串还原为JSON对象
客户端每次在访问那些有权限接口的时候,都需要主动通过请求头中的Authorization字段,将Token字符串发送到服务器进行身份认证。
此时服务器可以通过express-jwt这个中间件,自动将客户端发送过来的Token解析还原成JSON对象
使用req.user获取用户信息
当express-jwt这个中间件配置成功之后,即可在那些有权限的接口中,使用req.user对象,来访问从JWT字符串中解析出来的用户信息
捕获解析JWT失败后产生的错误
当使用express-jwt解析Token字符串时,如果客户端发送过来的Token字符申过期或不合法,会产生一个解析失败的错误,影响项目的正常运行。我们可以通过Express的错误中间件,捕获这个错误并进行相关的处理
// 导入 express 模块
const express = require('express')
// 创建 express 的服务器实例
const app = express()
// TODO_01:安装并导入 JWT 相关的两个包,分别是 jsonwebtoken 和 express-jwt
const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')
// 允许跨域资源共享
const cors = require('cors')
app.use(cors())
// 解析 post 表单数据的中间件
const bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({ extended: false }))
// TODO_02:定义 secret 密钥,建议将密钥命名为 secretKey
const secretKey = 'sunhui666'
// TODO_04:注册将 JWT 字符串解析还原成 JSON 对象的中间件
//注意:只要配置成功了express-jwt这个中间件,就可以把解析出来的用户信息,挂载到req.user属性上
app.use(expressJWT({
secret:secretKey
}).unless({path:[/^\/api\//]}))
// 登录接口
app.post('/api/login', function (req, res) {
// 将 req.body 请求体中的数据,转存为 userinfo 常量
const userinfo = req.body
// 登录失败
if (userinfo.username !== 'admin' || userinfo.password !== '000000') {
return res.send({
status: 400,
message: '登录失败!'
})
}
// 登录成功
// TODO_03:在登录成功之后,调用 jwt.sign() 方法生成 JWT 字符串。并通过 token 属性发送给客户端
//参数1:用户的信息对象
//参数2:加密的密钥
//参数3:配置对象,可以配置当前的token有效期
const tokenStr = jwt.sign({username:userinfo.usename},secretKey,{expiresIn:'30s'})
res.send({
status: 200,
message: '登录成功!',
token: tokenStr // 要发送给客户端的 token 字符串
})
})
// 这是一个有权限的 API 接口
app.get('/admin/getinfo', function (req, res) {
// TODO_05:使用 req.user 获取用户信息,并使用 data 属性将用户信息发送给客户端
console.log(req.user);
res.send({
status: 200,
message: '获取用户信息成功!',
data: req.user // 要发送给客户端的用户信息
})
})
// TODO_06:使用全局错误处理中间件,捕获解析 JWT 失败后产生的错误
app.use((err,req,res,next) => {
//这次错误是由token解析失败导致的
if(err.message === 'UnauthorizedError') {
return res.send({
status:401,
message:'无效的token'
})
}
res.send({
status:500,
message:'未知的错误'
})
})
// 调用 app.listen 方法,指定端口号并启动web服务器
app.listen(8888, function () {
console.log('Express server running at http://127.0.0.1:8888')
})
node结束,实战部分见文档。
Koa
路由
为什么使用路由
如果不使用路由,那无论访问什么路径,页面呈现的东西都是相同的
路由存在的意义
处理不同的URL;处理不同的HTTP方法;解析URL上的参数
中间件
输出顺序是:1、2、3、2-1、1-1
REST
REST是万维网软件架构风格
Representational:数据表现形式
State:当前状态或者数据
Transfer:数据传输
REST的六个限制
1.客户端-服务端(Client/Server)
- 关注点分离
- 服务端专注于数据存储,提升了简单性
- 前端专注于用户界面,提升了可移植性
2.无状态(Stateless)
- 所有用户会话信息都保存在客户端
- 每次请求必须包含所有信息
- 不能依赖上下文信息
- 服务端不用保存会话信息
- 简单性、可靠性、可见性
3.缓存(Cache)
- 所有服务端响应都要被标为可缓存或不可缓存
- 减少前后端交互,提升性能(缓存的用处)
4.统一接口(Uniform Interface)
- 接口设计尽可能统一通用
- 接口实现解耦,前后端独立开发
5.分层系统(Layered System)
每层只知道相邻的一层
客户端不知道是和代理还是 服务器通信
其它层:安全曾、负载均衡、缓存层
6.按需代码(Code-On-Demand)
- 客户端可以下载运行服务端传来的代码
- 通过减少一些功能,简化客户端
统一接口的限制
资源的标识:资源是任何可以命名的事物,每个资源可以通过URI被唯一标识
通过表述来操作资源:表述就是Representation,客户端不能直接操作服务端资源,客户端应该通过表述来操作资源
自描述信息:每个消息必须提供足够的信息让接受者理解,媒体类型、HTTP方法、是否缓存
超媒体作为应用状态引擎(点击一个链接跳转到其他页面)
RESTful API
符合REST架构规则的API
请求设计规范
- URI使用名词,尽量使用复数,如/users
- URI使用嵌套表示关联关系,如/users /12/repos/5
- 使用正确的HTTP方法,如GET/POST/PUT
- 不符合CRUD的情况:POST /action/子资源
控制器
拿到路由分配的任务并执行
为什么要使用控制器
获取HTTP请求参数;处理业务逻辑
获取HTTP请求参数
在koa里如果想使用ctx.request.body需要安装一个中间件 koa-bodyparser
const Koa = require('koa2')
const bodyparser = require('koa-bodyparser')
const Router = require('koa-router')
const app = new Koa()
const router = new Router({
prefix:'/user'
})
router.get('/',async(ctx) => {
ctx.body = 'home'
})
router.get('/del',async (ctx) => {
//let {id} = ctx.request.query
let {id} = ctx.query
ctx.body = 'delete'
console.log(id);
})
router.get('/find/:id',async (ctx) => {
let {id} = ctx.params
ctx.body = 'find'
console.log(id);
})
router.post('/add',async (ctx) => {
let {username,pwd} = ctx.request.body
console.log(username,pwd)
ctx.body = 'add'
})
app.use(bodyparser())
app.use(router.routes())
// app.use(ctx => {
// if(ctx.url === '/') {
// ctx.body = '直接首页'
// } else if(ctx.url === '/user') {
// if(ctx.method === 'GET') {
// ctx.body = 'user'
// } else if (ctx.method === 'POST') {
// ctx.body = 'add'
// }
// } else {
// ctx.status = 404
// }
// })
app.listen(3000)
发送HTTP响应
router.get('/',async(ctx) => {
// ctx.body = '<h1>home</h1>'
ctx.set('Allow','GET,POST') //设置请求头
ctx.status = 301 //设置状态码
ctx.body = {
code:200,
msg:'这是请求首页的信息'
}
})
简单的增删改查
const Koa = require('koa2')
const bodyparser = require('koa-bodyparser')
const Router = require('koa-router')
const app = new Koa()
const router = new Router({
prefix:'/user'
})
let userList = [{username:'tom',pwd:'123'}]
router.get('/',async(ctx) => {
// ctx.body = '<h1>home</h1>'
ctx.set('Allow','GET,POST')
ctx.status = 301
ctx.body = {
code:200,
msg:'这是请求首页的信息'
}
})
//删除用户
router.delete('/del',async (ctx) => {
let {id} = ctx.request.body
userList.splice(Number(id),1)
ctx.body = {
code:200,
msg:'delete success'
}
})
//查询
router.get('/find',async (ctx) => {
ctx.body = {
data:userList
}
})
//添加用户
router.post('/add',async (ctx) => {
let {username,pwd} = ctx.request.body
userList.push({
username,
pwd
})
ctx.body = {
code:200,
msg:'添加成功'
}
})
//修改用户
router.put('/update',async (ctx) => {
let user = ctx.request.body
userList.splice(Number(user.id),1,{
username:user.username,
pwd:user.pwd
})
ctx.body = {
code:200,
msg:'修改成功'
}
})
//使用路由参数做查询
router.get('/find/:id',async (ctx) => {
let id = ctx.params.id
ctx.body = {
code:200,
user:userList[Number(id)],
msg:'router查询成功'
}
})
app.use(bodyparser())
app.use(router.routes())
// app.use(ctx => {
// if(ctx.url === '/') {
// ctx.body = '直接首页'
// } else if(ctx.url === '/user') {
// if(ctx.method === 'GET') {
// ctx.body = 'user'
// } else if (ctx.method === 'POST') {
// ctx.body = 'add'
// }
// } else {
// ctx.status = 404
// }
// })
app.listen(3000)
错误处理
运行时错误:返回500
逻辑错误:404找不到;412先决条件失败;422无法处理的实体
安装中间件:koa-json-error
const Koa = require('koa2')
const Router = require('koa-router')
const parameter = require('koa-parameter')
const jsonerror = require('koa-json-error')
const bodyparser = require('koa-bodyparser')
const app = new Koa()
const router = new Router({
prefix:'/user'
})
let userList = [{username:'sun',pwd:'123'}]
const add = async(ctx) => {
ctx.verifyParams({
username: {
type:'string',
required:true
},
pwd:{
type:'string',
require:true
}
})
let {username,pwd} = ctx.request.body
userList.push({
username,
pwd
})
ctx.body = {
code:200,
msg:'添加成功'
}
}
const find = async(ctx) => {
ctx.body = {
data:userList
}
}
router.post('/add',add)
router.get('/',find)
app.use(jsonerror())
app.use(bodyparser())
app.use(parameter(app))
app.use(router.routes())
app.listen(3000)
NoSQL
列存储(HBase)
文档存储(MongoDB)
Key-Value存储(Redis)
图存储(FlockDB)
对象存储(db4o)
XML存储(BaseX)
为什么要用NoSQL
简单、便于横向扩展、适合超大规模数据的存储、很灵活地存储复杂数据的结构
MongoDB
性能好(内存计算);大规模数据存储(可拓展性);可靠安全(本地复制、自动故障转移);方便存储复杂数据结构(Schema Free)
Koa项目实战
1.系统模块
系统登录、个人中心、文章管理、评论管理、粉丝管理、点赞管理
2.设置路由(前端 & 后端),布置页面
3.设置数据库模型和登录接口
服务器端models文件夹用来统一管理模型对象 (使用mongoose插件)
jwt加密
设置路由逻辑