开发中会经常涉及到文件上传的需求,根据业务不同的需求,有不同的文件上传情况。
有简单的单文件上传,有多文件上传,因浏览器原生的文件上传样式及功能的支持度不算太高,很多时候我们会对样式进行美化,对功能进行完善。
本文根据一个例子,对多文件的上传样式做了一些简单的美化(其实也没怎么美化。。),同时支持选择文件后自定义删除相关的文件,最后再上传
文章篇幅较长,先简单看看图示:
目录
一、文件上传基础
1. 单文件上传
最简单的文件上传,是单文件上传,form标签中加入enctype="multipart/form-data",form表单中有一个input[type="file"]项
<form name="form1" method="post" action="/abc.php" enctype="multipart/form-data">
<input type="text" name="user" id="user" placeholder="请输入昵称">
<input type="file" name="userImage" id="userImage">
<input type="submit" name="sub" value="提交">
</form>
2. 多文件上传
1)类似单文件上传,简单的多文件上传其实就是多几个input[type="file"]项
<form name="form1" method="post" action="/abc.php" enctype="multipart/form-data">
<input type="text" name="user" id="user" placeholder="请输入昵称">
<input type="file" name="userImage1" id="userImage1">
<input type="file" name="userImage2" id="userImage2">
<input type="file" name="userImage3" id="userImage3">
<input type="submit" name="sub" value="提交">
</form>
2) HTML5为表单文件项新增了一个multiple属性,可以设置实现选择多个文件,如
<form name="form1" method="post" action="/abc.php" enctype="multipart/form-data">
<input type="text" name="user" id="user" placeholder="请输入昵称">
<input type="file" name="userImage" id="userImage" multiple>
<input type="submit" name="sub" value="提交">
</form>
要注意的是,对于multiple这个新属性,在IE9及以下版本中不被支持,在移动端安卓平台下会忽略,也就是只能选择一个文件
二、表单文件上传的美化
看了上面几个图片,可以知道原生的文件选择项样式是最基本的,主要体现在三个点:
- 无边框,与其他有边框的元素不合拍
- 选择文件的按钮样式太基础
- 选择多个文件后只显示总数,未显示详细选择的文件名
基于几个问题,可以按需对其进行美化
第一点可以直接添加边框的样式
第二点需要增添其他元素,可以新增一个按钮(自行按需美化),将原始文件框隐藏,用JS事件绑定,点击按钮后模拟文件框的点击
<input type="file" name="userImage" id="userImage" style="display: none;">
<input type="button" id="" value="选择文件" onclick="document.getElementById('userImage').click()">
第三点与第二点类似,也得添加新的元素,选择文件后,通过JS获取选择的文件信息,并在新的元素中显示出来
想着很简单,但随之而来的问题就是,如果选中的文件数量很多,新元素占空间的多少就是个问题,可以默认显示几个文件,再通过“查看更多文件”查看到更多的信息
随之另外的想法是,一次性选中的文件很多,想取消某个文件时,又得重新选择。这未免太繁琐,所以需要提供即时删除某个选中文件的操作
三、选中文件后的删除
要提供选中文件后可删除的操作,就必然需要提供相关入口及脚本操作,下面围绕这点来做些解析
1. 界面的处理
选择文件后,我们可以通过删除按钮删除选中的文件,因为会出现多文件的情况,所以需要一个信息模版
<!-- 当前选择的文件列表 文件信息模版 -->
<script type="text/template" id="file-temp-item-tpl">
<span class="file-temp-item" style="{{style}}">
<span class="file-temp-name">{{name}}</span>
<span class="file-temp-btn">×</span>
</span>
</script>
选中的文件一多,就得再增添一个下拉框做辅助,最多显示5个文件信息,然后通过下拉按钮展开下拉框(按钮样式自行设定)
这里5个文件间的位置计算的不是很到位,主要是这段代码,可以自行设定
// 计算每一项坐标left、占宽width
left = i === 0 ? 2 : 2 + i * (100 / fileTempLen);
width = 100 / fileTempLen - 2;
下拉列表里面的每一项也是一个模版
<!-- 查看更多文件 文件信息模版 -->
<script type="text/template" id="file-more-item-tpl">
<li>
<span class="file-item-more-name">{{name}}</span>
<span class="file-item-more-btn">×</span>
</li>
</script>
以下为初始的HTML结构
<form name="form" id="form" method="post" action="fileTest.php" enctype="multipart/form-data">
<!-- <input type="number" name="numberTest" value="100"> -->
<input type="file" name="fileTest[]" id="fileTest" multiple>
<!-- 当前选择的文件列表(最多显示5条) -->
<span class="file-temp">
</span>
<!-- 查看更多文件 -->
<ul class="item-more">
</ul>
<input type="button" class="btn btn-success" id="uploadBtn" value="上传">
<p class="upload-tip">文件上传成功</p>
</form>
以下为全部CSS样式
<link rel="stylesheet" type="text/css" href="bootstrap.min.css">
<style type="text/css">
html {
font-family: Arial;
} form {
margin: 50px auto;
width: 400px;
} input {
width: 300px;
padding: 4px;
} #uploadBtn {
margin-top: -3px;
margin-left: 5px;
width: 60px;
height: 30px;
font-weight: bold;
font-size: 12px;
} #fileTest {
display: inline-block;
border: 1px solid #ccc;
border-radius: 3px;
} .file-temp {
position: relative;
display: none;
width: 300px;
height: 31px;
} .file-temp-item {
position: absolute;
top: 4px;
height: 24px;
} .item-more-btn {
display: inline-block;
position: absolute;
top: 18px;
right: 0.5%;
width: 10px;
height: 10px;
color: #777;
cursor: pointer;
} .item-more-btn:hover {
border-top-color: #aaa;
} .file-temp-name {
display: inline-block;
overflow: hidden;
width: 90%;
height: 26px;
padding: 2px 15px 2px 5px;
border-radius: 2px;
background-color: #eaeaf3;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-temp-btn {
position: absolute;
display: inline-block;
top: 4px;
right: 11%;
width: 18px;
height: 18px;
line-height: 18px;
text-align: center;
border: 1px solid #ddd;
background-color: #ccc;
border-radius: 50%;
color: #fff;
font-size: 18px;
cursor: pointer;
} .item-more {
position: absolute;
overflow-y: auto;
display: none;
padding-left: 0;
width: 300px;
max-height: 150px;
list-style: none;
} .item-more li {
position: relative;
padding: 5px;
border: 1px solid #ccc;
border-top: none;
}
.item-more li:hover {
background-color: #f5f5f9;
} .file-item-more-name {
display: inline-block;
width: 90%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-item-more-btn {
position: absolute;
display: inline-block;
top: 8px;
right: 2%;
width: 18px;
height: 18px;
line-height: 18px;
text-align: center;
border: 1px solid #ddd;
background-color: #ddd;
border-radius: 50%;
color: #fff;
font-size: 18px;
cursor: pointer;
}
.file-item-more-btn:hover {
background-color: #ccc;
} .upload-tip {
display: none;
margin: 50px auto;
text-align: center;
font-size: 12px;
}
</style>
2. 脚本的处理
下面,着重介绍JS脚本的处理
要获取到选中文件的信息,自然想到用value属性,但通过文件项的value只能获取到一个文件路径(第一个),无论有没有multiple
无multiple
<input type="file" onchange="console.log(this.value);">
有multiple
<input type="file" multiple onchange="console.log(this.value);">
既然直接通过value获取不到所有选中的文件信息,只能寻求其他途径。
1)FileList
获取选中的文件信息,还可以用FileList对象,这是在HTML5中新增的,每个表单文件项都有个files属性,里边存储这选中的文件的一些信息
<input type="file" multiple onchange="console.log(this.files);">
选中两个文件后,查看文件信息
FileList对象看起来是个类数组,有length属性。所以我们应该可以通过修改或删除相关的项来自定义我们选择的文件(注意其实这是不能修改的,且继续看下去)
假如我选择了两个文件,想删除第二项目,使用splice删除,则
<input type="file" multiple onchange="console.log(Array.prototype.splice.call(this.files, 1, 1));">
报错,由此可知FileList的length属性是只读的,那直接修改为可写可配置呢
Object.defineProperty(FileList.prototype, 'length', {
writable: true,
configurable: true
});
配置之后length能修改了,乍一看还以为splice生效了,然而输出一看,FileList对象内容不变,仍为两项
查阅了一些资料后,了解到浏览器为了安全性的考虑,把FileList对象的内容设为了不可更改,只可以手动置空,但不能修改内容
所以,解决办法是,新增一个数组,初始复制FileList对象的文件内容,之后的修改操作则通过这个可更改的数组进行
// 存储更新所选文件
var curFiles = [];
... // 选中文件后 var files = this.files; if (files && files.length) {
// 原始FileList对象不可更改,所以将其赋予curFiles提供接下来的修改
Array.prototype.push.apply(curFiles, files);
}
假如点击了删除叉叉,可以直接更新文件信息数组
var name = $(this).prev().text();
// 去除该文件
curFiles = curFiles.filter(function(file) {
return file.name !== name;
});
这样一来,更新文件信息的问题得到解决,然后就可以进行文件的上传了
点击文件上传,如果直接调用$form.submit(); 则上传的文件信息依然是初始的FileList对象,达不到我们自定义的要求,所以需要用Ajax提交
那么,该怎么想后台提供一个文件对象呢?
2)FormData
HTML5引入了表单的新对象FormData, 它可以生成一个表单对象,我们可以向其中获取/设置键值对信息,再一并提交给后台
引用MDN的FormData使用方法,我们可以添加各种类型的数据,使用ajax提交
var oMyForm = new FormData(); oMyForm.append("username", "Groucho");
oMyForm.append("accountnum", 123456); // 数字123456被立即转换成字符串"123456" // fileInputElement中已经包含了用户所选择的文件
oMyForm.append("userfile", fileInputElement.files[0]); var oFileBody = '<a id="a"><b id="b">hey!</b></a>'; // Blob对象包含的文件内容
var oBlob = new Blob([oFileBody], { type: "text/xml"}); oMyForm.append("webmasterfile", oBlob); var oReq = new XMLHttpRequest();
oReq.open("POST", "http://foo.com/submitform.php");
oReq.send(oMyForm);
也可使用JQ的封装的ajax,不过要注意设置processData和contentType属性为false,防止JQ胡乱解析文件格式
var fd = new FormData(document.getElementById("fileinfo")); // 使用某个表单作为初始项
fd.append("CustomField", "This is some extra data");
$.ajax({
url: "stash.php",
type: "POST",
data: fd,
processData: false, // 告诉jQuery不要去处理发送的数据
contentType: false // 告诉jQuery不要去设置Content-Type请求头
});
这里有几个要注意的点:
1)FormData中的属性值接受的是单个文件信息,不能是复合性的对象。可能表意不明,且看
var fd = new FormData($('#form')[0]);
fd.append('myFileTest', curFiles);
$files = $_REQUEST['myFileTest'];
var_dump($files);
用PHP接收传过来的数据,数据却被直接转换成字符串了,非文件对象
curFiles是文件对象,那PHP端是不是应该用$_FILES来接收信息呢,试试换成$files = $_FILES['myFileTest'];
直接出问题了,说明不能这样处理,需要将curFiles内容一项一项拆开,即单个文件信息
var fd = new FormData($('#form')[0]);
for (var i = 0, j = curFiles.length; i < j; ++i) {
fd.append('myFileTest[]', curFiles[i]);
}
$files = $_FILES['myFileTest'];
var_dump($files);
文件接收成功,接下来就可以按需进行文件的操作了
2)后端获取文件信息的时候,是直接通过原始$_FILES获取的,其他一般的信息才用$_REQUEST获取
换成$files = $_REQUEST['myFileTest'];试试,直接就是出现找不到myFileTest的问题
试试添加一般的文件再提交
var fd = new FormData($('#form')[0]);
for (var i = 0, j = curFiles.length; i < j; ++i) {
fd.append('myFileTest[]', curFiles[i]);
} fd.append('myTest', [1, 2, 3]);
$files = $_FILES['myFileTest'];
$test = $_REQUEST['myTest'];
var_dump($test);
var_dump($files);
3)如果需要multiple的多文件上传,则需要在文件项的文件后添加[]号,表示这是一个多文件的数组,以供后端处理解析
fd.append('myFileTest[]', curFiles[i]);
如果没有后面的[],则连续的append会直接覆盖原来的,最后后端获取到的只是最后append进去的项
4)不要直接在JQ的ajax中实例化出一个FormData对象,会出问题
直接在data属性中生成FormData对象,会被JQ忽略,所以后端什么信息也拿不到
混合表单项简单的例子:
在表单处理中,很多时候我们会进行文件上传和其他基础项的提交,简单地多加一个input项目,看看是否处理成功
<input type="number" name="numberTest" value="100">
<?php
$files = $_FILES['myFileTest'];
$test = $_REQUEST['numberTest']; echo json_encode(array(
'len' => count($files['name']),
'num' => $test
)); ?>
以下为全部的JS脚本:
<script type="text/javascript">
/**
* 向文件列表元素中添加相应的文件项
* @param {Array} files 当前的文件列表数组对象
*/
function addItem(files) {
var fileTempItemTpl = $('#file-temp-item-tpl').html(),
fileMoreItemTpl = $('#file-more-item-tpl').html()
htmlTemp = [],
htmlMoreTemp = [],
// 文件列表中各文件坐标位置及所占空间
left = 2,
width = 100,
// 最多取前5个文件
fileTempLen = files.length > 5 ? 5 : files.length; for (var i = 0, j = files.length; i < j; ++i) {
// 当i > 4,即第6个文件开始
if (i > 4) {
htmlMoreTemp.push(fileMoreItemTpl.replace('{{name}}', files[i].name));
continue;
} // 计算每一项坐标left、占宽width
left = i === 0 ? 2 : 2 + i * (100 / fileTempLen);
width = 100 / fileTempLen - 2; htmlTemp.push(fileTempItemTpl
.replace('{{style}}', 'left: ' + left + '%;width: ' + width + '%;')
.replace('{{name}}', files[i].name)
);
} // 渲染相关元素内容
$('.file-temp').html(''
+ '<input type="text" style="background-color:#fff;" class="form-control" id="fileTemp" readonly>'
+ htmlTemp.join('')
+ (files.length > 5
? '<span class="item-more-btn" title="查看更多">=</span>'
: ''
)
); $('.item-more').html(htmlMoreTemp.join(''));
} // 保存当前选择的(更新后)文件列表
var curFiles = []; // 初始选择文件时触发
$('#fileTest').change(function() {
var $this = $(this),
$temp = $('.file-temp'),
files = this.files; if (files && files.length) {
// 原始FileList对象不可更改,所以将其赋予curFiles提供接下来的修改
Array.prototype.push.apply(curFiles, files);
addItem(curFiles); $this.hide();
$temp.css('display', 'inline-block');
}
}); $(document)
// 取消选择某个文件时,在文件列表数组对象中删除这个值,并更新列表
.on('click', '.file-temp-btn, .file-item-more-btn', function() {
$('.upload-tip').hide();
var name = $(this).prev().text();
// 去除该文件
curFiles = curFiles.filter(function(file) {
return file.name !== name;
});
// 文件列表数组对象长度大于5才显示“更多文件列表”下拉项
if (curFiles.length <= 5) {
$('.item-more').hide();
}
// 文件列表数组被清空则重置文件选择表单项
if (!curFiles.length) {
$('#fileTest').val('').show();
$('.file-temp').css('display', 'none');
} else {
addItem(curFiles);
} console.log(curFiles)
})
// 显示“更多文件列表”下拉项
.on('click', '.item-more-btn', function() {
$('.upload-tip').hide();
$('.item-more').show('normal');
}); // 上传操作
$('#uploadBtn').click(function() {
$('.upload-tip').hide(); // 构建FormData对象
var fd = new FormData($('#form')[0]);
for (var i = 0, j = curFiles.length; i < j; ++i) {
fd.append('myFileTest[]', curFiles[i]);
} $.ajax({
url: 'fileTest.php',
type: 'post',
data: fd,
processData: false,
contentType: false,
success: function(rs) {
rs = JSON.parse(rs);
$('.upload-tip')
.addClass('text-success')
.removeClass('text-error')
.text(rs.len + '个文件上传成功, number项值为' + rs.num)
.show();
},
error: function(err) { }
});
});
</script>
后记:
另外,可以考虑给文件上传增加进度条
而当文件太大时,会因为后端支持的最大文件size不够造成崩溃,这时可以考虑进行断点续传
断点续传,前端可以通过二进制流和本地存储的结合来实现,这里就不多说了