文件上传的本质和多种实现方式

文件上传是前端业务中一个比较重要的场景,有各种不同的实现方式,比如,单个文件上传,多个文件上传,文件夹上传,裁剪上传,大文件上传,拖拽上传等等。但是文件上传的核心逻辑很好理解,就是客户端将文件数据上传到服务端。而客户端这边需要做的就是样式,交互逻辑,以及消息格式,传输方式。

不管有多少种上传方式,最核心的就是,发送一个请求,把文件数据传递过去。接下来通过原生js方式实现几种文件上传,以便理解文件上传的本质

点击上传单个文件

静态页面

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <style>
    .wrapper {
      width: 200px;
    }

    .container {
      position: relative;
      margin-bottom: 10px;
      height: 200px;
      border-radius: 4px;
      border: 2px solid #a0a0a0;
      cursor: pointer;
    }

    .container .upload {
      opacity: 0;
    }

    .container img {
      position: absolute;
      width: 30%;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
    }

    .container img.preview {
      opacity: 0.5;
      width: 90%;
    }

    .container label {
      margin-left: 8px;
      width: 90%;
      align-items: center;
      position: absolute;
      bottom: 5px;
    }

    .container label progress {
      margin-right: 10px;
    }

    .container.select .select {
      display: block;
    }

    .container.select .preview {
      display: none;
    }

    .container.preview .preview {
      display: flex;
    }

    .container.preview .select {
      display: none;
    }

    .container.result img.preview {
      display: flex;
    }

    .container.result img.preview {
      opacity: 1;
    }

    .container.result .select,
    label {
      display: none;
    }

    .operate {
      display: flex;
      justify-content: space-evenly;
    }
  </style>

  <body>
    <div class="wrapper">
      <div class="container select">
        <input type="file" class="upload" />
        <img src="./upload.png" alt="select" class="select" />
        <img src="" alt="preview" class="preview" />
        <label for="file" class="preview">
          <progress id="file" max="100" value="0"></progress>
          <span>0</span>%
        </label>
      </div>
      <div class="operate">
        <button class="cancel">取消上传</button>
      </div>
    </div>
  </body>
  <script src="./upload.js"></script>
</html>

前端逻辑

使用 <input type='file' />,可以选择选择一个或多个文件通过表单方式 上传;

在上传过程中,可以使用 FileReader.readAsDataURL() 读取出data:URL 格式的字符串(base64 编码)表示的文件内容,显示预览图片;

在上传过程中,通过事件xhr.upload.onprogress 来获取上传进度;

通过 FormData设置编码类型被设为 "multipart/form-data"的消息格式

js 复制代码
const containerEl = document.querySelector(".container");
const inputEl = document.querySelector(".upload");
const cancelEl = document.querySelector(".cancel");
const imgEl = document.querySelector("img.preview");
const progressEl = document.querySelector("label.preview progress");
const progressTextEl = document.querySelector("label.preview span");

let cancelUpload = null;

// 点击容器弹出文件选择框
containerEl.onclick = function () {
  inputEl.click();
};

inputEl.onchange = function (e) {
  if (!this.files.length) {
    return;
  }
  const file = this.files[0];
  if (!validateFile(file)) {
    return;
  }

  // 读取文件数据预览
  const reader = new FileReader();
  reader.readAsDataURL(file);
  reader.onload = function (e) {
    showArea("preview");
    // e.target.result -> data:URL 格式的字符串(base64 编码)
    imgEl.src = e.target.result;
  };

  //调接口上传
  cancelUpload = upload(
    file,
    (res) => {
      showArea("result");
    },
    (value) => {
      setProgress(value);
    }
  );
};

//取消上传
cancelEl.onclick = () => {
  cancelUpload?.();
  showArea("select");
  setProgress(0);
  inputEl.value = null;
};

// 校验文件大小和类型
function validateFile(file, size, exts) {
  const sizeLimit = size || 2 * 1024 * 1024;
  const legalExts = exts || [".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif"];
  const fileName = file.name.toLowerCase();

  if (!legalExts.some((ext) => fileName.endsWith(ext))) {
    alert("文件类型不正确");
    return false;
  }

  if (file.size > sizeLimit) {
    alert("文件尺寸超过限制");
    return false;
  }

  return true;
}

function showArea(selector) {
  containerEl.className = `container ${selector}`;
}

