Node.js<十>——http模块补充和express框架核心

460 阅读19分钟

http发送网络请求

http模块不止可以搭建服务器,我们还可以利用它向其它服务器发送网络请求

  1. http发送get请求
const http = require('http')

http.get('http://localhost:3000/login', (res) => {
  res.on('data', data => {
    console.log(data.toString());
  })

  res.on('end', () => {
    console.log('获取到了所有的结果!');
  })
})
  1. http发送其它请求

使用http.request方法需要传递两个参数,第一个是包含了请求方式、目标主机名和端口号的对象,第二个是回调函数;和上面get方法有些不同,http.request会返回一个对象,我们必须要调用这个对象上的end方法才能真正将请求发送出去

const req = http.request({
  method: 'POST',
  host: 'localhost',
  port: 3000
}, (res) => {
  res.on('data', data => {
    console.log(data.toString());
  })
  res.on('end', () => {
    console.log('获取到了所有的结果!');
  })
})

req.end()
  1. axios库发送网络请求

axios既支持在浏览器也支持在Node中发送请求,使用该库之前会先进行平台判断

  • 在浏览器其是基于xmlHttpRequest发送请求的
  • Node中其是基于http模块来发送请求的

使用http原生实现文件上传

客户端发送请求时,携带数据的方式主要有三种,第一种是form-data,第二种是x-www-form-unlencoded,第三种是json格式,这三种携带数据的方式会使得请求头headers中的content-type属性值不一样,所以我们在后台提取数据的时候要根据请求头上的content-type属性先判断数据类型再选择合适的方法进行解析

错误示范

将客户端传递过来的所有数据都写入到一张图片中,但这种做法是错误的,因为客户端传递给我们的数据流中不仅仅只有图片数据,还包含了一些其它的数据(content-type、文件名称、文件类型、分隔符boundary等等)

const http = require('http')
const fs = require('fs')

// 创建一个web服务器
const serve1 = http.createServer((req, res) => {
  if (req.url === '/upload') {
    if (req.method === 'POST') {
      const fileWriter = fs.createServer('./foo.png', { flags: 'a+' })

      req.on('data', data => {
        fileWriter.write(data)
      })
      req.on('end', () => {
        res.end('文件上传成功!')
      })
    }
  }
})

// 启动服务器,并且指定端口号和主机
serve1.listen(3000, () => {
  console.log('服务器1成功启动!');
})

可以看到使用这种方法生成的图片是不能进行展示的,究其原因就是客户端返回给我们的数据中不只有图片数据,还有其它的数据

正确实现方案

  1. 首先,因为图片文件一般比较大,可能需要分几次才能传递完,如果我们需要看传递过来的总数据,可以通过累加的方式将数据都赋值给一个变量,例如body += data,这种方式会先将二进制buffer数据使用utf-8编码转化为字符串之后再进行拼接
const http = require('http')
const fs = require('fs')

// 创建一个web服务器
const serve1 = http.createServer((req, res) => {
  if (req.url === '/upload') {
    if (req.method === 'POST') {
      // 读取到的图片文件必须设置为二进制的
      req.setEncoding = 'binary'
      let body = ''
      req.on('data', data => {
        body += data
      })
      req.on('end', () => {
        console.log(body);
        console.log('文件上传成功!');
        res.end('文件上传成功!')
      })
    }
  }
})

// 启动服务器,并且指定端口号和主机
serve1.listen(3000, () => {
  console.log('服务器1成功启动!');
})

因为控制台一次性打印不了太多数据,所以我们可以通过断点调试的方式查看拼接好的数据(这些数据都是通过utf-8转码后的字符串);其实只有两杠红线之间的数据才是图片所对应的,其前一部分是文件的基本信息,比如说分隔符、文件名、content-type等等,后一部分也是分割符

所以我们如果想要得到图片所对应的数据,那么必须要对上面的数据进行截取处理,具体处理过程就不说了,因为后续我们都是用其它库来帮我们进行解析的,解析的难点其实就是分割符那一块,我们需要提前获取到分隔符后再进行字符串截取

const boundary = req.headers['content-type'].split(';')[1].split('=')[1]

利用分割符将图片数据截取出来之后再写入到另一个文件中就行了,但一定要记得,我们为了能够看到客户端传递过来的数据,已经将数据转换为了字符串,而图片是二进制的文件,所以在写入的时候必须要将数据转回二进制才可以

