提示

  具体内容查看 Node 的 API 文档 (opens new window)

无论是前面的学习,还是官网文档中的案例(同时演示了 CJS/ESM 规范两种写法),都表明 Node 也在跟随着时代发展的脚步

# 准备工作

  为了简化手动重复的重新启动 http 服务,我们可以使用 nodemon 辅助包,来监听变化并自动重启 http 服务。

# 安装
npm i nodemon -g

# 使用:
nodemon app.js

  另外,如果不想被浏览器影响到,我们在可以使用 apifox、postman 等工具进行接口测试。

# 一、http 服务

Content-Type 值
res.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8' })
  • 表单application/x-www-form-urlencoded;
  • json 数据application/json;
  • 纯文本text/plain
  • html 数据text/html
  • 上传文件multipart/form-data
http
├─ Class: http.Server
│   ├─ Event: 'request'
│   ├─ server.listen()
|   └─ ……
├─ Class: http.ServerResponse
│   ├─ response.writeHead()
│   ├─ response.write()
│   ├─ response.end()
|   └─ ……
├─ Class: http.IncomingMessage
│   ├─ message.headers
│   ├─ message.method
│   ├─ message.url
|   └─ ……
└─ http.createServer

# 1、createServer

http.createServer([options][, requestListener])
http 模块 与 Stream 流

  其实,http.createServer 和前面提到的 fs.createReadStreamfs.createWriteStream一样,都是常见流 Readable (opens new window)Writable (opens new window) 的应用之一。

requestListener 会自动添加到http.Server中的'request'事件中

const { createServer } = require('http')

const server = createServer((req, res) => {
  //

  res.end()
})
server.listen(6061)

# 2、server 实例

  在 http.Server 中,我们常常使用下面两个事件/方法:

  • request 事件
  • listen 方法
const { createServer } = require('http')

const server = createServer()

let count = 0
// 监听HTTP的请求响应
server.on('request', (req, res) => {
  count++
  console.log('客户端发起了第' + count + '个请求')
  console.log('请求地址为:' + req.url) // 浏览器中:"/" 和 "/favicon.ico"

  res.end()
})

// 推荐端口范围 1025~65535
server.listen(6061, '0.0.0.0', () => {
  console.log('端口为6061的服务器开启成功:http://localhost:6061')
})

# 3、请求处理 ✨

  客户端发送的信息,我们可以通过 http.IncomingMessage 获取,并根据实际需求进行处理。

提示

  在真实的开发框架中(如:express、koa 等),是不需要我们对请求类型、url 判断、参数处理等操作的,一般框架都会进行封装,例如:app.get('/problems/')req.body

  还有一点就是,IncomingMessage (opens new window) 本身继承自 <stream.Readable> (opens new window);所以前者是可以使用后者的流读取事件和方法的。

const { createServer } = require('http')
const { parse } = require('url')

const server = createServer()
server.on('request', (req, res) => {
  const url = req.url
  const method = req.method
  // const token = req.headers['authorization']

  if (method === 'GET') {
    // 1、query数据(在url路径中)
    const { pathname, query } = parse(url, true)

    switch (url) {
      case '/problems':
        //
        break
      default:
        break
    }
  }

  if (method === 'POST') {
    // req.setEncoding('utf-8')

    // 2、body数据(在stream流中)
    let data = ''
    req.on('data', (chunk) => {
      data += chunk // += 内部会默认调用 toString方法(默认采用 utf-8)
    })
    req.on('end', () => {
      console.log(JSON.parse(data)) // { username: '张三', password: 123456 }
    })

    switch (url) {
      case '/login':
        //
        break
      default:
        break
    }
  }

  res.end()
})
server.listen(6061)

# 2、响应处理 ✨

  可以通过 http.ServerResponse 对客户端的请求做出响应。

TIP

  res.writeHead 必须写在 res.write 的前面,不然会报错 😭

