4

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

 3 years ago
source link: https://juejin.cn/post/6980142557066067982
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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

在日常工作中,文件上传是一个很常见的功能。在项目开发过程中,我们通常都会使用一些成熟的上传组件来实现对应的功能。一般来说,成熟的上传组件不仅会提供漂亮 UI 或好的交互体验,而且还会提供多种不同的上传方式,以满足不同的场景需求。

一般在我们工作中,主要会涉及到 8 种文件上传的场景,每一种场景背后都使用不同的技术,其中也有很多细节需要我们额外注意。今天阿宝哥就来带大家总结一下这 8 种场景,让大家能更好地理解成熟上传组件所提供的功能。阅读本文后,你将会了解以下的内容:

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

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

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

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

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

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

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

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

一、单文件上传

对于单文件上传的场景来说,最常见的是图片上传的场景,所以我们就以图片上传为例,先来介绍单文件上传的基本流程。

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…

二、多文件上传

要上传多个文件,首先我们需要允许用户同时选择多个文件。要实现这个功能,我们可以利用 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…

三、目录上传

可能你还不知道,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) {
    // [email protected] => 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…

四、压缩目录上传

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…

五、拖拽上传

要实现拖拽上传的功能,我们需要先了解与拖拽相关的事件。比如 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 部分来讲解:

1、阻止默认拖拽行为

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();
}
复制代码

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

["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");
}
复制代码

3、处理图片预览

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);
  }
}
复制代码

4、文件上传

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…

六、剪贴板上传

在介绍如何实现剪贴板上传的功能前,我们需要了解一下 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…

七、大文件分块上传

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

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

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

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

github.com/semlinker/f…

八、服务端上传

服务器上传就是把文件从一台服务器上传到另外一台服务器。借助 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…

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

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

十、参考资源


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK