Javascript文件上传

什么是文件上传

文件上传包含两部分,

  • 一部分是选择文件,包含所有相关的界面交互。
  • 一部分是网络传输,通过一个网络请求,将文件的数据携带过去,传递到服务器中,剩下的,在服务器中如何存储,那就与前端无关了。

制作文件上传相关的功能时,一定要先确保文件上传的接口可用,否则之后会遇到无数的麻烦,无论怎么写都是写不通的。

可以使用apifox 或者其他的工具先把接口调通。

简单上传的实现

上传界面的制作

抛开文件上传的接口和功能,我们先在本地模拟一个文件上传功能,把静态界面写好,等接口完成后,将模拟的上传替换为真实的接口即可。

但是,很有可能说这些相关的页面无法实现,那就不是文件上传的问题,而是之前的基础不牢固导致的。跟文件上传没有任何关系。

制作下面的一个页面,分为三个区域:文件选择,上传中,上传完成。
image.png
image.png
image.png

这里的文件上传,我们使用一个定时器来模拟。


  1. 首先选择文件并且读取出文件中的数据
// 绑定点击事件
doms.choose.addEventListener('click', (e) => {doms.input.click();
})// 选择文件后执行
doms.input.addEventListener('change', (e) => {// 获取文件const file = e.target.files[0];// 使用文件读取器读取const reader = new FileReader();// 获取文件的DataUrlreader.readAsDataURL(file);// 监听文件加载完成render.onload((e) => {// 获取临时的DataUrl, 是文件在内存中的临时地址,直接从内存中读取文件const url = e.target.result;// 开始上传文件,上传的是File, 而不是DataUrl。const cancel = load(file, (e) => {console.log("上传进度:" + e);}, (e) => {console.log("上传完成:" + e);})})
})

  1. 封装上传函数

上传函数暂时先使用一个定时器模拟,之后只需要将这里替换为真实的上传即可。

function load(file, process, finished) {// 模拟上传进度let pro = 0;const timer = setInterval(() => {pro++;// 监听文件上传进度process(pro);if(pro >= 100) {const result = "文件上传结果"clearInterval(timer);// 文件上传完成的回调函数finished(result);}}, 50);// 返回取消上传的函数return function () {clearInterval(timer);}
}

其余的都是简单的界面交互和事件绑定,如果写不出来就该反思一下之前学的基础怎么样。

发送请求

发送请求只需要改写一下upload函数,将模拟上传改为真实的上传即可。

function upload(file, process, finished) {// 这里使用原生的xhr来发送请求const xhr = new XMLHttpRequent();xhr.onload = function() {const resp = JSON.parse(xhr.responseText);finished(resp);} xhr.upload.onprogress = e => {const percent = Math.floor((e.loaded / e.total) * 100);process(percent);}// 这一段看不懂就需要回去学习ajax相关的只是,不是说用axios等第三方库会写就行的xhr.open("POST", config.url);const form = new FormData();form.append('avatar', file);xhr.send(form);return function() {xhr.abort();}
}

拖拽上传

拖拽上传和之前的点击上传只有界面交互上的区别,其余完全相同。
所以这里主要写的就是拖拽的交互逻辑。

H5的input文件输入框本身是支持拖拽的,所以我们只需要将默认的文件选择器放置在选择界面并且设置宽高相同,通过调整透明度使其不显示,就可以实现拖拽上传,并且连原本的点击事件都不需要了。

    input {display: block;width: 100%;height: 100%;opacity: 0;position: absolute;left: 0;top: 0;}// doms.choose.addEventListener('click', (e) => {//   doms.input.click();// })

但是有些时候,为了兼容一些特殊的浏览器,这些浏览器的input不支持拖拽,那我们就必须为我们自己写的上传框注册拖动事件,让他也可以接收托入的文件。


要完成这个目标,首先就需要让输入框编程一个可拖动目标,也就是可以接收拖拽的物体。
只需要将两个事件的默认行为阻止即可。

