node语法教程
可以实现的用途:
基于Express框架,可以快速构建Web应用
基于Electron框架,可以构建跨平台的桌面应用
基于restify框架,可以快速构建API接口项目
学习路径:
js基础语法+node内置API模块(fs、path、http等)+第三方API模块(express、mysql等)
下载时候遇到的一些问题:
LTS为稳定版
urrent为抢先版(最新版)
注意点:
node.js中无法调用DOM和BOM等浏览器内置API
fs模块语法
使用前需要导入
const fs = require('fs')
1.读取文件 fs.readFile()方法
用来读取指定文件中的内容
语法格式: fs.readFile(path[, options], callback)
path: 必须参数,字符串类型,表示文件的路径
options:可选参数,字符串类型,表示用什么编码格式来读取文件。
callback: 必选参数,回调函数,文件读取完成后,通过回调函数拿到读取的结果。回调函数里的参数有err, dataStr err为读取失败后的结果 dataStr为读取成功后的结果
常用编码格式有: utf8、utf16
案例:
// js文件
const fs = require('fs')
fs.readFile('./123.txt', 'utf8', function(err, dataStr) {
console.log(err)
console.log('----------')
console.log(dataStr)
})
// 输出结果为:
// null
// ----------
// 666666
666666
如果读取失败:err会返回一个错误对象, dataStr会返回 undefined
2.写入文件 fs.writeFile()方法
用来向指定的文件中写入内容
语法格式: fs.writeFile(file, data[, options], callback)
file: 必须参数,字符串类型,需要指定一个文件路径的字符串,表示文件的存放路径。
data: 必须参数,字符串类型,表示要写入的内容
options: 可选参数,字符串类型,表示用什么编码格式来写入文件。默认值:utf8
callback: 必选参数,回调函数,文件写入完成后的回调函数,回调函数中有err参数
示例:
const fs = require('fs')
fs.writeFile('./123.txt', 'Hello Node.js!', function(err) {
console.log(err)
})
// 打印结果为: null
如果写入失败:err会返回一个错误对象
__dirname 表示当前文件所处的目录
path模块
用来处理路径的模块
使用前向导入
const path = require('path')
1.拼接路径 path.join()方法
用来将多个路径片段拼接成一个完整的路径字符串
语法格式:path.join([…paths])
…paths: 路径片段的序列
返回值: string
示例:
const path = require('path')
const paths = path.join('/a', '/b/c', '../', '/d', 'e')
console.log(paths)
// 打印结果为: \a\b\d\e
2.解析文件名 path.basename()方法
用来从路径字符串中,将文件名解析出来
语法格式:path.basename(path[,ext])
path: 必须参数,字符串类型,表示一个路径的字符串
ext: 可选参数,字符串类型,表示文件扩展名
返回: 字符串类型,表示路径中的最后一部分
示例:
const path = require('path')
// const url = 'a\\b\\c\\index.html'
const url = '/a/b/c/index.html'
const filename = path.basename(url)
console.log(filename)
// 打印结果为: index.html
const filename1 = path.basename(url, '.html')
console.log(filename1)
// 打印结果为: index
3.获取路径中的扩展名 path.extname()方法
用来从路径中获取扩展名
语法格式:path.extname(path)
path: 必须参数,字符串类型,表示一个路径的字符串
返回: 字符串类型,返回得到的扩展名字符串
示例:
const path = require('path')
const fpath = '/a/b/c/index.html'
const fext = path.extname(fpath)
console.log(fext)
// 打印结果为: .html
http模块
1. IP地址
是互联网上每台计算机的唯一地址,因此IP地址具有唯一性。如果把“个人电脑”比作“一台电话”,那么“IP地址”就相当于“电话号码”,只有在知道对方IP地址的前提下,才能与对应的电脑之间进行数据通信。
IP地址格式 :通常会用“点分十进制”表示成(a.b.c.d)的形式,其中,a.b.c.d都是0~255之间的十进制整数。例如:用点分十进制表示的 IP地址(192.168.1.1)
查看网站的ip地址(示例查看百度的):cmd终端中输入 ping www.baidu.com
2. 域名和域名服务器
尽管IP地址能够唯一地标记网络上的计算机,但IP地址是一长串数字,不直观,而且不便于记忆,于是人们又发明了另一套字符型的地址方案,即所谓的域名(Domain Name)地址
IP地址和域名是一一对应的关系,这份对应关系存放在一种叫做域名服务器(DNS, Domain name server)的电脑中。使用者只需要通过好记的域名访问对应的服务器即可,对应的转换工作由域名服务器实现。因此,域名服务器就是提供IP地址和域名之间的转换的服务器。
注意:
①单纯使用IP地址,互联网中的电脑也能够正常工作。但是有了域名的加持,能让互联网的世界变得更加方便。
②在开发测试期间,127.0.0.1对应的域名是localhost,它们都代表我们自己的这台电脑,在使用效果上没有任何区别。
3.端口号
计算机中的端口号,就好像是在现实生活中的门牌号一样。通过门牌号,外卖小哥可以在整栋楼众多的房间中,准确把外卖送到你的手中。
同样的道理,在一台电脑中,可以运行成百上千个web服务。每个web服务都对应一个唯一的端口号。客服端发送过来得到网络请求,通过端口号,可以被准确地交给 对应的web服务 进行处理。
注意:
①每个端口号不能同时被多个web服务占用。
②在实际应用中,URL中的80端口可以被省略
http.createServer()
创建基本的web服务器
1. 创建web服务器的基本步骤
① 导入http模块
const http = require('http')
② 创建web服务器实例
调用http.createServer()方法,即可快速创建一个web服务器实例
const server = http.createServer()
③ 为服务器实例绑定 request 事件,监听客户端的请求
// 使用服务器实例的.on()方法,为服务器绑定一个request事件
server.on('request', (req, res) => {
// 只要有客户端来请求我们自己的服务器,就会触发 request 事件,从而调用这个事件处理函数
console.log('Someone visit our web server.')
})
④ 启动服务器
调用服务器实例的 .listen() 方法,即可启动当前的web服务器
// 调用 server.listen(端口号, callback回调) 方法,即可启动 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 = `客户端请求的地址:${req.url}, 客户端请求的类型:${req.method}`
console.log(str)
})
res响应对象
在服务器的 request 事件处理函数中,如果想访问与服务器相关的数据与属性,可以使用如下的方式
res.end(str)
server.on('request', (req,res) => {
// res 是响应对象,它包含了与服务器相关的数据和属性
// 要发送到客户端的字符串
const str = `客户端请求的地址: ${req.url}, 客户端请求的类型:${req.method}`
// 调用 res.end()方法,向客户端响应一些内容
res.end(str)
})
解决中文乱码问题
当调用 res.end() 方法,向客户端发送中文内容的时候,会出现乱码问题,此时,需要手动设置内容的编码格式:
res.setHeader(‘Content-Type’,‘text/html; charset=utf-8’)
server.on('request', (req,res) => {
// 发送的内容包含中文
const str = `您请求的 URL 地址是 ${req.url}, 请求的 method 类型是 ${req.method}`
// 为了防止中文显示乱码的问题,需要设置响应头 Content-Type 的值为 text/html; charset=utf-8
res.setHeader('Content-Type','text/html; charset=utf-8')
// 把包含中文的内容,响应给客户端
res.end(str)
})
根据不同的url响应不同的html内容
核心实现步骤
① 获取请求的url地址
② 设置默认的响应内容为 404 Not found
③ 判断用户请求的是否为 / 或 /index.html 首页
④ 判断 用户请求的是否为 /about.html 关于页面
⑤ 设置Content-Type 响应头,防止中文乱码
⑥ 使用 res.end() 把内容响应给客户端
server.on('request', (req,res) => {
// 获取请求的url
const url = req.url
// 设置默认的内容为404 Not found
let content = '<h1>404 Not found!</h1>'
// 用户请求的是首页
if(url === '/' || url === '/index.html'){
content = '<h1>首页</h1>'
}else if(url === '/about.html'){
// 用户请求的是其他页面
content = '<h1>关于页面</h1>'
}
// 设置Content-Type 响应头,防止中文乱码
res.setHeader('Content-Type', 'text/html; charset=utf-8')
// 把内容发送给客户端
res.end(content)
})
模块化
模块化是指解决一个复杂问题时,自顶向下逐层把系统划分成若干模块的过程。对于整个系统来说,模块是可组合、分解和更换的单元。
编程领域中的模块化,就是遵守固定的规则,把一个大文件拆成独立并互相依赖的多个小模块。
好处:
① 提高代码的复用性
② 提高代码的可维护性
③ 可以实现按需加载
模块作用域
与函数作用域类似,在自定义模块中定义的变量、方法等成员,只能在当前模块内被访问,这种模块级别的访问限制,叫做模块作用域
模块向外共享成员
module.exports
let age = 18
module.exports.age = age
module.exports.name = '前端'
等价 exports
let age = 18
exports.age = age
exports.name = '前端'
CommonJS
npm管理包
npm的官网:https://www.npmjs.com/
npm服务器地址:https://registry.npmjs.org
注意:
从https://www.npmjs.com/ 网站上搜索自己所需要的包
从https://registry.npmjs.org 服务器上下载自己需要的包
package.json文件说明
npm init -y 创建一个package.json
dependencies节点 : 记录npm install 下载的包,开发阶段与上线后都会用到
devDependencies节点:如果某些包只在项目开发阶段会用到,在项目上线之后不会用到,建议把这些记录到devDependencies节点中
# 安装指定的包,并记录到devDependencies
npm i 包名 -D
# 完整写法
npm install 包名 --save-dev
切换npm的下包镜像源 切换成淘宝的
# 查看当前的下包镜像源
npm config get registry
# 将下包的镜像源切换为淘宝镜像源
npm config set registry=https://registry.npm.taobao.org/
# 检查镜像源是否下载成功
npm config get registry
nrm
为了更方便的切换下包的镜像源,我们可以安装nrm这个小工具,利用nrm提供的终端命令,可以快速查看和切换下包的镜像源。
# 通过 npm 包管理器,将 nrm 安装为全局可用的工具
npm i nrm -g
# 查看所有可用得到镜像源
nrm ls
# 将下包的镜像源切换为 taobao 镜像
nrm use taobao
开发自己的包
初始化包的基本结构
① 新建 itheima-tools 文件夹,作为包的根目录
② 在itheima-tools 文件夹中,新建如下三个文件夹:
- package.json(包管理配置文件)
- index.js (包的入口文件)
- README.md (包的说明文档)
初始化package.json
这里的name不能与包名和导入一样
{
"name": "itheima-tools1",
"version": "1.0.0",
"main": "index.js",
"description": "提供了格式化时间,HTMLEscape的功能",
"keywords": ["itheima","dateFormat","escape"],
"license": "ISC"
}
定义一个转义HTML的方法
function htmlEscape(htmlStr){
return htmlStr.replace(/<|>|"|&/g,(match) => {
switch(match){
case '<':
return '<'
case '>':
return '>'
case '"':
return '"'
case '&':
return '&'
}
})
}
定义还原HTML 字符串的函数
function htmlUnEscape(str) {
return str.replace(/<|>|"|&/g, (match) => {
switch(match){
case '<':
return '<'
case '>':
return '>'
case '"':
return '"'
case '&':
return '&'
}
})
}
编写包的说明文档 README.md文档
包根目录中的README.md文件,是包的使用说明文档。通过它,我们可以事先把包的使用说明,以mackdown的格式写出来,方便用户参考。
README文件中具体写什么内容,没有强制的要求;只要能够清晰地把包的作用、用法、注意事项等描述清楚即可。
我们所创建的这个包的README.md文档中,会包含以下6项内容:
安装方式、导入方式、格式化时间、转义HTML中的特殊字符、还原HTML中的特殊字符、开源协议
## 安装
```
npm install itheima-tools
```
## 导入
```js
const itheima = require('itheima-tools')
```
## 格式化时间
```js
// 调用 dateFormat 对时间进行格式化
const dtStr = iteima.dateFormat(new Date())
// 结果 2022-10-11 18:00:01
console.log(dtStr)
```
## 转义 HTML 中的特殊字符
```js
// 带转换的 HTML 字符串
const htmlStr = '<h1 title="abc">这是h1标签<span>123 </span></h1>'
// 调用 htmlEscape 方法进行转换
const str = itheima.htmlEscape(htmlStr)
// 转换的结果 <h1 title="abc">这是h1标签<span>123&nbsp;</span></h1>
console.log(str)
```
## 还原 HTML 中的特殊字符
```js
// 待还原的 HTML 字符串
const str2 = itheima.htmlUnEscape(str)
// 输出的结果 <h1 title="abc">这是h1标签<span>123 </span></h1>
console.log(str2)
```
## 开源协议
ISC
发布包
注册npm账号
① 访问 https://www.npmjs.com 网站,点击 sign up 按钮,进入注册用户界面
② 切换到包的根目录下 npm publish
删除已发布的包
npm unpublish 包名 --force
express模块 优于http模块
http内置模块用起来很复杂,开发效率低,Express是基于内置的http模块进一步封装出来的,能够极大的提高开发效率
用途:
Web网站服务器:专门对外提供Web网页资源的服务器
API接口服务器:专门对外提供API接口的服务器
使用Express,可以方便、快速的创建Web网站的服务器或API接口的服务器
安装:
npm i express@4.17.1
示例:
// 导入 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()
通过res.send()方法,可以把处理好的内容,发送给客户端
app.get('/user',(req,res) => {
// 向客户端发送 JSON 对象
res.send({ name: 'zs', age: 20, gender: '男' })
})
获取 URL 中携带的查询参数 req.query
通过 req.query对象,可以访问到客户端通过查询字符串的形式,发送到服务器的参数:
app.get('/',(req,res) => {
// req.query 默认是一个空对象
// 客户端使用 ?name=zs&age=20 这种查询字符串形式,发送到服务器的参数
// 可以通过 req.query 对象访问到 例如:
// req.query.name req.query.age
console.log(req.query)
})
获取 URL 中的动态参数
通过 req.params 对象,可以访问到URL中,通过:匹配到的动态参数:
// :id是动态参数
app.get('/del/:id', (req, res) => {
// req.params 默认是一个空对象
console.log(req.params)
res.send(`已删除id:${req.params.id} 的文章`)
})
匹配多个动态参数:
// :id是动态参数 :name也是一个动态参数
app.get('/del/:id/:name', (req, res) => {
// req.params 默认是一个空对象
console.log(req.params)
res.send(`已删除id:${req.params.id} 的文章, 书名是:${req.params.name}`)
})
托管静态资源 express.static() 方法
express 提供了一个非常好用的函数,叫做 express.static() , 通过它,我们可以非常方便地创建一个静态资源服务器
例如:通过如下代码就可以将 public 目录下的图片、CSS文件、JavaScript文件对外开放访问了:
app.use(express.static('public'))
// 现在,你就可以访问 public 目录中的所有文件了s
注意: Express在指定的静态目录中查找文件,并对外提供资源的访问路径。因此,存放静态文件的目录名不会出现在URL 中。
挂载路径前缀
如果希望在托管的静态资源访问路径之前,挂载路径前缀,则可以使用如下的方式的方式:
app.use('/public', express.static('public'))
nodemon
在编写调用Node.js项目的时候,如果修改项目的代码,则需要频繁的手动close掉,然后再重新启动,非常频繁。
可以使用nodemon这个工具,它能够监听项目文件的变动,当代码被修改后,nodemon会自动帮我们重启项目,极大方便了开发和调试。
地址:https://www.npmjs.com/package/nodemon
安装:
npm i nodemon -g
使用:
当基于Node.js编写了一个网站应用的时候,传统的方式,是运行node app.js命令,来启动项目。这样做的坏处是:代码被修改之后,需要手动重启项目。
现在,我们可以将node命令替换为nodemon命令,使用nodemon app.js 来启动项目。这样做的好处是:代码被修改之后,会被nodemon监听到,从而实现自动重启项目的效果。
nodemon 项目文件.js
**注意:**如果使用Nodemon 启动项目报错,多半是因为你是使用VScode里的终端启动的,因为你VScode没有获取管理员权限,因为权限不够所以报错了。如果不会调试,建议使用管理员权限启动一个CMD窗口来使用nodemon,现在是练习,不要死杠,先实现能用就行了,先做个留坑的笔记,等后面node开发服务器没问题的时候,再返回来研究怎么在VScode中使用nodemon 来启动服务器。
express路由 app.METHOD()
广义上来讲:路由就是映射关系。
在express 中,路由指的是客户端的请求与服务器处理函数之间的映射关系。
express中的路由分 3 部分组成,分别是请求的类型、请求的URL地址、处理函数,格式如下:
app.METHOD(PATH, HANDLER)
每当一个请求到达服务器之后,需要先经过路由的匹配,只有匹配成功之后,才会调用对应的处理函数。
在匹配时,会按照路由的顺序进行匹配,如果请求类型和请求的URL同时匹配成功,则Express会将这次请求,转交给对应的function函数处理
模块化路由
为了方便对路由进行模块化的管理,Express不建议将路由直接挂载到app上,而是推荐将路由抽离为单独的模块。
将路由抽离为单独模块的步骤如下:
① 创建路由模块对应的 .js文件
② 调用 express.Router() 函数创建路由对象
③ 向路由对象上挂载具体的路由
④ 使用 module.exports 向外共享路由对象
⑤ 使用 app.use() 函数注册路由模块
示例:
定义路由模块
// 导入express 模块
const express = require('express')
// 创建路由对象
const router = express.Router()
// 挂载获取用户列表的路由
router.get('/user', (req, res) => {
res.send('Get user list.')
})
// 挂载添加用户的路由
router.post('/user/add', (req, res) => {
res.send('Add new user.')
})
// 向外导出路由对象
module.exports = router
注册路由模块
// 导入路由模块
const userRouter = require('./创建路由模块.js')
// 使用 app.use() 注册路由模块
app.use(userRouter)
为路由模块添加前缀
类似于托管静态资源时,为静态资源统一挂载访问前缀一样,路由模块添加前缀的方式也非常简单:
// 导入路由模块
const userRouter = require('./router/user.js')
// 使用 app.use() 注册路由模块,并添加统一的访问前缀 /api
app.use('/api',userRouter)
express路由精简项目结构
express中间件
当一个请求到达 Express 的服务器之后,可以连续调用多个中间件,从而对这次请求进行预处理
Express 的中间件,本质上就是一个 function 处理函数,Express 中间件的格式如下:
const express = require('express')
const app = express()
app.get('/user', (req,res,next) => {
// next() 就是中间件处理函数
next()
})
定义中间件函数
可以通过如下的方式,定义一个最简单的中间件函数:
// 常量 mw 所指向的,就是一个中间件函数
const mw = function(req,res,next) {
console.log('这是一个最简单的中间件函数')
// 注意:在当前中间件业务处理完毕后,必须调用 next() 函数
// 表示把流转关系转交给下一个中间件或路由
next()
}
全局生效的中间件
客户端发起的任何请求,到达服务器之后,都会触发的中间件,叫做全局生效的中间件。
通过调用 app.use(中间件函数),即可定义一个全局生效的中间件,示例代码如下:
// 常量 mw 所指向的,就是一个中间件函数
const mw = function (req,res,next) {
console.log('这是一个最简单的中间件函数')
next()
}
// 全局生效的中间件
app.use(mw)
定义全局中间件的简化形式
// 定义一个全局中间件
app.use((req,res,next) => {
console.log('这是最简单的中间件函数')
next()
})
中间件的作用
多个中间件之间,共享同一份 req 和 res。基于这样的特性,我们可以在上游的中间件中,统一为req 或 res 对象添加自定义的属性和方法,供下游的中间件或路由进行使用。
定义多个全局中间件
可以使用 app.use() 连续定义多个全局中间件。客户端请求到达服务器之后,会按照中间件定义的先后顺序依次进行调用,示例代码如下:
app.use((req,res,next) => {
console.log('调用了第一个全局中间件')
next()
})
app.use((req,res,next) => {
console.log('调用了第二个全局中间件')
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('/user', mw1 ,(req,res) => {
res.send('Home page.')
})
// mw1 这个中间件不会影响下面的这个路由
app.get('/user',(req,res) => {
res.send('User page.')
})
定义多个局部中间件
可以在路由中,通过如下两种等价的方式,使用多个局部中间件
// 以下两种写法是“完全等价”的, 可根据自己的喜好,选择任意一种方式进行使用
app.get('/user', mw1, mw2, (req,res) => {
res.send('Home page.')
})
app.get('/user', [mw1, mw2], (req,res) => {
res.send('Home page.')
})
使用中间的5个注意事项
① 一定要在路由之前注册中间件
② 客户端发送过来的请求,可以连续调用多个中间件进行处理
③ 执行完中间件的业务代码之后,不要忘记调用 next() 函数
④ 为了防止代码逻辑混乱,调用 next() 函数后不要再写额外的代码
⑤ 连续调用多个中间件时,多个中间件之间,共享 req 和 res 对象
中间件的分类
① 应用级别 的中间件
通过 app.use() 或 app.get() 或 app.post() , 绑定到 app 实例上的中间件,叫做应用级别的中间件,代码如下示例:
// 应用级别的中间件(全局中间件)
app.use((req,res,next) => {
next()
})
// 应用级别的中间件(局部中间件)
app.get('/user', mw1, (req,res) => {
res.send('Home page.')
})
② 路由级别 的中间件
绑定到 express.Router() 实例上的中间件,叫做路由级别的中间件。它的用法和应用级别中间件没有任何区别。只不过,应用级别中间件时绑定到app 实例上,路由级别中间件绑定到 router 实例上,代码示例如下:
const express = require('express')
const router = express.Router()
// 路由级别的中间件
router.use((req,res,next) => {
console.log('Time:', Date.now())
next()
})
app.use('/', router)
③ 错误级别 的中间件
错误级别中间件的作用:专门用来捕获整个项目中发生的异常作物,从而防止项目异常崩溃的问题。
格式:错误级别中间件的 function处理函数中,必须有4个形参,形参顺序从前到后,分别是(err,req,res,next)
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}`)
})
注意:错误级别的中间件必须注册在所有路由之后!
④ 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 官方内置的,而是由第三方开发出来的中间件,叫做第三方中间件。在项目中,大家可以按需下载并配置第三方中间件,从而提高项目的开发效率。
例如:在express@4.16.0之前的版本中,经常使用body-parser 这个第三方中间件,来解析请求体数据。使用步骤如下:
一、运行 npm install body-parser 安装中间件
二、使用 require 导中间件
三、调用 app.use() 注册并使用中间件
注意:Express 内置的 express.urlencoded 中间件,就是基于 body-parser 这个第三方中间件进一步封装出来的。
自定义中间件
需求描述 与 实现步骤
自己手动模拟一个类似于 express.urlencoded 这样的中间件,来解析 POST 提交到服务器的表单数据。
实现步骤:
① 定义中间件
使用 app.use() 来定义全局生效的中间件,代码如下:
app.use((req,res,next) => {
// 中间件的业务逻辑
})
② 监听 req 的 data 事件
在中间件中,需要监听 req 对象的 data 事件,来获取客户端发送到服务器的数据。
如果数据比较大,无法一次性发送完毕,则客户端会把数据切割后,分批发送到服务器。所以 data 事件可能会触发多次,每一次触发 data 事件时,获取到数据只能是完整数据的一部分,需要手动对接收到的数据进行拼接。
// 定义变量,用来存储客户端发送过来的请求体数据
let str = ''
// 监听 req 对象的 data 事件(客户端发送过来的请求体数据)
req.on('data', (chunk) => {
// 拼接请求体数据,隐式转换为字符串
str += chunk
})
③ 监听 req 的 end 事件
当请求数据接收完毕之后,会自动触发 req 的 end 事件。
因此,我们可以在 req 的 end 事件中,拿到并处理完整的请求体数据。示例代码如下:
// 监听 req 对象的 end 事件(请求体发送完毕后自动触发)
req.on('end',() => {
// 打印完整的请求体数据
console.log(str)
// TODO: 把字符串格式的请求体数据,解析成对象格式
})
④ 使用 querystring 模块解析请求体数据
Node.js 内置了一个 querystring 模块,专门用来处理查询字符串。通过这个模块提供的 parse() 函数,可以轻松把查询字符串,解析成对象的格式。示例代码如下:
// 导入处理 querystring 的 Node.js 内置模块
const qs = require('querystring')
// 调用 qs.parse() 方法,把查询字符串解析为对象
const body = qs.parse(str)
注意:querystring 已被淘汰,可以改换使用自带的 URLSearchParams API
const body = new URLSearchParams(str); // 解析查询字串
const params = Object.fromEntries(body); // 将之转为 object
⑤ 将解析出来的数据对象挂载为 req.body
上游的中间件和下游的中间件及路由之间,共享同一份 req 和 res。因此,我们可以将解析出来的数据,挂载为 req 的自定义属性,命名为 req.body,供下游使用。示例代码如下:
req.on('end', () => {
// 调用 qs.parse() 方法,把查询字符串解析为对象
const body = qs.parse(str)
// 将解析出来的请求体对象,挂载为 req.body 属性
req.body = body
// 最后,一定要调用 next() 函数,执行后续的业务逻辑
next()
})
⑥ 将自定义中间件封装为模块
为了优化代码的结构,我们可以把自定义的中间件函数,封装为独立的模块,示例代码如下:
// custom-body-parser.js 模块中的代码
const qs = require('querystring')
function bodyParser(req,res,next) {
let str = ''
req.on('data', (chunk) => {
str += chunk
})
req.on('end', () => {
const body = qs.parse(str)
req.body = body
next()
})
}
module.exports = bodyParser
// 导入自定义的中间件模块
const myBodyParser = require('./custom-body-parser.js')
// 注册自定义的中间件模块
app.use(myBodyParser)
完整示例:
// 导入 querystring 模块
const qs = require('querystring')
app.use((req,res,next) => {
// 中间件的业务逻辑
// 定义变量,用来存储客户端发送过来的请求体数据
let str = ''
// 监听 req 的 data 事件
req.on('data',(chunk) => {
// 拼接请求体数据,隐式转换为字符串
str += chunk
})
// 监听 req 的 end 事件
req.on('end', () => {
// 在 str 中存放的式完整的请求体数据
console.log(str)
// TODO: 把字符串格式的请求体数据,解析成对象格式
const body = qs.parse(str)
console.log(body)
// 将解析出来的请求体对象,挂载为 req.body 属性
req.body = body
// 最后,一定要调用 next() 函数,执行后续的业务逻辑
next()
})
})
express 创建API接口
// apiRouter.js [路由模式]
const express = require('express')
const apiRouter = express.Router()
// bind your router here...
module.exports = apiRouter
// app.js [导入并注册路由模块]
const apiRouter = require('./apiRouter.js')
app.use('/api',apiRouter)
编写 GET 接口
apiRouter.get('/get',(req,res) => {
// 1.获取到客户端通过查询字符串,发送到服务器的数据
const query = req.query
// 2.调用 res.send() 方法,把数据响应给客户端
res.send({
// 状态,0表示成功, 1 表示失败
status: 0,
// 状态描述
msg: 'GET请求成功!',
// 需要响应给客户端的具体数据
data: query
})
})
编写 POST 接口
apiRouter.post('/post',(req,res) => {
// 获取客户端通过请求体,发送到服务器的 URL-encoded 数据
const body = req.body
// 调用 res.send() 方法,把数据响应给客户端
res.send({
// 状态,0 表示成功,1 表示失败
status: 0,
// 状态描述消息
msg: 'POST请求成功!',
// 需要响应给客户端的具体数据
data: body
})
})
注意: 如果要获取 URL-encoded 格式的请求体数据,必须配置中间件 app.use(express.urlencoded({ extended: false }))
express 中启用 cors 跨域资源共享
接口的跨域问题
以上编写的 GET 和 POST 接口,存在一个很严重的问题:不支持跨域请求。
解决接口跨域问题的方案主要有两种:
① CORS (主流的解决方案,推荐使用)
使用 cors 中间件解决跨域问题
cors 是 Express 的一个第三方中间件。通过安装和配置 cors 中间件,可以很方便地解决跨域问题。
使用步骤分如下3步:
① 运行 npm install cors 安装中间件
② 使用 const cors = require(‘cors’) 导入中间件
③ 在路由之前调用 app.use(cors()) 配置中间件
CORS( Cross-Origin Resource Sharing,跨域资源共享 ) 由一系列 HTTP 响应头组成,这些 HTTP 响应头决定浏览器是否阻止前端 JS 代码跨域获取资源。
浏览器的同源安全策略默认会阻止网页 “跨域” 获取资源。但如果接口服务器配置了 CORS 相关的 HTTP 响应头,就可以解除浏览器端的跨域访问限制。
如果服务器响应回来的数据 配置了 Access-Control-Allow-*相关的响应头就不会被浏览器拦截
CORS 的注意事项
① CORS 主要在服务器端进行配置。客户端浏览器无须做任何额外的配置,即可请求开启了 CORS 的接口。
② CORS 在浏览器中有兼容性。只有支持 XMLHttpRequest Level2的浏览器,才能正常访问开启了 CORS的服务端接口 (例如:IE10+、Chrome4+、FireFox3.5+)
一、CORS 响应头部 -Access-Control-Allow-Origin
语法: Access-Control-Allow-Origin : | *
其中,origin 参数的值指定了允许访问该资源的外域 URL
例如,下面的字段值将 只允许 来自 http://itcast.cn 的请求:
res.setHeader('Access-Control-Allow-Origin', 'http://itcast.cn')
如果允许来自任何域的请求,示例代码如下:
res.setHeader('Access-Control-Allow-Origin', '*')
二、CORS 响应头部 -Access-Control-Allow-Headers
默认情况下,CORS 仅支持客户端向服务器发送如下的 9个请求头:
Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width、Content-Type(值仅限于 text/plain、multipart/form-data、application/x-www-form-urlencoded 三者之一)
如果客户端向服务器发送了额外的请求头信息,则需要在服务器端,通过 Access-Control-Allow-Headers 对额外的请求头进行声明,否则这次请求会失败!
// 允许客户端额外向服务器发送 Content-Type 请求头和 X-Custom-Header 请求头
// 注意: 多个请求头之间使用英文的逗号进行分割
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Custom-Header')
三、CORS 响应头 -Access-Control-Allow-Methods
默认情况下,CORS 仅支持客户端发起GET、POST、HEAD 请求。
如果客户端希望通过 PUT、DELETE 等方式请求服务器的资源,则需要在服务器端,通过 Access-Control-Allow-Methods 来指明实际请求所允许使用的 HTTP 方法。
// 只允许 POST、GET、DELETE、HEAD 请求方法
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, DELETE, HEAD')
// 允许所有的 HTTP 请求方法
res.setHeader('Access-Control-Allow-Methods', '*')
CORS请求的分类
客户端在请求 CORS 接口时,根据请求方式和请求头的不同,可以将 CORS 的请求分为 两大类,分别时:
一、简单请求
同时满足以下两大条件的请求,就属于简单请求:
① 请求的方式: GET、POST、HEAD 三者之一
② HTTP 头部信息不超过以下几种字段:无自定义头部字段、Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width、Content-Type(只有三个值 text/plain、multipart/form-data、application/x-www-form-urlencoded)
二、预检请求
只要符合以下任何一个条件的请求,都需要进行预检请求:
① 请求方式为 GET、POST、HEAD 之外的请求 Method类型
② 请求头中包含自定义头部字段
③ 向服务器发送了 application/json 格式的数据
在浏览器与服务器正式通信之前,浏览器会先发送 OPTION 请求进行预检,以获知服务器是否允许该实际请求,所以这一次的 OPTION 请求称为 “预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据。
简单请求与预检请求的区别
简单请求的特点: 客户端与服务器之间只会发生一次请求。
预检请求的特点:客户端与服务器之间会发生两次请求,OPTION 预检请求成功之后,才会发起真正的请求。
② JSONP (有缺陷的解决方案:只支持 GET 请求)
概念:浏览器端通过
特点:
一、JSONP 不属于真正的 Ajax 请求,因为它没有使用 XMLHttpRequest 这个对象。
二、JSONP 仅支持 GET 请求,不支持 POST、PUT、DELETE 等请求。
创建 JSONP 接口的注意事项
如果项目中已经配置了 CORS 跨域资源共享,为了==防止冲突,必须在配置 CORS 中间件之前声明 JSONP 的接口。==否则 JSONP 接口会被处理成开启了 CORS 的接口。示例代码如下:
const express = require('express')
// 创建服务器实例
const app = express()
// 导入第三方的 cors包 解决跨域问题的插件
const cors = require('cors')
// 优先创建 JSONP 接口 [这个接口不会被处理成 CORS 接口]
app.get('/api/jsonp', (req, res) => {})
// 在全局中注册 cors 插件 要在路由之前使用
app.use(cors())
// 配置解析 application/x-www-form-urlencoded 格式数据的内置中间插件 要在路由之前使用
app.use(express.urlencoded({ extended: false }))
// 导入自己的路由模块
const router = require('./apiRouter/apiRouter.js')
// 挂载自己定义的路由模块
app.use('/api', router)
// 启动服务器
app.listen(80, () => {
console.log('express server running at http://127.0.0.1')
})
实现 JSONP 接口的步骤
一、获取客户端发送过来的回调函数的名字
二、得到要通过 JSONP 形式发送给客户端的数据
三、根据前两步得到的数据,拼接出一个函数调用的字符串
四、把上一步拼接得到的字符串,响应给客户端的
实现 JSONP 接口的具体代码
app.get('/api/jsonp', (req,res) => {
// 获取客户端发送过来的回调函数的名字
const funcName = req.query.callback
// 得到要通过 JSONP 形式发送给客户端的数据
const data = { name: 'lyx', age: 22 }
// 根据前两步得到的数据,拼接出一个函数调用的字符串
const scriptStr = `${funcName}(${JSON.stringify(data)})`
// 把上一步拼接得到的字符串,响应给客户端的 <script> 标签进行解析执行
res.send(scriptStr)
})
测试JSONP接口
使用的是 jQuery 发起 JSONP 请求
调用 $.ajax() 函数,提供 JSONP 的配置选项,从而发起 JSONP 请求,示例代码如下:
<!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.6.1/jquery.min.js"></script>
</head>
<body>
<button id="btnJSONP">JSONP</button>
<script>
$(function() {
// 测试JSONP的接口
$('#btnJSONP').on('click', function() {
$.ajax({
method: 'GET',
url: 'http://127.0.0.1/api/jsonp',
// 表示要发起 JSONP 的请求
dataType: 'jsonp',
success: function(res) {
console.log(res)
}
})
})
})
</script>
</body>
</html>
数据库知识点
如何配置 MySQL 数据库环境
数据库(database) 是用来组织、存储和管理数据的仓库。
市面上常见的数据库:
1. MySQL 数据库 (目前使用最广泛、流行度最高的开源免费数据库; Community(社区免费版) + Enterprise(企业收费版))
2. Oracle 数据库(收费)
3. SQL Server 数据库(收费)
4. Mongodb 数据库 (Community + Enterprise)
其中,MySQL、Oracle、SQL Server 属于 传统型数据库(又叫做:关系型数据库 或 SQL 数据库),这三者的设计理念相同,用法比较类似。
而 Mongodb 属于新型数据库(又叫做:非关系型数据库 或 NoSQL 数据库),它在一定程度上弥补了传统型数据库的缺陷。
MySQL 数据组织结构 与 Excel的数据组织结构类似
Excel 工作簿 -> 工作表 -> 数据行 -> 列
MySQL 数据库(database) -> 数据表(table) -> 数据行(row) -> 字段(field)
每个字段都有对应的数据类型
实际开发中库、表、行、字段的关系
① 在实际项目开发中,一般情况下,每个项目都对应独立的数据库。
② 不同的数据,要存储到数据库的不同表中,例如:用户数据存储到 users 表中,图书数据存储到 books 表中。
③ 每个表中具体存储哪些信息,由字段来决定,例如:我们可以为 users 表设计 id、username、password 这3个字段。
④ 表中的行,代表每一条具体的数据。
配置 安装MySQL
开发人员只需要安装 MySQL Server 和 MySQL Workbench 这两个软件,就能满足开发的需求了。
MySQL Server: 专门用来提供数据存储和服务的软件。
MySQL Workbench: 可视化的 MySQL 管理工具,通过它,可以方便的操作存储在 MySQL Server 中的数据。
MySQL 在 Mac 环境下的安装
在 Mac 环境下安装 MySQL 的过程比 Windows 环境下的步骤简单很多:
① 先运行 mysql-8.0.19-macos10.15-x86_64.dmg 这个安装包,将 MySQL Server 安装到 Mac 系统
② 再运行 mysql-workbench-community-8.0.19-macos-x86_64.dmg 这个安装包,将可视化的 MySQL Workbench 工具安装到 Mac 系统
MySQL 在 Windows 环境下的安装
在Windows 环境下安装 MySQL,只需要运行 mysql-installer-community-8.0.19.0.msi 这个安装包,就能一次性将 MySQL Server 和 MySQL Workbench 安装到自己的电脑上。
如果是windows7、8的系统,需要安装下面这个软件来配置依赖环境
NDP452-KB2901907-x86-x64-AIIOS-ENU.exe
DataType 数据类型:
① int 整数
② varchar(len) 字符串 (len) 里面的数字代表的是字符串的最大长度
③ tinyint(1) 布尔值
字段的特殊标识:
① PK (Primary Key) 主键、唯一标识
② NN (Not Null) 值不允许为空
③ UQ (Unique) 值唯一
④ AI (Auto Increment) 值自动增长
⑤ BIN (binary) 二进制数据(比text更大的二进制数据)
⑥ UN (unsigned) 无符号 整数(非负数)
⑦ ZF (zero fill) 填充0 例如字段内容是1 int(4), 则内容显示为0001
⑧ G (generated column) 生成列
在mysql查询语句中加\g、\G的意思:
\g 的作用是分号和在sql语句中写 " ; " 是等效的
\G 的作用是将查到的结构旋转90度变成纵向(换行打印)
常见的 SQL 语句操作数据库
SQL(英文全称:Structured Query Language) 是结构化查询语言,专门用来访问和处理数据库的编程语言。
我们以编程的形式,操作数据库里面的数据。
三个关键点:
① SQL 是一门数据库编程语言
② 使用 SQL 语言编写出来的代码,叫做 SQL 语句
③ SQL 语言只能在关系型数据库中使用(例如 MySQL、Oracle、SQL Server)。非关系型数据库(例如 Mongodb) 不支持SQL 语言
① 从数据库中 查询数据 select
SELECT 语句用于从表中查询数据。执行的结果被存储在一个结果表中(称为 结果集)。语法格式如下:
-- 这是注释
-- 从 FROM 指定的[表中],查询出[所有的]数据。 * 表示[所有列]
SELECT * FROM 表名称
-- 从 FROM 指定的[表中],查询出指定 列名称(字段)的数据。
SELECT 列名称 FROM 表名称
注意:SQL 语句中的 关键字对 大小写不敏感。SELECT 等效 select,FROM 等效于 from。
示例:
查询users表中所有列的数据
-- 通过 * 把 users 表中所有的数据查询出来
select * from users
查询user表中 username列的数据
-- 通过 username 把 users 表中 username列的数据查询出来
select username from users
查询多个列 user表中的 username password列的数据
-- 查询多个列 user表中的 username password列的数据
-- 列名与列名之间的逗号为:英文逗号
select username,password from users
② 向数据库中 插入新的数据 insert into
INSERT INTO 语法用于向数据表中插入新的数据,语法格式如下:
-- 语法解读:向指定的表中,插入如下几列数据,列的值通过 values 一一指定
-- 注意:列和值要一一对应,多个列和多个值之间,使用英文的逗号分隔
INSERT INTO table_name(列1, 列2, ...) VALUES(值1, 值2, ...)
示例:
-- 向users表中 的 username password 列 中插入新值
-- 向users表中的 username列添加新值 'lyf' password列中添加新值 '123456'
insert into users(username, password) values('lyf','123456')
③ 更新 数据库中的数据 update
Update 语句用于 修改表中的数据。 语法格式如下:
-- 语法解读
-- 1. 用UPDATE 指定要更新哪个表中的数据
-- 2. 用 SET 指定列对应的新值
-- 3. 用 WHERE 指定更新的条件
UPDATE 表名称 SET 列名称 = 新值 WHERE 列名称 = 某值
示例:
-- 把users表中 id为2 的用户的密码修改成 '666666'
UPDATE users SET password = '666666' WHERE id = 2
-- 更新 某一行 多个列的数据
-- 把 users 表中id = 2的用户的 status password 分别更新为 1 '888888'
UPDATE users SET password = '888888', status = 1 WHERE id = 2
④ 从数据库 删除数据 dalete
DELETE 语句用于删除表中的行。语法格式如下:
-- 语法解读:
-- 从指定的表中,根据 WHERE 条件,删除对应的数据行
DELETE FROM 表名称 WHERE 列名称 = 值
示例:
-- 从 users 表中,删除 id 为 1 的用户,示例如下:
DELETE FROM users WHERE id = 1
⑥ 可以创建新数据库
⑦ 可在数据库中创建新表
⑧ 可在数据库中创建存储过程、试图
⑨ etc…
四种SQL语法:
where条件
WHERE 子句 用于限定选择的标准。 在SELECT、UPDATE、DELETE 语句中,皆可使用 WHERE 子句来限定选择的标准。
-- 查询语句中的 WHERE 条件
SELECT 列名称 FROM 表名称 WHERE 列 运算符 值
-- 更新语句中的 WHERE 条件
UPDATE 表名称 SET 列 = 新值 WHERE 列 运算符 值
-- 删除语句中的 WHERE 条件
DELETE FROM 表名称 WHERE 列 运算符 值
可在 WHERE 子句中使用的运算符
操作符 | 描述 |
---|---|
= | 等于 |
<> | 不等于 |
> | 大于 |
< | 小于 |
>= | 大于等于 |
<= | 小于等于 |
BETWEEN | 在某个范围内 |
LIKE | 搜索某种模式 |
注意:在某些版本的SQL中,操作符<> 可以写为 !=
and 和 or 运算符
AND 和 OR 可在 WHERE 子语句中把两个或多个条件结合起来。
AND 表示必须同时满足多个条件,相当于 JavaScript 中的 && 运算符,例如:if(a !== 10 && a!== 20)
OR 表示只要满足任意一个条件即可,相当于JavaScript中的 || 运算符,例如 if(a !== 10 || a !== 20)
示例:
AND 运算符示例
-- 使用 AND 来显示所有 status 为0 , 并且 id 小于 5 的用户
SELECT username, status From users WHERE status = 0 AND id < 5
OR 运算符示例
-- 使用OR来显示所有 status 为 1, 或者 username 为 zs 的用户
SELECT * FROM users WHERE status = 1 OR username = 'zs'
order by 排序
ORDER BY 语句用于 根据指定的列对结果集进行排序
ORDER BY 语句默认按照升序对记录进行排序。
如果您希望按照降序对纪律进行排序,可以使用 DESC 关键字。
示例:
升序示例
-- 对 users 表中的数据,按照 status 字段进行 升序排列,示例如下:
SELECT * FROM users ORDER BY status
-- 等价下面的代码
SELECT * FROM users ORDER BY status ASC
降序示例
-- 对 users 表中的数据,按照 status 字段进行 降序排列,示例如下:
SELECT * FROM users ORDER BY status DESC
多重排序示例
-- 对 users 表中的数据,先按照 status 字段进行 降序排序,再按照 username的字母排序,进行 升序排序,示例如下:
SELECT * FROM users ORDER BY status DESC, username ASC
count(*) 函数
COUNT(*) 函数用于返回查询结果的总数据条数,语法如下:
SELECT COUNT(*) FROM 表名称
使用 AS 为列 设置别名
如果希望给查询出来的列名称设置别名,可以使用 AS 关键字,示例如下:
-- 将列名称从 COUNT(*) 修改为 total
SELECT COUNT(*) AS total FROM users WHERE status = 0
示例:
-- 查询 users 表中 status 为 0 的总数据条数:
SELECT COUNT(*) FROM users WHERE status = 0
在 Express 中操作 MySQL 数据库
在项目中操作数据库的步骤
① 安装操作 MySQL 数据库的第三方模块(mysql)
mysql 模块是托管于 npm 上的第三方模块。它提供了在Node.js 项目中连接和操作 MySQL 数据库的能力。
想要在项目中使用它,需要先运行如下命令,将 mysql 安装为项目的依赖包
安装:npm i mysql
② 通过 mysql 模块连接到MySQL 数据库
配置 mysql 模块
在使用mysql 模块操作 MySQL 数据库之前,必须先对 mysql 模块进行必要的配置,主要的配置步骤如下:
// 导入 mysql 模块
const mysql = require('mysql')
// 配置 mysql
const db = mysql.createPool({
// 数据库的 IP 地址
host: '127.0.0.1',
// 数据库的端口
port: '3307',
// 登录数据库的账号
user: "root",
// 登录数据库的密码
password: "admin123",
// 指定要操作哪个数据库
database: 'my_db_01',
//连接池的大小,默认也是15
connectionLimit: 15
})
测试 mysql 模块能否正常工作
调用 db.query() 函数,指定要执行的 SQL 语句,通过回调函数拿到执行的结果:
// 检测 mysql 模块能否正常工作
db.query('SELECT 1',(err, results) => {
if(err) return console.log(err.message)
// 只要能打印出 [ RowDataPacket { '1': 1 } ] 的结果,就证明数据库连接正常
console.log(results)
})
注意: 对于mysql模块总是报错的,大概率不要怀疑不要相信网上说的什么密码错误,重新安装mysql等傻逼答案,极大的可能是你的3306端口被占用,导致你的mysql文件是3307或其他的,所以你可以在 mysql.createPool({ port: ‘你自己的mysql的端口’ }) 一定要加上这port 属性,这样就极大避免了,默认连接3306端口的mysql文件
③ 通过 mysql 模块执行 SQL 语句
mysql 模块 查询数据
查询users 表中所有的数据: 执行 SELECT语句 返回的是一个数组
// 查询 users 表中所有的用户数据
db.query('SELECT * FROM users', (err, results) => {
// 查询失败
if(err) return console.log(err.message)
// 查询成功
console.log(results)
})
mysql 模块 插入数据
向 users 表中新增数据,其中 username 为 Spider-Man,password 为 pcc321。 示例代码如下:
注意:insert into 语句 返回的是一个对象 ,insert into 对象中有一个属性为affectedRows 如果为1 代表插入成功
// 要插入到 users 表中的数据对象
const user = { username: 'Spider-Man', password: 'pcc321' }
// 待执行的 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('插入数据成功') } // 成功
})
mysql 模块 快捷方式 插入数据
向表中新增数据时,如果数据对象的每个属性和数据表字段一一对应,则可以通过如下方式快速插入数据:
// 要插入到 users 表中的数据对象
const user1 = { username: 'Spider-Man2', password: 'pcc3212' }
// 待执行的 SQL 语句,其中英文的 ? 表示占位符
const sqlStr2 = 'INSERT INTO users SET ?'
// 使用数组的形式,依次为 ? 占位符指定具体的值
db.query(sqlStr2, user1, (err, results) => {
if(err) return console.log(err.message) // 失败
if(results.affectedRows === 1){ console.log('插入数据成功') } // 成功
})
mysql 模块 更新数据
通过如下方式,更新表中的数据:
注意:UPDATE 语句 返回的是一个对象
// 要更新的数据对象
const user7 = { id: 7, username: 'aaa', password: '000' }
// 要执行的 SQL 语句
const sqlStr7 = 'UPDATE users SET username = ?, password = ? WHERE id = ?'
// 调用 db.query() 执行 SQL 语句的同时,使用数组依次为占位符指定具体的值
db.query(sqlStr7, [user7.username, user7.password, user7.id], (err, results) => {
if(err) return console.log(err.message) //失败
if(results.affectedRows === 1) { console.log('更新数据成功!') } // 成功
})
mysql 模块 快捷方式 更新数据
更新表数据时,如果 数据对象的每个属性和数据表的字段一一对应,则可以通过如下方式快速更新表数据:
// 要更新的数据对象
const user7 = { id: 5, username: 'aaaa', password: '0000' }
// 要执行的 SQL 语句
const sqlStr7 = `UPDATE users SET ? WHERE id=?`
// 调用 db.query() 执行 SQL 语句的同时,使用数组依次为占位符指定具体的值
db.query(sqlStr7, [user7, user7.id], (err, results) => {
if(err) return console.log(err.message) // 失败
if(results.affectedRows === 1) { console.log('更新数据成功') } // 成功
})
mysql 模块 删除数据
在删除数据时,推荐根据 id 这样的唯一标识,来删除对应的数据。示例如下:
注意: DELETE 语句 返回的也是一个对象
// 要执行的 SQL 语句
const sqlStr8 = `DELETE FROM users WHERE id=?`
// 调用 db.query() 执行 SQL 语句的同时,为占位字符指定具体的值
// 注意:如果 SQL 语句中有多个占位符,则必须使用数组为每个占位符指定具体的值
// 如果 SQL 语句中只有一个占位符,则可以省略数组
db.query(sqlStr8, 7, (err, results) => {
if(err) return console.log(err.message) //失败
if(results.affectedRows === 1) { console.log('删除数据成功!') } // 成功
})
mysql 模块 标记删除数据
使用DELETE 语句,会真正的把数据从表中删除掉。为了保险起见,推荐使用标记删除的形式,来模拟删除的动作。
所谓的标记删除,就是在表中设置类似于 status 这样的状态字段,来标记当前这条数据是否被删除。
当用户执行了删除的动作时,我们并没有执行 DELETE 语句把数据删除掉,而是执行了 UPDATE 语句,将这条数据对应的 status 字段标记为删除即可。
// 标记删除:使用 UPDATE 语句代替 DELETE 语句:只更新数据的状态,并没有真正删除
const sqlStr8 = 'UPDATE users SET status=1 WHERE id=?'
db.query(sqlStr8, 6, (err, results) => {
if(err) return console.log(err.message) // 失败
if(results.affectedRows === 1 ) { console.log('删除数据成功!') } // 成功
})
WEB 开发模式
目前主流的 Web 开发模式有两种,分别是:
① 基于服务端渲染的传统 Web 开发模式
服务端渲染的概念:服务器发送给客户端的 HTML 页面,是在服务器通过字符串的拼接,动态生成的。因此,客户端不需要使用 Ajax 这样的技术额外请求页面的数据。代码示例如下:
app.get('/index.html', (req, res) => {
// 要渲染的数据
const user = { name: 'zs', age: 20 }
// 服务器端通过字符串的拼接,动态生成 HTML 内容
const html = `<h1>姓名:${user.name},年龄:${user.age}</h1>`
// 把生成好的页面内容响应给客户端。因此,客户端拿到的是带有真实数据的 HTML 页面
res.send(html)
})
优点:
一、 ==前端耗时少。==因为服务器负责动态生成 HTML 内容,浏览器只需要直接渲染页面即可。尤其是移动端,更省电。
二、 ==有利于SEO。==因为服务器端响应的是完整的 HTML 页面内容,所以爬虫更容易爬取获得信息,更有利于 SEO
缺点:
一、 ==占用服务器端资源。==即服务器端完成 HTML 页面内容的拼接,如果请求较多,会对服务器造成一定的访问压力。
二、 ==不利于前后端分离,开发效率低。==使用服务器端渲染,则无法进行分工合作,尤其对于前端复杂度高的项目,不利于项目高效开发。
② 基于前后端分离的新型 Web 开发模式
前后端分离的概念: 前后端分离的开发模式,依赖于 Ajax 技术的广泛应用。简而言之,前后端分离的 Web 开发模式,就是后端只负责提供 API 接口,前端使用 Ajax 调用接口的开发模式。
优点:
一、 开发体验好。 前端专注于 UI 页面的开发,后端专注于 api 的开发,且前端有更多的选择性。
二、 ==用户体验好。==Ajax 技术的广泛应用, 极大的提高了用户的体验,可以轻松实现页面的局部刷新。
三、 ==减轻了服务器端的渲染压力。==因为页面最终是在每个用户的浏览器中生成的。
缺点:
一、 ==不利于 SEO。==因为完整的 HTML 页面需要在客户端动态拼接完成,所以爬虫对无法爬取页面的有效信息。(解决方案:利用 Vue、React 等 前端框架的 SSR ( server side render ) 技术能够很好的解决 SEO 问题! )
如何选择 Web 开发模式
不谈业务场景而盲目选择使用何种开发模式都是耍流氓
① 比如企业级网站,主要功能是展示而没有复杂的交互,并且需要良好的 SEO ,则这时我们就需要使用服务器端渲染;
② 而类似后台管理项目,交互性比较强,不需要考虑 SEO,那么就可以使用前后端分离的开发模式。
③ 另外,具体使用何种开发模式并不是绝对的,为了同时兼顾了首页的渲染速度和前后端分离的开发效率,一些网站采用了 首屏服务器端渲染 + 其他页面前后端分离的开发模式。
身份认证
身份认证(Authentication) 又称 “身份验证” 、“鉴权”,是指通过一定的手段,完成对用户身份的确认。
对于服务端渲染和前后端分离这两种开发模式来说,分别有着不同的身份认证方案:
① 服务器渲染 的身份验证 推荐使用 Session 认证机制
② 前后端分离 的身份验证 推荐使用 JWT 认证机制
了解 Session 的实现原理
HTTP 协议的无状态性
了解 HTTP 协议的无状态性是进一步学习 Session 认证机制的必要前提。
HTTP 协议的无状态性,指的是客户端的每次 HTTP 请求是独立的,连续多个请求之间没有直接的关系,服务器不会主动保留每次 HTTP 请求的状态。
注意: 现实生活中的会员卡身份认证方式,在 Web 开发中的专业术语叫做 Cookie
CooKie 是存储在用户浏览器中的一段不超过 4 KB 的字符串。它由一个名称(Name)、一个值(Value) 和其它几个用于控制 Cookie 有效期、安全性、使用范围的可选属性组成
不同域名下的 Cookie 各自独立,每当客户端发起请求时,会自动把当前域名下所有未过期的 Cookie 一同发送到服务器。
Cookie的几大特性:
① 自动发送
② 域名独立
③ 过期时限
④ 4 KB限制
Cookie 在身份认证中的作用:
客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的 Cookie,客户端会自动将 Cookie 保存在浏览器中。
随后,当客户端浏览器每次请求服务器的时候,浏览器会自动将身份认证相关的 Cookie,通过请求头的形式发送给服务器,服务器即可验明客户端的身份。
Cookie 不具有安全性
由于 Cookie 是存储在浏览器中的,而且浏览器也提供了读写 Cookie 的 API,因此 Cookie 很容易被伪造,不具有安全性。因此不建议服务器将更重要的隐私数据,通过 Cookie 的形式发送给浏览器。
注意:千万不要使用 Cookie 存储重要且隐私的数据! 比如用户的身份信息、密码等。
安装 express-session 中间件
npm i express-session
配置 express-session 中间件
// 导入session 中间件
const session = require('express-session')
// 配置Session 中间件
app.use(session({
// secret 属性的值可以为任意字符串
secret: 'keyboard cat',
// 固定写法
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: '登录失败' } )
}
// 将用户的信息,存储到 Session 中
req.session.user = req.body
// 将用户的登录状态,存储到 Session 中
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/login', (req, res) => {
// 清空当前客户端对应的 session 信息
req.session.destroy()
res.send({
status: 0,
msg: '退出登录成功'
})
})
注意:Session 认证机制需要配合 Cookie 才能实现。由于 Cookie 默认不支持跨域访问,所以,当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域 Session 认证。
当前端请求后端接口不存在跨域问题的时候,推荐使用 Session 身份认证机制。
当前端需要跨域请求后端接口的时候,不推荐使用 Session 身份认证机制,推荐使用 JWT 认证机制。
了解 JWT 的实现原理
JWT (英文全称:JSON Web Token) 是目前最流行的跨域认证解决方案。
JWT 工作原理
用户的信息通过 Token 字符串的形式,保存在客户端浏览器中。服务器通过还原 Token 字符串的形式来认证用户的身份。
JWT 的组成部分
JWT 通常由三部分组成,分别是 Header (头部)、Payload (有效荷载)、Signature (签名)。
三者之间使用英文的 "."分隔,格式如下:
Header.Payload.Signature
下面是 JWT 字符串的示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNjY1NjA3MjExLCJleHAiOjE2NjU2MDc1MTF9.9F-CxXnirSzKcwCWXr5yY23Eqm_-idl5RLZSIrv1-L0
Payload 部分才是真正的用户信息,它是用户信息经过加密之后生成的字符串。
Header 和 Signature 是安全性相关的部分,只是为了保证 Token 的安全性。
JWT 的使用方式
客户端收到服务器返回的 JWT 之后,通常会将它储存在 localStorage 或 sessionStorage 中。
此后,客户端每次于服务器通信,都要带上这个 JWT 的字符串,从而进行身份认证。推荐的做法是把 JWT 放在 HTTP 请求头的 Authorization 字段中,格式如下:
Authorization: Bearer <token>
// 浏览器请求头示例:
"Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNjY1NjA3MjExLCJleHAiOjE2NjU2MDc1MTF9.9F-CxXnirSzKcwCWXr5yY23Eqm_-idl5RLZSIrv1-L0"
安装 JWT 相关的包
安装如下两个 JWT 相关的包:
jsonwebtoken 主要用来生成JSON Web Token
express-jwt 主要用来验证JSON Web Token 注意:7.x版本后更改了用法
npm i jsonwebtoken express-jwt
express-jwt 模块的使用说明 验证token
把token解析成json对象
官方地址:https://www.npmjs.com/package/express-jwt
7.x版本后更改了用法 导入express-jwt 需要解构出 expressjwt
注意:express-jwt最新版本需要配置algorithms算法,一般默认是HS256,配置格式expressJWT({secret: secretKey, algorithms: [‘HS256’]}).unless({ path: [“/api/login”] })
expressjwt({ secret: }) 设置密钥的属性 secretKey = “lam12138” 自定义保存仅限保存在服务器中的私钥
expressjwt({ algorithms: }) 设置算法的属性 默认 [“HS256”]
expressjwt({ audience: }) 受众
expressjwt({ issuer: })
unless({path:[“/api/login”]})是让express-jwt这个中间件排除 对/api/login接口的验证
jsonwebtoken 模块的使用说明 生成token
// 导入 jsonwebtoken 模块
const jwt = require('jsonwebtoken')
// 设置密钥 值可以任意自定义
secretKey = 'lyx-1995'
// 生成 token expiresIn 设置的是token的有效时间
jwt.sign({ 存放用户信息 },secretKey,{ expiresIn: "30s" } )
jwt.sign() 方法进行数据签名
jwt.verify() 方法进行签名验证
定义 secret 密钥
为了保证 JWT 字符串的安全性,防止 JWT 字符串在网络传输过程中被别人破解,我们需要专门定义一个用于加密和解密的 secret 密钥:
① 当生成 JWT 字符串的时候,需要使用 secret 密钥对用户的信息进行加密,最终得到加密好的 JWT 字符串
② 当把 JWT 字符串解析还原成 JSON 对象的时候,需要使用 secret 密钥进行解密
// 定义 secret 密钥,建议将密钥命名为 secretKey
const secretKey = 'lyx 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, algorithms: ['HS256'] }) 就是用来解析 Token 的中间件
// .unless({ path: ["/api/login"] }) 用来指定哪些接口不需要访问权限
app.use(expressJWT({ secret: secretKey, algorithms: ['HS256'] })).unless({ path: ["/api/login"] })
使用 req.auth 获取用户信息
当express-jwt 这个中间件配置成功之后,即可在那些有权限的接口中,使用 req.auth 对象,来访问从 JWT 字符串中解析出来的用户信息了,示例如下
// 这是一个有权限的 API 接口
app.get('/admin/getinfo', function(req, res) {
console.log(req.auth)
res.send({
status: 200,
message: '获取用户信息成功!',
data: req.auth
})
})
捕获解析 JWT 失败后产生的错误
当使用 express-jwt 解析 Token 字符串时,如果客户端发送过来的 Token 字符串过期或不合法,会产生一个解析失败的错误,影响项目的正常运行。我们可以通过 Express 的错误中间件,捕获这个错误并进行相关的处理,示例代码如下:
// 注册一个处理错误的中间件,防止项目崩溃
app.use((err, req, res, next) => {
console.log(`发生了错误:${err.message}`)
// token 解析失败导致的错误
if (err.name === "UnauthorizedError") {
res.send({
status: 401,
message: "您的登录失效!"
})
} else {
// 其他原因导致的错误
res.status(500).send(`Error! ${err.message}`)
}
})
使用 express-jwt 模块时 项目报错 问题点
① cmd 报如下的错:
throw new RangeError(‘express-jwt: algorithms
is a required option’);
这个错误的意思是,你没有在expressjwt({ }) 中添加 algorithms的算法
解决方案:
app.use(expressjwt({ secret: secretKey, algorithms: ['HS256'] }).unless({ path: ['/api/login'] }))
joi 模块的使用
安装:
npm i joi
注意:以前的老版本是,@hapi/joi,但是已经被弃用了。
导入 joi 模块
const joi = require('joi')
定义校验规则
const schema = joi.string()
校验数据类型:
string() 校验数据是否为 string类型
number() 校验数据是否为 number类型
array() 校验数据是否为 array类型
array().items(joi.string()) 校验数据是否为数组,并且子元素是否为字符串类型
length(length) 校验数据的长度
object() 校验数据是否为object类型
校验object对象 中的属性
const joi = require('joi')
// 校验数据是否为 object类型 并校验对象的属性值是否为 string类型
joi.object({
p1: joi.string()
})
date() 校验数据是否为日期
any() 校验任意数据
func() 校验数据是否为函数
integer() 值必须位整数
empty() 校验数据是否为 空 或 undefined
email() 验证是否是邮箱
alphanum() 值只能是包含 a-zA-Z0-9 的字符串
min(length) 最小长度
max(length) 最大长度
required() 值是必填项,不能为 undefined
pattern(正则表达式) 值必须符合正则表达式的规则
allow(‘’) 支持空字符串
ref() 引入已定义好的校验规则来校验自己的数据
const schema = joi.object({
username: joi.string().min(2).max(6).required(),
password: joi.string().alphanum().required(),
repassword: joi.ref('password')
})
使用校验规则校验数据
validate() 用来校验数据的方法
// 导入 joi 模块
const joi = require('joi')
// 定义要校验数据
let str = '我是字符串数据'
// 定义校验规则
// 校验数据的类型是否为 string 类型
const schema = joi.string()
// 使用自定义的校验规则来校验
const result = schema.validate(str)
// 打印校验数据后的结果
console.log(result)
模块化 joi
创建一个 schema的文件夹
定义一个 自动处理表单数据进行验证的功能 express-joi.js 文件
// 实现自动对表单数据进行验证的功能
// 导入 joi 模块
const Joi = require('joi')
const expressJoi = function(schemas, options = { strict: false }) {
// 自定义校验选项
// strict 自定义属性,默认不开启严格模式,会过滤掉那些未定义的参数项
// 如果用户指定了 strict 的值为 true,则开启严格模式,此时不会过滤掉那些未定义的参数项
if (!options.strict) {
// allowUnknown 允许提交未定义的参数项
// stripUnknown 过滤掉那些未定义的参数项
options = { allowUnknown: true, stripUnknown: true, ...options }
}
// 从 options 配置对象中,删除自定义的 strict 属性
delete options.strict
// TODO: 用户指定了什么 schema,就应该校验什么样的数据
return function(req, res, next) {
['body', 'query', 'params'].forEach(key => {
// 如果当前循环的这一项 schema 没有提供,则不执行对应的校验
if (!schemas[key]) return
// 执行校验
const schema = Joi.object(schemas[key])
const { error, value } = schema.validate(req[key], options)
if (error) {
// 校验失败
throw error
} else {
// 校验成功,把校验的结果重新赋值到 req 对应的 key 上
req[key] = value
}
})
// 校验通过
next()
}
}
module.exports = expressJoi
自定义用户信息验证规则模块化
// 用户信息验证规则模块化
// 导入 joi 模块
const joi = require('joi')
/**
* string() 值必须是字符串
* alphanum() 值只能是包含 a-zA-Z0-9 的字符串
* min(length) 最小长度
* max(length) 最大长度
* required() 值是必填项,不能为 undefined
* pattern(正则表达式) 值必须符合正则表达式的规则
*/
// 用户名的验证规则
const username = joi.string().alphanum().min(1).max(10).required()
// 密码的验证规则
const password = joi
.string()
.pattern(/^[\S]{6,12}$/)
.required()
// 注册和登录表单的验证规则对象
exports.reg_login_schema = {
// 表示需要对 req.body 中的数据进行验证
body: {
username,
password,
},
}
bcrypt 模块 密码加密
下载
npm i bcrypt
使用
const bcrypt = require('bcrypt')
用户的密码在数据库当中不能以明文的形式进行存储,需要进行加密处理
加密方法:
密码中混入一段“随机”的字符串再进行哈希加密,这个被字符串被称作盐值。
为什么要加盐:同一个密码经过哈希算法后得到的密码是一致,攻击者可以通过建立一个密码和哈希机密后的表的对应关系的表提高破解效率。
如果加盐,这使得同一个密码每次都被加密为完全不同的字符串。为了校验密码是否正确,我们需要储存盐值。通常和密码哈希值一起存放在账户数据库中,或者直接存为哈希字符串的一部分。
生成盐值
//genSalt用来生成随机字符串,参数代表字符串的复杂度
const salt = await bcrypt.genSalt(10);
密码用盐值加密
const result = await bcrypt.hash('123456',salt);
密码对比
const flag = await bcrypt.compare('123456',result);
不用async 的 密码对比方式
const flag = bcrypt.compareSync('123456',result);
生成盐密码并加密
// 对用户的密码,进行 bcrype 加密,返回值是加密之后的密码字符串
// 调用bcrypt.hashSync(明文密码, 随机盐的长度)
userinfo.password = bcrypt.hashSync(userinfo.password, 10)
完整代码,代码写在async异步函数当中
async function run(){
const salt = await bcrypt.genSalt(10);
console.log(salt);
const result = await bcrypt.hash('123456',salt);
console.log(result);
const flag = await bcrypt.compare('123456',result);
console.log(flag);
}
获取客户端IP
express-rate-limit 模块 速率限制
const restime = require('./resTime')
const limiter = rateLimit({
// 设置接口最大请求 数量
max: 5,
// 超过请求最大请求数量后,需要 5分钟后才能请求
// 只支持 number 类型
windowMs: 5 * 60 * 1000,
handler: (req,res,next,options) => {
// options.statusCode 是默认状态码:429
res.status(options.statusCode).send({
status: 1,
message: '您超过了,规定的时间!'
})
}
})