fs.writeFile('./foo.png', imageData, 'binary', err => {
  res.end('文件上传成功!')
})

express框架

认识Web框架

前面我们已经学习了使用http内置模块来搭建Web服务器,为什么还要使用框架呢?

  • 原生http在进行很多处理前,会较为复杂
  • URL判断、Method判断、参数处理、逻辑代码处理等,都需要我们自己来处理和封装
  • 并且所有的内容都放在一起,会非常的混乱

目前在Node中比较流行的Web服务器框架是expresskoa

  • 我们先来学习express、后面再学习koa,并且对他们进行对比

express早于koa出现,并且在Node社区中迅速流行起来

  • 我们可以基于express快速、方便的开发自己的Web服务器
  • 并且可以通过一些实用工具和中间件来扩展自己的功能

Express整个框架的核心就是中间件,理解了中间件其它一切都非常简单!

Express安装

express的使用过程有两种方法:

  • 方式一:通过express提供的脚手架,直接创建一个express应用的骨架
  • 方式二:从零搭建自己的express应用结构

方式一:安装express-generator

  • 安装脚手架:npm install -g express-generator
  • 创建项目:express express-demo
  • 安装依赖:npm install
  • 启动项目:yarn start -> node bin/www

将项目跑起来之后就可以在浏览器中看到下方的页面

Express初体验

我们来创建第一个express项目

  • 我们会发现,之后的开发过程中,可以方便的将请求进行分离
  • 无论是不同的URL,还是getpost等请求方式
  • 这样的方式非常方便我们进行维护和扩展
  • 当然,这只是初体验,接下来我们去探索更多的用法

请求的路径如果有一些参数,可以这样表达

  • /users/:userId
  • resuest对象中获取可以通过req.params.userId

返回数据,我们可以方便的使用res.json函数:

  • res.json(数据)方式
  • 其他方法自行查看文档
// express是一个函数:createApplication
const express = require('express')

// app其实也是一个函数
const app = express()

// 监听默认路径
app.get('/', (req, res, next) => {
  res.end('Hello World!')
})

// 监听默认路径
app.get('/user/:id', (req, res, next) => {
  console.log(req.params);
  res.json(req.params)
})

app.post('/login', (req, res, next) => {
  res.end('Welcome back!')
})

// 开启监听
app.listen(3000, () => {
  console.log('express框架初体验');
}) 

认识中间件

Express是一个路由和中间件的Web框架,它本身的功能非常少

  • Express应用程序本质上是一系列中间件函数的调用

中间件是什么呢?

  • 中间件的本质是传递给express的一个回调函数,可以理解为是一个拦截器或者过滤器

  • 这个回调函数接受三个参数

    • 请求参数(request对象)
    • 响应对象(response对象)
    • next函数(在express中定义的用于执行下一个中间件的函数)

中间件中可以执行哪些任务呢?

  • 执行任何代码
  • 更改请求(request)和响应(response)对象
  • 结束请求-响应周期(返回数据)
  • 调用栈中的下一个中间件

注意:如果当前中间件中没有结束请求-响应周期,则必须调用next函数将控制权传递个下一个中间件功能,否则,请求将会被挂起——客户端一直接收不到响应结果

应用中间件-自己编写

那么,如何将一个中间件应用到我们的应用程序中呢?

  • express主要提供了两种方式:app/router.useapp/router.methods
  • 可以是app,也可以是router,router我们后续再学习
  • methods指的是常用的请求方式,比如app.getapp.post

我们先来学习use的用法,因为methods的方法本质是use方法的特殊情况

最普通的中间件

通过app.use可以注册一个普通中间件,无论请求方式是什么,请求路径是什么,该中间件都会被执行;但如果你注册了多个中间件,先执行的是第一个匹配上的,也就是第一个符合条件的,但如果想执行下一个符合条件的中间件,则必须要在上一个中间件中执行next函数,否则下一个中间件不会被执行

res.end只是结束了请求-响应的周期,但是它并不会妨碍后续代码的执行,所以我们可以在第一个中间件中返回响应数据,使用next执行后续的中间件。注意:切记不可以使用多次res.end函数,因为在第一次使用的时候就会结束请求-响应周期,第二次使用就会报错了

// express是一个默认路径
const express = require('express')

const app = express()

