导读
Express创建服务器、监听GET、POST请求、把内容响应给客户端、获取URL中携带的查询参数、动态参数、托管静态资源、模块化路由、中间件、全局生效、局部生效、中间件的分类、自定义中间件、文件上传、日志保存、cors中间件解决跨域问题、cors响应头部、JSONP接口、session认证、jwt认证
Express
expresss和node内置的http模块类似,是专门用来 创建web服务器的。
创建服务器
// 导入express const express = require('express') //创建web服务器 const app = express() //调用app.listen(端口号,启动成功后的回调函数) app.listen(80,()=>{ console.log('server running at http://127.0.0.1') })
监听GET请求
app.get('请求URL',(req,res,next)=>{ })
- req:请求对象(包含了与请求相关的属性和方法
- res:响应对象(包含了与响应相关的属性和方法
监听POST请求
// req:请求对象(包含了与请求相关的属性和方法 // res:响应对象(包含了与响应相关的属性和方法 app.post('请求URL',(req,res)=>{ })
把内容响应给客户端
// 导入express const express = require('express') //创建web服务器 const app = express() // 把内容响应给客户端.必须有res.send/res.json,否则客户端将一直是挂起状态,而koa不是挂起状态 app.get('/user',(req,res)=>{ //res.status(500) 设置响应码 res.send({name:'wxl'}) //此时客户端接收到的类型是字符串 //res.json({name:'wxl'}) 此时客户端接收到类型就是json,常用 }) app.post('/user',(req,res)=>{ res.send('请求成功') }) //调用app.listen(端口号,启动成功后的回调函数) app.listen(80,()=>{ console.log('server running at http://127.0.0.1') })
获取URL中携带的查询参数
- req.query默认是一个空对象
- 客户端使用?name=z&age=20这种字符串形式发送给服务器的餐水
- 可以通过req.query对象访问到
- req.query.name. req.query.age
//获取URL中携带的查询参数 app.get('/',(req,res)=>{ console.log(req.query) res.send(req.query) })
获取URL中动态参数
- 通过req.params对象,可以访问到URL中,通过:匹配的动态参数
- req.params默认是一个空对象。里面存放在通过:动态匹配的参数值
app.get('/user/:id/:name',(req,res)=>{ console.log(req.params) res.send(req.params) }) /** { "id": "3232", "name":"wxl" } */
托管静态资源
通过express.static(),就可以 创建一个静态资源服务器
app.use(express.static('public')) //如果要托管多个静态资源目录,就写几次上面的代码
- 这样就可以访问public目录中所有的文件了
- 但是访问url中不会包含publc,例如/public/img/wxl.jpg
- 127.0.0.1/img/wxl.jpg
挂载路径前缀
app.use('/public',express.static('public'))
- 这样就可以通过带有 /public前缀地址来访public目录中的文件了:
- 127.0.0.1/public/img/wxl.jpg
模块化路由
是客户端的请求与服务器处理函数之间的映射关系
- 它有完整的中间件系统
- 路由由3部分组成,分别 是请求的类型、请求的url地址,处理函数
- 创建路由模块对应的js文件
- 调用express.Router()函数创建路由对象
- 向路由对象上挂载具体的路由
- 使用module.exportss向外共享路由 对象
- 使用app.use()函数注册路由 模块
创建路由模块
var express = require('express') var router = express.Router() router.get('/user/list',(req,res)=>{ res.send('get user list') }) router.post('/user/list',(req,res)=>{ res.send('post user list') }) module.exports = router
注册路由模块
// 导入express const express = require('express') //创建web服务器 const app = express() //导入上步创建的路由模块 const router = require('./router') //注册路由模块 app.use(router) //调用app.listen(端口号,启动成功后的回调函数) app.listen(80,()=>{ console.log('server running at http://127.0.0.1') })
为路由模块添加前缀
和静态托管资源前缀规则一样。
app.use('/api',router)
中间件
它本质上就是一个function处理函数(回调函数),中间件函数的形参列表中,必须包含next参数,而前面所说的路由处理函数中只包含req和res。
- 如果当前中间件功能没有结束请求(res.end),则必须调用next()将控制权传递给下一个中间件功能,否则,请求将会被挂起.
- 中间件可以执行的任务
- 执行任何代码
- 更改请求request和响应response对象
- 结束请求 -响应周期(返回数据)
- 调用栈中下一个中间件
- 如何将一个中间件应用到我们的应用程序中
- app.use、router.use、app.methods、router.methods
- methods指的是请求方式,例如app.get()
//单个中间件 app.get('/',(req,res,next)=>{ next() })
next函数的作用
它是实现多个中间件连续调用的关键,它表示 把流转关系 转交 给下一个已经匹配到的中间件或路由,即上次一个的输出作为下一次的输入 。
对于每一个请求,会匹配到它所对应的所有中间件,其中第一个中间件处理完毕后,再通过next(),会转交给匹配到的下一个中间件.
定义中间件函数
- 在当前中间件的业务处理完毕后,必须调用next()函数
const n = function(req,res,next){ next() }
路径中间件
app.use('/home',(req,res,next)=>{ res.end('home 中间件') })
全局生效的中间件
客户端发球的任何请求 ,到达服务器之后,都会触发的中间件,叫做全局生效中间件。
通过调用app.use(中间件函数),即可定义一个全局生效的中间件。
app.use(n)
演示全局生效中间件
// 导入express const express = require('express') //创建web服务器 const app = express() const n = function(req,res,next){ console.log('触发了中间件') //将流转关系,转交给下一个中间件或者路由,由于下面没有中间件了,所以 //直接把流转关系转交给路由了,即'/' next() } app.use(n) app.get('/',(rea,res)=>{ console.log('调用了 / 这个路由') res.send('home') }) //调用app.listen(端口号,启动成功后的回调函数) app.listen(80,()=>{ console.log('server running at http://127.0.0.1') }) /** * 触发了中间件 * 调用了 / 这个路由 */
中间件的作用
多个已经匹配到的中间件之间,共享同一份req和res,这样,就可以在上游的中间件中,统一为res和res对象添加自定义属性或者方法,供下游的中间件或路由使用。
// 导入express const express = require('express') const req = require('express/lib/request') //创建web服务器 const app = express() const n = function(req,res,next){ //获取请求到达服务器的时间 const time = Date.now() //为req对象,挂载自定义属性,从而把时间共享给后面所有路由 req.startTime = time next() } app.use(n) app.get('/',(req,res)=>{ res.send('home-'+req.startTime) }) //调用app.listen(端口号,启动成功后的回调函数) app.listen(80,()=>{ console.log('server running at http://127.0.0.1') })
定义多个全局中间件
客户端请求到达服务器之后,会按照中间件定义的先后顺序依次进行调用。
app.use(n) app.use(n2) app.use(n3)
局部生效的中间件
不使用app.use()定一个中间件,就是局部生效中间件
// 导入express const express = require('express') //创建web服务器 const app = express() const n = function(req,res,next){ console.log('调用了局部生效中间件') next() } // n这个中间件只在当前路由生效,即局部生效中间件 app.get('/',n(req,res)=>{ res.send('home') }) // n这个中间件不会影响这个路由 app.get('/user',(req,res)=>{ res.send('user') }) //调用app.listen(端口号,启动成功后的回调函数) app.listen(80,()=>{ console.log('server running at http://127.0.0.1') })
定义多个局部中间件
app.get('/',n,n2,(req,res)=>{ res.send() }) app.get('/',[n,n2],(req,res)=>{ res.send() })
中间件的分类
应用级别的中间件
通过app.use()或者app.get()后者app.post(),绑定到app实例上的中间件,叫做应用级别中间件。
路由级别的中间件
绑定到express.Router()实例上的中间件。
// 导入express const express = require('express') //创建web服务器 const app = express() //导入路由模块 const router = require('./router') router.use((req,res,next)=>{ next() }) //注册路由模块 app.use(router) //调用app.listen(端口号,启动成功后的回调函数) app.listen(80,()=>{ console.log('server running at http://127.0.0.1') })
错误级别的中间件
专门中来捕获整个项目中发生的异常错误,从而防止因异常而程序崩溃的问题。
// 导入express const express = require('express') //创建web服务器 const app = express() app.get('/',(req,res,next)=>{ if(true){res.send()} else{ next(new Error('user_is_null')) //next只有在错误中间件里才能传入参数 } }) //错误级别的中间件必须注册在所有路由之后 app.use((err,req,res,next)=>{ let status = 300; let message = "" switch(err.message){ case 'user_is_null': message='user is null!' status = 202 break default: message='error' status=400 } console.log('发生了错误',message) res.status(status)//错误码 res.json({ code:status, message:message }) }) //调用app.listen(端口号,启动成功后的回调函数) app.listen(80,()=>{ console.log('server running at http://127.0.0.1') })
Express内置的解析中间件
- express.static快速托管静态资源的内置中间件
- express.json 解析json个格式的请求体数据
- express.urlencoded解析url-encoded格式的请求体数据
- form-data的body解析,express没有内置解析中间件,需要自己下载npm install multer
固定配置格式
//a、配置解析application/json格式数据的内置中间件 app.use(express.json()) //b、配置解析application/x-www-form-urlencoded格式数据的内置中间件 app.use(express.urlencoded({extended:false})) //c、配置解析form-data格式 const multer = require('multer') cosnt upload = multer() //[注意] 下边的upload.any(即multer)只能这么局部使用,而不能使用到全局中间上 app.post('/login',upload.any(),(req,res,next)=>{ console.log(req.body) //就能拿到req.body分别解析(a、b、c)后的数据了 }
express.json
- 默认情况下,如果不配置解析表单数据的中间件,则req.body默认等于undefined
// 导入express const express = require('express') //创建web服务器 const app = express() //解析表单中 json 格式的数据 app.use(express.json()) app.post('/user',(req,res)=>{ //配置好解析json的中间件后,就可以访问json数据了 console.log(req.body) res.send('user') }) //调用app.listen(端口号,启动成功后的回调函数) app.listen(80,()=>{ console.log('server running at http://127.0.0.1') }) /** * server running at http://127.0.0.1 * { name: 'wxl', age: 30 } */
express.urlencoded
// 导入express const express = require('express') //创建web服务器 const app = express() app.use(express.urlencoded({extended:false})) app.post('/user',(req,res)=>{ console.log(req.body) res.send('user') }) //调用app.listen(端口号,启动成功后的回调函数) app.listen(80,()=>{ console.log('server running at http://127.0.0.1') }) /** * [Object: null prototype] { name: 'wxl', age: '30' } */
自定义中间件
封装自己的中间件diy-body.js
const qs = require('querystring') //node内置querystring模块 ,通过这个模块提供的parse()函数, // 可以把查询到的字符串,解析成对象格式 /** * 在中间件中,需要监听req对象的data事件,来获取 客户端发送到服务器的书就 * 如果数据量比较大,无法一次性发送完毕,则客户端会把数据切割后 ,分批发送给服务器, * 所以data事件可能会触发多次,每一次触发data事件,获取到的数据只是完整数据的一部分, * 需要收到对接收到数据进行拼接 */ const diyBodyParser =(req, res, next) => { let str = '' //监听req对象的data事件(客户端发送过来的新的请求体数据) req.on('data', (d) => { str += d }) //监听req的end事件 //当请求体数据接收完毕,会自动触发req的end事件,因此我们可以在req的end事件中, //拿到并处理完整的请求体数据 req.on('end', () => { //在str中存放的是 完整的请求体数据 const body = qs.parse(str) console.log(body) //将解析出来的请求体对象,挂载为req.body属性,供下游使用 req.body=body next() }) } module.exports = diyBodyParser
server.js使用自定义中间件
// 导入express const express = require('express') const diyBody = require('./diy-body.js') //创建web服务器 const app = express() app.use(diyBody) app.post('/user',(req,res)=>{ res.send(req.body) }) //调用app.listen(端口号,启动成功后的回调函数) app.listen(80, () => { console.log('server running at http://127.0.0.1') }) /** * server running at http://127.0.0.1 * [Object: null prototype] { name: 'wxl', age: '30' } */
文件上传
一般form-data的body形式主要用于文件上传,所以还是借助multer
const express = require('express') const multer = require('multer') const app = express() const storage = multer.diskStorage({ destination:(req,file,cb)=>{ cb(null,'./imgs') //存储的位置 }, filename:(req,file,cb)=>{ cb(null,Math.random()+'diy_name.png') //存储的文件名 } }) const upload = multer({ // dest:'./imgs' 存储的位置(简单配置) storage:storage //复杂配置 }) //通过中间件处理上传的单个文件 app.post('/upload',upload.single('file_key_name'),(req,res,next)=>{ console.log(req.file) res.end('上传成功') }) //处理上传的多个文件 app.post('/uploads',upload.array('file_key_name'),(req,res,next)=>{ console.log(req.files) res.end('上传成功') }) app.listen(9000,()=>{ console.log('启动成功') })
日志
const express = require('express') const morgan = require('morgan') const fs = require('fs') const app = express() const writerStream = fs.createWriteStream('./logs/1.log',{ flags:'a+' }) app.use(morgan('combined',{stream:writerStream})) app.post('/home',(req,res,next)=>{ res.end('hello') }) app.listen(9000,()=>{ console.log('启动成功') })
CORS跨域资源共享
使用cors中间件解决跨域问题
- 运行npm install cors 安装中间件
- 使用 const cors = require(‘cors’) 导入中间件
- 在路由之前 调用app.use(cors())配置中间件
// 导入express const express = require('express') //创建web服务器 const app = express() //一定要在路由之前,配置cors中间件 const cors = require('cors') app.use(cors) //导入路由模块 const router = require('./router') router.use((req,res,next)=>{ next() }) //注册路由模块 app.use(router) app.get('/list',(req,res)=>{ res.send('hello') }) //调用app.listen(端口号,启动成功后的回调函数) app.listen(80,()=>{ console.log('server running at http://127.0.0.1') })
CORS响应头部-Access-Control-Allow-Origin
允许访问该服务端资源的外域URL
可以设置的参数为 url | * res.setHeader('Access-Control-Allow-Origin','http://wxl.com')
Access-Control-Allow-Headers
默认情况下,cors仅支持客户端向服务端发送9个请求头,如果想发送其他请求头,需要 进行声明,例如:
res.setHeader('Access-Control-Allow-Headers','Content-Type, X-Custom-Header')
Access-Control-Allow-Methods
默认情况下,cors仅支持客户端发起GET、POST、HEAD请求,如果还希望发起PUT、DELETE等请求,必须进行以下配置
res.setHeader('Access-Control-Allow-Methods','POST,GET,DELETE,HEAD') // res.setHeader('Access-Control-Allow-Methods','*')
简单请求
同时满足以下两大条件的请求,就属于简单请求
- 请求方式:GET、POST、HEAS三者之一
- http头部信息,不能是以下几种字段:不是自定义的头部字段、….
预检请求
只要符合以下任何一个条件的请求,都需要进行预检请求。
- 请求方式为GET、POST、HEAD之外的请求Method类型
- 请求头中包含自定义的头部字段
- 向服务器发送了application/json格式的数据
JSONP接口
浏览器通过<script>标签的src属性,请求服务器上的数据,同时,服务器返回一个函数的调用。
- JSONP不属于真正的Ajax请求,因为它没有使用XMLHttpRequest对象
- JSONP仅支持GET请求。
实现JSONP接口的步骤
- 获取客户端发送过来的回调函数的名字
- 得到要通过JSONP形式发送给客户端的数据
- 根据前两步得到的数据,拼接出一个函数调用的字符串
- 把上一步拼接得到的字符串,响应给客户端的<script>标签进行解析执行
const express = require('express') const app = express() //必须在Cors中间件之前配置 JSONP请求 app.get('/user/list',(req,res)=>{ //得到函数的名称 const funName = req.query.callback //定义要发送给客户端的数据对象 const data = {name:'wxl'} //拼接出一个函数的调用 const scriptStr = `${funName}(${JSON.stringify(data)})` //把拼接的字符串你,响应给客户端 res.send(scriptStr) }) //一定要在路由之前,配置cors中间件 const cors = require('cors') app.use(cors) //导入路由模块 const router = require('./router') router.use((req,res,next)=>{ next() }) //注册路由模块 app.use('/user',router) app.get('/list',(req,res)=>{ res.send('hello') }) app.listen(80,()=>{ console.log('server running at http://127.0.0.1') })
前后端的身份认证
session认证
const express = require('express') const app = express() app.use(express.json()) const session = require('express-session') app.use(session({ secret:'keyboard',//secret属性的值可以为任意字符串 resave:false,//固定写法,下同 saveUninitialized:true })) app.post('/login',(req,res)=>{ console.log(req.body) //判断用户提交的登录信息是否正确 if(req.body.username !== 'admin' || req.body.password !== '123456'){ return res.send({ status:1, msg:'登录失败' }) } req.session.user = req.body //将用户的信息,存储到session中,即服务器中。 req.session.islogion=true //将用户的登录状态,存到session中 res.send({status:0,msg:'登录成功'}) }) //获取用户名 app.get('/username',(req,res)=>{ //判断用户是否登录 if(!req.session.islogion){ return res.send({ status:1, msg:'未登录' }) } res.send({ status:0, msg:'success', username:req.session.user.username }) }) app.post('/logout',(req,res)=>{ //清空 当前客户端 对应的session信息 req.session.destroy() res.send({ status:0, msg:'退出登录成功' }) }) app.listen(80,()=>{ console.log('server running at http://127.0.0.1') })
session认证的缺点
- copkie会被附加在每一个http请求中,增加了流量损耗
- cookie是明文传输,存在安全性的问题
- cookie大小限制是4kb
- 对于 浏览器外的其他客户端(比如ios,安卓),必须手动设置cookie和session
- 对于分布式和服务器集群中不方便保证其他系统也能正确解析session
jwt认证
用户的信息通过Token字符串的形式,保存在客户端浏览器中。它同城由三部分组成,分别是Header(头部)、Payload(有效荷载)、Signature(签名),三者之间用 . 分割,即Header.Payload.Signature
- Payload部分才是真正的用户信息,他是用户信息经过加密之后生成的字符串。
- Header和Signature是安全性相关的部分,只是为了保证Token的安全性。
使用方式
客户端收到服务器返回的jwt之后,通常会把它保存到localStorage或sessionStorage中,以后,客户端每次与服务器通信,都要带上这个jwt字符串,从而进行身份认证,。
推荐把jwt放在http请求头的Authorzation字段中。
Authorizationn: Bearer <token>
安装jwt相关的包
npm install jsonwebtoken express-jwt
- jsonwebtoken用于生成jwt字符串
- express-jwt用于将jwt字符串解析还原成json对象
定义 secret密钥
- 当生成jwt字符串的时候,需要使用secret密钥对用户信息进行加密,最终得到加缪好的jet字符串。
- 当把jwt字符串解析还原成json对象的时候,需要使用secret进行解密。
const express = require('express') const app = express() const jwt = require('jsonwebtoken') const expressJWT = require('express-jwt') const bodyParser = require('body-parser') app.use(bodyParser.urlencoded({ extended: false })) app.use( express.urlencoded({ extended: false, }) ) // 定义secret密钥,建议将密钥命名为secretKey,值 越复杂越好 const secretKey = 'wxl ^-^' //将JWT字符串再还原为json对象,一定要在路由之前配置 // expressJWT({secret:secretKey 用来解析Token中间件 // unless({path:[/^\/api\//]})) 用来指定哪些接口不需要访问权限 //然后jwt就会把解析出来的用户信息,挂载到 req.user属性上了,req.user是固定写法, app.use(expressJWT({ secret: secretKey, algorithms: ['HS256'] }).unless({ path: [/^\/api\//] })) app.post('/api/login', (req, res) => { if (req.body.username !== 'admin' || req.body.password !== '123456') { return res.send({ status: 400, message: '登录失败' }) } //调用jwt.sign() 生成JWT字符串, 三个参数分别是:用户信息 ,加密密钥 ,配置对象 //注意:千万不要把密码加密到token字符中 const tokenStr = jwt.sign({ username: req.body.username }, secretKey, { // token设置为30s过期 expiresIn: '30s', }) res.send({ status: 200, message: '登录成功', token: tokenStr, }) }) //有权限的api接口 app.get('/admin/getinfo', (req, res) => { console.log(req.user) res.send({ status: 200, message: '获取用户信息成功', data: req.user, // }) }) app.use((err,req,res,next)=>{ if(err.name === 'UnauthorizedError'){ //这次错误是由token解析失败导致的 return res.send({ status:401, message:'无效的token' }) } res.send({ status:500, message:'未知错误' }) }) app.listen(80, () => { console.log('server running at http://127.0.0.1') })
以上token生成机制是对称密钥(HS256算法),也就是说不管是颁发模块和其他使用模块都需要同一个key来操作数据,缺点明显.
使用非对称密钥(RS256算法)的话就可以解决,我们 只需要给颁发模块一个私钥负责生成token,其他模块系统通过公钥来验证token解析数据,即可.
步骤:
- 终端输入openssl 并回车
- 继续输入genrsa -out private.key 1024 回车生成私钥
- 必须通过私钥再生成公钥(因为这样才能通过公钥来验证私钥),所以接下来继续
- 输入rsa -in private.key -pubout -out public.key 回车生成公钥
- 在代码文件里的生成token中的签名位置,放入我们生成的私钥,验证token中的签名位置放入我们生成的公钥.