uploader.js组件原理

作者:zccst

2014-12-04
重复提交时的bug
(1)除file字段外的其他字段多次重复添加时出现重复。
需要在添加时先检查是否已经添加过,如果添加过就更新,没添加过再append。


(2)file字段第二次添加时filename为空。


2014-11-14

效果图:
[img]http://dl2.iteye.com/upload/attachment/0103/1887/fc2a0639-769d-3e95-8d44-8441fd972ade.jpg[/img]
优点:多浏览器统一样式。(原生input type="file"在不同浏览器下表现不一致,对于很多系统这是不能接受的)


html:

<span class="button_box1"><a href="javascript:;" name="file" id="file" style=""><span>选择文件</span></a></span>
<span class="batch-upload-filename"></span>



js:上传文件初始化

//初始化文件上传组件
initUploader:function(){
var _this = this;
var splitPathRe = /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;
var splitPath = function(filename) {
return splitPathRe.exec(filename).slice(1);
};

//Excel上传控件
this.batch_uploader = new Uploader({
trigger: '#file',
name: 'file',
action: CREATE_EXCELFEED_URL,
accept: 'application/vnd.ms-excel',
data: {},
dataType:'json',
multiple: false
}).change(function(files) {
for (var i = 0; i < files.length; i++) {
var fileType = splitPath(files[i].name)[3];
if (fileType !== ".xls") {
alert("文件格式错误, 请上传.xls格式文件!");
} else {
$('.batch-upload-filename').html(files[i].name);
}
}
}).success(function(response) {

/**/
var res;
if($.type(response) === "string"){ // IE浏览器走这个逻辑
res = $.parseJSON(response);
}else if($.type(response) === "object"){//FF,Chrome走这个逻辑
res = response;
}else{//奇怪的第三种情况
res = $.parseJSON($(response).text());
}
/*旧实现方式
if(/msie/.test(navigator.userAgent.toLowerCase())) {
if($.type(response) === "string"){
res = $.parseJSON(response);
}else{
res = $.parseJSON($(response).text());
}
} else {
console.log(response, $.type(response));
}*/

if (res.flag == 0) {
//成功后刷新
_this.createSuccessCallback();
} else {
var msg = res.msg || [];
alert("上传失败,原因:"+ (msg.length !== 0 ? msg.join('\r\n') : '未知。') );
}

//如果选择文件立即上传时
//_this.$el.find('.batch-upload-filename').html(' 未选择文件');
}).error(function(file) {
alert("上传"+file+"失败,请重试。");
});
},


js:上传文件提交

if(this.batch_uploader._uploaders[0]._files) {
this.$el.find(".errormsg").hide();
this.batch_uploader._uploaders[0].form.append(_this.createInputs(params));
this.batch_uploader.submit();

} else {
this.$el.find(".errormsg").html("请先选择要上传的文件,再提交!").show();
return false;
}



需要后端配合的是:
content-type:text/html。不能是text/javascript,踩过的坑坑,深深的痛。


接下来是组件分析
简单说一下实现原理,通过用户在初始化中填入的设置,默认创建一个透明的form表单和iframe。其中form的target设置为iframe的name,目的是为了跳转。巧妙的是该表单的宽高和看得见的上传按钮宽高完全一致,但zIndex高10(自己设置的)

用户点击按钮时,实际上是在点击表单的input输入框。而负责样式的a标签则没有任何事件。

为透明度为0的input输入框添加了change事件,用户一旦选择文件,则会触发该事件。可以做的事情,比如校验,将文件名写入右侧的span,让用户看上去选择了一个文件。

提交:分两种情况,IE和非IE。
如果是非IE,则使用$.ajax上传,同时设置一个xhr,还可以查看上传进度。
如果是IE,则使用form+iframe方式,并且注册iframe的onload监听事件,服务器端将返回结果放置到iframe中后,onload监听事件需要做的事情是:拿到后端返回的数据,做判断,然后传给setting里注册的回调函数success或error。


里面涉及好几个知识点,比如:
1,FileList
2,formData
3,取iframe的值
var doc = self.iframe[0].contentDocument ? self.iframe[0].contentDocument : self.iframe[0].contentWindow.document;
var str = doc.body.innerHTML;


为了方便看,附两张图:
一个是settings
[img]http://dl2.iteye.com/upload/attachment/0103/1907/66fb88f3-6ed6-3aa7-809a-6a941c67f89f.jpg[/img]


一个是Uploader对象在setup时的this
[img]http://dl2.iteye.com/upload/attachment/0103/1902/4ec17d30-a075-3d15-9a4b-b7a42581ccca.jpg[/img]


一个是Uploader对象在submit时的this(区别是多了_files)
[img]http://dl2.iteye.com/upload/attachment/0103/1904/e44b0947-1a7e-3bbc-83e4-d0820d712cc0.jpg[/img]


uploader.js组件


