JavaScript之Node.js(三):Express框架之路由、中间件、接口

 
一、什么是Express
1、Express
白龙网认为,Express是基于node.js平台的开发框架,它的作用和node.js的http模块类似,是专用来创建web服务器的。它的本质是一个npm上的第三方包,提供了快速创建web服务器的方法。
有了内置的http模块,为什么要用Express?Express是基于http模块进一步封装出来的,能够极大的提高开发效率。http与Express关系,类似于web api与jQuery的关系,后者是基于前者进一步封装出来的。
使用Express可以方便快捷的创建以下两种服务器:web网站服务器:、api接口服务器。
 
2、安装Express
npm install express
 
3、创建web服务器
创建服务器后,可以调用GET POST SEND等方法,还可以调用query params等参数,获取用户传递的相关数据。
//1.导入express
const express = require('express')
//2.创建web服务器
const app = express()
//4.调用express提供了get post 方法,并向客户端响应相应的内容
//4-1调用app.get()
app.get('/user',(req,res) => {
    //4-1-1res.send()内容可以是对象,测试时要注释响应的对象段代码
    res.send({name:'白龙网',business: 'SEO',age: '18'})
    //5.通过req.query参数,可以访问到客户通过查询字符串的形式,发送到服务器的参数; 默认情况下,返回的是空对象;当在URL中设置 ?age=18&name=bailong类的参数时,会被req.query获取到
    console.log(req.query)
    res.send(req.query)
})
 //6.通过req.params通过可以访问到URL中,通过:匹配的动态参数;默认是个空对象;:id是参数的名子,若是1,则打印1,若是2,则打印2……;id名称可任意写,例如ids等;动态参数可以是多个,例如,/user/:id/:name
 app.get('/user/:id',(req,res) => {
    console.log(req.params);
    res.send(req.params)
 })
 //7.托管静态资源,express提供了一个非常好用的函数,express.static(),通过它,可以方便的创建一个静态资源服务器。例如,可以把公共目录下的图片、CSS文件、JS文件对外开放访问。存放静态文件的目录名不会出现在URL中。如果要托管多个静态资源文件内的内容,则多次调用express.static()即可,按照先后顺序加载。
 app.use(express.static('./bailong'))
  //8.挂载路径前缀,即规划目录结构
  app.use('/list',express.static('./bailong'))
//4-2调用app.post
app.post('/user',(req,res) => {
    //4-2-1res.send()内容可以是文本
    res.send('请求成功')
})
//3.启动web服务器
app.listen(80,() => {
    console.log('web server running at 127.0.0.1');
})
 
4、nodemon
在编写测试node.js项目的时候,如果修改了项目的代码,则需要频繁的手动close掉,然后再重新启动,非常管繁琐。可以使用nodemon这个工具,来监听项目文件的变动,当代码被修改后,nodemon会自动帮助我们重启项目,方便调试。
安装了该工具之后,就不需要像之前那样,先重启服务器,再执行node app.js执行命令,直接执行nodemon app.js就可以自动重启项目并应用最新代码。期间可能会遇到服务器端口号冲突的问题,换个其它端口即可。
命令:npm i -g nodemon
 

二、Express路由

1、路由
就是映射关系。 在express中,路由是指客户端与服务器处理函数之间的映射关系。分为请求类型、请求的URL地址、处理函数等大部分。语法格式是:app.method(path,handler)。
 
2、路由的匹配过程
当一个请求到达服务器之后,需要先经过路由的匹配,只有匹配成功之后,才会调用对应的处理函数。
在匹配时,会按照请求的先后顺序进行匹配,如果请求的类型和请求的URL同时匹配成功,则express会将这次的请求,转交给对应的function函数处理。
路由挂载的越多,体积会越来越大,用的比较少。
 
3、路由的使用
//1.导入express
const express = require('express')
//2.创建一个服务器
const app = express()
app.get('/',function(req,res) {
    res.send('get请求成功')
})
app.post('/',function(req,res) {
    res.send('post请求成功')
})
//3.启动服务器
app.listen(8091,() => {
    console.log('server running at http:127.0.0.1:8080');
})
 
4、路由的模块化
路由挂载到实例上,会导致代码越来越多,体积越来越大,建议把路由抽离为单独的模块。
A.新建router.js文件,按照如下步骤创建路由模块
    //1.导入express
    const express = require('express')
    //2.创建路由对象
    const router = express.Router()
    //3.挂载具体的路由
    router.get('/user/list',(req,res) => {
        res.send('get user list')
    })
    router.post('/user/add',(req,res) => {
        res.send('add usr list')
    })
    //4.导出路由对象
    module.exports = router
