express框架
官方网站:expressjs.com.cn
静态资源处理
有时候需要暴露一个文件夹下的全部页面,可以使用express.static('public')
这样可以直接使用localhost:8000/index.html
访问
有时候需要在前面加前缀,则可以使用app.use('/public',express.static('public'))
,这样可以使用localhost:8000/public/index.html
访问
路由
路由指的是客户端的请求与服务器处理函数之间的映射关系。
路由分3部分组成,分为请求类型、请求URL地址、处理函数
app.METHOD(PATH,HANDLER)
一个请求先经过路由的匹配,匹配成功会调用对应处理函数
按照路由的顺序进行匹配,如果请求类型和URL同时匹配成功,则会处理这次请求
最简单的方法
app.get('/',(req,res)=>{res.send('Hellow World')})
app.post('/',(req,res)=>{res.send('POST')})
模块化路由
调用express.Router()
函数创建路由对象,然后将路由对象挂载,使用module.exports
向外共享路由对象,使用app.use()
函数注册路由模块
//router.js
const { Router } = require('express')
const express = require('express')
//创建路由对象
const router = Router()
//挂载路由
router.get('/user/list', (req, res) => {
res.send('Get /user/list')
})
router.post('/user/add', (req, res) => {
res.send('POST /user/add')
})
//向外导出路由
module.exports = router
在外使用
//server.js
const express=require('express');
//创建应用对象
const app=express();
//创建路由规则
const router=require('./router')
//注册路由
app.use(router)
//app.use() 用于注册全局中间件
//监听端口
app.listen(8000,()=>{
console.log("服务启动")
})
也可以添加访问前缀,如app.use('/user',router)
中间件
请求到达服务器之后,可以连续调用多个中间件进行处理,
中间件本质是一个function处理函数
next函数是实现多个中间街连续调用的关键,表示把流转关系转交给下一个中间件或路由。
简单中间件
//定义一个最简单的中间件函数
const mw =function (req,res,next){
//进行相关处理
//把流转关系转交给下一个中间件
next()
}
全局生效中间件
客户端发起的任何请求,到达服务器之后,都会触发的中间件,叫做全局生效的中间件。
通过调用app.use(中间件函数)
,即可定义一个全局生效的中间件,例如
//定义一个最简单的中间件函数
const mw =function (req,res,next){
console.log('mw')
next()
}
app.use(mw)
多个中间件共享同一份req,res,我们可以统一为req或res添加自定义的属性和方法。
例如
app.use((req,res,next)=>{
const time=Date.now()
//为req挂载自定义属性
req.startTime=time
next()
})
app.get('/user',(req,res)=>{
console.log(req.startTime)
})
可以定义多个中间件,执行顺序为定义中间件的顺序。
局部中间件
不适用app.use()的中间件,叫做局部生效中间件
const mw =function (req,res,next){
//进行相关处理
//把流转关系转交给下一个中间件
next()
}
app.get('/',mw,(req,res)=>{
console.log('/')
})
app.get('/user',(req,res)=>{
console.log('/user')
})
也可以同时多个中间件,以下两个方法均可
app.get('/',mw1,mw2,(req,res)=> console.log('/'))
app.get('/',[mw1,mw2],(req,res)=> console.log('/'))
注意
一定要在中间件调用之前注册中间件;不要忘记调用next();一般在next之后不再写额外代码。
中间件的拓展
Express官方把中间件分为5大类,即
- 应用级别
- 路由级别
- 错误级别
- Express内置
- 第三方
应用级别的中间件
绑定到app实例上的中间件,叫做应用级别的中间件。
app.use((req,res,next)=>{next()})
app.get('/',mw,(req,res)=> console.log('/'))
//两者都是应用级别,但一种是全局,一种是局部
路由级别的中间件
绑定到express.Router()
上的中间件叫做路由中间件
const router = Router()
router.use((req,res,next)=>{next()})
错误级别的中间件
专门用于捕获整个项目中发生的异常错误,防止项目异常崩溃。
错误级别的中间件必须有4个形参,分别是(err,req,res,next)。
注意,所有错误级别的中间件必须注册在所有路由之后
app.get('/',(req,res)=>{
throw new Error('发生错误')
res.send('/')
})
app.use((err,req,res,next)=>{
console.log(err)
res.send(err.message)
})
Express内置中间件
express.static
快速托管静态资源的内置中间件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}))
例子
app.use(express.json())
app.post('/user',(req,res)=>{
//可以使用req.body来接受客户端发送过来的请求数据
//默认情况下,不配置中间件,则req.body等于undefined
console.log(req.body)
res.send('post user')
})
app.use(express.urlencoded({extended:false}))
app.post('/user',(req,res)=>{
//可以使用req.body来接受客户端发送过来的请求数据
//默认情况下,不配置中间件,则req.body等于undefined
console.log(req.body)
res.send('post user')
})
第三方中间件
body-parser中间件
const parser=require('body-parser')
app.use(parser.urlencoded({extended:false}))
app.post('/user',(req,res)=>{
console.log(req.body)
res.send('post user')
})
自定义中间件
模拟一个类似于express.urlencoded中间件,解析表单数据
const qs = require('querysting');
app.use((req,res,next)=>{
//定义具体业务逻辑
//如果数据量较大,服务器分批发送,则需要进行拼接
let str=''
//监听req的data事件
req.on('data',(chunk)=>{
str+=chunk
})
//监听req的end事件
req.on('end',()=>{
//str存放的是完整的请求体数据
console.log(str)
//把字符串解析成json
const body=qs.parser(str)
console.log(body)
req.body=body
next()
})
})
使用CORS解决跨域问题
先下包
npm i cors
const cors = require('cors')
app.use(cors())
//添加全局中间件即可
CORS主要在服务器端进行配置,客户端无需做额外的配置
CORS在浏览器中有兼容性,只有支持XMLHttpRequest Level2的浏览器,才能正常访问(IE10+,Chrome4+,FireFox3.5)
预检请求
符合以下任一条件的请求,都需要进行预检请求:
- 请求方式为GET、POST、HEAD之外的请求Method类型
- 请求头中包含自定义头部字段
- 向服务器发送了application/json格式的数据
在浏览器与服务器正式通信之前,浏览器会先发送OPTION请求进行预检,以获知服务器是否允许该实际请求,所以这一次的OPTION请求称为“预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据
//只允许来自http://www.baidu.com的请求
res.setHeader('Access-Control-Allow-Origin','http://www.baidu.com')
//任何网站都可以
res.setHeader('Access-Control-Allow-Origin','*')
//只允许GET,POST,DELETE,HEAD
res.setHeader('Access-Control-Allow-Methods','GET,POST,DELETE,HEAD')
//允许所有HTTP请求
res.setHeader('Access-Control-Allow-Methods','*')
JSONP
通过<script>
标签的stc属性,请求服务器上的数据,同时服务器返回一个回调,这种请求叫做JSONP,
JSONP不属于真正的Ajax请求,他没有XMLHttpRequest对象。
JSONP仅支持GET请求,不支持POST,DELETE,HEAD等等
如果项目中已经配置了CORS跨域资源共享,为了防止冲突,必须配置在CORS中间件之前声明JSONP的接口,否则JSONP接口会被处理成开起来CORS的接口
//必须在配置CORS之前配置JSONP
app.get('/api/jsonp',(req,res)=>{})
//在配置CORS中间件
app.use(cors())
//这是开启了CORS的接口
app.get('/api/get',(req,res)=>{})
实现JSONP的步骤
- 获取客户端发送过来的回调函数的名字
- 得到要通过JSONP形式发送给客户端的数据
- 根据前两步的数据,拼接一个函数调用的字符串
- 把字符串响应给客户端的
<script>
标签进行解析执行
//必须在配置CORS之前配置JSONP
app.get('/api/jsonp',(req,res)=>{
//得到函数的名称
const funcName=req.query.callback
//定义要发送到客户端的数据对象
const data={name:'nihao',age:20}
//拼接一个函数的调用
const scriptStr=`${funcName}(${JSON.stringify(data)})`
//把拼接的字符串,响应给客户端
res.send(scriptStr)
})
使用MySql
项目中使用mysql
npm i mysql
配置mysql
//导入mysql模块
const mysql = require('mysql')
//建立链接
const db = mysql.createPool({
host:'127.0.0.1',//IP地址
user:'root', //数据库账号
password:'admin123',//数据库密码
database:'test'//指定操作的数据库
})
//检测mysql模块能否正常工作,没啥用
db.query('SELECT 1',(err,results)=>{
if(err) return console.log(err.message)
console.log(results)
})
使用mysql语句
查询数据
const sqlStr='SELECT * FROM users'
//查询数据
db.query(sqlStr,(err,results)=>{
//失败
if(err) return console.log('查询失败',err.messgae)
//select查询语句,则返回结果为数组
console.log(results)
})
插入数据
//要插入的数据对象
const user = {username:'superxmyyy',password:'123456'}
//待执行的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)//失败
//影响行数为1,表示成功
//插入语句,则results是一个对象
if(results.affectedRows === 1){console.log('插入成功')}
})
简便方式
//要插入的数据对象
const user = {username:'superxmyyy',password:'123456'}
//待执行的SQL语句,用英文?表示占位符
const sqlStr='INSERT INTO users SET ?'
db.query(sqlStr,user,(err,results)=>{
if(err) return console.log(err.message)//失败
//影响行数为1,表示成功
//插入语句,则results是一个对象
if(results.affectedRows === 1){console.log('插入成功')}
})
更新数据
const user={id:1,username:'super',password:'123456'}
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)//失败
//影响行数为1,表示成功
if(results.affectedRows === 1){console.log('更新成功')}
})
简便写法
const user={id:1,username:'super',password:'1234'}
const sqlStr='UPDATE users set ? where id=?'
db.query(sqlStr,[user,user.id],(err,results)=>{
if(err) return console.log(err.message)//失败
//影响行数为1,表示成功
if(results.affectedRows === 1){console.log('更新成功')}
})
删除数据
这样删除过于危险
const sqlStr='delete from users where id=?'
db.query(sqlStr,3,(err,results)=>{
if(err) return console.log(err.message)//失败
//影响行数为1,表示成功
if(results.affectedRows === 1){console.log('删除数据成功')}
})
标记删除
在表中设置类似status的状态字段,标记该数据是否删除
当用户执行删除时,并没有执行DELETE,而是用UPDATE吧status更改status
const sqlStr='UPDATE users set status=1 where id=?'
db.query(sqlStr,1,(err,results)=>{
if(err) return console.log(err.message)//失败
//影响行数为1,表示成功
if(results.affectedRows === 1){console.log('更新成功')}
})
身份认证
Session认证
session认证需要配合Cookie实现,而Cookie默认不支持跨域访问,当涉及前端跨域请求时,需要做很多额外的配置
安装包
npm install express-session
配置session中间件
const session = require('express-session')
//使用app.use使用session
app.use(session({
secret:'strings', //任意字符串
resave:false, //固定写法
saveUninitialized:true//固定写法
}))
配置成功后,可以通过req.session来访问和使用session对象
向session中存数据
app.post('/api/login',(req,res)=>{
if(req.body.username!=='admin' ||req.body.password!='123456'){
return res.send({status:1,msg:'登录失败'})
}
//配置session之后才有session属性
req.session.user=req.body //用户信息
req.session.isLogin=true //用户登录状态
res.send({status:0,msg:'登录成功'})
})
从session中取数据
app.get('/api/username',(req,res)=>{
if(!req.session.islogin){return res.send({status:1,msg:'fail'})}
res.send({
status:0,
msg:'sucess',
username:req.session.user.username
})
})
清空session
//退出登录
app.post('/api/logout',(req,res)=>{
req.session.destory()
res.send({
status:0,
msg:'退出成功'
})
})
JWT认证
JWT通常用三部分组成,即Header(头),Payload(有效褐藻),Signature(签名),例如
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOE2NzEyNjc5ODIsInVzZXJfbmFtZSI6Ik1pc2FraSIsImp0aSI6ImNkOTYyOTA2LTVlODYtNGMwNS05YzA4LTNlMzY5OTRkNDliNCIsImNsaWVudF9pZCI6InBjIiwic2NvcGUiOlsiYWxsIl19.n1ZLMQ97_3GH6VPM_Vs6neZvl88jPKZv5lw9PJ8ovU
只有Payload部分才是真正的用户信息,Header和Signature是安全性相关的部分,为了保证Token安全性
客户端收到服务器的JWT之后,通常存储在localstorage和sessionStorage中。
把JWT放在HTTP请求头的Authorization字段中
Authorization:Bearer <token>
使用JWT
安装包
npm i jsonwebtoken express-jwt
jsonwebtoken用于生成JWT字符串,express-jwt用于将JWT解析还原成JSON对象
定义secret密钥
保证JWT字符串的安全性,防止JWT在网络传播被破解,我们需要定义一个用于加密和解密的密钥:
//本质就是字符串,但是越复杂越好
const secretKey="niubi"
加密
调用jsonwebtoken包的sign()方法将用户信息加密成JWT字符串
const jwt=require('jsonwebtoken')
const expressJWT=require('express-jwt')
const secretKey="superxmy nb"
app.post('/api/login',(req,res)=>{
//...省略错误处理
const userinfo=req.body
const token=jwt.sign({username:userinfo.username},secretKey,{expiresIn:'24h'})//有效期24h
res.send({
status:200,
message:'登录成功',
//调用jwt.sign()生成JWT字符串,参数为:用户信息对象,加密密钥,配置对象
token:token
})
})
还原json对象
// expressJWT({secret:secretKey})用于解析中间件
// .unless({path:[/^\/api\//]})) 用于指定哪些接口不需要权限
app.use(expressJWT({secret:secretKey}).unless({path:[/^\/api\//]}))
app.get('/admin/getinfo',(req,res)=>{
console.log(req.auth)
res.send({
status:200,
message:'获取成功',
data:req.auth
})
})
捕获jwt失败后的错误
可以通过express的错误中间件来捕获
app.use((err,req,res,next)=>{
if(err.name === 'UnauthorizedError'){
return res.send({status:401,message:'无效的token'})
}
res.send({status:500,message:'未知的错误'})
})
Q.E.D.