define(function(require, exports, module){
//var $ = require('jquery');
var iframeCount = 0;

function Uploader(options) {
if (!(this instanceof Uploader)) {
return new Uploader(options);
}
if (isString(options)) {
options = {trigger: options};
}

var settings = {
trigger: null,
name: null,
action: null,
data: null,
accept: null,
change: null,
error: null,
multiple: true,
success: null
};
if (options) {
$.extend(settings, options);
}
var $trigger = $(settings.trigger);

settings.action = settings.action || $trigger.data('action') || '/upload';
settings.name = settings.name || $trigger.attr('name') || $trigger.data('name') || 'file';
settings.data = settings.data || parse($trigger.data('data'));
settings.accept = settings.accept || $trigger.data('accept');
settings.success = settings.success || $trigger.data('success');
this.settings = settings;

this.setup();
this.bind();
}

// initialize
// create input, form, iframe
Uploader.prototype.setup = function() {
this.form = $(
'<form class="earth-upload" method="post" enctype="multipart/form-data"'
+ 'target="" action="' + this.settings.action + '"></form>'
);

this.iframe = newIframe();//<iframe name="iframe-uploader-0" style="display: none;">
//<form class="earth-upload" action="/feed/createExcelFeed.action" target="iframe-uploader-0" enctype="multipart/form-data" method="post"></form>
this.form.attr('target', this.iframe.attr('name'));
var data = this.settings.data;

this.form.append(createInputs(data));

if (window.FormData) {
//<input value="formdata" name="_uploader_" type="hidden">
this.form.append(createInputs({'_uploader_': 'formdata'}));
} else {
//<input value="iframe" name="_uploader_" type="hidden">
this.form.append(createInputs({'_uploader_': 'iframe'}));
}

var input = document.createElement('input');
input.type = 'file';
input.name = this.settings.name;
if (this.settings.accept) {
input.accept = this.settings.accept;
}
if (this.settings.multiple) {
input.multiple = true;
input.setAttribute('multiple', 'multiple');
}
this.input = $(input);
//<input type="file" name="file" accept="application/vnd.ms-excel">

var $trigger = $(this.settings.trigger);
this.input.attr('hidefocus', true).css({
position: 'absolute',
top: 0,
right: 0,
opacity: 0,
outline: 0,
cursor: 'pointer',
height: $trigger.outerHeight(),
fontSize: Math.max(64, $trigger.outerHeight() * 5)
});
//<input type="file" name="file" accept="application/vnd.ms-excel" hidefocus="true" style="position: absolute; top: 0px; right: 0px; opacity: 0; outline: 0px none; cursor: pointer; height: 30px; font-size: 150px;">

this.form.append(this.input);

this.form.css({
position: 'absolute',
top: $trigger.offset().top,
left: $trigger.offset().left,
overflow: 'hidden',
width: $trigger.outerWidth(),
height: $trigger.outerHeight(),
zIndex: findzIndex($trigger) + 10
}).appendTo('body');

/*
* <form action="/feed/createExcelFeed.action" target="iframe-uploader-0" enctype="multipart/form-data" method="post" class="earth-upload" style="position: absolute; top: 177px; left: 371px; overflow: hidden; width: 80px; height: 30px; z-index: 1037;">
* <input type="hidden" name="_uploader_" value="formdata">
* <input type="file" name="file" accept="application/vnd.ms-excel" hidefocus="true" style="position: absolute; top: 0px; right: 0px; opacity: 0; outline: 0px none; cursor: pointer; height: 30px; font-size: 150px;">
* </form>
* */
return this;
};

// bind events
Uploader.prototype.bind = function() {
var self = this;
var $trigger = $(self.settings.trigger);
$trigger.mouseenter(function() {
self.form.css({
top: $trigger.offset().top,
left: $trigger.offset().left,
width: $trigger.outerWidth(),
height: $trigger.outerHeight()
});
});
self.bindInput();
};

Uploader.prototype.bindInput = function() {
var self = this;
self.input.change(function(e) {
// ie9 don't support FileList Object
// http://stackoverflow.com/questions/12830058/ie8-input-type-file-get-files
self._files = this.files || [{
name: e.target.value
}];// files 是一个 FileList 对象(类似于NodeList对象)

var file = self.input.val();
if (self.settings.change) {
self.settings.change.call(self, self._files);
} else if (file) {
return self.submit();
}
});
};

// handle submit event
// prepare for submiting form
Uploader.prototype.submit = function() {
var self = this;
if (window.FormData && self._files) {
// build a FormData
var form = new FormData(self.form.get(0));
// use FormData to upload
form.append(self.settings.name, self._files);

var optionXhr;
if (self.settings.progress) {
// fix the progress target file
var files = self._files;
optionXhr = function() {
var xhr = $.ajaxSettings.xhr();
if (xhr.upload) {
xhr.upload.addEventListener('progress', function(event) {
var percent = 0;
var position = event.loaded || event.position; /*event.position is deprecated*/
var total = event.total;
if (event.lengthComputable) {
percent = Math.ceil(position / total * 100);
}
self.settings.progress(event, position, total, percent, files);
}, false);
}
return xhr;
};
}

$.ajax({
url: self.settings.action,
type: 'post',
processData: false,
contentType: false,
data: form,
xhr: optionXhr,
context: this,
dataType:self.settings.dataType,
success: self.settings.success,
error: self.settings.error
});
return this;
} else {
// iframe upload
self.iframe = newIframe();
self.form.attr('target', self.iframe.attr('name'));
$('body').append(self.iframe);
self.iframe.one('load', function() {
var doc = self.iframe[0].contentDocument
? self.iframe[0].contentDocument
: self.iframe[0].contentWindow.document;
var str = doc.body.innerHTML;
if(str){
if (self.settings.success) {
self.settings.success(str);
}
}else{
if (self.settings.error) {
self.settings.error(self.input.val());
}
}
/*实现方法二
// https://github.com/blueimp/jQuery-File-Upload/blob/9.5.6/js/jquery.iframe-transport.js#L102
// Fix for IE endless progress bar activity bug
// (happens on form submits to iframe targets):
$('<iframe src="javascript:false;"></iframe>')
.appendTo(self.form)
.remove();

var response = $(this).contents().find('body').html();
$(this).remove();
if (!response) {
if (self.settings.error) {
self.settings.error(self.input.val());
}
} else {
if (self.settings.success) {
self.settings.success(response);
}
}*/
});
self.form.submit();
}
return this;
};

Uploader.prototype.refreshInput = function() {
//replace the input element, or the same file can not to be uploaded
var newInput = this.input.clone();
this.input.before(newInput);
this.input.off('change');
this.input.remove();
this.input = newInput;
this.bindInput();
};

// handle change event
// when value in file input changed
Uploader.prototype.change = function(callback) {
if (!callback) {
return this;
}
this.settings.change = callback;
return this;
};

// handle when upload success
Uploader.prototype.success = function(callback) {
var me = this;
this.settings.success = function(response) {
me.refreshInput();
if (callback) {
callback(response);
}
};

return this;
};

// handle when upload success
Uploader.prototype.error = function(callback) {
var me = this;
this.settings.error = function(response) {
if (callback) {
me.refreshInput();
callback(response);
}
};
return this;
};

// enable
Uploader.prototype.enable = function(){
this.input.prop('disabled', false);
this.input.css('cursor', 'pointer');
};

// disable
Uploader.prototype.disable = function(){
this.input.prop('disabled', true);
this.input.css('cursor', 'not-allowed');
};

// Helpers
// -------------

function isString(val) {
return Object.prototype.toString.call(val) === '[object String]';
}

function createInputs(data) {
if (!data) return [];

var inputs = [], i;
for (var name in data) {
i = document.createElement('input');
i.type = 'hidden';
i.name = name;
i.value = data[name];
inputs.push(i);
}
return inputs;
}

function parse(str) {
if (!str) return {};
var ret = {};

var pairs = str.split('&');
var unescape = function(s) {
return decodeURIComponent(s.replace(/\+/g, ' '));
};

for (var i = 0; i < pairs.length; i++) {
var pair = pairs[i].split('=');
var key = unescape(pair[0]);
var val = unescape(pair[1]);
ret[key] = val;
}

return ret;
}

function findzIndex($node) {
var parents = $node.parentsUntil('body');
var zIndex = 0;
for (var i = 0; i < parents.length; i++) {
var item = parents.eq(i);
if (item.css('position') !== 'static') {
zIndex = parseInt(item.css('zIndex'), 10) || zIndex;
}
}
return zIndex;
}

function newIframe() {
var iframeName = 'iframe-uploader-' + iframeCount;
var iframe = $('<iframe src="" id="' + iframeName + '" name="' + iframeName + '"></iframe>').hide();
iframeCount += 1;
return iframe;
}

function MultipleUploader(options) {
if (!(this instanceof MultipleUploader)) {
return new MultipleUploader(options);
}

if (isString(options)) {
options = {trigger: options};
}
var $trigger = $(options.trigger);

var uploaders = [];
$trigger.each(function(i, item) {
options.trigger = item;
uploaders.push(new Uploader(options));
});
this._uploaders = uploaders;
}
MultipleUploader.prototype.submit = function() {
$.each(this._uploaders, function(i, item) {
item.submit();
});
return this;
};
MultipleUploader.prototype.change = function(callback) {
$.each(this._uploaders, function(i, item) {
item.change(callback);
});
return this;
};
MultipleUploader.prototype.success = function(callback) {
$.each(this._uploaders, function(i, item) {
item.success(callback);
});
return this;
};
MultipleUploader.prototype.error = function(callback) {
$.each(this._uploaders, function(i, item) {
item.error(callback);
});
return this;
};
MultipleUploader.prototype.enable = function (){
$.each(this._uploaders, function (i, item){
item.enable();
});
return this;
};
MultipleUploader.prototype.disable = function (){
$.each(this._uploaders, function (i, item){
item.disable();
});
return this;
};
MultipleUploader.Uploader = Uploader;

module.exports = MultipleUploader;

});



如果您觉得本文的内容对您的学习有所帮助,您可以微信:
[img]http://dl2.iteye.com/upload/attachment/0109/0668/fb266dfa-95ca-3d09-b41e-5f04a19ba9a1.png[/img]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值