// 这个事件会在进入拖拽元素后触发,类似mouseenter
doms.choose.ondragenter = (e) => {e.preventDefault();
}// 只要拖拽物一直在拖拽元素之上,就会一直触发,类似mouseover
doms.choose.ondragover = (e) => {e.preventDefault();
}

注册上面的两个事件之后,元素就会变为可拖拽目标,就可以监听ondrop事件,这个事件的触发时机是拖拽物在触发元素上松手时,正好符合拖拽上传的操作逻辑。

doms.choose.ondrop = (e) => {e.preventDefault();const file = e.dataTransfer.files;// 开启拖拽后一些奇奇怪怪的东西也可以被拖拽进来,需要做校验if (!e.dataTransfer.types.includes("Files")) {alert("仅支持拖拽文件");return;}// 现在学习的是单文件上传if(file.length !== 1) {return;}doms.input.files = file; // 这样修改并不会触发input标签的change事件,所以需要提取change事件中的函数手动触发。changeFile.call(doms.input);
}doms.input.addEventListener('change', changeFile)

特殊格式上传

base64格式上传

要上传base64格式就不能再使用FormData来构建表单数据了,而是要使用json格式来上传。
依然只需要改写一下uplaod函数即可,为了区分我们另外写一个uploadBase64函数

function uploadBase64(file, process, finished) {const ext = file.name.split('.').pop();// 使用之前读取DataUrl的方法,逗号后面的字符就是对用的Base64const render = new FileReader();render.onload = (e) => {const base64 = e.target.result.split(',').pop();// 拿到base64后开始发送请求xhr.open("POST", config.url);// 传输json文本需要的请求头xhr.setRequestHeader('content-type', 'application/json');// 发送json文本shr.send(JSON.stringify({ext,avatar: base64}))}// 这里使用原生的xhr来发送请求const xhr = new XMLHttpRequent();xhr.onload = function() {const resp = JSON.parse(xhr.responseText);finished(resp);} xhr.upload.onprogress = e => {const percent = Math.floor((e.loaded / e.total) * 100);process(percent);}return function() {xhr.abort();}
}

二进制文件上传

二进制格式文件的上传是最常见的,也是最简单的,只需要带一个请求头,并且把数据带过去即可。不需要什么FormData呀,base64呀。

function uploadBinary(file, process, finished) {// 这里使用原生的xhr来发送请求const xhr = new XMLHttpRequent();xhr.onload = function() {const resp = JSON.parse(xhr.responseText);finished(resp);} xhr.upload.onprogress = e => {const percent = Math.floor((e.loaded / e.total) * 100);process(percent);}xhr.open("POST", config.url);// 传输二进制文件需要的请求头xhr.setRequestHeader('content-type', 'application/octet-stream');// 自定义请求头,根据文档的需求更改xhr.setRequestHeader('x-ext', file.name.split('.').pop());// 最后直接把文件发过去就可以// 这种方式不只是图片,音频、视频等在二进制格式上都是打平的,一视同仁,任何格式的文件都可以上传。xhr.send(file);return function() {xhr.abort();}
}

多文件上传

如果想要一次选中多个文件上传,或者是上传一个文件夹中的所有文件,应该怎么做?

如果是点击上传,那么很简单,只需要在input标签上加上几个属性即可
input最后拿到的是一个数组,数组中的每一项都是一个File对象,通过File就可以拿到任何想要的文件信息。

  • multiple: 允许多选
  • webkitdirectory mozdirectory odirectory: 这个属性是让input只能选择文件夹

因为文件夹选择还在实验阶段,所以需要适配不同内核的浏览器


如果是拖拽上传,就相对比较麻烦,需要区分拖拽进来的是文件还是文件夹。
这个相对比较麻烦,暂时先留个坑

裁剪上传

什么是裁剪上传?就是选择一张图片或者其他文件后,可以截取其中的一部分发送到后端,这就是裁剪上传。