B.在服务器上(index.js)导入、注册路由模块
    const express = require('express')
    const app = express()
    //1.导入路由模块
    const router = require('../router')
    //2.注册路由模块,app.use()函数的作用,是用来注册中间件;
    // app.use(router)
    //2-2为跌幅添加前缀
    app.use('/api',router)
    app.listen(3050,() => {
        console.log('express server running at http://127.0.0.1:3050');
    })
C.执行nodemon index.js,查看返回数据
 

三、Express中间件

middleware,业务流程的中间处理环节。
当一个请求到达express服务器后,可以连续调用多个中间件,从而对这次请求进行预处理。上一个中间件的输出,作为下一个中间件的输入。
express中间件,本质是一个function处理函数。例如,app.get('/',function(req,res,next) {next()})。中间件的形参中,必须包含一个next参数,而路由的处理函数只有req,res。
next()函数的作用,是实现多个中间件连续调用的关键,它表示流转关系转交给下一个中间件的或者路由。
 
1、定义中间件函数
  const express = require('express')
  const app = express()
  //定义一个中间件函数
  const mw = function(req,res,next) {
      console.log('这是一个中间件函数');
      //把流转关系提交给一下函数或者路由
      next()
  }
  app.listen(8070,() => {
      console.log('server running at http://127.0.0.1');
  })
 
2、全局生效中间件
客户端发起的任何请求,到达服务器后,都会触发中间件,这样的中间件叫全局生效中间件。通过调用app.use(中间件函数),即可定义一个全局生效的中间件。
const express = require('express')
const app = express()
//定义一个中间件函数
const mw = function(req,res,next) {
    console.log('这是一个中间件函数');
    //把流转关系提交给一下函数或者路由
    next()
}
//把MW注册为全局生效中间件
  app.use(mw)
  //请求到达服务器后,先触发全局生效中间件,然后触发定义的路由
  app.get ('/',(req,res) => {
      console.log('调用了/这个路由');
      res.send('home page')
  })
  app.get('/user',(req,res) => {
      console.log('调用了/USER路由');
      res.send('user page')
  })
  app.listen(8070,() => {
      console.log('server running at http://127.0.0.1');
  })
 
3、定义全局中间件的简化形式
就是把定义与注册全局生效中间件函数合并为一步完成。
  const express = require('express')
  const app = express()
  //定义并注册全局有效中间件函数
  app.use((req,res,next) => {
      console.log('定义并注册了一个全局生效中间件函数');
      next()
  })
  //请求到达服务器后,先触发全局生效中间件,然后触发定义的路由
  app.get ('/',(req,res) => {
      console.log('调用了/这个路由');
      res.send('home page')
  })
  app.get('/user',(req,res) => {
      console.log('调用了/USER路由');
      res.send('user page')
  })
  app.listen(8070,() => {
      console.log('server running at http://127.0.0.1');
  })
 
4、中间件的作用
多个中间件之间,共享同一份req,res,基于这样的特性,可以在上游中间件中,统一为对象添加自定义的属性和方法,供下游的中间件或者路由使用。
  const express = require('express')
  const app = express()
  app.use((req,res,next) => {
      const time = new Date()
      //在中间件定义一个属性,那么下级中间件或者路由都可以使用
      req.startTime = time
      next()
  })
  app.get ('/',(req,res) => {
      res.send('home page' + req.startTime)
  })
  app.get('/user',(req,res) => {
      res.send('user page' + req.startTime)
  })
  app.listen(8070,() => {
      console.log('server running at http://127.0.0.1');
  })
 
5、定义多个全局中间件
使用app.use()连续定义多个全局足件,客户端请求到达之后,按照中间件定义的先后顺序去执行。
  const express = require('express')
  const app = express()
  //定义第一个中间件
  app.use((req,res,next) => {
      console.log('这是第一个中间件');
      next()
  })
  //定义第二个中间件
  app.use((req,res,next) => {
      console.log('这是第二个中间件');
      next()
  })
  //定义一个路由
  app.get('/user',(req,res) => {
      res.send('访问了/user页面')
  })
  app.listen(80,() => {
      console.log('server running http://127.0.0.1');
  })
 
6、定义局部中间件
不使用app.use()定义的中间件,叫做局部生效中间件。
  const express = require('express')
  const app = express()
  //定义局部中间件函数
  const mw = (req,res,next) => {
      console.log('调用了局部中间件函数');
      next()
  }
  //定义第一个路由,局部中间件只能在第一个路由生效,而在第二个路由中无法生效
  app.get('/',mw,(req,res) => {
      res.send('home page')
  })
  //定义第二个个路由
  app.get('/user',(req,res) => {
      res.send('访问了/user页面')
  })
  app.listen(80,() => {
      console.log('server running http://127.0.0.1');
  })
 
