【Node.js】初學 2
8. Express
8.1 Express 简介
express 是基于 node.js 平台的 web 开发框架。
express 的作用和 node.js 内置的 http 模块类似,是专门用来创建 web 服务器的。
express 的本质:就是一个 npm 上的第三方包,提供了快速创建 web 服务器的方法。
express 中文官网:http://www.expressjs.com.cn
express 与 http 内置模块区别?http 内置模块使用起来复杂开发效率低,express 是基于内置的 http 模块进一步封装的,能提高开发效率。
express 与 http 内置模块是什么关系?类似于浏览器中 Web API 和 jQuery 的关系,后者是基于前者封装出来的。
express 能做什么?最常见的两种服务器:Web 网站服务器(专门对外提供 web 网页资源的服务器)、API 接口服务器(专门对外提供 API 接口的服务器)。
8.2 Express 的基本使用
1. 安装
在项目所处目录中,运行终端命令,即可将 express 安装到项目中。
npm i express
2. 创建基本的 Web 服务器
// 1. 导入 express 模块
const express = require('express')
// 2. 创建 web 服务器
const app = express()
// 3. 启动 web 服务器, 调用 app.listen(端口号, 启动成功后的回调函数)
app.listen(80, () => {
console.log('服务器已启动:http://127.0.0.1:80')
})
3. 监听 GET 与 POST 请求
通过 app.get()
方法,可以监听客户端的 GET 请求。
通过 app.post()
方法,可以监听客户端的 POST 请求。
语法:
app.get('请求URL', function(req, res) { /* 处理函数 */ } )
app.post('请求URL', function(req, res) { /* 处理函数 */ } )
其中,有两个参数,分别是:
参数一:客户端请求的 URL 地址。
参数二:请求对应的处理函数。里面有两个形参,req请求,res响应。
req:请求对象,包含了与请求相关的属性和方法。
res:响应对象,包含了与响应相关的属性和方法。
4. 把内容响应给客户端 res.send( )
通过 res.send()
方法,可以把处理好的内容,发送给客户端。
// 3. 监听客户端的 GET 和 POST 请求
// get 请求
app.get('/user', (req, res) => {
// 调用 express 提供的 res.send() 方法,向客户端响应一个 JSON 对象
res.send( {name:'加菲猫', like:'千层面'} )
})
// psot 请求
app.post('/user', (req, res) => {
// 向客户端发送 文本字符串
res.send('您请求成功,请求方法是 POST')
})
5. 获取 URL 中携带的查询参数 req.query
通过 req.query
对象,可以访问到客户端通过查询字符串的形式,发送到服务器的参数:
req.query 默认是一个空对象。
客户端使用 xxxx?name=Tom&age=20 这种查询字符串形式,发送到服务器的参数。
可以通过 req.query 对象访问,例如:req.query.name req.query.age
// req.query 对象 获取 url 中的参数
app.get('/q', (req, res) => {
// req.query 默认是一个空对象。
// 客户端使用 xxxx?name=Tom&age=20 这种查询字符串形式,发送到服务器的参数。
// 可以通过 req.query 对象访问,例如:req.query.name req.query.age
console.log(req.query) // 打印 req.query 对象的内容
console.log(req.query.name) // 打印 name 的值 Tom
res.send(req.query)
})
6. 获取 URL 中的动态参数 req.params :动态参数
通过 req.params
对象,可以访问到 URL 中,通过 : 匹配到的动态参数
// req.params 对象 获取 URL 中的动态参数
// 注意:这里的 :u_id 是一个动态的参数
app.get('/user/:u_id', (req, res) => {
// req.params 默认是一个空对象
// req.params 是动态匹配到的 URL 参数,里面存放着通过 : 动态匹配到的参数值
console.log(req.params)
res.send(req.params) // 例如 /user/tom 然后里面就是 "u_id":"tom"
})
// 注意:
// :u_id 不是固定写法,冒号 : 才是固定的写法,后面的参数名必须合法。
// 动态参数可以有多个,例如:/user/:u_id/:name 然后请求 /user/tom/TOM123 就是{"u_id":"tom","name":"TOM123"}
8.3 托管静态资源 express.static()
1. 托管静态资源目录
express 提供了 express.static() 函数,通过它,可以创建一个静态资源服务器。
例如,通过如下代码就可以将 public 目录下的图片、css文件、JavaScript文件对外开放了。
app.use( express.static('./public') )
现在,就可以访问 public 目录里的所有文件了。
http://localhost:3000/images/bg.png
http://localhost:3000/css/index.css
http://localhost:3000/js/index.js
注意:Express 在指定的静态目录中查找文件,并对外提供资源的访问路径。因此,存放静态文件的目录名不会出现在 URL 中。
也就是 public 这层文件夹不会出现在访问路径中。
2. 托管多个静态资源目录
如果要托管多个静态资源目录,就多次调用 express.static() 函数。
app.use( express.static('./public') )
app.use( express.static('./files') )
访问静态资源文件时,express.static() 函数会根据目录的添加顺序查找所需的文件。
例子:先添加了 ./40_static 然后添加了 ./40_static_2 目录,当访问目录下 index.html 文件时,
因为按顺序来,所以先去 ./40_static 目录下找 index.html,找到了,就不继续找了,找不到就找第二个文件夹 ./40_static_2 的,以此类推。
3. 挂载路径前缀
// 挂载路径前缀
app.use('/home', express.static('./40_static'))
/*
* 在调用 app.use() 的时候,可以提供一个参数,就是在前面加一个前缀字符串 /home,
* 然后访问目录里面任何资源时,都需要在前面挂载一个 /home 访问前缀才行。
* 现在就可以通过带有 /home 前缀地址来访问 40_static 目录下的文件了
* http://localhost/home/index.html
* http://localhost/home/img/bg.jpg
*/
8.4 nodemon 自动重启项目
使用 nodemon 命令启动的项目,当更改代码时,会自动重启项目,无需手动。
传统方式是运行 node xxx 命令来启动项目,当更改代码后还需要手动重启项目。
安装:npm i -g nodemon
启动:nodemon xxx.js
如果启动失败,使用语句: npx nodemon xxx.js
启动,或删除 C:\Users\用户\AppData\Roaming\npm\nodemon.ps1 文件。
8.5 Express 路由 与 路由模块化
1. Express 路由
路由,就是映射关系。
类似于打电话查话费按数字1键就可以提供查话费服务,按键与服务之间的映射关系
Express 中,路由指的是 客户端的请求与服务器处理函数 之间的映射关系
路由分3部分组成,分别是:请求的类型、请求的 URL 地址、处理函数。
格式: app.METHOD(PATH, HANDLER)
例如:app.get('/home', function(){...})
其中 METHOD和PATH 是和客户端有关,HANDLER是和服务端有关
// 匹配 GET 请求,请求 URL 为 /
app.get('/', (req,res) => {
res.send('get 请求,url 是 /')
})
最简单的用法:
把路由挂载到 app 上。
// 挂载路由
app.get('/home', (req, res) => { res.send('hello get /home') })
app.post('/home', (req, res) => { res.send('hello post /home') })
2. 模块化路由
为了方便对路由进行模块化管理,Express 不建议将路由直接挂载到 app 上,而是推荐将路由抽离为单独的模块。
步骤如下:
1、创建路由模块对应的 .js 文件。
2、调用 express.Router() 函数创建路由对象。
3、向路由对象上挂载具体的路由。
4、使用 module.exports 向外共享路由对象。这样外界 require 这个文件时,得到的就是这个 router 对象。
5、使用 app.use() 函数注册路由模块。
创建路由模块:
// 创建路由模块 //
// 1. 导入 express 模块
const express = require('express')
// 2. 创建路由对象
const router = express.Router()
// 3. 挂载具体的路由
router.get('/home', (req, res) => {
res.send('GET /home')
})
router.post('/game', (req, res) => {
res.send('POST /game')
})
// 4. 向外导出路由对象,将 router 路由对象向外共享出去
module.exports = router
注册路由模块:
const express = require('express')
const app = express()
// 1. 导入路由模块
const my_router = require('./43_路由的模块化.js')
// 2. 使用 app.use() 注册路由模块
app.use(my_router)
// 注意: app.use() 函数的作用,是注册全局中间件。
// 比如之前的 app.use(express.static('./bg')) 向外托管静态资源
app.listen(80 , () => {
console.log('http://127.0.0.1:80')
})
为路由模块添加前缀
类似于托管静态资源时,在前面加前缀,路由模块添加前缀的方式是:
// 3. 为路由模块添加前缀
app.use('/user', my_router) // 之后访问它,就得以 /user 开头,例如:http://localhost/user/home
8.6 Express 中间件
1. 中间件概念
中间件(Middleware),特指业务流程的中间处理环节。
类似于现实生活中的过滤网,输入污水然后处理最后输出,处理方式是一层过滤二层过滤,中间的过滤网可以叫做 中间件。
Express 中间件的调用流程:
当一个请求到达 Express 的服务器之后,可以连续调用多个中间件,对这次请求进行预处理。
客户端发请求--->[ 中间件1 -> 中间件2 ->中间件3...-> 处理完毕,结果以路由的形式响应给客户端] ---> 响应给客户端
Express 中间件的格式:
本质上就是一个 function 处理函数,中间件函数的形参列表中,必须包含 next 参数。
格式:
app.get('/', function (req, res) { // 路由处理函数
})
app.get('/', function (req, res, next) { // 中间件函数
next();
})
如果包含了 next 参数,就是中间件函数,如果没有,就不是,是路由处理函数。
next 函数的作用:
next 函数是实现多个中间件连续调用的关键,它表示把流转关系转交给下一个中间件或路由。
2. 定义中间件函数
// 1. 定义中间件函数
const mw = function(req, res, next) {
console.log('这是最简单的中间件函数')
// 把流转关系,转交给下一个 中间件或路由。如果不调用,那就无法流转到下一个中间件了。
next()
}
3. 全局生效的中间件 app.use()
客户端发起的任何请求,到达服务器之后,都会触发的中间件,叫做 全局生效的中间件。
通过调用 app.use(中间件函数) ,即可定义一个全局生效的中间件。
4. 定义全局中间件的 简化形式
之前是用一个常量变量来接收中间件函数,简化形式是直接调用 app.use() 函数,在里面提供一个中间件函数就可以了。
// 全局生效的中间件
app.use((req,res,next) => {
console.log('这是简化的中间件函数')
next() // 流转关系交给下一个中间件或路由
})
5. 中间件的作用
中间件和路由都共享同一个 req 和 res。为其添加自定义属性与方法,供下面的中间件和路由使用。就不需要每个路由都写一遍了。
多个中间件之间,共享同一个 req 和 res,基于这样的特性,可以在上游的中间件中,统一为 res 和 req 对象添加自定义的属性或方法。然后供下游的中间件或路由进行使用。
// 4. 中间件的作用
/*
* 多个中间件之间,共享同一个 req 和 res。
* 基于这样的特性,可以在上游的中间件中,统一为 res 和 req 对象添加自定义的属性或方法。
* 然后供下游的中间件或路由进行使用。
*/
app.use((req,res,next) => {
// 获取到请求到达服务器的时间
const my_time = Date.now()
// 为 req 对象,挂载自定义属性,从而把时间共享给后面的所有路由。下游的路由都可以使用这个
req.startTime = my_time
next()
})
app.get('/time', (req,res) => {
res.send('您访问了 /time,现在时间戳:'+ req.startTime) //使用了上游中间件的自定义属性
})
app.get('/time2', (req,res) => {
res.send('您访问了 /time2,现在时间戳:'+ req.startTime)
})
6. 定义多个全局中间件
使用 app.use() 连续定义多个全局中间件。客户端请求到达服务器后,会按照中间件定义的 先后顺序 依次调用。
// 5. 定义多个全局中间件
// 使用 app.use() 连续定义多个全局中间件。客户端请求到达服务器后,会按照中间件定义的 先后顺序 依次调用。
app.use((req,res,next) => {
console.log('调用了第 1 个中间件')
next()
})
app.use((req,res,next) => {
console.log('调用了第 2 个中间件')
next()
})
app.get('/many',(req,res) => {
res.send('请求/many 路由,会多次触发上面的 中间件,并按照先后顺序依次调用。')
})
7. 局部生效 的中间件
这个中间件只会在当前路由內生效,不会影响到其他的路由。
语法:
app.get( 请求URL地址, 中间件函数, 路由处理函数)
// 不使用 app.use() 定义的中间件,叫做 局部生效的中间件。
// 1. 定义中间件函数 mw1
const mw1 = function(req, res, next) {
console.log('这是中间件函数')
next()
}
// 2. 创建路由
// mw1 这个中间件只在“当前路由中生效”,这种用法属于“局部生效中间件”
app.get('/', mw1 ,(req, res) => {
res.send('get / 路由')
})
// mw1 中间件不会影响下面的路由,因为 mw1 是局部生效的中间件
app.get('/user', (req, res) => {
res.send('get /user 路由')
})
8. 定义多个 局部生效的中间件
可以在路由中,通过如下两种等价的方式,使用多个局部中间件。
/*
2、 定义多个 局部生效的中间件
*/
// 可以在路由中,通过如下两种等价的方式,使用多个局部中间件。
// 以下两种写法是完全等价的,任意一种都可以。
app.get('/', mw1, mw2, (req,res) => { res.send('路由 get /') })
app.get('/', [mw1, mw2], (req,res) => { res.send('路由 get /') })
/*----------------------------------------------------
// 其中 mw1 和 mw2 都是中间件函数
const mw1 = (req, res, next) =>{
console.log('第二个中间件函数')
next()
}
const mw1 = (req, res, next) =>{
console.log('第二个中间件函数')
next()
}
*/
9. 中间件注意事项
1、一定要在路由之前注册中间件。代码从上到下执行。
2、可以连续调用多个中间件进行处理。
3、执行完中间件的业务代码,不要忘记调用 next() 函数。不然就作废了,无法转交给下一个中间件或路由。
4、为了防止代码逻辑混乱。next() 后面不要写额外的代码。
5、连续调用多个中间件时,多个中间件之间,共享 req 和 res 对象。
8.7 Express 中间件 分类
Express 官方把常见的中间件分为 5 大类。
1. 应用级别的中间件
通过 app.use() 或 app.get() 或 app.post() ,绑定到 app 实例上的中间件,叫做应用级别中间件。
// 应用级别的中间件(全局中间件)
app.use( (req,res,next) => {
next()
})
// 应用级别的中间件(局部中间件)
app.get('/', mw1, (req,res,next) => {
res.send('请求 / get')
})
2. 路由级别的中间件
绑定到 express.Router() 实例上的中间件,叫做路由级别的中间件。它的用法和应用级别中间件没有区别,只不过是,应用级别中间件绑定到 app 实例上,而路由级别中间件是绑定到 router 实例上。
// 路由级别中间件
router.use( (req,res,next) => {
console.log('路由级别中间件')
next()
})
app.use('/', router)
3. 错误级别的中间件
错误级别中间件的处理函数中,必须有 4 个形参,按前后顺序分别是:(err, req, res, next)
作用是,专门捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题。
注意:错误级别的中间件,必须注册在所有路由之后。否则捕获不到错误。除了错误级别的中间件,其他中间件必须在路由之前。
app.get('/', (req,res) => {
throw new Error('服务器内部发生了错误!') // 抛出一个自定义异常错误
res.send('此时这条是执行不到的,因为上面就报错了')
})
// 错误级别的中间件
app.use( (err,req,res,next) => {
console.log('发生了错误:' + err.message) //在服务器打印错误内容
res.send('错误:' + err.message) //向客户端响应错误内容
})
之前的中间件,放在前面是为了在路由还没执行前处理一些代码再执行路由,而这个错误中间件要放在路由后面才能捕获错误。
只要发生错误,就立即进入了错误级别的中间件里去执行了,比如先打印错误信息,然后向客户端响应消息。
4. Express 内置的中间件
从Express 4.16版本开始,内置了 3 个常用的中间件。
(1) express.static 快速托管静态资源的内置中间件,例如 html css 图片等。
(2) express.json 解析 JSON 格式的请求体数据。
语法:app.use(express.json())
(3) express.urlencoded 解析 URL-encoded 格式的请求体数据。
语法:app.use(express.urlencoded({extnded: false}) )
// (2) express.json 中间件。请求体要放到body里发送。
// 注意:处理错误级别中间件,其他的都要写在路由前面
// 通过 express.json() 中间件,解析表单中的 JSON 格式的数据
app.use(express.json())
app.post('/user', (req,res) => {
// 在服务器可用使用 req.body 属性,来接收客户端发送过来的请求体数据
// 默认情况下,如果不配置解析表单数据的中间件,则 req.body 默认等于 undefined
console.log(req.body)
res.send('ok user JSON')
})
// *在 postcode 插件中发送请求,然后点击 body 里的 raw,写一些 json 格式的数据,然后点击发送。
// (3) express.urlencoded 中间件。解析 URL-encoded 格式的请求体数据
// 这个不是 JSON 格式,而是 body 里的 x-www-form-urlencoded 格式,以键值对形式,向服务器发送数据
app.use(express.urlencoded( {extended: false} ))
app.post('/user-e', (req,res) => {
// 在服务器端,可以通过 req.body 来获取 JSON 格式的表单数据和 url-encoded 格式的数据
console.log(req.body)
res.send('ok user URL-encoded')
})
5. 第三方的中间件
非官方内置的中间件。
例如 在 4.16 版本之前,经常使用的 body-parser 这个第三方中间件,来解析请求体数据
// 例如 在 4.16 版本之前,经常使用的 body-parser 这个第三方中间件,来解析请求体数据
// 步骤如下:npm i body-parser、然后使用 require 导入中间件、调用 app.use() 注册并使用中间件
const parser = require('body-parser')
app.use(parser.urlencoded( {extended: flase} )) // 第三方中间件用法
app.use(express.urlencoded( {extended: false} )) // 内置中间件,这个内置的是基于body-parser封装的
// Express 内置的 express.urlencoded 中间件,是基于第三方 body-parser 封装出来的。
8.8 Express 自定义中间件
将自定义中间件封装为模块。
function MY_zjj(req,res,next) {
// 处理一些东西
}
// 向外暴露出去
module.exports = MY_zjj
// -------- 分割线 ----------
// 1. 导入自己封装的中间件模块
const my_zjj = require('./自定义中间件.js')
// 2. 将自定义中间件函数,注册为全局可用的中间件
app.use(my_zjj)
app.post('/', (req,res) => {
//路由
})
8.9 Express API 接口
get请求参数在query里,post请求参数在body里。
get的数据存放在req.query中 post的数据存放在req.body中,且需要配置urlencoded中间件
1. 编写 GET 接口
创建基本的 API 接口
// 路由模块
const express = require('express')
const router = express.Router()
// 在这里挂载对应的路由
router.get('/get', (req,res) => {
// 通过 req.query 获取客户端通过查询字符串,发送到服务器的数据
const query = req.query
// 调用 res.send() 方法,向客户端响应处理的结果
res.send({
status: 0, // 0 表示成功,1 表示失败
msg: 'GET 请求成功!', // 状态的描述
data: query // 需要响应给客户端的数据
})
})
// 向外共享出去
module.exports = router
创建基本的服务器
// 启动服务器
const express = require('express')
const app = express()
// 导入并注册路由模块
const apiRouter = require('./50_APIRouter')
app.use('/api', apiRouter) // 注册路由,前缀为 /api
app.listen(80, () => {
console.log('server runing at http://127.0.0.1')
})
然后访问 127.0.0.1/api/get ,带上查询字符串,即可返回相应数据,127.0.0.1/api/get?name=hm&age=18
2. 编写 POST 接口
创建基本的 API 接口
post请求参数在 body 里。
注意:要获取 URL-encoded 格式的请求体数据,必须配置中间件 app.use(express.urlencoded( {extended: flase} ) ),
如果不配置,就无法通过 req.body 获取到请求体里的数据, body 里的
// 挂载对应路由 POST接口
router.post('/post', (req,res) => {
// 1. 获取客户端通过请求体,发送到服务器的 URl-encoded 数据
const body = req.body
// 2. 调用 res.send() 方法,把数据响应给客户端
res.send({
status: 0, // 0 成功,1 失败
msg: 'POST 请求成功!', // 状态描述信息
data: body
})
})
3. 接口 跨域问题 CORS
上面编写的 GET 和 POST 接口,存在一个严重问题,不支持跨域请求。
npm install cors
安装中间件const cors = require('cors')
导入中间件app.use(cors())
配置中间件
例如,服务器端
const express = require('express')
const app = express()
app.use(express.urlencoded( {extended: false})) // 解析表单的中间件 URL-encoded 格式
// 一定要在路由之前,配置 CORS 这个中间件,解决跨域问题
const cors = require('cors')
app.use(cors())
// 导入并注册路由
const router = require('./50_APIRouter')
app.use('/api', router)
// 启动服务器
app.listen(80, () => {
console.log('server runing at http://127.0.0.1:80')
})
路由模块 API:
// 路由模块
const express = require('express')
const router = express.Router()
// 在这里挂载对应的路由
router.get('/get', (req,res) => {
// 通过 req.query 获取客户端通过查询字符串,发送到服务器的数据
const query = req.query
// 调用 res.send() 方法,向客户端响应处理的结果
res.send({
status: 0, // 0 表示成功,1 表示失败
msg: 'GET 请求成功!', // 状态的描述
data: query // 需要响应给客户端的数据
})
})
// 挂载对应路由 POST接口
router.post('/post', (req,res) => {
// 1. 获取客户端通过请求体,发送到服务器的 URl-encoded 数据
const body = req.body
// 2. 调用 res.send() 方法,把数据响应给客户端
res.send({
status: 0, // 0 成功,1 失败
msg: 'POST 请求成功!', // 状态描述信息
data: body
})
})
// 向外共享出去
module.exports = router
HTMl 页面,测试接口:
HTML 页面 测试接口.html
<button id="btnGET">GET</button>
<button id="btnPOST">POST</button>
<script>
$(function() {
// 1. 测试 GET 接口
$('#btnGET').on('click', function() {
$.ajax({
type: 'GET',
url: 'http://127.0.0.1/api/get',
data: {
name: 'mea', age: 16
},
success: function(res) {
console.log(res)
}
})
})
// 2. 测试 PSOT 接口
$('#btnPOST').on('click', function() {
$.ajax({
type: 'POST',
url: 'http://127.0.0.1/api/post',
data: {
name: 'moo', age: 16
},
success: function(res) {
console.log(res)
}
})
})
})
</script>
使用 file:// 协议打开 html 文件,发送请求时,服务器端与其不同,协议不同 http,域名不同 127.0.0.1,端口号不同 80,由于同源策略,所以存在跨域问题。
使用 CORS 跨域解决方案,有了相应的响应头,就解决了跨域问题。
8.10 CORS 跨域
CORS 跨域资源共享
1、CORS 响应头部 Access-Countrol-Allow-Origin
格式:Access-Countorl-Allow-Origin: <origin> | *
参数值为 URL,其中 origin 参数的值指定了允许访问该资源的外域 URl
例如,只允许来自 http://google.com 的请求:
res.setHeader('Access-Countrol-Allow-Origin', 'http://google.com')
如果指定了参数值为 * 通配符,则表示允许来自任何域的请求:
res.setHeader('Access-Countrol-Allow-Origin', '*')
2、CORS 响应头部 Access-Countrol-Allow-Headers
默认情况下,CORS 仅支持客户端向服务器发送 9 个响应头。
Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width、Content-Type。
格式:Access-Countorl-Allow-Headers: xxx
如果发送额外的请求头信息,则需要在服务端,对额外的请求头进行声明。
例如,允许客户端额外向服务端发送 Content-Type 请求头和 x-Custom-Header 请求头:
rse.setHeader('Access-Countrol-Allow-Header', 'Content-Type, x-Custom-Header')
3、CORS 响应头部 Access-Countrol-Allow-Methods
设置允许的请求类型。
格式:Access-Countrol-Allow-Methods: POST
默认情况下,CORS 仅支持客户端发起 GET、POST、HEAD 请求
如果客户端通过 PUT、DELETE 等方式请求服务器资源,则需要在服务端指明允许使用的 HTTP 方法。
例如:只允许 POST、DELETE 请求方法
res.setHeader('Access-Countrol-Allow-Methods', 'PSOT, DELETE')
通配符 * 允许所有的 HTTP 请求方法
res.setHeader('Access-Countrol-Allow-Methods', '*')
8.11 CORS 请求的分类
客户端在请求 CORS 接口时,根据请求方式与请求头不同,可将请求分类两大类。
简单请求、预检请求。
1、简单请求
同时满足以下两个条件的请求,就是简单请求。
(1) 请求方式:GET、PSOT、HEAD 三者之一
(2) HTTP 头部信息,无自定义头部字段,不超过这 9 个响应头 Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width、Content-Type。
2、预检请求
符合其中一个条件的请求,都需要进行预检请求。
(1) 请求方式为 GET、PSOT、HEAD 之外的请求 Method 类型
(2) 请求头中包含自定义头部字段
(3) 向服务器发送了 application/json 格式的数据
在浏览器与客户端正式通信之前,浏览器会先发送 OPTION 请求进行预检,以获知服务器是否允许该实际请求。所以这一次 OPTION 请求称为预检请求。服务器成功响应预检请求后,才会发送真正的请求,并携带真实的数据。
3、简单请求与预检请求 的区别
简单请求的特点:客户端与服务器之前只会发生 1 次请求。
预检请求的特点:客户端与服务器之前会发生 2 次请求。OPTION 预检请求成功之后,才会发起真正的请求。
8.12 JSONP 请求
浏览器通过 <script> 标签的 src 属性,请求服务器上的数据,同时,服务器返回一个函数的调用。这种请求数据的方式就是 JSONP。
特点:JSONP 不属于 ajax 请求,因为它没有使用 XMLHttpRequest 这个对象。
JSONP 仅支持 GET 请求。
实现 JSONP 接口的主要部分代码:
// 实现 JSONP 接口的代码
// 必须在配置 cors 中间件之前,配置 JSONP 的接口,否则就不是 JSONP 的了
app.get('/api/jsonp', (req,res) => {
// 1. 获取客户端发送过来的回调函数的名字
const funcName = req.query.callback
// 2. 得到要通过 JSONP 形式发送给客户端的数据
const data = { name: 'mea' }
// 3. 根据前两步得到的数据,拼接出一个 函数调用 字符串
const scriptStr = `${funcName}( ${JSON.stringify(data)} )`
// 4. 把上一步拼接得到的字符串,响应给客户端的 <script> 标签进行解析执行
res.send(scriptStr)
})
使用 jQuery 调用 ajax 函数发 JSONP 请求.html
<button id="btnJSONP">GET JSONP</button>
<script>
$(function() {
// 1. 请求 JSONP 接口
$('#btnJSONP').on('click', function() {
$.ajax({
method: 'GET',
url: 'http://127.0.0.1/api/jsonp',
dataType: 'jsonp', // 表示要发起 JSONP 的请求
success: function(res) {
console.log(res)
console.log('请求中...')
}
})
})
})
</script>
9. 数据库
数据库(database)是用来组织、存储和管理数据的仓库。
常见的数据库:MySQL 数据库(免费开源,社区版)、Oracle、SQL Server、Mongodb
其中,MySQL、Oracle、SQL Server 属于传统型数据库(又叫做:关系型数据库 或 SQL 数据库),这三者设计理念相同。而 MongoDB 属于新型数据库(非关系型数据库 或 NoSQL数据库),在一定程度上弥补了传统型数据库的缺陷。
传统型数据库中,数据的组织结构分为:数据库(database)、数据表(table)、数据行(row)、字段(field),这 4 大部分组成。
一个数据库里,有若干个表。每个表中存储的哪些信息,由字段来决定,id username 等等。表中的行,代表每一条具体的数据。
1. SQL
SQL,结构化查询语言,是专门访问和处理数据库的编程语言
1、SQL 是一门数据库编程语言
2、使用 SQL 语言编写的代码,叫做 SQL 语句
3、SQL 语言只能在关系型数据库中使用(例如 MySQL、Oracle等),非关系型不支持SQL语言(例如Mongodb)
作用有:增删改查。
1、SELECT 查询
注意:SQL 语句中的关键字对大小写不敏感,select 等效于 SELECT
2、INSERT INTO 插入
向指定的表中插入数据,列和值一一对应,多个列和值用 , 号分隔
INSERT INTO 表名 (列1, 列2...) VALUES (值1, 值2....)
INSERT INTO users (username, userpswd) VALUES ('mo','123');
3、UPDATE 修改
修改表中的数据
UPDATE 表名 SET 列名 = 新值 WHERE 列名 = 某值
gUPDATE 指定表,SET 指定列与值,WHERE 指定条件。
更新一行的某个列,例如:修改 username_roma 为 na ,条件是 id=3 的。换句话说,就是把 id 为 3 的 username_roma 改为 na
UPDATE users SET username_roma = 'na' WHERE id = 3;
更新一行的多个列,例如:把 id 为 3 的 username 和 userpswd 同时改为 Moli 和 123456
UPDATE users SET username = 'Moli', userpswd = '123456' WHEERE id = 3;
注意:如果不写 WHERE 条件,则所有的列都会被修改,例如所有的 userpswd 为 123456。
4、DELETE 删除
删除表中的数据
DELETE FROM 表名 WHERE 列名 = 某值
例如,修改 id 为 4 的:
DELETE FROM users WHERE id = 4;
注意:不加 WHERE 条件会删除所有的数据!
5、ORDER BY 排序
asc 表示升序,默认情况下就是升序,desc 表示降序
SELECT * FROM 表 ORDER BY 列 为升序 (或 ORDER BY 列 ASC)
SELECT * FROM 表 ORDER BY 列 DESC 为降序
对表中的数据,按照 id 字段进行排序:select * from users order by id;
多重排序:
多个排序的添加,用英文逗号分隔
例如,对 users 表中的数据,先按照 username 字段的汉字进行降序排序,再按照 username_roma 的罗马音进行升序排序 :
SELECT * FROM users ORDER BY username DESC, username_roma ASC; (这个 ASC 可省略,写上更加语义化,简单明了)
6、COUNT(*) 统计
7、AS 设置别名
给查询出来的列名称设置别名,可以使用 AS 关键字。
例如,将 count(*) 设置别名为 id_sum:
select count(*) AS id_sum from users where id < 5;
然后显示的列名就是 id_sum 了,而不是 count(*)
例如,SELECT username AS Uname, userpswd AS Upd FROM users;
返回的列名分别是 Uname 和 Upd
10. mysql 模块
在项目中操作数据库的步骤;
1、安装 MySQL 的第三方模块(npm i mysql)
2、通过 mysql 模块连接到 MySQL 数据库
3、通过 mysql 模块执行 SQL 语句
1. 安装 mysql 模块
npm install mysql
2. 配置 mysql 模块
先导入模块,配置 ip、账号、密码、目标数据库等信息。
// 配置 mysql 模块
// 1. 导入 mysql 模块
const msyql = require('mysql')
// 2. 建立与 MySQL 数据库的连接
const db = msyql.createPool({
host: '127.0.0.1', // 数据库的 IP 地址
user: 'root', // 登陆数据库的账号
password: 'admin123', // 登陆数据库的密码
database: 'my_db' // 要操作的数据库
})
// 3. 测试 mysql 模块是否正常工作
// 调用 db.query() 函数,指定要执行的 SQL 语句,通过回调函数拿到执行的结果:
// 回调函数参数 err 是错误信息, result 是正常执行后的结果
db.query('SELECT 1', (err,result) => {
// mysql 模块工作期间报错了,则err 里有错误信息,err 为真
if (err) return console.log(err.message) // 如果失败,err成立则打印错误信息
// 能够成功的执行 SQL 语句
// 只要能打印出 [ RowDataPacket { '1': 1 } ] 的结果,就证明数据库连接正常
console.log(result)
})
// 注意: SELECT 1 这个语句没有实质性的作用,只是来测试是否可以连通的。
3. mysql 模块 查询数据
调用 db.query() 函数执行 SQL 语句
例如查询 users 表中所有数据:db.query( 'select * from users', (err, result) => { .... })
其中,err 表示失败后的错误对象,result 表示成功后的对象。
// 3. 查询数据
const sqlStr = 'select * from users' // 查询 users 表中所有的数据
db.query(sqlStr, (err, result) => {
// 查询失败
if(err) return console.log(err.message)
// 查询成功
// 注意:如果执行的是 select 查询语句,则执行的结果是【数组】
console.log(result)
})
4. mysql 模块 插入数据
注意:如果执行的是 insert into 插入语句,则执行的结果是【对象】
// 向 user 表新增一条数据,定义要插入到 users 表中的数据对象
const user = { uName:'加菲猫', uPswd:'123456'}
// 待执行的 SQL 语句,其中英文问号 ? 表示占位符
const sqlStr = 'INSERT INTO users (username, userpswd) VALUES (?,?)'
// 执行 SQL 语句
// 使用数组的形式,依次为 ? 占位符指定具体的值
db.query(sqlStr, [user.uName, user.uPswd], (err,result) => {
// 执行 SQL 语句失败
if(err) return console.log(err.message)
// 执行 SQL 语句成功
// 注意:如果执行的是 insert into 插入语句,则执行的结果是【对象】
// 这个 result.affectedRows 对象属性返回的是影响的行数
if(result.affectedRows === 1){
console.log('插入数据成功!')
}
})
插入数据 的便携方式
如果 数据对象的每个属性 和 数据表的字段 一一对应,则可以快速插入数据:
// 如果 数据对象的每个属性 和 数据表的字段 一一对应,则可以快速插入数据
const user = { username: '汤姆猫', userpswd:'123456'}
const sqlStr = 'INSERT INTO users SET ?'
db.query(sqlStr, user, (err,result) => {
if(err) return console.log(err.message)
if(result.affectedRows === 1) {
console.log('插入成功!')
}
})
5. mysql 模块 更新数据
// 定义要更新的新数据对象
const user = { id: 8, username: '小栗帽', userpswd: '654321'}
// 定义待执行的 SQL 语句 使用占位符
const sqlStr = 'UPDATE users SET username=? ,userpswd=? WHERE id=?'
// 更新
db.query(sqlStr, [user.username, user.userpswd, user.id], (err,result) => {
if(err) return console.log(err.message)
if(result.affectedRows === 1) {
console.log('更新成功!')
}
})
// 更新数据的便携方式
const user = { id: 8, username: '小栗帽', userpswd: 'qqqqqq'}
// 使用占位符。把 user 信息对象都做更新,条件是 id
const sqlStr = 'UPDATE users SET ? WHERE id=?'
// 把 user 对象传给第一个占位符 ? ,把 id 传给第二个占位符 ?
db.query(sqlStr, [user, user.id], (err,result) => {
if(err) return console.log(err.message)
if(result.affectedRows === 1) {
console.log('更新成功!')
}
})
6. mysql 模块 删除数据
推荐根据 id 这样的唯一标识(主键)来删除对应数据。
注意:执行了 DELETE 语句,则执行的结果是一个【对象】,可以通过 affectedRows 判断是否成功。影响的行数。
注意:如果 SQL 语句中有多个占位符,则必须以数组的形式来指定每个占位符具体的值
如果 SQL 语句只有一个占位符,则可以省略数组
// 6. 删除数据
// 推荐根据 id 这样的唯一标识(主键)来删除对应数据。
// 1、要执行的 SQL 语句
const sqlStr = 'DELETE FROM users WHERE id=?'
// 2、调用 db.query() 执行 SQL 语句的同时,为占位符指定具体的值
// 注意:如果 SQL 语句中有多个占位符,则必须以数组的形式来指定每个占位符具体的值
// 如果 SQL 语句只有一个占位符,则可以省略数组
db.query(sqlStr, 9, (err,result) => {
if(err) return console.log('删除失败!')
if(result.affectedRows === 1) {
console.log('删除成功!')
}
})
7. mysql 模块 删除数据 标记删除
使用 DELETE 语句,会真正把数据删掉,需要谨慎操作,为了保险起见,推荐使用 标记删除,来模拟删除的动作,逻辑删除
标记删除,就是在表中设置类似 status 这样的字段,来标记是否被删除了
例如,当用户执行删除时,status 字段值改为 1,标记为删除了,也就是逻辑删除
db.query('UPDATE users SET status=? WHERE id=?', [1,233], (err,result) => {
if(err) return console.log(err.message)
if(result.affectedRows === 1) {
console.log('标记删除成功!status 改为了 1')
}
})