这里有两个重点:

  1. 如何实现本地裁剪预览。
  2. 如何实现文件的部分上传。

这里用一个简单的头像上传为例:


实现本地预览很简单,只需要用前面学到的知识,读取出DataUrl即可

const img = $('img')
const input = $('input');input.onchange = (e) => {const file = e.target.files[0];const reader = new FileReader();reader.readAsDataUrl(file);reader.onload = (e) => {// 这里拿到的就是DataUrl;const url = e.target.result;img.src = url;}
}

对于图片的裁剪上传,首先需要用之前学过的知识,用原生三剑客写出一个裁剪框来获取用户的裁剪信息,然后利用Canvas就可以实现裁剪和裁剪的预览。

裁剪完成后,通过CanvastoBlob方法拿到Canvas的像素信息对应的Blob对象,进而通过Blob对象合成出File对象实现上传。

const postBtn = $('button');
const img = $('img');// 这里模拟一个裁剪结果
const cutInfo = {x: 100,          // 图像开始裁剪的位置xy: 100,          // 图像开始裁剪的位置ycutWidth: 300,   // 裁剪图像的宽度cutHeight: 300,  // 裁剪图像的高度width: 100,      // 裁剪后真实显示的宽度height: 100      // 裁剪后真实显示的高度
}postBtn.onclick = () => {const { x, y, cutWidth, cutHeight, width, height } = cutInfo;canvas.width = width;canvas.height = height;ctx.drawImage(img, x, y, cutWidth, cutHeight, 0, 0, width, height);// 这个API可以将canvas中的像素信息转换为Blob,有了Blob就可以很容易得获取到File,因为File是Blob的子类,两者之间的转换很容易,有了File就可以上传文件。canvas.toBlob((blob) => {// new File时传递的时一个数组,因为一个File可以由多个Blob组成。const file = new File([blob], 'avatar.jpg', {type: 'image/jpeg'})// 最后通过网络请求将File上传即可。ajax(file);}, 'image/jpeg');
}

大文件分片上传

对于一些较大的文件,我们一般需要分成一小段一小段,分成多次ajax请求发送到后端。这是因为大文件一旦在传输中出现问题,因为对文件做出了分片,不再需要对整个文件进行上传。

分片上传最大的难点在于如何对文件进行分片?
如何标记已经上传过的文件?

我们知道通过input标签可以拿到File对象,但其实可以将File对象做为一个数组来处理,将File对象用数组的slice方法进行分割,就可以得到若干的Blob对象,Blob对象也是可以直接上传的,这样就实现的文件的分片上传,后端再去将这些分片合成为一个完成的文件。

input.change = (e) => {const file = e.target.files[0];const blob1 = file.slice(0, 99);
}// 可以写一个函数来完成文件的分片,接收文件的分片大小,返回Blob数组。
function createChunks(file, size) {const cnt = Math.ceil(file.size / size);const result = new Array(cnt).fill(0);for(let i = 0; i < cnt; i++) {result[i] = file.slice(i * size, (i+1) * size);}return result;
}

注意这个分片是可以瞬间完成的,因为File对象和Blob对象里都只有文件的基本信息,并没有保存详细数据,所以这只是一个简单的数学运算。


之后就需要解决下一个问题,如何标记上传过的文件,这就需要介绍一个算法:文件哈希
文件哈希是一个文件的唯一标识,它可以将一个文件的字节数据按照一定的算法压缩成一个固定长度的字符串,但是这个字符串是不可逆的md5是一个常用的文件哈希算法,可以使用第三方库spark-md5来完成文件哈希的算法。

// 因为计算哈希需要读取内存数据,一次性读取大文件的全部数据吃不消,所以需要使用增量算法,spark-md5这个库有做相关的处理
function hash(chunks) {const spark = new SparkMD5();function _read(i) {if(i >= chunks.length) {return spark.end(); // 读取完成}const blob = chunks[i];const reader = new FileReader();reader.onload = e => {const bytes = e.target.result; // 获取字节数组。spark.append(bytes);  // 添加字节到hash运算中。read(i + 1);}// 读取blob对象的字节信息。reader.readAsArrayBuffer(blob);}_read(0);
}

总结

抛开文件上传的外衣,其实就是界面交互和网络请求。

学了这么多的场景,应该足以应对绝大多数的场景,对于element-ui或者是ant等组件库内提供的文件上传组件也能做到知其然且知其所以然。

也学到了对于图片文件的很多处理方式。学到了File和Blob的转换。

对于File和Blob,其实不只是用于图片文件,任何格式的文件在浏览器中都会被打平为File和Blob,只不过不同的文件需要用到不同的辅助处理。

例如图片文件的处理需要接触Image和Canvas,音频文件的处理需要借助Auduo和AudioContent。

这就文件上传部分所有的笔记了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/149993.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

【11】c++设计模式——>单例模式

单例模式是什么 在一个项目中&#xff0c;全局范围内&#xff0c;某个类的实例有且仅有一个&#xff08;只能new一次&#xff09;&#xff0c;通过这个唯一的实例向其他模块提供数据的全局访问&#xff0c;这种模式就叫单例模式。单例模式的典型应用就是任务队列。 为什么要使…

C++(STL容器适配器)

前言&#xff1a; 适配器也称配接器&#xff08;adapters&#xff09;在STL组件的灵活组合运用功能上&#xff0c;扮演着轴承、转换器的角色。 《Design Patterns》对adapter的定义如下&#xff1a;将一个class的接口转换为另一个class的接口&#xff0c;使原本因接口不兼容而…

Python之字符串构造

Python之字符串构造 字符串str 一个个字符组成的有序的序列&#xff0c;是字符的集合使用单引号、双引号、三引号引住的字符序列字符串是不可变对象&#xff0c;是字面常量 Python3起&#xff0c;字符串都是Unicode类型 x abcde使用for循环遍历x的值&#xff0c;打印并查看…

2023 年 Web 安全最详细学习路线指南,从入门到入职(含书籍、工具包)【建议收藏】

第一个方向&#xff1a;安全研发 你可以把网络安全理解成电商行业、教育行业等其他行业一样&#xff0c;每个行业都有自己的软件研发&#xff0c;网络安全作为一个行业也不例外&#xff0c;不同的是这个行业的研发就是开发与网络安全业务相关的软件。 既然如此&#xff0c;那其…

从零开始学习线性回归:理论、实践与PyTorch实现

文章目录 &#x1f966;介绍&#x1f966;基本知识&#x1f966;代码实现&#x1f966;完整代码&#x1f966;总结 &#x1f966;介绍 线性回归是统计学和机器学习中最简单而强大的算法之一&#xff0c;用于建模和预测连续性数值输出与输入特征之间的关系。本博客将深入探讨线性…

解决报错: require is not defined in ES module scope

用node启动mjs文件报错&#xff1a;require is not defined in ES module scope 现象如下&#xff1a; 原因&#xff1a; 文件后缀是mjs, 被识别为es模块&#xff0c;但是node默认是commonjs格式&#xff0c;不支持也不能识别es模块。 解决办法&#xff1a;把文件后缀从.mjs改…

Spring web security

儅使用spring的web security時&#xff0c;默認會轉向自帶的spring security example page。而不會轉向error page。 TODO: <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId> &l…

【智能家居项目】裸机版本——字体子系统 | 显示子系统

&#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;专栏&#xff1a;《智能家居项目》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; 今天实现上图整个项目系统中的字体子系统和显示子系统。 目录 &#x1f004;设计思路&#x1…

【网络安全-信息收集】网络安全之信息收集和信息收集工具讲解

一&#xff0c;域名信息收集 1-1 域名信息查询 可以用一些在线网站进行收集&#xff0c;比如站长之家 域名Whois查询 - 站长之家站长之家-站长工具提供whois查询工具&#xff0c;汉化版的域名whois查询工具。https://whois.chinaz.com/ 可以查看一下有没有有用的信息&#xf…

【熬夜爆肝版】JAVA基础入门专栏——1.JAVA开发入门

JAVA开发入门 1、Java概述1&#xff09;起源2&#xff09;特点3&#xff09;应用领域 2、JDK1&#xff09;定义2&#xff09;作用3&#xff09;组成4&#xff09;JDK版本与兼容性5&#xff09;JDK的安装与配置6&#xff09;JDK的发行版 3、系统环境变量1&#xff09;定义2&…

回归预测 | MATLAB实现PSO-SVR粒子群优化支持向量机回归多输入单输出预测

回归预测 | MATLAB实现PSO-SVR粒子群优化支持向量机回归多输入单输出预测 目录 回归预测 | MATLAB实现PSO-SVR粒子群优化支持向量机回归多输入单输出预测预测效果基本介绍模型描述程序设计预测效果 <

Chrome(谷歌浏览器)如何关闭搜索栏历史记录

目录 问题描述解决方法插件解决&#xff08;亲测有效&#xff09;自带设置解决步骤首先打开 地址 输入&#xff1a;chrome://flags关闭浏览器&#xff0c;重新打开Chrome 发现 已经正常 问题描述 Chrome是大家熟知的浏览器&#xff0c;但是搜索栏的历史记录如何自己一条条的删…

时序预测 | MATLAB实现EMD-iCHOA+GRU基于经验模态分解-改进黑猩猩算法优化门控循环单元的时间序列预测

时序预测 | MATLAB实现EMD-iCHOAGRU基于经验模态分解-改进黑猩猩算法优化门控循环单元的时间序列预测 目录 时序预测 | MATLAB实现EMD-iCHOAGRU基于经验模态分解-改进黑猩猩算法优化门控循环单元的时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 EMD-iCHOAGR…

LVGL_基础控件滚轮roller

LVGL_基础控件滚轮roller 1、创建滚轮roller控件 /* 创建一个 lv_roller 部件(对象) */ lv_obj_t * roller lv_roller_create(lv_scr_act()); // 创建一个 lv_roller 部件(对象),他的父对象是活动屏幕对象// 将部件(对象)添加到组&#xff0c;如果设置了默认组&#xff0c…

MFC文本输出学习

void CTxttstView::OnDraw(CDC* pDC) {CTxttstDoc* pDoc GetDocument();ASSERT_VALID(pDoc);// TODO: add draw code for native data hereCString str1;pDC->SetBkColor(RGB(0,0,0));pDC->TextOut(50, 50, "一段文字");pDC->SetBkColor(RGB(255,255,255))…

Linux Vi编辑器基础操作指南

Linux Vi编辑器基础操作指南 Linux中的Vi是一个强大的文本编辑器&#xff0c;虽然它有一些陡峭的学习曲线&#xff0c;但一旦掌握了基本操作&#xff0c;它就变得非常高效。以下是Vi编辑器的一些基本用法&#xff1a; 打开Vi编辑器&#xff1a; vi 文件名退出Vi编辑器&#xff…

kafka初体验基础认知部署

kafka 基础介绍 Apache Kafka是一个分布式流处理平台&#xff0c;最初由LinkedIn开发并于2011年开源。它主要用于解决大规模数据的实时流式处理和数据管道问题。 Kafka是一个分布式的发布-订阅消息系统&#xff0c;可以快速地处理高吞吐量的数据流&#xff0c;并将数据实时地分…

【状态估计】将变压器和LSTM与卡尔曼滤波器结合到EM算法中进行状态估计(Python代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

数据挖掘(3)特征化

从数据分析角度&#xff0c;DM分为两类&#xff0c;描述式数据挖掘&#xff0c;预测式数据挖掘。描述式数据挖掘是以简介概要的方式描述数据&#xff0c;并提供数据的一般性质。预测式数据挖掘分析数据建立模型并试图预测新数据集的行为。 DM的分类&#xff1a; 描述式DM&#…