7、定义多个局部中间件
可以用逗号分隔多个局部中间件,也可以用数组的形式调用多个局部中间件。
  const express = require('express')
  const app = express()
  //定义第一个局部中间件函数
  const mw1 = (req,res,next) => {
      console.log('定义第一个局部中间件函数');
      next()
  }
  //定义第二个局部中间件函数
  const mw2 = (req,res,next) => {
      console.log('定义第二个局部中间件函数');
      next()
  }
  //调用多个路由的方法:mw1,mw2;或者[mw1,mw2]
  app.get('/',mw1,mw2,(req,res) => {
      res.send('home page')
  })
  //定义第二个个路由
  app.get('/user',(req,res) => {
      res.send('访问了/user页面')
  })
  app.listen(80,() => {
      console.log('server running http://127.0.0.1');
  })
 
注意事项
一定要在路由之间注册中间件;正确的方式,声明、注册中间件,添加路由;
客户端发送过来的请问,可以连续调用多个中间件;
执行业务代码之后,必须要有next()函数;
为了防止代码的逻辑混乱,调用next()函数之后,不再写额外的代码;
连续调用多个中间件的时候,多个中间件之间是共享req,res的。
 
8、中间件的分类
A.应用级别的中间件
app.use() app.get() app.post()等,绑定到app实例上的中间件,都叫应用级别中间件。涉及全局或者局部中间件。
B.路由级别中间件
绑定到express.Router()实例上的中间件,叫路由级别的中间件。它的用法与应用级别的中间件没有区别,只不过,应用级别中间件是绑定到app实例上;路由级别的中间件是绑定到router上。
C.错误级别中间件
作用是专门用来捕获项目中发生异常的错误,从而防止项目异常导致崩溃的问题。错误中间件的错误处理函数中必须包含4个形参,形参顺序从前到后分别是:err,req,res,next。
  const express = require('express')
  const app = express()
  app.get('/',(req,res) => {
      //抛出异常
    throw new Error('服务器发生了错误')
      res.send('home page')
  })
  //定义一个错误中间件,捕获错误并给出提示,错误中间件必须注册在所有路由之后;否则无法正常工作
  app.use((err,req,res,next) => {
      console.log('发生了错误' + err.message);
      res.send('Error:' + err.message)
  })
  app.listen(80,() => {
      console.log('server running http://127.0.0.1');
  })
D.express内置中间件
express4.16版本开始,express内置了3个常用的中间件:
【express.static】快速托管静态资源的中间件,例如,HTML/CSS/JS文件;
【express.json】解析json格式的请求数据,有兼容性要求,仅在4.16+版本中可用;例如,app.use(express.json())
    const express = require('express')
    const app = express()
    //express.json()解析客户端提交的JSON数据,测试时,可以在postman工具下使用post请求服务器,在body->raw->json下输入JSON格式的数据对象{"name":"白龙网","age":18}
    app.use(express.json())
    app.post('/user',(req,res) => {
        //req.body用来获取客户端提交的数据;默认情况下,如果没有express.json()的加持,req.body获取的数据是undefined
        console.log(req.body);
        res.send('ok')
    })
 
    app.listen(80,() => {
        console.log('server running http://127.0.0.1');
    })
【express.urlencoded】解析url-encoded格式的请求数据,有兼容性要求,仅仅在4.16+版本中可用;例如,app.use(express.urlencoded(extended: false))
    const express = require('express')
    const app = express()
    app.use(express.urlencoded({extended: false}))
    app.post('/book',(req,res) => {
        console.log(req.body);
        res.send('book ok')
    })
    app.listen(80,() => {
        console.log('server running http://127.0.0.1');
    })
E.第三方中间件
非内置,第三方开发的中间件,叫做第三方中间件。按需下载即可。
要使用第三方中间件,先安装body-parser,再导入,配置
    const express = require('express')
    const app = express()
    //1.导入模块
    const parser = require('body-parser')
    //2.配置客户端请求的表单数据格式,类似于中间件
    app.use(parser.urlencoded({extended: false}))
    app.post('/user',(req,res) => {
        console.log(req.body);
        res.send('books ok')
    })
    app.listen(80,() => {
        console.log('server running http://127.0.0.1');
    })
