简述
使用form和iframe实现异步上传文件的功能
原理
form表单可以上传文件,但是提交form表单会打开新页面或刷新页面,为了不刷新页面,将form指向一个隐藏的iframe,form提交后它所指向的iframe会重新加载,iframe的内容即为服务器返回的内容。
优点
兼容各种浏览器各个版本的文件异步上传
缺点
如果要上传的文件需要校验(类型,大小),对于无法获取files属性的浏览器而言,用户体验性不好。
文件需要校验(类型,大小)的处理
能够获取到files就在前台校验,否则后台校验。
示例代码
HTML
<input type="file" name="file" />
JavaScript
$(function(){
/* 监听change事件 */
$('input[name=file]').on('change', function(e){
var $target = $(e.target);
/* 获取/创建 iframe */
var $frame = $('iframe[name=upload_frame]');
if(!$frame.length){
$frame = $('<iframe>', {name: 'upload_frame', style: 'display: none;'});
}
/* 创建form */
var $form = $('<form>', {
method: 'POST', // post方式提交
action: 'sever_address', // 文件上传的服务器地址
target: 'upload_frame', // 指向iframe
enctype:'multipart/form-data' // 向服务器发送二进制数据
});
/* 嵌套form */
$target.wrap($form)
/* 提交表单 */
$target.parent().submit();
/* 处理回调 */
$frame.on('load', function(){
/* 去除嵌套的form */
$target.parent().replaceWith($target);
/* 接收服务器返回的数据 */
var resp = $frame.contents().text();
try {
resp = JSON.parse(resp);
/* 提示上传成功?按理说接下的代码才是真正的回调 */
} catch (error) {
console.error(error);
}
});
});
});
完整的示例
HTML
<input type="file" name="file" />
JavaScript
$(function(){
var utils = new this.Utils();
$('input[type=file]').on('change', function(e){
utils.uploadQiNiu({
$target: $(e.target), # 文件选择器
prefix: 'subjective/', # 前缀
limitSize: 5, # 限制文件最大为5M
limitType: ['png', 'jpg'], # 限制文件类型,
callback: function(uri){
utils.showMsg('上传成功: ' + uri);
}
});
});
})
后台代码 (Python方式实现)
def upload_file(request):
# 返回信息
def render(msg_='上传失败', uri_='', name_='', type_=''):
return HttpResponse(
content=json.dumps({
'uri': uri_,
'msg': msg_,
'name': name_,
'type': type_,
}),
content_type="text/html;charset=utf-8",
status=200
)
# 获取文件
file_obj = request.FILES.get('file', None)
# 无文件对象的处理
if not file_obj:
return render("请选择文件!")
post_vars = request.POST
file_type = file_obj.content_type
# 判断是否需要验证(类型,大小)
if post_vars.get('validate', False):
# 验证文件大小
limit_size = int(post_vars.get('limitSize', 0))
if limit_size > 0 and file_obj.size > limit_size * 1024 ** 2:
return render('文件大小不能超过{}M!'.format(limit_size))
# 验证文件类型
limit_type = post_vars.get('limitType', '')
reg = re.compile(file_type, re.I)
if limit_type and not reg.match(limit_type.replace(',', '|')):
return render('类型不符, 请上传{}格式的文件!'.format(limit_type))
# 验证文件后缀
suffix_type = post_vars.get('suffixType', '')
if suffix_type:
file_type = file_obj.name.split('.').pop()
reg = re.compile(file_type, re.I)
if not reg.match(suffix_type.replace(',', '|')):
return render('类型不符, 请上传{}格式的文件!'.format(suffix_type))
filename = file_obj.name
prefix = post_vars.get('prefix', '')
timestamp = int(time.time())
key = '{}{}{}'.format(prefix, timestamp, filename)
# 获取配置信息
access_key = settings.QINIU_ACCESS_KEY
secret_key = settings.QINIU_SECRET_KEY
bucket = settings.QINIU_CMS_RESOURCE_BUCKET
host = settings.QINIU_CMS_RESOURCE_URL
# 获取上传凭证
token = Auth(access_key, secret_key).upload_token(bucket, key, 7200)
# 上传到七牛
try:
ret, info = put_data(
token, key, file_obj.read(), mime_type=file_obj.content_type
)
uri = '{}/{}'.format(host, ret['key'])
msg = '上传成功'
except Exception as ex:
logging.error(ex)
uri = ''
msg = '上传失败'
return render(msg, uri, filename, file_type)
JavaScript 提取的公共方法 utils.js
// Generated by CoffeeScript 1.12.4
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
this.Utils = (function() {
function Utils() {
this.bodyLock = bind(this.bodyLock, this);
this.showLoading = bind(this.showLoading, this);
this.confirmMsg = bind(this.confirmMsg, this);
this.showMsg = bind(this.showMsg, this);
this.uploadQiNiu = bind(this.uploadQiNiu, this);
}
Utils.prototype.uploadQiNiu = function(options) {
var $form, $frame, $target, file, fileType, files, limitSize, limitType, settings, suffixType;
settings = {
$target: null,
prefix: 'project/',
limitSize: 0,
limitType: [],
suffixType: [],
callback: (function(_this) {
return function() {};
})(this)
};
$.extend(settings, options);
$target = settings.$target;
if (!$target) {
console.error('Missing "$target" in settings');
return false;
}
files = $target[0].files;
if (files) {
file = files[0];
if (file == null) {
this.showMsg('请选择文件!');
return false;
}
limitSize = parseFloat(settings.limitSize);
if (limitSize > 0 && file.size > limitSize * 1024 * 1024) {
this.showMsg("文件大小不能超过" + limitSize + "M!");
$target.val('');
return false;
}
limitType = settings.limitType;
fileType = file.type.toLowerCase();
if (limitType.length && !fileType.match(eval("/" + (limitType.join('|')) + "/"))) {
this.showMsg("类型不符, 请上传" + limitType + "格式的文件!");
$target.val('');
return false;
}
suffixType = settings.suffixType;
if (suffixType.length) {
fileType = file.name.split('.').pop().toLowerCase();
if (!fileType.match(eval("/" + (suffixType.join('|')) + "/"))) {
this.showMsg("类型不符, 请上传" + suffixType + "格式的文件!");
$target.val('');
return false;
}
}
}
this.showLoading('正在上传...');
$frame = $('iframe[name=upload_frame]');
if (!$frame.length) {
$frame = $('<iframe>', {
name: 'upload_frame',
style: 'display: none'
});
$(document.body).append($frame);
}
$frame.on('load', (function(_this) {
return function() {
var resp;
_this.bodyLock(false);
$target.parent().replaceWith($target);
resp = $frame.contents().text();
if (resp.indexOf('413 Request Entity Too Large') !== -1) {
_this.showMsg('上传文件过大!');
console.error('413 Request Entity Too Large');
return;
}
resp = JSON.parse(resp);
_this.showMsg(resp.msg);
return settings.callback(resp.uri, resp.name, (files ? fileType : resp.type), resp.msg);
};
})(this));
$target.attr('name', 'file');
$form = $('<form>', {
method: "post",
name: "upload_form",
target: "upload_frame",
action: "/qi_niu_upload/",
enctype: "multipart/form-data",
style: 'display:none'
});
$target.wrap($form);
$target.after("<input type='hidden' name='prefix' value='" + settings.prefix + "'>");
if (!files) {
$target.after("<input type='hidden' name='validate' value='True'>");
$target.after("<input type='hidden' name='limitSize' value='" + settings.limitSize + "'>");
$target.after("<input type='hidden' name='limitType' value='" + settings.limitType + "'>");
$target.after("<input type='hidden' name='suffixType' value='" + settings.suffixType + "'>");
}
$target.parent().submit();
};
Utils.prototype.showMsg = function(msg, timeout) {
if (timeout == null) {
timeout = 1000;
}
return $.jBox.tip(msg, '', {
timeout: timeout
});
};
Utils.prototype.confirmMsg = function(msg, fun) {
return $.jBox.confirm(msg, '提示', (function(_this) {
return function(v) {
if (v === 'ok') {
return fun();
}
};
})(this));
};
Utils.prototype.showLoading = function(msg) {
$.jBox.tip(msg, 'loading');
$('.jbox-fade').css('height', ($(document).height()) + "px");
return this.bodyLock(true);
};
Utils.prototype.bodyLock = function(lock) {
var body, flag;
body = $('body');
flag = lock ? 'hidden' : 'auto';
return body.css('overflow', flag);
};
return Utils;
})();
}).call(this);
utils.js 对应的源文件 CoffeeScript
class @Utils
# 上传文件
uploadQiNiu: (options) =>
settings =
$target: null # 文件选择器
prefix: 'project/' # 七牛空间前缀
limitSize: 0 # 限制大小
limitType: [] # 文件类型
suffixType: [] # 后缀类型
callback: () => # 回调函数
# 配置信息
$.extend settings, options
# 判断文件选择器是否存在
$target = settings.$target
if not $target
console.error 'Missing "$target" in settings'
return false
files = $target[0].files
# 能获取到files属性
if files
file = files[0]
# 判断是否选中文件
if not file?
@showMsg '请选择文件!'
return false
# 判断是否需要限制文件大小
limitSize = parseFloat settings.limitSize
if limitSize > 0 and file.size > limitSize * 1024 * 1024
@showMsg "文件大小不能超过#{limitSize}M!"
$target.val ''
return false
# 判断是否需要限制文件类型
limitType = settings.limitType
fileType = file.type.toLowerCase()
if limitType.length and not fileType.match eval "/#{limitType.join '|'}/"
@showMsg "类型不符, 请上传#{limitType}格式的文件!"
$target.val ''
return false
# 判断是否需要限制文件后缀
suffixType = settings.suffixType
if suffixType.length
fileType = file.name.split('.').pop().toLowerCase()
if not fileType.match eval "/#{suffixType.join '|'}/"
@showMsg "类型不符, 请上传#{suffixType}格式的文件!"
$target.val ''
return false
@showLoading '正在上传...'
$frame = $ 'iframe[name=upload_frame]'
if not $frame.length
$frame = $ '<iframe>',
name: 'upload_frame'
style: 'display: none'
$(document.body).append $frame
$frame.on 'load', () =>
@bodyLock false
$target.parent().replaceWith($target)
resp = $frame.contents().text()
if resp.indexOf('413 Request Entity Too Large') != -1
@showMsg '上传文件过大!'
console.error('413 Request Entity Too Large')
return
resp = JSON.parse resp
@showMsg resp.msg
settings.callback resp.uri, resp.name, (if files then fileType else resp.type), resp.msg
# 后台获取文件的key为file
$target.attr 'name', 'file'
$form = $ '<form>',
method: "post"
name: "upload_form"
target: "upload_frame"
action: "/qi_niu_upload/"
enctype: "multipart/form-data"
style: 'display:none'
# 外层包装form
$target.wrap($form)
$target.after("<input type='hidden' name='prefix' value='#{settings.prefix}'>")
if not files
$target.after("<input type='hidden' name='validate' value='True'>")
$target.after("<input type='hidden' name='limitSize' value='#{settings.limitSize}'>")
$target.after("<input type='hidden' name='limitType' value='#{settings.limitType}'>")
$target.after("<input type='hidden' name='suffixType' value='#{settings.suffixType}'>")
$target.parent().submit()
return
# 提示信息
showMsg: (msg, timeout=1000) =>
$.jBox.tip msg, '', {timeout: timeout}
# 确认窗口
confirmMsg: (msg, fun) =>
$.jBox.confirm msg, '提示', (v) =>
if v is 'ok'
fun()
# 加载中
showLoading: (msg) =>
$.jBox.tip msg, 'loading'
$('.jbox-fade').css 'height', "#{$(document).height()}px"
@bodyLock true
# 设置body是否可以滚动
bodyLock: (lock) =>
body = $ 'body'
flag = if lock then 'hidden' else 'auto'
body.css 'overflow', flag