function setProgress(value) {
  progressEl.value = value;
  progressTextEl.innerText = value;
  if (value === 100) {
    cancelEl.innerText = "返回";
  } else {
    cancelEl.innerText = "取消上传";
  }
}

//上传文件
function upload(file, onFinish, onProgress) {
  const xhr = new XMLHttpRequest();
  xhr.open("POST", "http://localhost:2024/upload/single");
  xhr.onload = () => {
    const resp = JSON.parse(xhr.responseText);
    onFinish(resp);
  };
  xhr.upload.onprogress = (e) => {
    const value = Math.floor((e.loaded / e.total) * 100);
    onProgress(value);
  };
  const form = new FormData();
  form.append("avatar", file);
  xhr.send(form);
  return () => xhr.abort();
}

服务端逻辑

使用 koa 搭建一个简单的文件上传服务器

js 复制代码
const path = require("path");
const Koa = require("koa");
const KoaRouter = require("@koa/router");
const multer = require("@koa/multer");
const cors = require("@koa/cors");

const FileDir = path.resolve(__dirname, "./images");

const upload = multer({
  storage: multer.diskStorage({
    // 文件存储的文件夹
    destination(req, file, cb) {
      cb(null, FileDir);
    },
    // 文件名
    filename(req, file, cb) {
      cb(null, file.originalname);
    },
  }),
});

const router = new KoaRouter({ prefix: "/upload" });

// upload.single 处理单个上传文件
router.post("/single", upload.single("avatar"), (ctx, next) => {
  console.log("ctx:", ctx);
  ctx.body = {
    code: 200,
    msg: "文件上传成功",
  };
});

const app = new Koa();
app.use(cors()); //解决跨域
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(2024, () => console.log("Koa文件服务器启动"));

拖拽上传单个文件

<input type='file' />本身就可以支持文件的拖拽上传,但是为了兼容性,最好是使外面的容器div变成一个拖动目标,在监听这个容器拖动事件上要禁止默认行为

静态页面

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <style>
    .wrapper {
      width: 200px;
    }

    .container {
      position: relative;
      margin-bottom: 10px;
      height: 200px;
      border-radius: 4px;
      border: 2px solid #a0a0a0;
      cursor: pointer;
    }

    .container .upload {
      display: none;
    }

    .container img {
      position: absolute;
      width: 30%;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
    }

    .container img.preview {
      opacity: 0.5;
      width: 90%;
    }

    .container label {
      margin-left: 8px;
      width: 90%;
      align-items: center;
      position: absolute;
      bottom: 5px;
    }

    .container label progress {
      margin-right: 10px;
    }

    .container.select .select {
      display: block;
    }

    .container.select .preview {
      display: none;
    }

    .container.preview .preview {
      display: flex;
    }

    .container.preview .select {
      display: none;
    }

    .container.result img.preview {
      display: flex;
    }

    .container.result img.preview {
      opacity: 1;
    }

    .container.result .select,
    label {
      display: none;
    }

    .operate {
      display: flex;
      justify-content: space-evenly;
    }
  </style>

  <body>
    <div class="wrapper">
      <div class="container select">
        <input type="file" class="upload" />
        <img src="./upload.png" alt="select" class="select" />
        <img src="" alt="preview" class="preview" />
        <label for="file" class="preview">
          <progress id="file" max="100" value="0"></progress>
          <span>0</span>%
        </label>
      </div>
      <div class="operate">
        <button class="cancel">取消上传</button>
      </div>
    </div>
  </body>
  <script src="./拖拽上传文件.js"></script>
</html>

前端逻辑

js 复制代码
const containerEl = document.querySelector(".container");
const inputEl = document.querySelector(".upload");
const cancelEl = document.querySelector(".cancel");
const imgEl = document.querySelector("img.preview");
const progressEl = document.querySelector("label.preview progress");
const progressTextEl = document.querySelector("label.preview span");

let cancelUpload = null;

// 点击容器弹出文件选择框
containerEl.onclick = function () {
  inputEl.click();
};

// 支持点击上传
inputEl.onchange = filesChangeHandler;

// e.preventDefault() 阻止默认行为
containerEl.ondragenter = function (e) {
  e.preventDefault();
};

containerEl.ondragover = function (e) {
  e.preventDefault();
};

containerEl.ondragleave = function (e) {
  e.preventDefault();
};

// 支持拖拽上传
containerEl.ondrop = function (e) {
  e.preventDefault();
  inputEl.files = e.dataTransfer.files;
  filesChangeHandler();
};