// 监听默认路径,通过use注册一个中间件(回调函数)
app.use((req, res, next) => {
  console.log('注册了第一个中间件');
  // 返回数据给客户端,否则请求会一直挂起
  res.end('Hello World')
  // res.end只是结束了请求-响应的周期,但是它并不会妨碍后续代码的执行
  next()
})

app.use((req, res, next) => {
  console.log('注册了第二个中间件');
  // 返回数据给客户端,否则请求会一直挂起
  next()
})

app.use((req, res) => {
  console.log('注册了第三个中间件');
  // 返回数据给客户端,否则请求会一直挂起
})

app.listen(3000, () => {
  console.log('express框架初体验');
}) 

当客户端任意发送了一个请求后,服务端的打印结果:

path匹配中间件

只要请求路径匹配上就会执行中间件,无论请求参数是哪个;和普通中间件一样,如果注册了多个中间件,而且都可以匹配同一个请求,那么首先执行的肯定是第一个中间件,如果在第一个中间件中有执行next函数,才会去执行第二个匹配上的中间件,以此类推...

// express是一个默认路径
const express = require('express')

const app = express()

// 监听默认路径,通过use注册一个中间件(回调函数)
app.use('/login', (req, res, next) => {
  console.log('注册了第一个中间件');
  // 返回数据给客户端,否则请求会一直挂起
  res.end('Hello World')
  // res.end只是结束了请求-响应的周期,但是它并不会妨碍后续代码的执行
})

app.use('/login', (req, res, next) => {
  console.log('注册了第二个中间件');
})

app.listen(3000, () => {
  console.log('express框架初体验');
}) 
path和method匹配中间件

app函数对象上面有对应的方法可以指定中间件函数被执行所需要的条件——请求方法和请求路径

// express是一个默认路径
const express = require('express')

const app = express()

// 监听默认路径,通过use注册一个中间件(回调函数)
app.post('/login', (req, res, next) => {
  console.log('注册了第一个中间件');
  res.end('第一个中间件')
})

app.get('/user', (req, res, next) => {
  console.log('注册了第2个中间件');
  res.end('第2个中间件')
})

app.listen(3000, () => {
  console.log('express框架初体验');
}) 
注册多个中间件

注册中间件的时候,我们可以在中间件函数的后面加上其它的中间件函数,这样就相当于我们连续注册了多个中间件,但是和分离开注册的效果一样,想要执行下一个中间件必须要执行next函数

// express是一个默认路径
const express = require('express')

const app = express()

// 监听默认路径,通过use注册一个中间件(回调函数)
app.post('/login', (req, res, next) => {
  console.log('注册了第一个中间件');
  next()
}, (req, res, next) => {
  console.log('注册了第2个中间件');
  next()
}, (req, res, next) => {
  console.log('注册了第3个中间件');
  next()
}, (req, res, next) => {
  console.log('注册了第4个中间件');
  res.end('注册了第4个中间件')
})


app.listen(3000, () => {
  console.log('express框架初体验');
}) 
补充

通过next函数执行下一个匹配的中间件的操作是同步的,也就是说只要执行了next函数,那么就会去执行下一个中间件,不管你当前中间件函数的代码有没有执行完,只有等到对应中间件的代码执行完毕之后才会继续执行后面的代码

// express是一个默认路径
const express = require('express')

const app = express()

// 监听默认路径,通过use注册一个中间件(回调函数)
app.use('/login', (req, res, next) => {
  next()
  console.log('注册了第一个中间件');
})

app.use('/login', (req, res, next) => {
  next()
  console.log('注册了第2个中间件');
})
app.use('/login', (req, res, next) => {
  console.log('注册了第3个中间件');
})


app.listen(3000, () => {
  console.log('express框架初体验');
}) 

从打印结果来看,我们通过next方法到了第三个匹配的中间件函数中,然后再一步步回退到了第一个中间件

总结

无论中间件是怎么注册的,最先执行的中间件一定是匹配到的第一个,至于后续匹配到的中间件会不会被执行要看其对应的上一个匹配到的中间件中有没有调用next函数

中间件应用-json解析

  1. 传统方法拿到客户端传递过来的数据

我们需要使用req对象中的on方法来接受客户端传递过来的数据流,并且需要监听数据传输的过程,等到数据传输完全后将对应的数据响应回去;如果有很多请求路径,那么每个中间件都需要写上类似的代码

// express是一个默认路径
const express = require('express')

const app = express()

