05月02, 2023

文件上传2-前端简单上传

文件上传2-前端简单上传

无论如何,上传文件操作与后端是紧密相关的,我们应该先保证后端上传文件接口是没有问题,确定参数,返回的响应类型是什么,最好测试调通之后,再进行前端操作。

了解了基本理论,以及有了后端接口并且测试通过了之后,接下来,就是前端的任务了。

其实我们理清思路,前端上传文件给后端,其实无非就是两个点:

1、显示样式 2、传递方式

如果说,我们不在意样式,传递方式直接使用form表单,这就根本轮不到前端啥事了,直接在form标签中加上enctype完事。

<form action="http://127.0.0.1:3002/upload/single" method="post" enctype="multipart/form-data">
    <input type="file" name="avatar" id="avatar">
    <button type="submit">提交</button>
</form>

当然,前端现在的传递至少至少都应该是ajax的方式,因此我们需要知道ajax上传文件是怎么处理的--------其实和以前一样处理,没有差别

但是唯一的不同是什么?

以前我们大多数时候是get请求,并且传递的是普通值 现在我们是post请求,并且传递的是文件

但是大家还记得最开始我们讲解的原理吗?

头信息中必须要有Content-Type:multipart/form-data; boundary=----分隔符, 请求体是用分隔符隔开的一个个需要上传的数据

也就是说,ajax基本操作都没变,变化是传递数据的时候加上头信息,加上特殊的请求体就行。

但是这个请求体弄起来很麻烦,所以,浏览器考虑到了这个问题,我们可以使用FormData对象,我们只需要实例化这个FormData对象,它就会帮我们创建上传文件的头信息,以及组装请求体。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <form action="http://127.0.0.1:3002/upload/single" method="post" enctype="multipart/form-data">
    <input type="file" name="avatar" id="avatar">
    <button type="submit">提交</button>
  </form>
  <hr>
  <button id="ajaxUpload" type="button">提交</button>
</body>
<script>
  const ajaxUpload = document.getElementById('ajaxUpload');

  ajaxUpload.addEventListener('click', function (e) {
    e.preventDefault();
    const avatar = document.getElementById('avatar');
    if(avatar.files.length === 0){
      alert("请选择文件");
      return;
    }

    // 创建一个FormData对象
    // FormData对象的作用其实就是帮我们生成Content-Type为multipart/form-data的请求体形态
    const formData = new FormData();
    //有数据就往FormData中append添加就行
    //会帮助形成带有boundary分隔符的请求体
    formData.append('avatar', avatar.files[0]);

    let xhr = new XMLHttpRequest();
    xhr.open('POST', 'http://127.0.0.1:3002/upload/single');

    //当xhr.readyState的值为4时,触发onload事件
    xhr.onload = function () {
      console.log(xhr.responseText);
    }

    //发送formData数据
    xhr.send(formData);
  })

</script>
</html>

可以看到上面的ajax上传文件,和普通的ajax请求,其实并没有太大的差别,唯一多的就是FormData帮我们发送上传文件的请求头和组装请求体,其实也就多了3句关键代码

const formData = new FormData();
formData.append('avatar', avatar.files[0]);
......
xhr.send(formData);

有了这个基本操作之后,我们接下来如果想要把界面美化,那多数都是HTML+CSS+JS的基本操作了,和上传文件就没有太多的关系了。

比如现在要实现如下效果:

2023-04-23 11.26.30

看着好像很难,其实重要的内容我们大部分都已经实现了,其他基本都是html+css

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="https://kit.fontawesome.com/56085ed2ba.js" crossorigin="anonymous"></script>
  <style>

 .file-upload {
  position: relative;
  display: inline-block;
  padding: 30px;
  border: 2px dashed #ccc;
  border-radius: 5px;
  text-align: center;
  font-size: 18px;
  color: #ccc;
  cursor: pointer;
  transition: all 0.3s ease;
}

.file-upload:hover {
  border-color: #007bff;
  color: #007bff;
}

.file-upload input[type="file"] {
  position: absolute;
  top: 0;
  left: 0;
  opacity: 0;
  cursor: pointer;
  width: 100%;
  height: 100%;
}


.file-upload .file-upload-icon i {
  font-size: 48px;
  color: #ccc;
}

.file-upload-text {
  margin-top: 20px;
}

.file-upload-progress {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 5px;
  background-color: #f5f5f5;
  border-radius: 5px;
  /* overflow: hidden; */
  display: none;
}

.file-upload-progress-bar {
  position: absolute;
  top: 0;
  left: 0;
  width: 0;
  height: 100%;
  background-color: #007bff;
  border-radius: 5px;
  transition: width 0.3s ease;
}

.file-upload-progress i{
  position: absolute;
  font-size: 20px;
  color:#999;
  right:-30px;
  top:-10px;
}
.file-upload-progress i:hover{
  color:#007bff;
}

.file-upload-divider {
  display: none;
  line-height: 0;
  margin: 10px 0;
  padding: 0;
  border: none!important;
  border-bottom: 1px solid #eee!important;
  clear: both;
  background: 0 0;
}

.file-preview {
  display: none;
  margin-top: 10px;
  width: 200px;
  height: 200px;
  overflow: hidden;
  border-radius: 5px;
  border: 1px solid transparent;
  transition: all 0.3s ease;
}

.file-preview img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: all 1s ease;
}
.file-upload:hover img{
  transform: scale(1.02,1.02);
}
  </style>