function filesChangeHandler() {
  if (!inputEl.files.length) {
    return;
  }
  const file = inputEl.files[0];
  if (!validateFile(file)) {
    return;
  }

  // 读取文件数据预览
  const reader = new FileReader();
  reader.readAsDataURL(file);
  reader.onload = function (e) {
    showArea("preview");
    // e.target.result -> data:URL 格式的字符串(base64 编码)
    imgEl.src = e.target.result;
  };

  //调接口上传
  cancelUpload = upload(
    file,
    (res) => {
      showArea("result");
    },
    (value) => {
      setProgress(value);
    }
  );
}

//取消上传
cancelEl.onclick = () => {
  cancelUpload?.();
  showArea("select");
  setProgress(0);
  inputEl.value = null;
};

// 校验文件大小和类型
function validateFile(file, size, exts) {
  const sizeLimit = size || 2 * 1024 * 1024;
  const legalExts = exts || [".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif"];
  const fileName = file.name.toLowerCase();

  if (!legalExts.some((ext) => fileName.endsWith(ext))) {
    alert("文件类型不正确");
    return false;
  }

  if (file.size > sizeLimit) {
    alert("文件尺寸超过限制");
    return false;
  }

  return true;
}

function showArea(selector) {
  containerEl.className = `container ${selector}`;
}

function setProgress(value) {
  progressEl.value = value;
  progressTextEl.innerText = value;
  if (value === 100) {
    cancelEl.innerText = "返回";
  } else {
    cancelEl.innerText = "取消上传";
  }
}

//上传文件
function upload(file, onFinish, onProgress) {
  const xhr = new XMLHttpRequest();
  xhr.open("POST", "http://localhost:2024/upload/single");
  xhr.onload = () => {
    const resp = JSON.parse(xhr.responseText);
    onFinish(resp);
  };
  xhr.upload.onprogress = (e) => {
    const value = Math.floor((e.loaded / e.total) * 100);
    onProgress(value);
  };
  const form = new FormData();
  form.append("avatar", file);
  xhr.send(form);
  return () => xhr.abort();
}

服务端逻辑

同上

上传base64格式

静态页面

同上,可点击,可拖拽

前端逻辑

js 复制代码
const containerEl = document.querySelector(".container");
const inputEl = document.querySelector(".upload");
const cancelEl = document.querySelector(".cancel");
const imgEl = document.querySelector("img.preview");
const progressEl = document.querySelector("label.preview progress");
const progressTextEl = document.querySelector("label.preview span");

let cancelUpload = null;

// 点击容器弹出文件选择框
containerEl.onclick = function () {
  inputEl.click();
};

inputEl.onchange = filesChangeHandler;

// e.preventDefault() 阻止默认行为
containerEl.ondragenter = function (e) {
  e.preventDefault();
};

containerEl.ondragover = function (e) {
  e.preventDefault();
};

containerEl.ondragleave = function (e) {
  e.preventDefault();
};

containerEl.ondrop = function (e) {
  e.preventDefault();
  inputEl.files = e.dataTransfer.files;
  filesChangeHandler();
};

function filesChangeHandler() {
  if (!inputEl.files.length) {
    return;
  }
  const file = inputEl.files[0];
  if (!validateFile(file)) {
    return;
  }
  // 读取文件数据预览
  const reader = new FileReader();
  reader.readAsDataURL(file);
  reader.onload = function (e) {
    showArea("preview");
     // e.target.result -> data:URL 格式的字符串(base64 编码)
    imgEl.src = e.target.result;
    //调接口上传
    cancelUpload = uploadBase64(
      { name: file.name, base64: e.target.result.split(",")[1] },
      (res) => {
        showArea("result");
      },
      (value) => {
        setProgress(value);
      }
    );
  };
}

//取消上传
cancelEl.onclick = () => {
  cancelUpload?.();
  showArea("select");
  setProgress(0);
  inputEl.value = null;
};

// 校验文件大小和类型
function validateFile(file, size, exts) {
  const sizeLimit = size || 2 * 1024 * 1024;
  const legalExts = exts || [".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif"];
  const fileName = file.name.toLowerCase();
  if (!legalExts.some((ext) => fileName.endsWith(ext))) {
    alert("文件类型不正确");
    return false;
  }
  if (file.size > sizeLimit) {
    alert("文件尺寸超过限制");
    return false;
  }
  return true;
}

