Express
Express初识
作用
构建Web服务器
对于程序员来说,最常见的两种服务器:
- W e b 网 站 服 务 器 \color{red}{Web网站服务器} Web网站服务器:专门对外提供Web 网页资源的服务器
- A p i 接 口 服 务 器 \color{red}{Api接口服务器} Api接口服务器:专门对外提供API接口的服务器
使用Express,我可以方便、快速的创建 Web网站 的服务器或 API接口
创建基本的Web服务
const express = require('express')
// 创建web服务器
const app = express()
//调用 app.listen(ip,port,回调函数),启动服务器
app.listen(ip,port,()=>{
console.log(`express server running at http://${ip}:${port}`)
})
//ndoejs 下的衍生框架 可以访问ejs文件 ejs动态数据
const express = require('express');
const morgan = require('morgan'); //中间件
const app = express();
//接受post数据
const bodyParser = require("body-parser");
//与Blog路由产生关联
const blogRoutes = require("./routes/BlogRoutes");
const port = process.env.PORT || 8081;
const host = process.env.HOST || "127.0.0.1";
app.set('view engine', "ejs");//声明使用什么文件(寻找)
// app.set("views","my-views");//views 文件夹必须,按此改名
//使用中间件
// app.use((req, res, next)=>{
// console.log("获得新请求");
// console.log("host:",req.hostname);
// console.log("path:",req.path);
// console.log("method:",req.method);
// next();//指示下一步,否则一直执行中间件
// })
app.use(express.static("public"));//静态资源文件夹
app.use(morgan("tiny"));
//使用中间件,配置接受内容的中间件,
// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }))
// parse application/json
app.use(bodyParser.json())
app.get('/',(req, res)=>{
// res.send("<p>Hello</p>")
//项目路径root + 当前路径
// res.sendFile("./views/index.html",{root:__dirname});
//访问ejs页面 render可传值(对象 数组对象 对象数组 json)
// const blogs = [
// {title:"Vue.js快速学习指南",snippet:"使用范围最广,学习人数最多的框架"},
// {title:"React.js快速学习指南",snippet:"使用范围最广,学习人数最多的框架"},
// {title:"Node.js快速学习指南",snippet:"使用范围最广,学习人数最多的框架"}
// ]
// res.render('index',{title:"首页",blogs});
//重定向到博客页面
res.redirect("/blogs");
});
中间件此处无效
// app.use((req, res, next)=>{
// console.log("只在req和res之间生效");
// next();//指示下一步,否则一直执行中间件
// })
app.get('/about',(req, res)=>{
// res.sendFile("./views/about.html",{root:__dirname});
res.render('about',{title:"关于我们"});
});
// app.get('/about_us',(req, res)=>{
// res.redirect('/about');
// });
//可定义根路由
app.use("/blogs",blogRoutes);
//中间件 的执行 一定是在req和res之间
//中间件 一定是有一定作用
app.use((req, res)=>{
// res.status(404).sendFile("./views/404.html",{root:__dirname})
res.status(404).render('404',{title:"404"});
})
app.listen(port,host,()=>{
console.log(`服务器运行中 ${host}:${port}`)
});
获取URL中携带的参数查询
通过 req.query 对象,可以访问到客户端通过 查询字符串 的形式,发送到服务器的参数:
app.get('/',(req,res)=>{
//req.query 默认是一个空对象
// 客户端使用 ?name=zs&age=20 这种查询字符串形式,发送到服务器的参数
// 可以通过 req.query 对象访问到:
// req.query.name == zs
console.log(req.query)
})
获取URL中的动态参数
通过 req.params 对象,可以访问到URL中,通过 “:” 匹配到的动态参数:
//:id 是动态参数
app.get('/delete/:id',(req,res)=>{
// req.params 匹配动态参数(键值对),默认为空对象
console.log(req.params)
res.send(req.params)
})
托管静态资源
express.static() 创建一个 静态资源服务器
- 将public 目录下的图片、CSS文件、JavaScript文件对外开发:
app.use(express.static('public'))
// 不需要写全路径,会自动到静态资源文件夹去寻找,设置多个静态资源文件夹时按顺序查找
- Express 在指定的静态目录中查找文件,并对外提供资源的访问路径。因此,存放静态文件的目录不会出现在URL中。
访问public下的 index.html ===> http://localohost:80/index.html
挂载路径前缀
设置通过带有/public 前缀的地址来访问public目录中的文件:
app.use('/public',express.static('public'))
http://localohost:80/public/index.html
Express路由
概念
-
客户端请求 与 服务器处理函数 之间的映射关系
-
Express中的路由份3部分组成,分别是 请求的类型、请求的URL地址、处理函数:
app.METHOD(PATH,HANDLER)
路由匹配过程
- 按照路由顺序进行匹配,从上到下
- 请求类型和请求的URL 同时匹配成功,才会方法对应的处理函数
模块化路由
- 为了 方 便 对 路 由 进 行 模 块 化 管 理 \color{blue}{方便对路由进行模块化管理} 方便对路由进行模块化管理,Express 不建议将路由直接挂载到app(express服务器实例)上,而是 推 荐 将 路 由 抽 离 为 单 独 的 模 块 \color{red}{推荐将路由抽离为单独的模块} 推荐将路由抽离为单独的模块
- 步骤:
- 创建路由模块对应的 .js 文件 (用户自定义模块,专门负责挂载路由)
- 调用 express.Router() 函数创建路由的实例对象 (express() 返回服务器实例)
- 向路由对象上挂载具体的路由
- 使用 module.exports 向外共享路由对象
- 使用app.use() 函数注册路由模块 ·
// 路由模块 UserRouters.js
//登录 注册 接口
const express = require('express');
//能使用user创建对应路由
const users = express.Router();
//生成token
const jwt = require("jsonwebtoken");
//密码加密
const bcrypt = require('bcryptjs');
//引入模型。与模型关联,实现增删改查
const User = require('../model/User');
//dzw_secret放入环境变量
process.env.SECRET_KEY = "dzw_secret";
//localhost:5000/api/v1/test
users.get("/test", (req, res) => {
res.send({ msg: "test is working." })
});
//注册接口 调试时 post需要数据
users.post("/register", (req, res) => {
console.log(req.body);
let userData = {
username: req.body.username,
password: req.body.password
}
//注册检查 findAll返回一个数组对象
User.findOne({
where: {
username: req.body.username
}
}).then((user) => {
if (!user) {
//加密
bcrypt.hash(req.body.password,10,(err,hash)=>{
userData.password = hash;
User.create(userData).then((user) => {
res.json({ status: user.username + "registered" });
}).catch((err)=>{
res.send("error:"+err);
});
});
} else {
//用户已存在
res.json({ error: "User already exists!" });
}
}).catch((err) => {
res.send("error:" + err);
});
});
//登录接口
users.post("/login", (req,res)=>{
// console.log(req.body);
//查询数据
User.findOne({
where:{
username: req.body.username
}
}).then((user)=>{
//查到用户
if (user){
//匹配密码 加密匹配compareSync
if (bcrypt.compareSync(req.body.password,user.password)){
// 创建cookies
// res.cookie("loginToken",user.dataValues,{
// maxAge:1000 * 60 * 15,
// httpOnly:true
// });
//生成token 签名
let token = jwt.sign(user.dataValues,process.env.SECRET_KEY,{
expiresIn:1000//过期时间
})
res.send(token);
}else{
res.send("用户名或密码错误!");
}
}else{
res.status(400).json({error:"User does not exist"})
}
}).catch((err)=>{
res.send("error:"+err);
});
});
module.exports = users;//给login_server使用
// 登录模块使用路由 login_sever.js
const express = require('express');
const app = express();
const port = process.env.PORT || 5000; //获取服务器的口号
const bodyParser = require('body-parser');//获取POST传递的数据
app.use(bodyParser.json());//获取JSON数据
app.use(bodyParser.urlencoded({extended: false}));//urlencoded数据
//添加路由
app.get('/', (req,res)=>{
res.send({msg:"Server is working."});
});
const User = require("./routes/UserRouters");
//localhost:5000/api 到Users寻找路由
app.use("/api/v1",User);
app.listen(port,()=>{
console.log("Server is running on port:"+ port);
});
中间件
特指业务流程的中间处理环节
- 当一个请求到达 Express 的服务器后,可以连续调用多个中间件,从而对这次请求进行预处理
- Express的中间件,本质上是一个 function 处理函数,中间件的形参列表中必须包含 next 参数。
- next 表示把流转关系转交给下一个 中间件或路由
全局生效的中间件
- 客户端发起的任何请求,到达服务器之后,都会触发的中间件,叫做全局生效的中间件
- 通过调用 app.use(中间件函数)
局部生效的中间件
- 不使用 app.use()定义的中间件,叫做局部中间件
// 定义中间件函数mw1
const mw1 = function(req,res,next){
console.log('这是中间件函数')
next()
}
// 当前路由中生效 中间可加入多个中间件
app.get('/',mw1,functino(req,res){
res.send('Home Page')
})
中间件的作用
- 多个中间件之间,共享同一份 req和res。 上游挂载属性,下游访问
注意事项
- 在 路由之前 注册中间件
- 客户端发送过来的请求,可以连续调用多个 中间件进行处理
- 执行完毕中间件的业务代码之后,不要忘记调用 next()函数
- 为了防止代码逻辑混乱,调用 next() 函数后不再写额外代码
- 多个中间件共享 req和res对象
中间件的分类
-
应用级别的中间件
- 通过 app.use()或 app.get()或app.post() ,绑定到app实例上的中间件,叫做应用级别的中间件(全局中间件,局部中间件)
-
路由级别的中间件
- 绑定到 express.Router()实例上的中间件,叫做路由级别的中间件,绑定到router实例上。
-
错误级别的中间件
- 专门用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题。
- 格式: 多一个参数 ==> function(err,req,res,next)。
- 必须注册在所有路由之后,否则无法捕获错误。
-
Express内置的中间件
- 内置3个常用中间件:
- e x p r e s s . s t a t i c \color{blue}{express.static} express.static 快速托管静态资源的内置中间件,例如:HTML文件、图片、CSS样式等(无兼容性)
- e x p r e s s . j s o n \color{blue}{express.json} express.json 解析JSON格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用 )
- e x p r e s s . u r l e n c o d e d \color{blue}{express.urlencoded} express.urlencoded 解析URL-encoded 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)
// 配置解析 application/json 格式数据的内置中间件
app.use(express.json())
// 配置解析 application/x-www-form-urlencoded 格式数据的内置中间件
app.use(express.urlencoded({extended:false}))
app.post('/user',(req,res)=>{
//在服务器,可以使用 req.body 这个属性,来接受客户端发送过来的请求体数据
// 默认情况下,如果不配置解析表单数据的中间件,则 req.body 默认等于 undefined
console.log(req.body)
res.send("ok")
})
- 第三方的中间件
自定义中间件
手动模拟一个类似 express.urlencoded 这样的中间件,来解析POST提交到服务器的表单数据
实现步骤:
- 定义中间件
- 监听 req 的 data 事件 (触发data事件,证明有数据提交到服务器)
- 在中间件中,需要监听 req 对象的data事件,来获取客户端发送到服务器的数据
- 数据量比较大, 客 户 端 会 把 数 据 切 割 后 , 分 批 发 送 到 服 务 器 \color{red}{客户端会把数据切割后,分批发送到服务器} 客户端会把数据切割后,分批发送到服务器。所以data事件可能触发多次,每一次触发data事件时, 获 取 到 的 数 据 只 是 完 整 数 据 的 一 部 分 \color{blue}{获取到的数据只是完整数据的一部分} 获取到的数据只是完整数据的一部分,需要手动对接收到的数据进行拼接。
//定义变量存储 客户端发送来的数据 let str = '' //监听 req 对象的 data 事件(客户端发送过来的新的请求体数据) req.on('data',(chunk)=>{ //拼接请求体数据,隐式转换为字符串 str += chunk })
- 监听 req 的 end 事件 (触发end事件,证明数据发送完毕,服务器端完整接收到post提交的数据)
- 当请求体数据 接 收 完 毕 \color{red}{接收完毕} 接收完毕之后,会自动触发 req 的 end 事件。
- 可以在 req 的 end 事件中, 拿 到 并 处 理 完 整 的 请 求 体 数 据 \color{red}{拿到并处理完整的请求体数据} 拿到并处理完整的请求体数据。
// 监听 req 对象的 end 事件(请求体发送完毕后自动触发) req.on('end',()=>{ // 打印完整的请求体数据 console.log(str) // TODO: 把字符串格式的请求体数据,解析成对象格式 })
- 使用 querystring 模块解析请求体数据
- 将解析出来的数据对象挂载为 req.body
- 将自定义中间件封装为模块
// 解析表单数据的中间件
app.use((req,res,next)=>{
})
CORS跨域资源共享
同源策略是浏览器的行为,是为了保护本地数据不被JavaScript代码获取回来的数据污染,因此拦截的是客户端发出的请求回来的数据接收,即请求发送了,服务器响应了,但是无法被浏览器接收。
使用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 字段:
Access-Control-Allow-Origin:<origin> | *
其中,origin 参数的值指定了 允许访问该资源的外域URL
。
例如,下面的字段值将只允许
来自 http://itcast.cn 的请求
res.setHeader('Access-Control-Allow-Origin','http://itcast.cn')
CORS 响应头- Access-Control-Allow-Headers
默认情况下,CORS
仅
支持客户端向服务器
发送如下9个请求头
:
- Accept
- Accept-Language
- Content-Language
- DPR
- Downlink
- Save-Data
- Vierport-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','*')
简单请求
同时
满足以下两大条件的请求:
请求方式
:GET 、POST 、HEAD三者之一HTTP头部信息
不超过上文提到的9种字段字段:无自定义头部字段
预检请求
只要
符合以下任何一个条件,都需要进行预检请求:
- 请求方式为
GET 、POST 、HEAD之外的请求Method类型
- 请求头中
包含自定义头部字段
- 向服务器发送了
application/json 格式的数据
在浏览器与服务器正式通信之前,浏览器会
先发送OPTION请求进行预检,以获知服务器是否允许该实际请求
,所以这一次的OPTINO请求称为“预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并携带真实数据
简单请求
和预检请求
的区别
简单请求
的特点:客户端与服务器之间只会发生一次请求
。预检请求
的特点:客户端与服务器之间会发生两次请求,OPTION预检请求成功之后,才会发起真正的请求
。
JSONP接口
概念与特点
概念
:浏览器端通过<script> 标签的src属性,请求服务器上的数据,同时,服务器返回一个函数的调用。
特点
:
- JSONP 不属于真正的Ajax请求,因为它没有使用XMLHttpRequest这个对象。
- JSONP仅支持GET请求,不支持POST、PUT、DELETE等请求。
创建JSONP接口的注意事项
如果项目中
已经配置了CORS
跨域资源共享,为了防止冲突
,必须在配置CORS中间件之前声明JSONP的接口
。否则JSONP接口会被处理成开启了CORS接口:
// 优先创建JSONP 接口【这个接口不会处理成CORS接口】
app.get('/api/jsonp',(req,res)=>{})
// 再配置CORS中间件【后续的所有接口,都会被处理成CORS接口】
app.use(cors())
// 这是一个开启了CORS的接口
app.get('/api/get',(req,res)=>{})
实现JSONP接口步骤
获取
客户端发送过来的回调函数的名字
得到要
通过JSONP形式发送给客户端的数据
- 根据前两步得到的数据,
拼接出一个函数调用的字符串
- 把上一步拼接得到的字符串,响应给客户端的 <script> 标签进行解析执行
app.get('/api/jsonps',(req,res)=>{
// 1. 获取客户端发送过来的回调函数的名字
const funcName = req.query.callback
// 2. 得到要通过JSONP 形式发送给客户端的数据
const data = {name:'zs',age:22}
// 3. 根据前两步得到的数据,拼接出一个函数调用的字符串
const scriptStr = `${funcName}(${JSON.stringify(data)})`
// 4. 把上一步拼接得到的字符串,响应给客户端的 <script>标签进行解析执行
res.send(scriptStr)
})
在网页中使用jQuery发起JSONP请求
调用$.ajax()函数,
提供JSONP的配置选项
,从而发起JSONP请求:
$.ajax({
type:'get',
url: 'http://127.0.0.1:8181/api/jsonps',
data:{
name:1
},
// 如果要使用 $.ajax() 发起JSONP请求,必须指定datatype 为 jsonp
dataType:'jsonp',
// 发送到服务端的参数名称,默认值为callback
jsonp:'callback',
// 自定义的回调函数名称,默认值为jQueryxxx格式
jsonpCallbaack:'abc',
success:function(res){
console.log(res)
}
})