// 监听默认路径,通过use注册一个中间件(回调函数)
app.post('/login', (req, res, next) => { 
  req.on('data', data => {
    console.log(data.toString());
  })

  req.on('end', () => {
    res.end('Hello')
    console.log('数据获取完毕');
  })
})


app.listen(3000, () => {
  console.log('express框架初体验');
}) 
  1. 利用普通中间件统一处理数据

因为普通中间件会拦截所有的请求,所以我们只需要根据请求头中的content-type字段值决定如何解析客户端传递过来的数据,然后统一处理完成之后传递给下一个匹配上的中间件

next函数里面并不能有参数,有参数就代表有错误出现,所以不能通过给next传参的形式将数据传递给下一个中间件,但我们可以把数据放置到req.body中,req这个对象在其它的中间件中是可以共享的

// express是一个默认路径
const express = require('express')

const app = express()

app.use((req, res, next) => {
  if (req.headers['content-type'] === 'application/json') {
    let body = ''
    req.on('data', data => {
      body += data.toString()
    })

    req.on('end', () => {
      req.body = JSON.parse(body)
      res.end('Hello')
      // next函数里面并不能有参数,有参数就代表有错误,所以不能通过给next传参的形式将数据传递给下一个中间件
      next()
    })
  }
})

// 监听默认路径,通过use注册一个中间件(回调函数)
app.post('/login', (req, res, next) => {
  console.log(req.body);
})


app.listen(3000, () => {
  console.log('express框架初体验');
}) 
使用编写好的库来统一进行数据解析

express.jsonexpress框架内置好的一个库,可以帮助我们解析content-typeapplication/json的请求数据;express.json函数会返回一个函数,我们用这个函数来作为普通中间件即可实现json数据的统一解析;

express.urlencoded也是express内置好的一个库,其用于解析content-type为x-www-form-unlencoded的请求数据,其第二个参数需要一个对象,该对象有一个extended属性

  • 如果extended属性值为true,则进行解析时,它使用的库是第三方库:qs
  • 如果extended属性值为false,则进行解析时,它使用的库是node的内置模块:quertString
// express是一个默认路径
const express = require('express')

const app = express()

// 表示统一对content-type为application/json的数据进行解析
app.use(express.json())
// 表示统一对content-type为x-www-form-unlencoded的数据进行解析
app.use(express.urlencoded({ extended: true }))

// 监听默认路径,通过use注册一个中间件(回调函数)
app.post('/login', (req, res, next) => {
  console.log(req.body);
})

app.listen(3000, () => {
  console.log('express框架初体验');
}) 

应用中间件—第三方中间件

morgan

如果我们希望将请求日志记录下来,那么可以安装express官网开发的第三方库:morgan

  1. 首先需要安装morgan依赖并导入到文件中
  2. 如果希望打印用户的所有请求日志,可以将morgan函数的返回值作为全局中间件;如果只想打印有关某个路由的日志,那么就将该函数以中间件的形式配置在对应路由下即可
  3. 我们一般在调用morgan函数的时候会传入'combined'参数进去,其决定了打印日志的方式
  4. 其次我们还要指定当有日志的时候以哪种写入方式写入到对应的文件中
const express = require('express')
const morgan = require('morgan')

const app = express()
// flags设为a+表示我们每次的日志都是做一个追加 
const writerStream = fs.createWriteStream('./logs/access.log', {
  flags: 'a+'
})
// morgan函数可以充当中间件,如果放到全局下就会打印所有请求的日志,combined作为参数决定了打印日志的格式
// stream表示当有日志的时候我们以哪种方式写入
app.use(morgan("combined", { stream: writerStream }))

app.get('/home', (req, res, next) => {
  res.end('Hello')
})

app.listen(3000, () => {
  console.log('express框架初体验');
}) 

发送了三个请求之后,日志文件中的打印结果如下:

multer

中间件应用-表单提交(form-data)解析

express里面没有对form-data的数据解析的库,但是express官方开发了一个名为multer的第三方库,我们可以利用这个库来解析表单提交的数据

  1. 首先我们需要在项目中安装multer的依赖
  2. 从multer库中导入的是一个函数,我们执行它之后能得到另一个函数,一般命名为upload
  3. upload中有一个any方法,其返回值也是一个函数,是专门是用来解析用户通过表单提交的信息的,所以我们可以将any方法的返回值作为上传文件路径的中间件注册,最终解析好的数据会被放到req.body