function showArea(selector) {
  containerEl.className = `container ${selector}`;
}

function setProgress(value) {
  progressEl.value = value;
  progressTextEl.innerText = value;
  if (value === 100) {
    cancelEl.innerText = "返回";
  } else {
    cancelEl.innerText = "取消上传";
  }
}

//上传文件
function uploadBase64(file, onFinish, onProgress) {
  const xhr = new XMLHttpRequest();
  xhr.open("POST", "http://localhost:2024/upload/base64");
  xhr.onload = () => {
    const resp = JSON.parse(xhr.responseText);
    onFinish(resp);
  };
  xhr.upload.onprogress = (e) => {
    const value = Math.floor((e.loaded / e.total) * 100);
    onProgress(value);
  };
  xhr.setRequestHeader("content-type", "application/json");
  xhr.send(JSON.stringify(file));
  return () => xhr.abort();
}

服务端逻辑

js 复制代码
const path = require("path");
const fs = require("fs");
const Koa = require("koa");
const KoaRouter = require("@koa/router");
const cors = require("@koa/cors");
const bodyParser = require("koa-bodyparser");
const static = require("koa-static");

const fileDir = path.resolve(__dirname, "./upload");

const router = new KoaRouter({ prefix: "/upload" });

router.post("/base64", async (ctx) => {
  const { name, base64 } = ctx.request.body;
  const buffer = Buffer.from(base64, "base64");
  await fs.writeFile(`${fileDir}/${name}`, buffer, null, () => {});
  ctx.body = {
    code: 200,
    msg: "文件上传成功",
    url: `http://localhost:2024/${name}`,
  };
});

const app = new Koa();
app.use(cors()); //解决跨域
app.use(static(fileDir)); //访问静态资源
app.use(bodyParser());
app.use(router.routes()).use(router.allowedMethods());
app.listen(2024, () => console.log("Koa文件服务器启动"));

上传二进制文件

静态页面

同上,可点击,可拖拽

前端逻辑

js 复制代码
const containerEl = document.querySelector(".container");
const inputEl = document.querySelector(".upload");
const cancelEl = document.querySelector(".cancel");
const imgEl = document.querySelector("img.preview");
const progressEl = document.querySelector("label.preview progress");
const progressTextEl = document.querySelector("label.preview span");
let cancelUpload = null;

// 点击容器弹出文件选择框
containerEl.onclick = function () {
  inputEl.click();
};

inputEl.onchange = filesChangeHandler;

// e.preventDefault() 阻止默认行为
containerEl.ondragenter = function (e) {
  e.preventDefault();
};

containerEl.ondragover = function (e) {
  e.preventDefault();
};

containerEl.ondragleave = function (e) {
  e.preventDefault();
};

containerEl.ondrop = function (e) {
  e.preventDefault();
  inputEl.files = e.dataTransfer.files;
  filesChangeHandler();
};

function filesChangeHandler() {
  if (!inputEl.files.length) {
    return;
  }
  const file = inputEl.files[0];
  if (!validateFile(file)) {
    return;
  }

  // 读取文件数据预览
  const reader = new FileReader();
  reader.readAsDataURL(file);
  reader.onload = function (e) {
    showArea("preview");
    // e.target.result -> data:URL 格式的字符串(base64 编码)
    imgEl.src = e.target.result;
  };

  //调接口上传
  cancelUpload = uploadBinary(
    file,
    (res) => {
      showArea("result");
    },
    (value) => {
      setProgress(value);
    }
  );
}

//取消上传
cancelEl.onclick = () => {
  cancelUpload?.();
  showArea("select");
  setProgress(0);
  inputEl.value = null;
};

// 校验文件大小和类型
function validateFile(file, size, exts) {
  const sizeLimit = size || 2 * 1024 * 1024;
  const legalExts = exts || [".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif"];
  const fileName = file.name.toLowerCase();
  if (!legalExts.some((ext) => fileName.endsWith(ext))) {
    alert("文件类型不正确");
    return false;
  }
  if (file.size > sizeLimit) {
    alert("文件尺寸超过限制");
    return false;
  }
  return true;
}

function showArea(selector) {
  containerEl.className = `container ${selector}`;
}

function setProgress(value) {
  progressEl.value = value;
  progressTextEl.innerText = value;
  if (value === 100) {
    cancelEl.innerText = "返回";
  } else {
    cancelEl.innerText = "取消上传";
  }
}