9、自定义中间件
  (1)完整功能
    const express = require('express')
    //导入内置的模块,使用其中的方法parse()把字符串解析为对象
    const qs = require('querystring')
    const app = express()
    //1.定义一个解析表单数据的中间件
    //2.定义一个字符串,用来存储客户端发来的请求数据
    let str = ''
    app.use((req,res,next) => {
    //3.监听req的data事件
    req.on('data',(chunk) => {
        str += chunk
    })
    //4.监听req的end事件,在str中存放的是完整的请求数据
    req.on('end',() => {
        console.log(str);
        //5.把字符串解析成对象
      const body = qs.parse(str)
      console.log(body);
      //6.挂载body数据,供下游路由使用
      req.body = body
      next()
    })
    })
    //5.向服务器请求数据
    app.post('/user',(req,res) => {
        //7.直接使用上游挂载的req.body数据
        res.send(req.body)
    })
    app.listen(80,() => {
        console.log('server running http://127.0.0.1');
    })
(2)封装成模块需要2步
  A.新建custom-body-parse.js文件,主体功能如下:
  //导入内置的模块,使用其中的方法parse()把字符串解析为对象
  const qs = require('querystring')
  let str = ''
  const customBodyParse =  (req,res,next) => {
  //3.监听req的data事件
  req.on('data',(chunk) => {
      str += chunk
  })
  //4.监听req的end事件,在str中存放的是完整的请求数据
  req.on('end',() => {
      console.log(str);
      //5.把字符串解析成对象
    const body = qs.parse(str)
    console.log(body);
    //6.挂载body数据,供下游路由使用
    req.body = body
    next()
  })
  }
  //导出函数
  module.exports = customBodyParse
B.在服务器上导入自定义模块并注册
  const express = require('express')
  const app = express()
  //1.导入自定义的中间件
  const customBodyParse = require('./custom-body-parse.js')
  //2.注册自定义中间件
  app.use(customBodyParse)
  //5.向服务器请求数据
  app.post('/user',(req,res) => {
      //7.直接使用上游挂载的req.body数据
      res.send(req.body)
  })
  app.listen(80,() => {
      console.log('server running http://127.0.0.1');
  })
 

四、使用Express写接口

 
1、创建服务器、api路由模块、GET/POST接口
 
(1)API路由模块
如下代码放入apiRouter文件即可
    //1.定义一个api路由模块并公开出去
    const express = require('express')
    const router = express.Router()
    //2.定义一个get接口,需要注意的是,网站地址后面的目录是:/api/get
    router.get('/get',(req,res) => {
        //2-1通过req.query获取客户端通过查询字符串,发送到服务器的数据
        const query = req.query
        //2-2通过res.send()方法,向客户端响应处理的结果
        res.send({
            status: 0,
            msg: 'GET请求成功',
            data: query
        })
    })
    //3.定义一个post接口
    router.post('/post',(req,res) => {
        //3-1接收客户端提交的数据,使用postman工具测试提交数据时,要在url-encoded格式下发送数据
      const body = req.body
      //3-2向客户端响应数据
      res.send({
        status: 0,
        msg: 'POST请求成功',
        data: body
      })
    })
    //4.定义一个delete接口
    router.delete('/delete',(req,res) => {
        res.send({
            staus: 0,
            msg: 'DELETE请求成功'
        })
    })
    module.exports = router
 
(2)在服务器中导入并注册API路由模块
如下代码放入index.js执行即可
    //1.创建服务器
    const express = require('express')
    const app = express()
    //3.获取encodeed格式的数据,必须配置中间件
    app.use(express.urlencoded({extended: false}))
    //4-1导入中间件cors,解决跨域问题
    const cors = require('cors')
    //4-2注册cors中间件
    app.use(cors())
    //2-1.导入apiRouter
    const router = require('./apiRouter')
    //2-2注册
    app.use('/api',router)
    app.listen(80,() => {
        console.log('server running http://127.0.0.1');
    })
 