常见的状态码
常见 HTTP 状态码 状态描述 信息说明
200 OK 客户端请求成功
201 Created POST 请求,创建新的资源
301 Moved Permanently 请求资源的 URL 已经修改,响应中会给出新的 URL
400 Bad Request 客户端的错误,服务器无法或者不进行处理
401 Unauthorized 未授权的错误,必须携带请求的身份信息
403 Forbidden 客户端没有权限访问,被拒接
404 Not Found 服务器找不到请求的资源
500 Internal Server Error 服务器遇到了不知道如何处理的情况
503 Service Unavailable 服务器不可用,可能处理维护或者重载状态,暂时无法访问
const { createServer } = require('http')

const server = createServer()

server.on('request', (req, res) => {
  // 状态码 与 响应头
  res.statusCode = 403
  res.setHeader('Content-Type', 'multipart/form-data;charset=utf-8')
  // res.writeHead(403, { 'Content-Type': 'multipart/form-data;charset=utf-8' })

  // 1、返回文本
  res.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8' })
  res.write(`
      <html>
        <b>你好呀! Node.js</b>
      </html>
    `)
  res.end()

  // 2、返回接口
  res.writeHead(200, { 'Content-Type': 'application/json' })
  res.end(
    JSON.stringify({
      code: 1,
      data: {
        user: 'admin',
        pwd: 123456
      }
    })
  )
})
server.listen(6061)

# 二、http 代理

  http 模块除了可以作为服务端处理请求,还可以作用客户端向第三方服务器发送请求。

并且,由于 Node 在服务器中,所以 http 模块发起的请求是不存在跨域问题的。

http
├─ http.createServer()
├─ http.get()
├─ http.request()
└─ ……

提示

  注意的是,http.get()http.request() 简化版本而言,并且它们的 option 参数内容是一致的。因此个人习惯于所有类型的请求都使用 http.request()

# 1、post 请求

const { request } = require('node:http')

// 请求数据
const postData = JSON.stringify({
  title: 'foo',
  body: 'bar',
  userId: 1
})
// 请求选项
const option = {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  }
}

// 1、发起post请求
const req = request('http://jsonplaceholder.typicode.com/posts', option, (res) => {
  res.setEncoding('utf8')
  res.on('data', (chunk) => {
    const data = chunk.toString() // 响应结果
    console.log(JSON.parse(data))
    //
  })
  res.on('end', () => {
    //
  })
})
// 2、追加body数据
req.write(postData)

req.on('error', (err) => {
  //
})
req.end()

# 2、get 请求

const { request } = require('node:http')

// 发起get请求
const req = request('http://jsonplaceholder.typicode.com/posts/1', { method: 'GET' }, (res) => {
  // res.setEncoding('utf8')
  res.on('data', (chunk) => {
    const data = chunk.toString() // 响应结果
    console.log(JSON.parse(data))
    //
  })
  res.on('end', () => {
    //
  })
})

req.on('error', (err) => {
  //
})
req.end()

# 3、axios 库

  Axios 是一个基于 promise 网络请求库,作用于 node.js 和浏览器中

在服务端它使用原生 node.js http 模块, 而在客户端 (浏览端) 则使用 XMLHttpRequests

  自己可以对比一下使用原生 http 模块 和 使用 axios 之间的区别。

const axios = require('axios')

// 1、post 请求
axios
  .post('http://jsonplaceholder.typicode.com/posts', {
    title: 'foo',
    body: 'bar',
    userId: 1
  })
  .then((res) => {
    console.log(res.data)
  })

// 2、get 请求
axios.get('http://jsonplaceholder.typicode.com/posts/1').then((res) => {
  console.log(res.data)
})

# 4、cheerio 包

爬虫模型:

  有时前端可能不会要所有的数据,我们后端当然也可以直接拿取网页爬取数据,然后按需为前端提供数据。

npm i cheerio -S

官方:

cheerio (opens new window)是一款非常实用的 nodejs 第三方包,适用于服务端(nodejs 端)处理 html。它有着与 jquery 及其相似(几乎是一致)的 api,速度飞快,使用灵活,而且不仅能够处理 html,同样也能处理 xml。

需求:

  获取电影列表的电影名、评分、主演组成的 json 数据。

代码编写

① 前端

<body>
  <script>
    fetch('http://127.0.0.1:3000/api/list')
      .then((res) => res.json())
      .then((res) => {
        console.log(res)
      })
  </script>
</body>

② 后端

