文件上传,搞懂这8种场景就够了

⚠️本文为掘金社区首发签约文章,未获受权禁止转载javascript

在平常工做中,文件上传是一个很常见的功能。在项目开发过程当中,咱们一般都会使用一些成熟的上传组件来实现对应的功能。通常来讲,成熟的上传组件不只会提供漂亮 UI 或好的交互体验,并且还会提供多种不一样的上传方式,以知足不一样的场景需求。css

通常在咱们工做中,主要会涉及到 8 种文件上传的场景,每一种场景背后都使用不一样的技术,其中也有不少细节须要咱们额外注意。今天阿宝哥就来带你们总结一下这 8 种场景,让你们能更好地理解成熟上传组件所提供的功能。阅读本文后,你将会了解如下的内容:html

  • 单文件上传:利用 input 元素的 accept 属性限制上传文件的类型、利用 JS 检测文件的类型及使用 Koa 实现单文件上传的功能;前端

  • 多文件上传:利用 input 元素的 multiple 属性支持选择多文件及使用 Koa 实现多文件上传的功能;java

  • 目录上传:利用 input 元素上的 webkitdirectory 属性支持目录上传的功能及使用 Koa 实现目录上传并按文件目录结构存放的功能;node

  • 压缩目录上传:在目录上传的基础上,利用 JSZip 实现压缩目录上传的功能;ios

  • 拖拽上传:利用拖拽事件和 DataTransfer 对象实现拖拽上传的功能;git

  • 剪贴板上传:利用剪贴板事件和 Clipboard API 实现剪贴板上传的功能;github

  • 大文件分块上传:利用 Blob.sliceSparkMD5 和第三方库 async-pool 实现大文件并发上传的功能;web

  • 服务端上传:利用第三方库 form-data 实现服务端文件流式上传的功能。

1、单文件上传

对于单文件上传的场景来讲,最多见的是图片上传的场景,因此咱们就以图片上传为例,先来介绍单文件上传的基本流程。

1.1 前端代码

html

