【文件上传那些事儿】- 01 简单的拖拽上传和进度条

803 阅读3分钟

说在前面的

在日常的开发工作中,文件上传是一个不可避免的需求,通常我们会使用诸如 elementantd 之类组件库自带的上传组件来实现功能。但若止步于此的话,一旦场景开始变得复杂起来,我们很容易就丧失了进一步解决问题的能力。

而本系列文章的主旨就帮助我们理解从最基础的文件上传开始,到拖拽上传,添加进度条,大文件上传以及断点续传等等背后的原理,以及如何实现。

相信读完本系列文章之后,你会对文件上传有更加深刻的认识和理解,那么废话不多说,我们直接开始吧。

环境搭建

💡:由于文件上传的工作主要在前端实现,所以后端就简单的选择了 koa。

前端:vue3

当然,读者也可自行添加诸如 ts,unit 之类的配置。

这里没什么需要特别注意的,直接使用 vue-cli 创建项目即可,选项如下:

▶ vue create file-upload

Vue CLI v4.5.10
? Please pick a preset: Manually select features
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to inve
rt selection)
❯◉ Choose Vue version
 ◉ BabelTypeScriptProgressive Web App (PWA) SupportRouterVuexCSS Pre-processors
 ◉ Linter / FormatterUnit TestingE2E Testing

依赖如下:

img01

后端:Koa

这里除了 Koa 之外,还需要:

  • koa-router:设置路由
  • koa-body:解析 formdata,需要设置 multipart: true
  • koa-static:设置静态目录
  • fs-extra:操作文件(用原生的 fs 也行)

依赖如下:

img02

V1.0:基础文件上传

components 目录下创建 Upload.vue 组件,然后我们就可以开始实现文件上传了。

首先实现 html 结构,这里需要一个 file 类型的 input,并且要监听它的 change 事件,这样才能获取到文件数据:

<template>
  <div class="upload">
    <input type="file" name="file" @change="handleFileChange" />
  </div>
</template>

<script>
export default {
  name: "Upload",
  setup() {
    const handleFileChange = e => {
      console.log("[+] data", e.target.files);
    };
    return {
      handleFileChange
    };
  }
};
</script>

效果如下:

img03

可以看到,e.target.files 是一个 FileList,这里我们只做基础的单文件上传,所以取其首位即可:

const [file] = e.target.files;

在选择文件之后,要上传到后端,这里就需要用到网络请求,我们可以使用 axios 来完成这个功能,需要注意的是,如果遇到跨域问题,可以在 vue.config.js 中配置代理:

devServer: {
  proxy: {
    [process.env.VUE_APP_BASE_API]: {
      target: `${process.env.VUE_APP_SERVER_URL}/api/v1`,
      changeOrigin: true,
      pathRewrite: {
        ["^" + process.env.VUE_APP_BASE_API]: ""
      }
    }
  }
}

接下来可以做一个简单的测试:

const res = await axios.get("/dev-api/test");
console.log("[+] res", res);

结果如下:

img04

说明网络请求已经没有问题了,那么接下来就可以将文件数据装进 formData 中,发送给后端:

<template>
  <div class="upload">
    <input type="file" name="file" @change="handleFileChange" />
    <button @click="hanleFileUpload">upload</button>
  </div>
</template>

<script>
import axios from "axios";
import { ref } from "vue";
export default {
  name: "Upload",
  setup() {
    const file = ref(null);

    const handleFileChange = e => {
      const [_file] = e.target.files;
      file.value = _file;
    };

    const hanleFileUpload = async () => {
      const formData = new FormData();
      formData.append("name", "file");
      formData.append("file", file.value);
      await axios.post("/dev-api/upload", formData);
    };

    return {
      handleFileChange,
      hanleFileUpload
    };
  }
};
</script>

当然现在后端功能还没实现,但依然可以测试一下从控制台上看看文件是否真的发送出去了:

img05

可见文件已经发送出去了,那么在后端创建路由接收即可:

router.post("/api/v1/upload", async ctx => {
  const file = ctx.request.files.file;
  console.log("[+] file: ", file);
});

结果如下:

img06

接下来就只需要将文件移动到公共目录即可:

router.post("/api/v1/upload", async ctx => {
  const file = ctx.request.files.file;
+ const { name: filename, path: cache } = file;
+ const basename = path.basename(file.path);
+ await fse.move(cache, path.resolve(__dirname, "public/uploads/" + filename));
+ ctx.body = { url: `${ctx.origin}/uploads/${basename}` };
});

最终结果如下:

img07

img08

V1.1:拖拽上传

在基础的铺垫完成之后,我们可以为这个上传新增一些体验向的功能,比如拖拽上传。

首先,可以给文件上传增加一个边框:

<template>
  <div class="upload">
    <div class="drag-wrap">
      <input type="file" name="file" @change="handleFileChange" />
    </div>
    <button @click="hanleFileUpload">upload</button>
  </div>
</template>

<style lang="scss" scoped>
.drag-wrap {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100px;
  border: 2px dashed #eee;
}
</style>

效果如下:

img09

接下来,可以简单分析一下:

  • 当文件拖进区域内的时候,给出提示
  • 当文件离开区域的时候,恢复状态
  • 当文件在区域内松开的时候,触发存储文件

那么这里需要监听三个事件:

const useDrage = elRef => {
	const el = elRef.value;
  el.addEventListener("dragover", e => {
    console.log("dragover");
    e.preventDefault();
  });
  el.addEventListener("dragleave", e => {
    console.log("dragleave");
    e.preventDefault();
  });
  el.addEventListener("drop", e => {
    console.log("drop");
    e.preventDefault();
  });
};

el 则由 ref 指定即可:

<template>
  <div class="upload">
    <div class="drag-wrap" ref="dragEle">
      <input type="file" name="file" @change="handleFileChange" />
    </div>
  </div>
</template>

<script>
import { ref, onMounted } from "vue";

export default {
  name: "Upload",
  setup() {
    const fileData = ref(null);
    const dragEle = ref();

    onMounted(() => {
      useDrage(dragEle.value);
    });

    return {
      dragEle
    };
  }
};
</script>

💡:必须要在挂在之后,才能获取到 dom,所以事件监听也要在 mouted 之后执行。

接下来,只需要做三件事即可:

  • dragover:改变边框颜色
  • dragleave:恢复边框颜色
  • drop:存储文件,并且恢复边框颜色
el.addEventListener("dragover", e => {
  el.style.borderColor = "red";
  e.preventDefault();
});
el.addEventListener("dragleave", e => {
  el.style.borderColor = "#eee";
  e.preventDefault();
});
el.addEventListener("drop", e => {
  const [file] = e.dataTransfer.files;
  fileData.value = file;
  el.style.borderColor = "#eee";
  e.preventDefault();
});

结果如下:

img10

🗒️:可以看到,这里还有一个小问题,那就是拖拽文件之后,input 的文本依然是 ”未选择任何文件“,这里可以留一个小的 todo,在未来进行优化。

V1.2:上传进度条

关于上传进度条,这里我们选择借助 elementprogressaxiosonUploadProgress 来实现,当然,由于只使用了 progresselement 可以按需引入:

<template>
  <div class="upload">
    <el-progress :percentage="uploadProgress"></el-progress>
  </div>
</template>

<script>
import axios from "axios";
import { ref, onMounted } from "vue";

export default {
  name: "Upload",
  setup() {
    const uploadProgress = ref(0);

    const hanleFileUpload = async () => {
      await axios.post("/dev-api/upload", formData, {
        onUploadProgress: progress => {
          const { loaded, total } = progress;
          uploadProgress.value = Number(((loaded / total) * 100).toFixed(2));
        }
      });
    };

    return {
      uploadProgress
    };
  }
};
</script>

结果如下:

img11

结束语

今天的讲解就到这里,下一篇我们将实现二进制层面对于文件类型的校验。