npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

multipartor

v1.0.5

Published

A streaming parser for multipart/form-data

Downloads

4

Readme

⚠️ This repo has Moved to @stream-toolbox/multipart

multipartor

multipartor 是一款用于处理 multipart/form-data 类型请求的 Node.js 请求体解析工具,这种类型的请求常用于文件上传。

简体中文English

特点

  • 速度快,比 busboyformidable 快 30% 左右
  • 轻量,仅 300 行左右的代码
  • 简单易用的 API
  • Typescript 友好

安装

npm i multipartor

快速开始

const fs = require("fs");
const http = require("http");
const path = require("path");
const { pipeline } = require("stream");

const multipartor = require("multipartor");

http
  .createServer((req, res) => {
    if (req.method === "POST" && req.headers["content-type"].startsWith("multipart/form-data")) {
      multipartor(req, {
        // 解析到文件区块时 onFile 会触发,用于处理上传的文件
        onFile(file, meta, cb) {
          // 下面的示例代码是把上传的文件保存到了 upload 文件夹中(假设 upload 文件夹已存在)
          const filePath = path.join("upload", `${Math.random()}`.slice(2) + path.extname(meta.filename));
          pipeline(file, fs.createWriteStream(filePath), (err) => {
            if (err) {
              cb(err);
            } else {
              cb(null, filePath);
            }
          });
        },
      })
        .then((result) => {
          res.setHeader("content-type", "application/json; charset=utf-8");
          res.end(JSON.stringify(result, null, 2));
        })
        .catch((err) => {
          console.log("multipartor parse error", err);
        });
    } else {
      // 返回一个 form 表单,用于提交 multipart/form-data 类型的请求
      res.setHeader("content-type", "text/html; charset=utf-8");
      res.end(`<form action="/submit" method="post" enctype="multipart/form-data">
    <input name="username" type="text" />
    <select name="interest" multiple>
      <option value="coding">编码</option>
      <option value="music">音乐</option>
      <option value="game">游戏</option>
      <option value="dance">舞蹈</option>
    </select>
    <input name="avatar" type="file" accept="image/*" />
    <input name="doc" type="file" multiple />
    <button type="submit">提交</button>
  </form>
  `);
    }
  })
  .listen(9000, () => {
    console.log("open http://127.0.0.1:9000");
  });

multipartor 的参数

multipartor(rs, opts)

  • rs
  • opts
    • 类型:
      {
        // 可指定 boundary,默认自动从 rs.headers['content-type'] 中解析
        boundary?: string;
        // 请求体的解析过程出错时,是否自动调用 rs.destroy(),默认是 false
        autoDestroy?: boolean;
        // 各种限制相关的参数,超出限制后会报错并停止解析
        limits?: {
          // 普通区块的数量,默认最多 256 个
          fields?: number;
          // 一个普通区块的值的字节数,默认最多 64KB(读取普通区块的值时,数据会暂存在内存里,所以需要限制,否则可能导致内存泄露,数据库中一个字段所占的空间一般也不会超过这个值,比如 MySQL 的 text 类型的字段,也就只占 65535 个字节,所以不需要给太大的空间)
          fieldSize?: number;
          // 文件区块的数量,默认最多 256 个
          files?: number;
          // 一个文件的字节数,默认不限制(读取文件内容时,是以流的方式进行的,数据不会被暂存在内存里,所以没太大必要限制)
          fileSize?: number;
          // 一个区块中头部的个数,默认最多 3 个,因为每个区块的头部一般也就只有 Content-Disposition, Content-Type 和 Content-Transfer-Encoding
          partHeaders?: number;
          // 一个头部的字节数,默认最多 1 KB(读取头部时,数据会暂存在内存中,所以需要限制,否则可能导致内存泄露)
          partHeaderSize?: number;
          // 请求体的总字节数,默认不限制
          totalSize?: number;
        };
        // 当解析到普通区块时触发,如果未提供 onField 函数,区块内容会被转成 utf-8 字符串
        // 1. data 参数是区块的内容,可以自定义转化
        // 2. meta 参数是区块的一些基本信息,比如: name (区块的名称,即 form 表单项中的 name 属性值), encoding (区块头部 Content-Transfer-Encoding 的值)
        // 3. cb   参数是回调函数,如果自行转化的过程中发生了错误,需要回调错误信息,以通知 multipartor 结束整个请求体的解析,如果没有发生错误,回调的第一个参数是 null,第二个参数是转化结果,转化结果会放入 multipartor 函数的返回值
        // 4. onField 函数也可以不使用 cb 参数回调结果,而是通过返回一个 Promise 来告知结果
        onField?: (data: Buffer, meta: fieldMeta, cb: (err: null | Error, data?: any) => void) => void | Promise<any>;
        // 当解析到文件区块时触发,如果未提供 onFile 函数,区块内容会被忽略
        // 1. file 参数是区块所含文件的可读流,可以将该可读流 pipe 到本地磁盘或其他存储位置,这个可读流必须被耗尽,否则整个请求的解析过程可能会卡住
        // 2. meta 参数是区块的一些基本信息,比如: filename (原始文件名), mimeType (文件的 mime 类型)
        // 3. cb   参数是回调函数,如果转存的过程中发生了错误,需要回调错误信息,以通知 multipartor 结束整个请求体的解析,如果没有发生错误,回调的第一个参数是 null,第二个参数是转存结果,转存结果会放入 multipartor 函数的返回值
        // 4. onFile 函数也可以不使用 cb 参数回调结果,而是通过返回一个 Promise 来告知结果
        onFile?: (file: Readable, meta: fileMeta, cb: (err: null | Error, data?: any) => void) => void | Promise<any>;
        // 返回值的格式,默认值 "common"
        resultFormat?: "array" | "common";
      }
    • 说明: 解析配置,具体参考上面注释。