2、解决接口跨域问题
像本地服务器引用在线JQ库的接口跨域问题,推荐使用cors中间件解决,不推荐JSONP,因为JSONP存在缺陷,不只支持GET,不支持POST。
(1)CORS中间件
通过安装和配置cors中间件,方便的解决跨域问题。一定要在路由之前配置CORS中间件,实施步骤:
安装:npm i cors
导入:const cors = require('cors')
注册:app.use(cors())
例如,下面代码,本地测试,导入在线JQ,就是跨域问题,利用cors中间件解决数据互通的问题。
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
        <script src="https://cdn.jsdelivr.cn/npm/jquery@1.12.4/dist/jquery.min.js"></script>
    </head>
    <body>
        <button id="btnGet">GET请求</button>
        <button id="btnPost">POST请求</button>
        <button id="btnDelete">删除</button>
        <script>
 
            $(function() {
            //测试gety请求
                $('#btnGet').on('click',function() {
                    $.ajax({
                        type: 'get',
                        url: 'http://127.0.0.1/api/get',
                        data: {name: '白龙网',age: 16},
                        success: function(res) {
                            console.log(res);
                        }
                    })
                })
            //测试POST请求
                $('#btnPost').on('click',function() {
                    $.ajax({
                        type: 'post',
                        url: 'http://127.0.0.1/api/post',
                        data: {author: '白龙',gender: '男'},
                        success: function(res) {
                            console.log(res);
                        }
 
                    })
                })
            //为删除按钮绑定事件,测试
                $('#btnDelete').on('click',function() {
                    $.ajax({
                        type: 'delete',
                        url: 'http://127.0.0.1/api/delete',
                        success: function(res) {
                            console.log(res);
                        }
                    })
                })
            })
        </script>
    </body>
    </html>
CORS是跨域资源共享,由一系列的HTTP响应头组成,这些HTTP响应头决定浏览器是否阻止JS代码跨域捕获资源。
浏览器的同源安全策略默认会阻止网页跨域获取资源,但是,如果接口服务器配置了CORS相关的HTTP响应头,就可以解决浏览器的跨域访问限制。
 
(2)注意事项
只在服务器端配置,客户端无须任何额外的配置,即可请求开启了CORS的接口;
有兼容性,只有支持XHR 2的浏览器,才支持
 
(3)响应头部
  A.允许所有网站请求/指定网站请求
   res.setHeader('Access-Control-Allow-Origin','*')
   res.setHeader('Access-Control-Allow-Origin','bailong.org.cn')
 
  B.默认情况下,CORS仅支持客户端向服务器端发送的如下9个请求头:Accept、Accept-Language、Content-Language、DPR、DownLink、Save-Data、Viewport-Width、Width、Content-Type。
   res.setHeader('Access-Control-Allow-Headers','Content-Type,x-custom-Header')
 
  C.服务器支持所有的请求方式/或者指定的请求方式,默认只支持下述4个
   res.setHeader('Access-Control-Allow-methods','*')
   res.setHeader('Access-Control-Allow-Origin','GET,POST,PUT,DELETE')
 
(4)请求的类型
A.简单请求
请求方式:GET/POST/HEAD三者之一
HTTP头部信息不超过常用的9个,无自定义头部字段,Accept、Accept-Language、Content-Language、DPR、DownLink、Save-Data、Viewport-Width、Width、Content-Type。
 
B.预检请求
简单请求的对立面,只要符合以下任何一个条件:
请求方式为get ,post,head之外的请求类型
请求头中包含自定义头部字段
向服务器发送了application/json格式的数据
 
在浏览器与服务器正式通信之前,浏览器先发送OPTION请求进行预检,以获知服务器是否允许该实际请求,所以这一次的OPTION请求称为“预检请求”,服务器成功响应预检请求后,才会发送真正的请求,并且拾真实的数据。
 
C.简单请求和预检请求的区别
简单请求的特点:客户端与服务器之间只会发生一次请求;
预检请求的特点:客户端与服务器之间会发生两次请求,OPTION预检请求之后,才会发起真正的请求;
 
3、JSONP接口
(1)JSONP及其特点
浏览器通过<script>标签的src属性,请求服务器上的数据,服务器返回一个函数的调用,这种请求数据的方式叫做JSONP。其特点如下:
JSONP不属于ajax请求,因为它没有使用XHR这个对象;
JSONP仅仅支持GET请求,不支持POST PUT DELETE等请求。
 
(2)创建JSONP注意事项
如果项目中已经配置好了CORS跨域资源共享,为了防止冲突,必须在配置CORS中间件之前声明JSONP的接口,否则JSONP接口会被处理成开启了CORSR的接口。如下代码:
 
A.JSONP接口代码
  //JSONP必须在CORS中间件之前配置
  app.get('/api/jsonp',(req,res) => {
      //todo
      //得到函数的名称
      const funName =req.query.callback;
      //定义要发送到客户端的数据对象
      const data = {sitename: '白龙网',age: 18}
      //拼接出一个函数的调用
      const scriptStr = '${funName}(${JSON.stringify(data)})'
      //把拼接的字符串响应到客户端
      res.send(scriptStr)
  })
 
B.通过页面发起请求,测试JSONP接口
        $('#btnJson').on('click',function() {
            $.ajax({
                type: 'get',
                url: 'http://127.0.0.1/api/jsonp',
                dataType: 'jsonp',
                success: function(res) {
                    console.log(res);
                }
            })
        })