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处理函数
2022.12.17.16.08
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的步骤
  1. 获取客户端发送过来的回调函数的名字
  2. 得到要通过JSONP形式发送给客户端的数据
  3. 根据前两步的数据,拼接一个函数调用的字符串
  4. 把字符串响应给客户端的<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之后,通常存储在localstoragesessionStorage中。

把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.