注意:确保你总是处理了用户的文件上传。永远不要将multer作为全局中间件使用,因为用户可以上传一个文件到你一个没有预料到的路由,应该只在你需要处理上传文件的路由上使用

也就是说,哪里需要处理文件,就在对应的地方注册解析文件的中间件即可,不要将解析文件的中间件注册到全局上

const express = require('express')

const multer = require('multer')

const app = express()
// 因为在大多数情况下我们用表单提交都是来上传文件的,所以一般将其命名为upload
const upload = multer()

app.post('/upload', upload.any(), (req, res, next) => {
  console.log(req.body); // { name: ' asd ', age: ' 20' }
  res.json({
    code: 200,
    message: '上传成功!'
  })
})

app.listen(3000, () => {
  console.log('express框架初体验');
}) 

下面是我们使用postman进行测试的结果:

中间件应用-form-data文件上传

upload.single(filename)

接收一个以filename命名的文件。这个文件信息保存在req.file

  1. 我们需要在multer函数执行的时候传递一个对象进去,该对象有一个dest属性,对应着我们要把用户上传的文件存储到哪个目录下面
  2. 如果中间件是upload.single,那么在其下一个中间件中可以通过req.file获取到上传的单个文件信息
const express = require('express')

const multer = require('multer')

const app = express()

// dest表示用户上传的文件我们要存到哪里去
const upload = multer({
  dest: './uploads/'
})

// 默认情况下,上传的文件是没有后缀名的
app.post('/upload', upload.single('avatar'), (req, res, next) => {
  // 如果上传的是单个文件,可以通过req.file的方式获取到上传的文件信息
  console.log(req.file);
  res.json({
    code: 200,
    message: '文件上传成功!'
  })
})

app.listen(3000, () => {
  console.log('express框架初体验');
}) 

下面打印的是req.file中的信息

upload.array(filename)

接收一个以filename命名的文件数组,可以配置maxCount来限制最大上传数量。这些文件信息保存在req.files

刚刚的方法有个缺陷,就是我们在node中存储的文件没有后缀,而且文件名也不是由我们来控制的,那有没有办法可以让我们自己来控制存储在node中的文件名以及后缀名称呢?

  1. 首先我们不能在给multer传入dest属性了,现在需要传入个storage属性,这个属性对应了我们在node中存储文件的一些规则,比如说目标文件夹以及文件名称等
  2. 需要通过multer上面的diskStorage方法来创建storage对象,其需要传入一个对象,这个对象中需要有destinationfilename两个属性且都要求是一个函数,而且有关存储文件的信息都需要用函数内的callback回调函数进行设置
const path = require('path')

const express = require('express')
const multer = require('multer')

const app = express()

// multer.diskStorage表明要将用户上传的文件传到硬盘里
const storage = multer.diskStorage({
  destination: (req, file, callback) => {
    // 这里是制定文件存储的文件夹
    callback(null, './uploads/')
  },
  filename: (req, file, callback) => {
    // 我们这里简单的使用了当前的时间戳和文件的后缀名来决定文件名称
    callback(null, Date.now() + path.extname(file.originalname))
  }
})

// 默认情况下,上传的文件是没有后缀名的,如果想要自己设置文件名,则必须传入storage属性
const upload = multer({
  storage
})

app.post('/upload', upload.single('avatar'), (req, res, next) => {
  console.log(req.file);
  res.json({
    code: 200,
    message: '文件上传成功!'
  })
})

app.listen(3000, () => {
  console.log('express框架初体验');
}) 

如果我们想一次性上传多张图片,那么可以将upload.single改为upload.array,但是这些图片的名称都要是我们指定的才行。而且现在如果想查看图片信息的话就不能再使用req.file了,而是需要替换成req.files

app.post('/upload', upload.array('avatar', 3), (req, res, next) => {
  console.log(req.files);
  res.json({
    code: 200,
    message: '文件上传成功!'
  })
})

req.files在控制台中的打印结果:

upload.fields(fields)

接收制定fileds的混合文件。这些文件的信息保存在req.files中;fields应该是一个对象数组,应该具有name和可选的maxCount属性。其可以让用户一次性上传多个不同名称和数量的文件:

app.post('/upload', upload.fields([
  { name: 'avatar', maxCount: 1 }, 
  { name: 'file', maxCount: 2 }
]), (req, res, next) => {
  console.log(req.files);
  res.json({
    code: 200,
    message: '文件上传成功!'
  })
})