</head>
<body>
  <div class="file-upload">
    <input type="file" name="avatar" class="file-upload-input" />

    <div class="file-upload-icon">
      <i class="fas fa-plus"></i>
    </div>
    <p class="file-upload-text">选择文件,或者拖拽上传</p>
    <div class="file-upload-progress">
      <div class="file-upload-progress-bar">
      </div>
      <i class="fas fa-trash-can"></i>
    </div>
    <hr class="file-upload-divider" />
    <div class="file-preview"></div>
  </div>
</body>
<script>
  const progressBar = document.querySelector('.file-upload-progress-bar');
  const progress = document.querySelector('.file-upload-progress');
  const fileUpload = document.querySelector('.file-upload');
  const fileUploadText = document.querySelector('.file-upload-text');
  const divider = document.querySelector('.file-upload-divider');
  const avatar = document.querySelector('.file-upload-input');
  const filePreview = document.querySelector('.file-preview');
  const previewImage = document.createElement('img');
  const trash = document.querySelector('.fa-trash-can');


  //拖拽事件
  fileUpload.ondragenter = function (e) {
    e.preventDefault();
    fileUpload.style.backgroundColor = '#f2f2f2';
    fileUpload.style.borderColor = '#007bff';
  }
  fileUpload.ondragleave = function (e) {
    e.preventDefault();
    fileUpload.style.backgroundColor = '';
    fileUpload.style.borderColor = '';
  }

  let cancelUpload = null;

  avatar.addEventListener('change', function (e) {
    fileUpload.style.backgroundColor = '';
    fileUpload.style.borderColor = '';

    if(avatar.files.length === 0){
      alert("请选择文件");
      return;
    }
    if(avatar.files.length > 1){
      alert("仅支持单文件上传");
      return;
    }
    //前端验证
    if (!validateFile(avatar.files[0])) {
      return;
    }

    progress.style.display = 'block';

    cancelUpload = upload(
      avatar.files[0],
      'http://localhost:3002/upload/single',
      showPreview,
      setProgress,
      ()=>{
        progress.style.display = 'none';
        progressBar.style.width = '0';
      }
    )
  }) 

  trash.onclick = ()=>{
    cancelUpload && cancelUpload();
  };

  const showPreview = (resp) => {
    fileUploadText.style.display = 'block';
    divider.style.display = 'block';
    filePreview.style.display = 'block';
    previewImage.src = resp.data;
    filePreview.appendChild(previewImage);
  }

  const setProgress = (percent) => {  
    progressBar.style.width = percent + '%';
  }

  const upload = (file,url,onFinish,onProgress,onloadend)=>{
    let xhr = new XMLHttpRequest();
    xhr.open('POST', url);

    let formData = new FormData();
    formData.append('avatar', file);

    xhr.onload = function () {
      const resp = JSON.parse(xhr.responseText);
      onFinish && onFinish(resp);
    }

    xhr.upload.onprogress = (e) => {
      // 上传进度变化时更新 UI
      const percent = Math.floor((e.loaded / e.total) * 100);
      onProgress && onProgress(percent);
    };

    xhr.onloadend = onloadend;

    xhr.send(formData);

    return () => {
      xhr.abort();
    }
  }
  function validateFile(file) {
    const sizeLimit = 2 * 1024 * 1024;
    const legalExts = ['.jpg', '.jpeg', '.gif', '.png', '.bmp', '.webp'];
    if (file.size > sizeLimit) {
      alert('文件尺寸过大');
      return false;
    }
    const name = file.name.toLowerCase();
    if (!legalExts.some((ext) => name.endsWith(ext))) {
      alert('文件类型不正确');
      return false;
    }
    return true;
  }
</script>
</html>

上面的代码中,js相关代码,也就多了进度条和预览图片相关的代码,也没有过多的逻辑,无非就是显示隐藏效果,最多也就是躲了进度条长度的计算,其他都是html+css的效果了。

至于拖拽上传呢?好像没有拖拽的代码啊?html的input标签,本身就支持拖拽,直接拖拽到div的区域就行了,上传的部分是没有任何影响的,都是一样的。

对上传业务的封装

整个上传的业务代码应该是一套整体流程,因此我们可以将其封装为一个函数

//......其他代码省略
let cancelUpload = null;

avatar.addEventListener('change', function (e) {
fileUpload.style.backgroundColor = '';
fileUpload.style.borderColor = '';
progress.style.display = 'block';

if(avatar.files.length === 0){
  alert("请选择文件");
  return;
}
if(avatar.files.length > 1){
  alert("仅支持单文件上传");
  return;
}

cancelUpload = upload(
  avatar.files[0],
  'http://localhost:3002/upload/single',
  setProgress,
  showPreview,
  ()=>{
    progress.style.display = 'none';
    progressBar.style.width = '0';
  }
)
}) 

trash.onclick = ()=>{
    cancelUpload && cancelUpload();
};

const showPreview = (resp) => {
    fileUploadText.style.display = 'block';
    divider.style.display = 'block';
    filePreview.style.display = 'block';
    previewImage.src = resp.data;
    filePreview.appendChild(previewImage);
}

const setProgress = (percent) => {  
    progressBar.style.width = percent + '%';
}

const upload = (file,url,onProgress,onFinish,onloadend)=>{
    let xhr = new XMLHttpRequest();
    xhr.open('POST', url);

    let formData = new FormData();
    formData.append('avatar', file);

    xhr.onload = function () {
      const resp = JSON.parse(xhr.responseText);
      onFinish(resp);
    }

    xhr.upload.onprogress = (e) => {
      // 上传进度变化时更新 UI
      const percent = Math.floor((e.loaded / e.total) * 100);
      onProgress(percent);
    };

    xhr.onloadend = onloadend;

    xhr.send(formData);

    return () => {
      xhr.abort();
    }

}

本文链接:http://www.yanhongzhi.com/post/upload2.html

-- EOF --

Comments