//上传文件
function uploadBinary(file, onFinish, onProgress) {
  const xhr = new XMLHttpRequest();
  xhr.open("POST", "http://localhost:2024/upload/binary");
  xhr.onload = () => {
    const resp = JSON.parse(xhr.responseText);
    onFinish(resp);
  };
  xhr.upload.onprogress = (e) => {
    const value = Math.floor((e.loaded / e.total) * 100);
    onProgress(value);
  };
  const form = new FormData();
  form.append("files", file);
  xhr.send(form);
  return () => xhr.abort();
}

服务端逻辑

js 复制代码
const path = require("path");
const fs = require("fs");
const Koa = require("koa");
const KoaRouter = require("@koa/router");
const cors = require("@koa/cors");
const static = require("koa-static");
const { koaBody } = require("koa-body");

const fileDir = path.resolve(__dirname, "./upload");

const router = new KoaRouter({ prefix: "/upload" });

router.post(
  "/binary",
  koaBody({
    multipart: true,
    formidable: {
      keepExtensions: true,
      maxFieldsSize: 20 * 1024 * 1024, //文件大小
      uploadDir: fileDir, //文件夹
    },
  }),
  async (ctx) => {
    ctx.body = {
      code: 200,
      msg: "文件上传成功",
      url: `http://localhost:2024/${ctx.request.files.files.newFilename}`,
    };
  }
);

const app = new Koa();
app.use(cors()); //解决跨域
app.use(static(fileDir)); //访问静态资源
app.use(router.routes()).use(router.allowedMethods());
app.listen(2024, () => console.log("Koa文件服务器启动"));

上传多个文件

静态页面

同上,给input标签加上 multiple

js 复制代码
<input type="file" multiple />

前端逻辑

同样是可以点击,可以拖拽,拿到input的change事件里面的e.target.files,一个伪数组

js 复制代码
const containerEl = document.querySelector(".container");
const inputEl = document.querySelector(".upload");
const cancelEl = document.querySelector(".cancel");
const imgEl = document.querySelector("img.preview");
const progressEl = document.querySelector("label.preview progress");
const progressTextEl = document.querySelector("label.preview span");

let cancelUpload = null;

// 点击容器弹出文件选择框
containerEl.onclick = function () {
  inputEl.click();
};

inputEl.onchange = filesChangeHandler;

//拖拽 e.preventDefault() 阻止默认行为
containerEl.ondragenter = function (e) {
  e.preventDefault();
};

containerEl.ondragover = function (e) {
  e.preventDefault();
};

containerEl.ondragleave = function (e) {
  e.preventDefault();
};

containerEl.ondrop = function (e) {
  e.preventDefault();
  // 拿到拖拽的文件
  inputEl.files = e.dataTransfer.files;
  filesChangeHandler();
};

function filesChangeHandler() {
  if (!inputEl.files.length) {
    return;
  }
  // 暂且只预览第一张
  const file = inputEl.files[0];
  // 读取文件数据预览
  const reader = new FileReader();
  reader.readAsDataURL(file);
  reader.onload = function (e) {
    showArea("preview");
    imgEl.src = e.target.result;
  };

  //调接口上传
  cancelUpload = uploadMultiple(
    inputEl.files,
    (res) => {
      showArea("result");
    },
    (value) => {
      setProgress(value);
    }
  );
}

//取消上传
cancelEl.onclick = () => {
  cancelUpload?.();
  showArea("select");
  setProgress(0);
  inputEl.value = null;
};

function showArea(selector) {
  containerEl.className = `container ${selector}`;
}

function setProgress(value) {
  progressEl.value = value;
  progressTextEl.innerText = value;
  if (value === 100) {
    cancelEl.innerText = "返回";
  } else {
    cancelEl.innerText = "取消上传";
  }
}

//上传文件
function uploadMultiple(files, onFinish, onProgress) {
  const xhr = new XMLHttpRequest();
  xhr.open("POST", "http://localhost:2024/upload/multiple");
  xhr.onload = () => {
    const resp = JSON.parse(xhr.responseText);
    onFinish(resp);
  };
  xhr.upload.onprogress = (e) => {
    const value = Math.floor((e.loaded / e.total) * 100);
    onProgress(value);
  };
  const form = new FormData();
  // 用fromData一起打包传送
  Array.from(files).forEach((file) => form.append("files", file));
  xhr.send(form);
  return () => xhr.abort();
}