const http = require('http')
const url = require('url')
const https = require('https')
const cheerio = require('cheerio')

// 创建web服务器
http
  .createServer((req, res) => {
    const urlobj = url.parse(req.url, true)
    callbackName = urlobj.query.callback

    res.writeHead(200, {
      'content-type': 'application',
      // 跨域问题解决方案:CORS头
      'Access-Control-Allow-Origin': '*'
    })

    switch (urlobj.pathname) {
      case '/api/list':
        http_get((data) => {
          // 对网页数据进行过滤
          res.end(spider(data))
        })

        break
      default:
        res.end('404')
        break
    }
    res.end()
  })
  .listen(3000, () => {
    console.log('Server running at http://localhost:3000')
  })

function http_get(cb) {
  var data = ''
  // 使用node✨发起get请求(帮助前端跨域获取“猫眼”数据)
  https.get(`https://i.maoyan.com/`, (res) => {
    res.on('data', (chunk) => {
      data += chunk
    })

    res.on('end', () => {
      // response.end(data)
      cb(data)
    })
  })
}

function spider(data) {
  let $ = cheerio.load(data)

  let $moviewlist = $('.column.content')
  let movies = []

  $moviewlist.each((index, value) => {
    movies.push({
      title: $(value).find('.title').text(),
      grade: $(value).find('.grade').text(),
      actor: $(value).find('.actor').text()
    })
  })

  return JSON.stringify(movies)
}

# 三、代理服务器

  前面我们已经知道了 Node 服务器既可以作为服务端的形式存在,也可以作为客户端的形式存在。

所以在服务端中,node 其实更多的是扮演 '房屋中介'(中间层) 的角色。

提示

  类似的 web 服务器还有:Nginx、Apache Tomcat 等

# 1、代理分析

  之所以 node 服务器可以作为代理商,是因为:

答案
  • 由于浏览器存在同源策略,它不允许非同源的 URL 之间进行资源的交互。
  • node 环境不同于浏览器环境,它不使用 XHR、Fetch 发起数据请求,也不存在浏览器的同源策略,服务器之间使用传统的 http 进行数据交互是没有跨域限制。

  利用这个特性,我们其实是已经间接的解决请求跨域问题的(后面的请求跨域 (opens new window)章节中还有更多的解决方案)。

  如下图所示(后面的演示案例图解):

在本地客户端中是无法获取猫眼的接口数据的

# 2、注意事项

  要想实现上述的构想,要使用 node 的 https / http 的 API 方法。

以下面的案例为例:请求的数据是 https 协议的,所以使用 https 模块

  • https.get()
  • https.request()

# 3、具体实现

# ① 前端

  • get_post.html
<body>
  <h4>1、请求方式</h4>
  <button class="btn-get">发起get请求</button>
  <button class="btn-post">发起post请求</button>

  <h4>2、服务器地址</h4>
  <p>express-http服务器:<br /><br />http://localhost:5050/data</p>

  <script>
    document.querySelector('.btn-get').onclick = function () {
      axios.get('http://localhost:5050/get/data').then((res) => {
        console.log(res.data)
      })
    }
    document.querySelector('.btn-post').onclick = function () {
      axios
        .post('http://localhost:5050/post/data', {
          username: 'lencamo',
          age: '21'
        })
        .then((res) => {
          console.log(res.data)
        })
    }
  </script>
</body>

# ② 后端

代理服务器 5050
  • get_post.js
const http = require('http')
const url = require('url')

const server = http.createServer()

// 监听是否有🤔浏览器请求
server.on('request', (req, res) => {
  res.setHeader('Access-Control-Allow-Origin', 'http://localhost:8081')
  res.setHeader('Access-Control-Allow-Methods', 'GET,POST')
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
  res.setHeader('Content-Type', 'application/json')

  if (req.method === 'GET') {
    http.get('http://localhost:6061/get/data', (response) => {
      // 数据流方式返回数据(非阻塞式)
      let data = ''
      response.on('data', (chunk) => {
        data += chunk
      })

      response.on('end', () => {
        res.end(data)
      })
    })
  }

  if ((req.method = 'POST')) {
    // 1、option配置项
    let options = {
      hostname: 'localhost',
      port: 6061,
      path: '/post/data',
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      }
    }
    // 2、获取body数据(express中就简单的多,直接 😒req.body)
    let postData = ''
    req.on('data', (chunk) => {
      postData += chunk
    })
    req.on('end', () => {
      // 3、发起post请求
      let require = http.request(options, (response) => {
        let data = ''
        response.on('data', (chunk) => {
          data += chunk
        })
        response.on('end', () => {
          // 返回代理获取的数据
          res.end(data)
        })
      })
      require.write(postData)
      require.end()
    })
  }
})