multipartor 的返回值

  • 如果 opts.resultFormat = "array",则返回值类型为 Promise<{ [key: string]: any[] }>,比如:

    {
      "username": ["😊"],
      "interest": ["coding", "music", "game"],
      "avatar": ["upload/08948277734749754.png"],
      "doc": ["upload/8910049773055626.MP4", "upload/5344086262626364.pdf"]
    }

    由于表单的同一个字段可能有多个值(比如可以多选的 selectcheckbox 以及支持多文件上传的 file),所以字段的值会始终以数组的形式保存,即使该字段的值只有一个,这种格式可以保证读取任意字段的值时,读取方式都是一致的。

  • 如果 opts.resultFormat = "common"(默认值),则返回值类型为 Promise<{ [key: string]: any }>,比如:

    {
      "username": "😊",
      "interest": ["coding", "music", "game"],
      "avatar": "upload/08948277734749754.png",
      "doc": ["upload/8910049773055626.MP4", "upload/5344086262626364.pdf"]
    }

    只有在表单的同一个字段有多个值时才会使用数组保存。

Benchmarks

下图的测试结果为分别使用 busboy, formidablemultipartor 解析同一个包含了 30 个普通字段 (64KB 大小) 和 30 个文件字段(64MB 大小) 的请求体的情况。其中的 Content-Length 是请求体的大小,毫秒数代表解析开始到结束的时间差值,内存相关的数据是解析前后的进程内存占用差值。4 个图分别对应了 4 种不同的数据读写场景。

  1. memory-read-no-write: 请求体的数据已在内存中,解析到文件时,文件不写入磁盘。
  2. memory-read-disk-write: 请求体的数据已在内存中,解析到文件时,文件写入磁盘。
  3. disk-read-no-write: 请求体的数据从磁盘读取,解析到文件时,文件不写入磁盘。
  4. disk-read-disk-write: 请求体的数据从磁盘读取,解析到文件时,文件写入磁盘。

其中测试方法 1 可以测出理论上的最大解析速度,可以看到 multipartor 理论上会比 busboy 快 4 倍左右,比 formidable 快 6 倍左右! 但即使是最差的 formidable 也有高达 3GB/s 的理论解析速度 (由 1.88GB/621ms 得出),由于请求体通常从网络读取,解析到的文件也通常需要写入磁盘,所以实际场景中的性能瓶颈往往并不在解析速度上,而在于磁盘的写入速度和服务器的上行带宽。

其中测试方法 4 采用从本地文件读取数据(用文件可读流模拟网络请求的可读流),并将解析到的文件写入本地磁盘,该测试方法最能反应实际网络 IO 中三者的性能差距,可以看到 multipartor 依然有 30% 以上的性能优势。