服务端逻辑

js 复制代码
const path = require("path");
const Koa = require("koa");
const KoaRouter = require("@koa/router");
const multer = require("@koa/multer");
const cors = require("@koa/cors");

const fileDir = path.resolve(__dirname, "./upload");

const upload = multer({
  storage: multer.diskStorage({
    destination(req, file, cb) {
      cb(null, fileDir);
    },
    filename(req, file, cb) {
      cb(null, file.originalname);
    },
  }),
});

const router = new KoaRouter({ prefix: "/upload" });
// upload.array 接收多个文件
router.post("/multiple", upload.array("files"), (ctx) => {
  ctx.body = {
    code: 200,
    msg: "文件上传成功",
  };
});

const app = new Koa();
app.use(cors()); //解决跨域
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(2024, () => console.log("Koa文件服务器启动"));

上传文件夹

静态页面

同上,给input标签加上 webkitdirectory

js 复制代码
<input type="file" multiple webkitdirectory/>

前端逻辑

文件夹可以点击,在input的change事件中拿到files上传

js 复制代码
// 拖拽文件夹
containerEl.ondrop = function (e) {
  e.preventDefault();
  for (const item of e.dataTransfer.items) {
    // 判断是文件还是文件夹
    const entry = item.webkitGetAsEntry();
    if (entry.isDirectory) {
      // 目录
      const reader = entry.createReader();
      reader.readEntries(
        (en) => {
          console.log("entry:", en);
          // en 还是一个entry对象,递归遍历
        },
        (e) => console.log(e)
      );
    } else {
      // 文件
      entry.file((file) => {
        console.log("file:", file);
        // 收集file,放到数组中,最后还是调用 uploadMultiple
      });
    }
  }

服务端逻辑

同上 ,多文件上传

文件裁剪上传

要解决两个问题,预览图片和发送部分图片。 预览图片依然通过 FileReader读出文件的base64编码。 接下来就是获取裁剪部分的文件数据:

js 复制代码
// 获取裁剪的文件数据
function getCroppedFile(cutInfo) {
  // 准备裁剪的数据
  const cutData = cutInfo || {
    //原图中的裁剪的坐标
    x: 100,
    y: 100,
    //原图中要裁剪的宽高
    cutWidth: 300,
    cutHeight: 300,
    // 裁剪后要缩放的尺寸
    width: 100,
    height: 200,
  };

  const imgEl = document.querySelector("img.preview");
  const canvas = document.createElement("canvas");
  canvas.width = cutData.width;
  canvas.height = cutData.height;
  const ctx = canvas.getContext("2d");
  ctx.drawImage(
    //要绘制到上下文的元素
    imgEl,
    //需要绘制到目标上下文中的,image 的矩形(裁剪)选择框的左上角 X 轴坐标
    cutData.x,
    cutData.y,
    //需要绘制到目标上下文中的,image 的矩形(裁剪)选择框的宽度
    cutData.cutWidth,
    cutData.cutHeight,
    //image 的左上角在目标画布上 X 轴坐标
    0,
    0,
    //image 在目标画布上绘制的宽度
    cutData.width,
    cutData.height
  );
  canvas.toBlob((blob) => {
    // 获取file对象
    const file = new File([blob], "avatar.png", { type: "image/png" });
    // 调用 ajax 上传
  }, "image/jpg");
}
相关推荐
m0_748256782 分钟前
SpringBoot 依赖之Spring Web
前端·spring boot·spring
web1350858863531 分钟前
前端node.js
前端·node.js·vim
m0_5127446432 分钟前
极客大挑战2024-web-wp(详细)
android·前端
若川41 分钟前
Taro 源码揭秘:10. Taro 到底是怎样转换成小程序文件的?
前端·javascript·react.js
潜意识起点1 小时前
精通 CSS 阴影效果:从基础到高级应用
前端·css
奋斗吧程序媛1 小时前
删除VSCode上 origin/分支名,但GitLab上实际上不存在的分支
前端·vscode
IT女孩儿1 小时前
JavaScript--WebAPI查缺补漏(二)
开发语言·前端·javascript·html·ecmascript
醒了就刷牙1 小时前
黑马Java面试教程_P9_MySQL
java·mysql·面试
m0_748256563 小时前
如何解决前端发送数据到后端为空的问题
前端