在如下代码中,咱们经过 input 元素的 accept 属性限制了上传文件的类型。这里使用 image/* 限制只能选择图片文件,固然你也能够设置特定的类型,好比 image/pngimage/png,image/jpeg

<input id="uploadFile" type="file" accept="image/*" />
<button id="submit" onclick="uploadFile()">上传文件</button>
复制代码

须要注意的是,虽然咱们把 input 元素的 accept 属性设置为 image/png。但若是用户把 jpg/jpeg 格式的图片后缀名改成 .png,就能够成功绕过这个限制。要解决这个问题,咱们能够经过读取文件中的二进制数据来识别正确的文件类型。

要查看图片对应的二进制数据,咱们能够借助一些现成的编辑器,好比 Windows 平台下的 WinHex 或 macOS 平台下的 Synalyze It! Pro 十六进制编辑器。这里咱们使用 Synalyze It! Pro 这个编辑器,来查看阿宝哥头像对应的二进制数据。

那么在前端可否不借助工具,读取文件的二进制数据呢?答案是能够的,这里阿宝哥就不展开介绍了。感兴趣的话,你能够阅读 JavaScript 如何检测文件的类型? 这篇文章。另外,须要注意的是 input 元素 accept 属性有存在兼容性问题。好比 IE 9 如下不支持,具体以下图所示:

(图片来源 —— caniuse.com/input-file-…

js

const uploadFileEle = document.querySelector("#uploadFile");

const request = axios.create({
  baseURL: "http://localhost:3000/upload",
  timeout: 60000, 
});

async function uploadFile() {
  if (!uploadFileEle.files.length) return;
  const file = uploadFileEle.files[0]; // 获取单个文件
  // 省略文件的校验过程,好比文件类型、大小校验
  upload({
    url: "/single",
    file,
  });
}

function upload({ url, file, fieldName = "file" }) {
  let formData = new FormData();
  formData.set(fieldName, file);
  request.post(url, formData, {
    // 监听上传进度
    onUploadProgress: function (progressEvent) {
      const percentCompleted = Math.round(
        (progressEvent.loaded * 100) / progressEvent.total
      );
      console.log(percentCompleted);
     },
  });
}
复制代码

在以上代码中,咱们先把读取的 File 对象封装成 FormData 对象,而后利用 Axios 实例的 post 方法实现文件上传的功能。 在上传前,经过设置请求配置对象的 onUploadProgress 属性,就能够获取文件的上传进度。

1.2 服务端代码

Koa 是一个简单易用的 Web 框架,它的特色是优雅、简洁、轻量、自由度高。因此咱们选择它来搭建文件服务,并使用如下中间件来实现相应的功能:

const path = require("path");
const Koa = require("koa");
const serve = require("koa-static");
const cors = require("@koa/cors");
const multer = require("@koa/multer");
const Router = require("@koa/router");

const app = new Koa();
const router = new Router();
const PORT = 3000;
// 上传后资源的URL地址
const RESOURCE_URL = `http://localhost:${PORT}`;
// 存储上传文件的目录
const UPLOAD_DIR = path.join(__dirname, "/public/upload");

const storage = multer.diskStorage({
  destination: async function (req, file, cb) {
    // 设置文件的存储目录
    cb(null, UPLOAD_DIR);
  },
  filename: function (req, file, cb) {
    // 设置文件名
    cb(null, `${file.originalname}`);
  },
});

const multerUpload = multer({ storage });

router.get("/", async (ctx) => {
  ctx.body = "欢迎使用文件服务(by 阿宝哥)";
});

router.post(
  "/upload/single",
  async (ctx, next) => {
    try {
      await next();
      ctx.body = {
        code: 1,
        msg: "文件上传成功",
        url: `${RESOURCE_URL}/${ctx.file.originalname}`,
      };
    } catch (error) {
      ctx.body = {
        code: 0,
        msg: "文件上传失败"
      };
    }
  },
  multerUpload.single("file")
);

// 注册中间件
app.use(cors());
app.use(serve(UPLOAD_DIR));
app.use(router.routes()).use(router.allowedMethods());

app.listen(PORT, () => {
  console.log(`app starting at port ${PORT}`);
});
复制代码

以上代码相对比较简单,咱们就不展开介绍了。Koa 内核很简洁,扩展功能都是经过中间件来实现。好比示例中使用到的路由、CORS、静态资源处理等功能都是经过中间件实现。所以要想掌握 Koa 这个框架,核心是掌握它的中间件机制。若是你想深刻了解的话,能够阅读 如何更好地理解中间件和洋葱模型 这篇文章。其实除了单文件上传外,在文件上传的场景中,咱们也能够同时上传多个文件。

单文件上传示例:single-file-upload

github.com/semlinker/f…

2、多文件上传

要上传多个文件,首先咱们须要容许用户同时选择多个文件。要实现这个功能,咱们能够利用 input 元素的 multiple 属性。跟前面介绍的 accept 属性同样,该属性也存在兼容性问题,具体以下图所示:

(图片来源 —— caniuse.com/mdn-api_htm…

2.1 前端代码

html

相比单文件上传的代码,多文件上传场景下的 input 元素多了一个 multiple 属性:

<input id="uploadFile" type="file" accept="image/*" multiple />
<button id="submit" onclick="uploadFile()">上传文件</button>
复制代码

js

在单文件上传的代码中,咱们经过 uploadFileEle.files[0] 获取单个文件,而对于多文件上传来讲,咱们须要获取已选择的文件列表,即经过 uploadFileEle.files 来获取,它返回的是一个 FileList 对象。

async function uploadFile() {
  if (!uploadFileEle.files.length) return;
  const files = Array.from(uploadFileEle.files);
  upload({
    url: "/multiple",
    files,
  });
}
复制代码

由于要支持上传多个文件,因此咱们须要同步更新一下 upload 函数。对应的处理逻辑就是遍历文件列表,而后使用 FormData 对象的 append 方法来添加多个文件,具体代码以下所示:

function upload({ url, files, fieldName = "file" }) {
  let formData = new FormData();
  files.forEach((file) => {
    formData.append(fieldName, file);
  });
  request.post(url, formData, {
    // 监听上传进度
    onUploadProgress: function (progressEvent) {
      const percentCompleted = Math.round(
        (progressEvent.loaded * 100) / progressEvent.total
      );
      console.log(percentCompleted);
    },
  });
}
复制代码

2.2 服务端代码

在如下代码中,咱们定义了一个新的路由 —— /upload/multiple 来处理多文件上传的功能。当全部文件都成功上传后,就会返回一个已上传文件的 url 地址列表:

router.post(
  "/upload/multiple",
  async (ctx, next) => {
    try {
      await next();
      urls = ctx.files.file.map(file => `${RESOURCE_URL}/${file.originalname}`);
      ctx.body = {
        code: 1,
        msg: "文件上传成功",
        urls
      };
    } catch (error) {
      ctx.body = {
        code: 0,
        msg: "文件上传失败",
      };
    }
  },
  multerUpload.fields([
    {
      name: "file", // 与FormData表单项的fieldName想对应
    },
  ])
);
复制代码

介绍完单文件和多文件上传的功能,接下来咱们来介绍目录上传的功能。

多文件上传示例:multiple-file-upload

github.com/semlinker/f…

3、目录上传

可能你还不知道,input 元素上还有一个的 webkitdirectory 属性。当设置了 webkitdirectory 属性以后,咱们就能够选择目录了。

<input id="uploadFile" type="file" accept="image/*" webkitdirectory />
复制代码

当咱们选择了指定目录以后,好比阿宝哥桌面上的 images 目录,就会显示如下确认框:

点击上传按钮以后,咱们就能够获取文件列表。列表中的文件对象上含有一个 webkitRelativePath 属性,用于表示当前文件的相对路径。

虽然经过 webkitdirectory 属性能够很容易地实现选择目录的功能,但在实际项目中咱们还须要考虑它的兼容性。好比在 IE 11 如下的版本就不支持该属性,其它浏览器的兼容性以下图所示:

(图片来源 —— caniuse.com/?search=web…

了解完 webkitdirectory 属性的兼容性,咱们先来介绍前端的实现代码。

3.1 前端代码

为了让服务端能按照实际的目录结构来存放对应的文件,在添加表单项时咱们须要把当前文件的路径提交到服务端。此外,为了确保@koa/multer 能正确处理文件的路径,咱们须要对路径进行特殊处理。即把 / 斜杠替换为 @ 符号。对应的处理方式以下所示:

function upload({ url, files, fieldName = "file" }) {
  let formData = new FormData();
  files.forEach((file, i) => {
    formData.append(
      fieldName, 
      files[i],
      files[i].webkitRelativePath.replace(/\//g, "@");
    );
  });
  request.post(url, formData); // 省略上传进度处理
}
复制代码

3.2 服务端代码

目录上传与多文件上传,服务端代码的主要区别就是 @koa/multer 中间件的配置对象不同。在 destination 属性对应的函数中,咱们须要把文件名中 @ 还原成 /,而后根据文件的实际路径来生成目录。

const fse = require("fs-extra");
const storage = multer.diskStorage({
  destination: async function (req, file, cb) {
    // images@image-1.jpeg => images/image-1.jpeg
    let relativePath = file.originalname.replace(/@/g, path.sep);
    let index = relativePath.lastIndexOf(path.sep);
    let fileDir = path.join(UPLOAD_DIR, relativePath.substr(0, index));
    // 确保文件目录存在,若不存在的话,会自动建立
    await fse.ensureDir(fileDir); 
    cb(null, fileDir);
  },
  filename: function (req, file, cb) {
    let parts = file.originalname.split("@");
    cb(null, `${parts[parts.length - 1]}`); 
  },
});
复制代码

如今咱们已经实现了目录上传的功能,那么可否把目录下的文件压缩成一个压缩包后再上传呢?答案是能够的,接下来咱们来介绍如何实现压缩目录上传的功能。

目录上传示例:directory-upload

github.com/semlinker/f…

4、压缩目录上传

JavaScript 如何在线解压 ZIP 文件? 这篇文章中,介绍了在浏览器端如何使用 JSZip 这个库实如今线解压 ZIP 文件的功能。 JSZip 这个库除了能够解析 ZIP 文件以外,它还能够用来 建立和编辑 ZIP 文件。利用 JSZip 这个库提供的 API,咱们就能够把目录下的全部文件压缩成 ZIP 文件,而后再把生成的 ZIP 文件上传到服务器。

4.1 前端代码

JSZip 实例上的 file(name, data [,options]) 方法,能够把文件添加到 ZIP 文件中。基于该方法咱们能够封装了一个 generateZipFile 函数,用于把目录下的文件列表压缩成一个 ZIP 文件。如下是 generateZipFile 函数的具体实现:

function generateZipFile( zipName, files, options = { type: "blob", compression: "DEFLATE" } ) {
  return new Promise((resolve, reject) => {
    const zip = new JSZip();
    for (let i = 0; i < files.length; i++) {
      zip.file(files[i].webkitRelativePath, files[i]);
    }
    zip.generateAsync(options).then(function (blob) {
      zipName = zipName || Date.now() + ".zip";
      const zipFile = new File([blob], zipName, {
        type: "application/zip",
      });
      resolve(zipFile);
    });
  });
}
复制代码

在建立完 generateZipFile 函数以后,咱们须要更新一下前面已经介绍过的 uploadFile 函数:

async function uploadFile() {
  let fileList = uploadFileEle.files;
  if (!fileList.length) return;
  let webkitRelativePath = fileList[0].webkitRelativePath;
  let zipFileName = webkitRelativePath.split("/")[0] + ".zip";
  let zipFile = await generateZipFile(zipFileName, fileList);
  upload({
    url: "/single",
    file: zipFile,
    fileName: zipFileName
  });
}
复制代码

在以上的 uploadFile 函数中,咱们会对返回的 FileList 对象进行处理,即调用 generateZipFile 函数来生成 ZIP 文件。此外,为了在服务端接收压缩文件时,能获取到文件名,咱们为 upload 函数增长了一个 fileName 参数,该参数用于调用 formData.append 方法时,设置上传文件的文件名:

function upload({ url, file, fileName, fieldName = "file" }) {
  if (!url || !file) return;
  let formData = new FormData();
  formData.append(
    fieldName, file, fileName
  );
  request.post(url, formData); // 省略上传进度跟踪
}
复制代码

以上就是压缩目录上传,前端部分的 JS 代码,服务端的代码能够参考前面单文件上传的相关代码。

压缩目录上传示例:directory-compress-upload

github.com/semlinker/f…

5、拖拽上传

要实现拖拽上传的功能,咱们须要先了解与拖拽相关的事件。好比 dragdragenddragenterdragoverdrop 事件等。这里咱们只介绍接下来要用到的拖拽事件:

  • dragenter:当拖拽元素或选中的文本到一个可释放目标时触发;
  • dragover:当元素或选中的文本被拖到一个可释放目标上时触发(每100毫秒触发一次);
  • dragleave:当拖拽元素或选中的文本离开一个可释放目标时触发;
  • drop:当元素或选中的文本在可释放目标上被释放时触发。

基于上面的这些事件,咱们就能够提升用户拖拽的体验。好比当用户拖拽的元素进入目标区域时,对目标区域进行高亮显示。当用户拖拽的元素离开目标区域时,移除高亮显示。很明显当 drop 事件触发后,拖拽的元素已经放入目标区域了,这时咱们就须要获取对应的数据。

那么如何获取拖拽对应的数据呢?这时咱们须要使用 DataTransfer 对象,该对象用于保存拖动并放下过程当中的数据。它能够保存一项或多项数据,这些数据项能够是一种或者多种数据类型。若拖动操做涉及拖动文件,则咱们能够经过 DataTransfer 对象的 files 属性来获取文件列表。

介绍完拖拽上传相关的知识后,咱们来看一下具体如何实现拖拽上传的功能。

5.1 前端代码

html

<div id="dropArea">
   <p>拖拽上传文件</p>
   <div id="imagePreview"></div>
</div>
复制代码

css

#dropArea {
  width: 300px;
  height: 300px;
  border: 1px dashed gray;
  margin-bottom: 20px;
}
#dropArea p {
  text-align: center;
  color: #999;
}
#dropArea.highlighted {
  background-color: #ddd;
}
#imagePreview {
  max-height: 250px;
  overflow-y: scroll;
}
#imagePreview img {
  width: 100%;
  display: block;
  margin: auto;
}
复制代码

js

为了让你们可以更好地阅读拖拽上传的相关代码,咱们把代码拆成 4 部分来说解:

一、阻止默认拖拽行为

const dropAreaEle = document.querySelector("#dropArea");
const imgPreviewEle = document.querySelector("#imagePreview");
const IMAGE_MIME_REGEX = /^image\/(jpe?g|gif|png)$/i;

["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
   dropAreaEle.addEventListener(eventName, preventDefaults, false);
   document.body.addEventListener(eventName, preventDefaults, false);
});

function preventDefaults(e) {
  e.preventDefault();
  e.stopPropagation();
}
复制代码

二、切换目标区域的高亮状态

["dragenter", "dragover"].forEach((eventName) => {
    dropAreaEle.addEventListener(eventName, highlight, false);
});
["dragleave", "drop"].forEach((eventName) => {
    dropAreaEle.addEventListener(eventName, unhighlight, false);
});

// 添加高亮样式
function highlight(e) {
  dropAreaEle.classList.add("highlighted");
}

// 移除高亮样式
function unhighlight(e) {
  dropAreaEle.classList.remove("highlighted");
}
复制代码

三、处理图片预览

dropAreaEle.addEventListener("drop", handleDrop, false);

function handleDrop(e) {
  const dt = e.dataTransfer;
  const files = [...dt.files];
  files.forEach((file) => {
    previewImage(file, imgPreviewEle);
  });
  // 省略文件上传代码
}

function previewImage(file, container) {
  if (IMAGE_MIME_REGEX.test(file.type)) {
    const reader = new FileReader();
    reader.onload = function (e) {
      let img = document.createElement("img");
      img.src = e.target.result;
      container.append(img);
    };
    reader.readAsDataURL(file);
  }
}
复制代码

四、文件上传

function handleDrop(e) {
  const dt = e.dataTransfer;
  const files = [...dt.files];
  // 省略图片预览代码
  files.forEach((file) => {
    upload({
      url: "/single",
      file,
    });
  });
}

const request = axios.create({
  baseURL: "http://localhost:3000/upload",
  timeout: 60000,
});

function upload({ url, file, fieldName = "file" }) {
  let formData = new FormData();
  formData.set(fieldName, file);
  request.post(url, formData, {
    // 监听上传进度
    onUploadProgress: function (progressEvent) {
      const percentCompleted = Math.round(
        (progressEvent.loaded * 100) / progressEvent.total
      );
      console.log(percentCompleted);
    },
  });
}
复制代码

拖拽上传算是一个比较常见的场景,不少成熟的上传组件都支持该功能。其实除了拖拽上传外,还能够利用剪贴板实现复制上传的功能。

拖拽上传示例:drag-drop-upload

github.com/semlinker/f…

6、剪贴板上传

在介绍如何实现剪贴板上传的功能前,咱们须要了解一下 Clipboard API。Clipboard 接口实现了 Clipboard API,若是用户授予了相应的权限,就能提供系统剪贴板的读写访问。在 Web 应用程序中,Clipboard API 可用于实现剪切、复制和粘贴功能。该 API 用于取代经过 document.execCommand API 来实现剪贴板的操做。

在实际项目中,咱们不须要手动建立 Clipboard 对象,而是经过 navigator.clipboard 来获取 Clipboard 对象:

在获取 Clipboard 对象以后,咱们就能够利用该对象提供的 API 来访问剪贴板,好比:

navigator.clipboard.readText().then(
  clipText => document.querySelector(".editor").innerText = clipText
);
复制代码

以上代码将 HTML 中含有 .editor 类的第一个元素的内容替换为剪贴板的内容。若是剪贴板为空,或者不包含任何文本,则元素的内容将被清空。这是由于在剪贴板为空或者不包含文本时,readText 方法会返回一个空字符串。

利用 Clipboard API 咱们能够很方便地操做剪贴板,但实际项目使用过程当中也得考虑它的兼容性:

(图片来源 —— caniuse.com/async-clipb…

要实现剪贴板上传的功能,能够分为如下 3 个步骤:

  • 监听容器的粘贴事件;
  • 读取并解析剪贴板中的内容;
  • 动态构建 FormData 对象并上传。

了解完上述步骤,接下来咱们来分析一下具体实现的代码。

6.1 前端代码

html

<div id="uploadArea">
   <p>请先复制图片后再执行粘贴操做</p>
</div>
复制代码

css

#uploadArea {
   width: 400px;
   height: 400px;
   border: 1px dashed gray;
   display: table-cell;
   vertical-align: middle;
}
#uploadArea p {
   text-align: center;
   color: #999;
}
#uploadArea img {
   max-width: 100%;
   max-height: 100%;
   display: block;
   margin: auto;
}
复制代码

js

在如下代码中,咱们使用 addEventListener 方法为 uploadArea 容器添加 paste 事件。在对应的事件处理函数中,咱们会优先判断当前浏览器是否支持异步 Clipboard API。若是支持的话,就会经过 navigator.clipboard.read 方法来读取剪贴板中的内容。在读取内容以后,咱们会经过正则判断剪贴板项中是否包含图片资源,若是有的话会调用 previewImage 方法执行图片预览操做并把返回的 blob 对象保存起来,用于后续的上传操做。

const IMAGE_MIME_REGEX = /^image\/(jpe?g|gif|png)$/i;
const uploadAreaEle = document.querySelector("#uploadArea");

uploadAreaEle.addEventListener("paste", async (e) => {
  e.preventDefault();
  const files = [];
  if (navigator.clipboard) {
    let clipboardItems = await navigator.clipboard.read();
    for (const clipboardItem of clipboardItems) {
      for (const type of clipboardItem.types) {
        if (IMAGE_MIME_REGEX.test(type)) {
           const blob = await clipboardItem.getType(type);
           insertImage(blob, uploadAreaEle);
           files.push(blob);
         }
       }
     }
  } else {
      const items = e.clipboardData.items;
      for (let i = 0; i < items.length; i++) {
        if (IMAGE_MIME_REGEX.test(items[i].type)) {
          let file = items[i].getAsFile();
          insertImage(file, uploadAreaEle);
          files.push(file);
        }
      }
  }
  if (files.length > 0) {
    confirm("剪贴板检测到图片文件,是否执行上传操做?") 
      && upload({
           url: "/multiple",
           files,
         });
   }
});
复制代码

若当前浏览器不支持异步 Clipboard API,则咱们会尝试经过 e.clipboardData.items 来访问剪贴板中的内容。须要注意的是,在遍历剪贴板内容项的时候,咱们是经过 getAsFile 方法来获取剪贴板的内容。固然该方法也存在兼容性问题,具体以下图所示:

(图片来源 —— caniuse.com/mdn-api_dat…

前面已经提到,当从剪贴板解析到图片资源时,会让用户进行预览,该功能是基于 FileReader API 来实现的,对应的代码以下所示:

function previewImage(file, container) {
  const reader = new FileReader();
  reader.onload = function (e) {
    let img = document.createElement("img");
    img.src = e.target.result;
    container.append(img);
  };
  reader.readAsDataURL(file);
}
复制代码

当用户预览完成后,若是确认上传咱们就会执行文件的上传操做。由于文件是从剪贴板中读取的,因此在上传前咱们会根据文件的类型,自动为它生成一个文件名,具体是采用时间戳加文件后缀的形式:

function upload({ url, files, fieldName = "file" }) {
  let formData = new FormData();
  files.forEach((file) => {
    let fileName = +new Date() + "." + IMAGE_MIME_REGEX.exec(file.type)[1];
    formData.append(fieldName, file, fileName);
  });
  request.post(url, formData);
}
复制代码

前面咱们已经介绍了文件上传的多种不一样场景,接下来咱们来介绍一个 “特殊” 的场景 —— 大文件上传

剪贴板上传示例:clipboard-upload

github.com/semlinker/f…

7、大文件分块上传

相信你可能已经了解大文件上传的解决方案,在上传大文件时,为了提升上传的效率,咱们通常会使用 Blob.slice 方法对大文件按照指定的大小进行切割,而后经过多线程进行分块上传,等全部分块都成功上传后,再通知服务端进行分块合并。具体处理方案以下图所示:

由于在 JavaScript 中如何实现大文件并发上传? 这篇文章中,阿宝哥已经详细介绍了大文件并发上传的方案,因此这里就不展开介绍了。咱们只回顾一下大文件并发上传的完整流程:

前面咱们都是介绍客户端文件上传的场景,其实也有服务端文件上传的场景。好比在服务端动态生成海报后,上传到另一台服务器或云厂商的 OSS(Object Storage Service)。下面咱们就以 Node.js 为例来介绍在服务端如何上传文件。

大文件分块上传示例:big-file-upload

github.com/semlinker/f…

8、服务端上传

服务器上传就是把文件从一台服务器上传到另一台服务器。借助 Github 上 form-data 这个库提供的功能,咱们能够很容易地实现服务器上传的功能。下面咱们来简单介绍一下单文件和多文件上传的功能:

8.1 单文件上传

const fs = require("fs");
const path = require("path");
const FormData = require("form-data");

const form1 = new FormData();
form1.append("file", fs.createReadStream(path.join(__dirname, "images/image-1.jpeg")));
form1.submit("http://localhost:3000/upload/single", (error, response) => {
  if(error) {
    console.log("单图上传失败");
    return;
  }
  console.log("单图上传成功");
});
复制代码

8.2 多文件上传

const form2 = new FormData();
form2.append("file", fs.createReadStream(path.join(__dirname, "images/image-2.jpeg")));
form2.append("file", fs.createReadStream(path.join(__dirname, "images/image-3.jpeg")));
form2.submit("http://localhost:3000/upload/multiple", (error, response) => {
  if(error) {
    console.log("多图上传失败");
    return;
  }
  console.log("多图上传成功");
});
复制代码

观察以上代码可知,建立完 FormData 对象以后,咱们只须要经过 fs.createReadStream API 建立可读流,而后调用 FormData 对象的 append 方法添加表单项,最后再调用 submit 方法执行提交操做便可。

其实除了 ReadableStream 以外,FormData 对象的 append 方法还支持如下类型:

const FormData = require('form-data');
const http = require('http');

const form = new FormData();
http.request('http://nodejs.org/images/logo.png', function(response) {
  form.append('my_field', 'my value');
  form.append('my_buffer', new Buffer(10));
  form.append('my_logo', response);
});
复制代码

服务端文件上传的内容就介绍到这里,关于 form-data 这个库的其余用法,感兴趣的话,能够阅读对应的使用文档。其实除了以上介绍的八种场景外,在平常工做中,你也可能会使用一些同步工具,好比 Syncthing 文件同步工具实现文件传输。好的,本文的全部内容都已经介绍完了,最后咱们来作一个总结。

服务端上传示例:server-upload

github.com/semlinker/f…

9、总结

本文阿宝哥详细介绍了文件上传的八种场景,但愿阅读完本文后,你对八种场景背后使用的技术有必定的了解。因为篇幅有限,阿宝哥就没有展开介绍与 multipart/form-data 类型相关的内容,感兴趣的小伙伴能够自行了解一下。

此外,在实际项目中,你能够考虑直接使用成熟的第三方组件,好比 Github 上的 Star 数 11K+filepond。该组件采用插件化的架构,以插件的方式,提供了很是多的功能,好比 File encodeFile renameFile posterImage previewImage crop 等。总之,它是一个很不错的组件,之后有机会的话,你们能够尝试一下。

10、参考资源

相关文章
相关标签/搜索