server.listen(5050, () => {
  console.log('Server running at http://localhost:5050')
})
目标服务器 6061

  如果不想自己搭建,可以:

  • 采用猫眼的(猫眼:https://i.maoyan.com/#movie ,然后在 Fetch/XHR 随便查看一个 get 请求)
  • 采用有品的(小米有品:https://m.xiaomiyoupin.com/main ,然后在 Fetch/XHR 随便查看一个 post 请求)
const express = require('express')
const app = express()

app.get('/get/data', (req, res) => {
  const data = { name: 'lencamo', age: 18 }

  res.send(JSON.stringify(data))
})

app.post('/post/data', (req, res) => {
  const data = { code: 1, message: 'Post请求成功!' }

  res.send(data)
})

app.listen(6061, () => {
  console.log('HTTP服务器启动:http://localhost:6061')
})
目标服务器 在线
  • get 请求:

https://i.maoyan.com/api/mmdb/movie/v3/list/hot.json?ct=%E5%8C%97%E4%BA%AC&ci=1&channelId=4

  • post 请求

https:m.xiaomiyoupin.com/mtop/market/search/placeHolder
body 数据:[{}, { baseParam: { ypClient: 1 } }]

# 四、文件上传

# 1、图片处理

  前端通过 form-data 格式传来的图片数据,是不能直接使用的,因为该数据存储了: Content-Disposition、name、filename、Content-Type 等附属信息,需要我们将纯粹的图片数据从中提取出来

设备识别图片时,需要图片是二进制数据

form-data 的各项数据是通过自动生成的 boundary 数据分隔开的,它存储在 content-type 中

请求示例
<body>
  <input id="fileSelect" type="file" />
  <button class="upload">上传图片</button>

  <script>
    const uploadBtn = document.querySelector('.upload')

    uploadBtn.onclick = function () {
      const fileList = document.querySelector('#fileSelect').files

      // 收集 from-data 数据
      const formData = new FormData()
      formData.append('title', 'icon')
      formData.append('photo', fileList[0])

      // 发送 xhr 请求
      var xhr = new XMLHttpRequest()
      xhr.open('POST', 'http://localhost:6061/upload')
      xhr.send(formData)
    }
  </script>
</body>
const { createServer } = require('node:http')
const { createWriteStream, writeFile } = require('node:fs')

const server = createServer((req, res) => {
  if (req.url === '/upload') {
    req.setEncoding('binary') // 获取设备可解析的二进制图片数据

    // 进度监控
    let curSize = 0
    const fileSize = req.headers['content-length']

    // 1、提取boundary数据
    const boundary = req.headers['content-type'].split('; ')[1].replace('boundary=', '')

    // 2、获取form-data数据
    let formData = ''
    req.on('data', (chunk) => {
      // 进度监控
      curSize += chunk.length
      console.log('文件上传进度:', ((curSize / fileSize) * 100).toFixed(2))

      formData += chunk
    })

    req.on('end', () => {
      // console.log(formData)
      const imageType = 'image/png'
      const imageTypePostion = formData.indexOf(imageType) + imageType.length

      // 3、提取图片数据
      const imageData = formData
        .substring(imageTypePostion)
        .replace(/^\s\s*/, '')
        .replace(`--${boundary}--`, '')

      // 4、生成图片
      writeFile('./icon-copy.png', imageData, { encoding: 'binary' }, (err) => {
        if (err) throw err
        console.log('图片处理成功')
      })
    })
  }

  res.end()
})
server.listen(6061)

# 2、sharp 库

更新于 : 8/7/2024, 2:16:31 PM