Node.js TypeScript#6. 发送http请求,理解multipart/form-data

731 阅读6分钟

原文链接

HTTP是一种协议,它允许你请求例如JSON数据HTML文档这项的资源。它连接clientserver,来帮助你传递和交换信息。当数据从client发出就叫request。当数据从server发出就叫response。在这篇文章中,我们主要讲的是如何发出request

本文介绍了使用原生Node.js中进行HTTP请求的方法。其他可行的解决方案是使用axios这样的库。

发送一个http请求

想要发送一个http请求,我们需要使用http模块。它包含了请求方法。

import { request } from "http";

const req = request(
  {
    host: "jsonplaceholder.typicode.com",
    path: "/todos/1",
    method: "GET",
  },
  (response) => {
    console.log(response.statusCode);
  }
);

req.end();

request方法的第一个参数是一个对象,他们的意思都比较好理解。

最后一个参数是一个回调函数,这个回调的第一个参数是服务端的response实例。它包含了我们想得到的一些响应信息,比如statusCode(状态码)

还有一个重要的东西就是readable stream。由于我们在之前的文章中就已经介绍过了,这里就不再赘述,直接上代码。

import { request } from 'http';
import { createWriteStream } from "fs";
const fileStream = createWriteStream("./file.txt");

const req = request(
  {
    host: "jsonplaceholder.typicode.com",
    path: "/todos/1",
    method: "GET",
  },
  (response) => {
    response.pipe(fileStream);
  }
);
req.end();

在上面这个代码中,我们读取了jsonplaceholder.typicode.com/todos/1这个路径下的json资源,然后我们创建了一个可写流,并且将json资源的可读流写入到了file.txt

{
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false
}

另一个我们可能会用到的就是在一个变量中存储请求body。因为它是一个可读流,我们只需要解析它的chunks即可。

const req = request(
  {
    host: "jsonplaceholder.typicode.com",
    path: "/todos/1",
    method: "GET",
  },
  (response) => {
    const chunks: Uint8Array[] = [];
    response.on("data", (chunk) => {
      chunks.push(chunk);
    });
    response.on("end", () => {
      const result = Buffer.concat(chunks).toString();
      console.log(result);
    });
  }
);

req.end();

在这当中有很多操作,我们可以通过Promise来简化这一流程


interface Response {
  data: object;
  headers: IncomingHttpHeaders;
}

function performRequest(options: RequestOptions) {
  return new Promise((resolve, reject) => {
    request(options, function (response) {
      const { statusCode, headers } = response;
      if (statusCode >= 300) {
        reject(new Error(response.statusMessage));
      }
      const chunks: Uint8Array[] = [];
      response.on("data", (chunk) => {
        chunks.push(chunk);
      });
      response.on("end", () => {
        const data = Buffer.concat(chunks).toString();
        const result: Response = {
          data: JSON.parse(data),
          headers,
        };
        resolve(result);
      });
    }).end();
  });
}

performRequest({
  host: "jsonplaceholder.typicode.com",
  path: "/todos1",
  method: "GET",
})
  .then((response) => {
    console.log(response);
  })
  .catch((error) => {
    console.log(error);
  });

这里会返回一个Not Found的错误信息,因为我们故意将资源的路径写成了todo1。这是为了展示一下通过封装可以处理一些通用的信息。

http.ClientRequest

request函数返回一个继承与StreamClientRequest的实例。我们可以用它来发送一些POST请求。

在测试这个功能前,我们先用express实现搭建一个server

首先先自己创建另一个项目express-demo

yarn init -y
yarn add typescript express ts-node

然后配置tsconfig.json:

{
  "compilerOptions": {
    "sourceMap": true,
    "target": "ESNext",
    "outDir": "./dist",
    "baseUrl": "./src"
  },
  "include": [
    "src/**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}

package.json添加脚本

"scripts": {
  "dev": "ts-node ./src/server.ts"
}

然后,在src/server.ts中编写接口

import * as express from 'express';
 
const app = express();
 
app.post("/upload", (request, response) => {
  response.send("Hello world!");
});
 
app.listen(5000);

到这,一个基础的服务就完成了。


然后,回到之前的项目中,在index.ts写下请求相关代码

const req = request(
  {
    host: "localhost",
    port: "5001",
    path: "/upload",
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
  },
  (response) => {
    console.log(response.statusCode); // 200
  }
);

req.write(
  JSON.stringify({
    author: "Marcin",
    title: "Lorem ipsum",
    content: "Dolor sit amet",
  })
);

req.end();

你会发现每个例子都需要以end函数结尾,这是用来表示请求已经结束。

使用multipart/form-data上传文件

另一个需要将请求作为流的就是上传文件。我们需要使用multipart/form-data来上传文件。

FormData提供了一个方法去构造key/value对来作为form对象的字段。当我们在浏览器环境时,我们可以很简单的使用FormData()构造函数来创建爱你。但Node中并没有提供,我们使用一个第三方包form-data(这里只是用来演示,该库已经停止维护了,开发中不建议使用) 来完成。

yarn add form-data

Multipart来源于MIME,一个扩展电子邮件格式的标准,代表多用途互联网邮件扩展。该类型的请求将一组或多组数据合并到一个body,并以 boundary(随机字符串) 分隔。通常,在发送文件时,我们使用multipart/form-data,这是Multipart的一个子类型,在网络上被广泛支持

import * as FormData from "form-data";
import { createReadStream } from "fs";

const readStream = createReadStream("./photo.jpg");

const form = new FormData();
form.append("photo", readStream);
form.append("firstName", "Marcin");
form.append("lastName", "Wanago");

const req = request(
  {
    host: "localhost",
    port: "5000",
    path: "/upload",
    method: "POST",
    headers: form.getHeaders(),
  },
  (response) => {
    console.log(response.statusCode); // 200
  }
);

form.pipe(req);

form-data库创建了可读流,我们将其与请求一起发送。上面的代码中有一个有趣的部分是form.getHeaders()

Boundary

当发送multipart/form-data时,我们需要使用适当的headers。我们可以通过以下示例来看form-data库为我们生成了什么:

import * as FormData from 'form-data';
import { createReadStream } from 'fs';
 
const fileStream = createReadStream('./photo.jpg');
 
const form = new FormData();
form.append('photo', readStream);
form.append('firstName', 'Marcin');
form.append('lastName', 'Wanago');
 
console.log(form.getHeaders());

上面这段代码输出如下:

{
  'content-type': 'multipart/form-data; boundary=--------------------------898552055688392969814829'
}

如你所见,他将内容的类型设置为multipart/form-data,并在其中设置一个每次都不同的随机字符串的boundary。它被传递到headers中去定义一个字符串来划分表单数据的不同部分。

为了充分理解它,我们需要将我们的form通过pipe写入到一个文件中去读取

import * as FormData from 'form-data';
import { createReadStream, createWriteStream } from 'fs';
 
const readStream = createReadStream('./photo.jpg');
const writeStream = createWriteStream('./file.txt');
 
const form = new FormData();
form.append('photo', readStream);
form.append('firstName', 'Marcin');
form.append('lastName', 'Wanago');
console.log(form.getHeaders());
 
form.pipe(writeStream);

上面首先会输出

{ 
  'content-type': 'multipart/form-data; boundary=--------------------------966991448654339731356450'
}

最终,我们可以看到file.txt中的文件如下

----------------------------966991448654339731356450
Content-Disposition: form-data; name="photo"; filename="photo.jpg"
Content-Type: image/jpeg
 
���� JFIF    �� ;CREATOR: gd-jpeg v1.0 (using IJG JPEG v90), quality = 82
�� C    
 
!'"#%%%),($+!$%$�� C   $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$�� ,," ��   
(...)
----------------------------966991448654339731356450
Content-Disposition: form-data; name="firstName"
 
Marcin
----------------------------966991448654339731356450
Content-Disposition: form-data; name="lastName"
 
Wanago
----------------------------966991448654339731356450--

form的每一部分都使用生成的boundary来划分,最后一个boundary在最后有两个额外的破折号。

总结

在这篇文章中,我们介绍了如何在Node中进行http请求,要做到这点,需要我们前面所学的关于流的知识。我们实现的功能之一就是上传文件。为了实现这一点,我们解释了multipart/form-data格式。这些知识也适用于前端。