>  기사  >  웹 프론트엔드  >  분리 및 상속 아이디어로 이미지 업로드 후 미리보기 기능 구현 : ImageUploadView_javascript 기법

분리 및 상속 아이디어로 이미지 업로드 후 미리보기 기능 구현 : ImageUploadView_javascript 기법

WBOY
WBOY원래의
2016-05-16 15:06:131433검색

이 글에서 소개할 내용은 웹 페이지에 일반적인 이미지를 업로드한 후 페이지에서 직접 작은 이미지 미리보기를 생성하는 구현 아이디어입니다. 이 기능이 특정 적용 가능성을 고려하여 관련 로직을 실제 ImageUploadView 구성 요소에 캡슐화합니다. 다음 단락의 git 렌더링에서 효과를 볼 수 있습니다. 이 컴포넌트를 구현하는 과정에서 우리는 모든 컴포넌트에 대해 상속 라이브러리인 class.js, 이벤트 관리 라이브러리인 eventBase.js 등 이전 블로그에서 소개한 관련 콘텐츠를 사용했으며 자체적인 책임, 성능 및 두 가지 측면에 대한 몇 가지 생각을 읽고 교환할 수 있습니다.

데모 효과:

참고: 데모용 코드는 모두 정적이므로 파일 업로드 구성 요소는 setTimeout을 사용하여 시뮬레이션됩니다. 그러나 호출 방법은 실제 작업에서 사용하는 업로드 구성 요소와 완전히 동일하므로 데모 효과용 코드는 다음과 같습니다. 실제 기능 요구 사항을 완벽하게 충족합니다.

이전 블로그의 아이디어에 따라 먼저 업로드 미리보기 기능에 대한 요구사항을 소개하겠습니다.

1. 수요 분석

이전 시연 렌더링에 따르면 분석 요구 사항은 다음과 같습니다.

1) 처음에는 업로드 영역에 클릭 가능한 업로드 버튼만 표시됩니다. 버튼을 클릭하면 성공적으로 업로드된 이미지가
뒤의 미리보기 영역에 표시됩니다.

2) 업로드한 이미지를 미리보기 영역에 추가한 후 삭제버튼을 눌러 삭제할 수 있습니다

3) 업로드된 총 이미지 수가 특정 한도에 도달하면(예: 데모의 업로드 한도는 4개) 업로드 버튼이 제거됩니다.

4) 업로드된 사진의 총 개수가 일정 한도에 도달한 경우, 삭제 작업을 통해 사진이 제거되면 업로드 버튼이 다시 표시되어야 합니다.

위의 요구사항이 눈에 보이는데, 경험을 토대로 분석할 수 있는 요구사항은 다음과 같습니다.

1) 페이지가 편집 상태(데이터베이스에서 쿼리된 상태)인 경우 사진 목록이 비어 있지 않은 한 처음에 사진이 표시되어야 하며 검색된 사진 목록의 길이를 따라야 합니다. 업로드 버튼 표시 여부를 제한합니다.

2) 현재 페이지가 보기만 가능하고 변경이 불가능한 상태라면 업로드 버튼과 삭제 버튼을 초기에 제거해야 합니다.

이제 요구사항 분석이 완료되었으므로 구현 아이디어를 설명하겠습니다.

2. 구현 아이디어

양식 페이지이기 때문에 이미지를 업로드한 후 백엔드에 제출하려면 반드시 텍스트 필드가 필요하므로 정적 페이지를 만들 때 이 텍스트 필드를 고려했습니다. 이미지가 업로드되었으므로 이미지를 삭제한 후에도 이 텍스트 필드의 값을 수정해야 합니다. 정적인 페이지를 만들 때 이 부분의 구조는 다음과 같았습니다.

<div class="holy-layout-am appForm-group appForm-group-img-upload clearfix">
<label class="holy-layout-al">法人身份证电子版</label>
<div class="holy-layout-m">
<input id="legalPersonIDPic-input" name="legalPersonIDPic" class="form-control form-field"
type="hidden">
<ul id="legalPersonIDPic-view" class="image-upload-view clearfix">
<li class="view-item-add"><a class="view-act-add" href="javascript:;" title="点击上传">+</a>
</li>
</ul>
<p class="img-upload-msg">
请确保图片清晰,文字可辨
<a href="#" title="查看示例"><i class="fa fa-question-circle"></i> 查看示例</a>
</p>
</div>
</div>

从这个结构还可以看出,我把整个上传区域都放在一个ul里面,然后把ul的第一个li作为上传按钮来使用。为了完成这个功能,我们主要的任务是:上传及上传后的回调,新增或删除图片预览以及文本域值的管理。从这一点,结合职责分离的思想,这个功能至少需要三个组件,一个负责文件上传,一个负责图片预览的管理,一个负责文本域值的管理。千万不能把这三个功能,两两或者全部都封装在一起,那样的话功能耦合太强,写出来的组件可扩展性可重用性不高。如果这三个组件之间需要交互,我们只要借助回调函数或者发布-订阅模式定义它们给外部调用的接口即可。

不过文本域值的管理本身就很简单,写不写成组件都关系不大,但是至少函数级别的封装是得有的;文件上传组件虽然不是本文的重点,但是网上有很多现成的开源插件,比如webuploader,不管是直接用还是做二次封装都可以应用进来;图片预览的功能是本文的核心内容,ImageUploadView这个组件就是对它的封装,从需求来看,这个组件有语义的实例方法无非就是三个,分别是render, append, delItem,其中render用来在初始化完成之后显示初始的预览列表,append用来在上传成功后添加新的图片预览,delItem用来删除已有的图片预览,按照这个基本思路,我们只需要再结合需求和组件开发的经验为它设计好options和事件即可。

从前面的需求我们发现,这个ImageUploadView组件的render会受到页面状态的影响,当页面为查看模式时,这个组件不能做上传和删除的操作,所以可以考虑给它加一个readonly的option。同时它的上传和删除操作还会影响到上传按钮的UI逻辑,这个跟上传限制有关系,为了灵活性,也得把上传限制作为一个option。从前一段提到的三个实例方法来说,按照自己以前定义事件的经验,一般一个实例方法会定义一对事件,就像bootstrap的插件的做法一样,比如render方法,可以定义一个render.before,这个事件在render的主要逻辑执行前触发,如果外部监听器调用了这个事件的preventDefault()方法,那么render的主要逻辑都不会执行;还有一个render.after事件,这个事件在render的主要逻辑执行后触发。这种成对定义事件的好处是,既给外部提供扩展组件功能的方法,又能增加组件默认行为的管理。

最后从我之前的工作经验来说,除了有上传图片进行预览这样的功能,我曾经还做过上传视频,上传音频,上传普通文档等类似的,所以这一次碰到这个功能的时候我就觉得应该把这些功能里面相似的东西抽取出来,作为一个基类,图片上传,视频上传等分别继承这个基类去实现各自的逻辑。这个基类还有一个好处,就是能够让那些通用的逻辑完全与HTML结构分离,在这个基类里面只做一些通用的事情,比如options与组件行为(render, append, delItem)的定义,以及通用事件的监听和触发,它只要留有固定的接口留给子类来实现即可。在后面的实现中,我定义了一个FileUploadBaseView组件来完成这个基类的功能,这个基类不包含任何html或css处理的逻辑,它只是抽象了我们要完成的功能,不处理任何业务逻辑。根据业务逻辑实现的子类会受html结构的限制,所以子类的适用范围小;而基类因为做到了与html结构完全分离,所以有更大的适用范围。

3. 实现细节

从第2部分的实现思路,要实现的类有:FileUploadBaseView和ImageUploadView,前者是后者的基类。同时考虑到要给组件提供事件管理的功能,所以要用到上一篇博客的eventBase.js,FileUploadBaseView得继承该库的EventBase组件;考虑到要有类的定义和继承,还要用到之前写的继承库class.js来定义组件以及组件的继承关系。相关组件的继承关系为:ImageUploadView extend FileUploadBaseView extend EventBase。

(注:以下相关代码中模块化用的是seajs。)

FileUploadBaseView所做的事情有:

1)定义通用的option以及通用的事件管理

在该组件的DEFAULTS配置中可以看到所有的通用option和通用事件的定义:

var DEFAULTS = {
data: [], //要展示的数据列表,列表元素必须是object类型的,如[{url: 'xxx.png'},{url: 'yyyy.png'}]
sizeLimit: 0, //用来限制BaseView中的展示的元素个数,为0表示不限制
readonly: false, //用来控制BaseView中的元素是否允许增加和删除
onBeforeRender: $.noop, //对应render.before事件,在render方法调用前触发
onRender: $.noop, //对应render.after事件,在render方法调用后触发
onBeforeAppend: $.noop, //对应append.before事件,在append方法调用前触发
onAppend: $.noop, //对应append.after事件,在append方法调用后触发
onBeforeDelItem: $.noop, //对应delItem.before事件,在delItem方法调用前触发
onDelItem: $.noop //对应delItem.after事件,在delItem方法调用后触发
};

在该组件的init方法中可以看到对通用option和事件管理的初始化逻辑:

init: function (element, options) {
//通过this.base调用父类EventBase的init方法
this.base(element);
//实例属性
var opts = this.options = this.getOptions(options);
this.data = resolveData(opts.data);
delete opts.data;
this.sizeLimit = opts.sizeLimit;
this.readOnly = opts.readOnly;
//绑定事件
this.on('render.before', $.proxy(opts.onBeforeRender, this));
this.on('render.after', $.proxy(opts.onRender, this));
this.on('append.before', $.proxy(opts.onBeforeAppend, this));
this.on('append.after', $.proxy(opts.onAppend, this));
this.on('delItem.before', $.proxy(opts.onBeforeDelItem, this));
this.on('delItem.after', $.proxy(opts.onDelItem, this));
},

2)定义组件的行为,预留可供子类实现的接口:

render: function () {
/**
* render是一个模板,子类不需要重写render方法,只需要重写_render方法
* 当调用子类的render方法时调用的是父类的render方法
* 但是执行到_render方法时,调用的是子类的_render方法
* 这样就能把before跟after事件的触发操作统一起来
*/
var e;
this.trigger(e = $.Event('render.before'));
if (e.isDefaultPrevented()) return;
this._render();
this.trigger($.Event('render.after'));
},
//子类需实现_Render方法
_render: function () {
},
append: function (item) {
var e;
if (!item) return;
item = resolveDataItem(item);
this.trigger(e = $.Event('append.before'), item);
if (e.isDefaultPrevented()) return;
this.data.push(item);
this._append(item);
this.trigger($.Event('append.after'), item);
},
//子类需实现_append方法
_append: function (data) {
},
delItem: function (uuid) {
var e, item = this.getDataItem(uuid);
if (!item) return;
this.trigger(e = $.Event('delItem.before'), item);
if (e.isDefaultPrevented()) return;
this.data.splice(this.getDataItemIndex(uuid), 1);
this._delItem(item);
this.trigger($.Event('delItem.after'), item);
},
//子类需实现_delItem方法
_delItem: function (data) {
}

为了统一处理行为前后的事件派发逻辑,将render, append ,delItem的主要逻辑抽出来成为需被子类实现的方法_render, _append和_delItem。当调用子类的render方法时,调用的实际上父类的方法,但是当父类执行到_render方法时,执行的就是子类的方法,另外两个方法也是类似的处理。需要注意的是子类不能去覆盖render, append ,delItem三个方法,否则就得自己去处理相关事件的触发逻辑。

FileUploadBaseView整体实现如下:

define(function (require, exports, module) {
var $ = require('jquery');
var Class = require('mod/class');
var EventBase = require('mod/eventBase');
var DEFAULTS = {
data: [], //要展示的数据列表,列表元素必须是object类型的,如[{url: 'xxx.png'},{url: 'yyyy.png'}]
sizeLimit: 0, //用来限制BaseView中的展示的元素个数,为0表示不限制
readonly: false, //用来控制BaseView中的元素是否允许增加和删除
onBeforeRender: $.noop, //对应render.before事件,在render方法调用前触发
onRender: $.noop, //对应render.after事件,在render方法调用后触发
onBeforeAppend: $.noop, //对应append.before事件,在append方法调用前触发
onAppend: $.noop, //对应append.after事件,在append方法调用后触发
onBeforeDelItem: $.noop, //对应delItem.before事件,在delItem方法调用前触发
onDelItem: $.noop //对应delItem.after事件,在delItem方法调用后触发
};
/**
* 数据处理,给data的每条记录都添加一个_uuid的属性,方便查找
*/
function resolveData(data) {
var time = new Date().getTime();
return $.map(data, function (d) {
return resolveDataItem(d, time);
});
}
function resolveDataItem(data, time) {
time = time || new Date().getTime();
data._uuid = '_uuid' + time + Math.floor(Math.random() * 100000);
return data;
}
var FileUploadBaseView = Class({
instanceMembers: {
init: function (element, options) {
//通过this.base调用父类EventBase的init方法
this.base(element);
//实例属性
var opts = this.options = this.getOptions(options);
this.data = resolveData(opts.data);
delete opts.data;
this.sizeLimit = opts.sizeLimit;
this.readOnly = opts.readOnly;
//绑定事件
this.on('render.before', $.proxy(opts.onBeforeRender, this));
this.on('render.after', $.proxy(opts.onRender, this));
this.on('append.before', $.proxy(opts.onBeforeAppend, this));
this.on('append.after', $.proxy(opts.onAppend, this));
this.on('delItem.before', $.proxy(opts.onBeforeDelItem, this));
this.on('delItem.after', $.proxy(opts.onDelItem, this));
},
getOptions: function (options) {
return $.extend({}, this.getDefaults(), options);
},
getDefaults: function () {
return DEFAULTS;
},
getDataItem: function (uuid) {
//根据uuid获取dateItem
return this.data.filter(function (item) {
return item._uuid === uuid;
})[0];
},
getDataItemIndex: function (uuid) {
var ret;
this.data.forEach(function (item, i) {
item._uuid === uuid && (ret = i);
});
return ret;
},
render: function () {
/**
* render是一个模板,子类不需要重写render方法,只需要重写_render方法
* 当调用子类的render方法时调用的是父类的render方法
* 但是执行到_render方法时,调用的是子类的_render方法
* 这样就能把before跟after事件的触发操作统一起来
*/
var e;
this.trigger(e = $.Event('render.before'));
if (e.isDefaultPrevented()) return;
this._render();
this.trigger($.Event('render.after'));
},
//子类需实现_Render方法
_render: function () {
},
append: function (item) {
var e;
if (!item) return;
item = resolveDataItem(item);
this.trigger(e = $.Event('append.before'), item);
if (e.isDefaultPrevented()) return;
this.data.push(item);
this._append(item);
this.trigger($.Event('append.after'), item);
},
//子类需实现_append方法
_append: function (data) {
},
delItem: function (uuid) {
var e, item = this.getDataItem(uuid);
if (!item) return;
this.trigger(e = $.Event('delItem.before'), item);
if (e.isDefaultPrevented()) return;
this.data.splice(this.getDataItemIndex(uuid), 1);
this._delItem(item);
this.trigger($.Event('delItem.after'), item);
},
//子类需实现_delItem方法
_delItem: function (data) {
}
},
extend: EventBase,
staticMembers: {
DEFAULTS: DEFAULTS
}
});
return FileUploadBaseView;
});

ImageUploadView 的实现就比较简单了,跟填空差不多,有几个点需要说明一下:

1)这个类的DEFAULTS需要扩展父类的DEFAULTS,以便添加这个子类的默认options,同时还保留父类默认options的定义;根据静态页面结构,新增了一个onAppendClick事件,外部可在这个事件中调用文件上传组件的相关方法:

//继承并扩展父类的默认DEFAULTS
var DEFAULTS = $.extend({}, FileUploadBaseView.DEFAULTS, {
onAppendClick: $.noop //点击上传按钮时候的回调
});

2)在init方法中,需要调用父类的init方法,才能完成那些通用的逻辑处理;同时在init的最后还得手动调用一下render方法,以便在组件实例化之后就能看到效果:

其它实现纯粹是业务逻辑实现,跟第2部分的需求密切相关。

ImageUploadView的整体实现如下:

define(function (require, exports, module) {
var $ = require('jquery');
var Class = require('mod/class');
var FileUploadBaseView = require('mod/fileUploadBaseView');
//继承并扩展父类的默认DEFAULTS
var DEFAULTS = $.extend({}, FileUploadBaseView.DEFAULTS, {
onAppendClick: $.noop //点击上传按钮时候的回调
});
var ImageUploadView = Class({
instanceMembers: {
init: function (element, options) {
var $element = this.$element = $(element);
var opts = this.getOptions(options);
//调用父类的init方法完成options获取,data解析以及通用事件的监听处理
this.base(this.$element, options);
//添加上传和删除的监听器及触发处理
if (!this.readOnly) {
var that = this;
that.on('appendClick', $.proxy(opts.onAppendClick, this));
$element.on('click.append', '.view-act-add', function (e) {
e.preventDefault();
that.trigger('appendClick');
});
$element.on('click.remove', '.view-act-del', function (e) {
var $this = $(e.currentTarget);
that.delItem($this.data('uuid'));
e.preventDefault();
});
}
this.render();
},
getDefaults: function () {
return DEFAULTS;
},
_setItemAddHtml: function () {
this.$element.prepend($('<li class="view-item-add"><a class="view-act-add" href="javascript:;" title="点击上传">+</a></li>'));
},
_clearItemAddHtml: function ($itemAddLi) {
$itemAddLi.remove();
},
_render: function () {
var html = [], that = this;
//如果不是只读的状态,并且还没有达到上传限制的话,就添加上传按钮
if (!(this.readOnly || (this.sizeLimit && this.sizeLimit <= this.data.length))) {
this._setItemAddHtml();
}
this.data.forEach(function (item) {
html.push(that._getItemRenderHtml(item))
});
this.$element.append($(html.join('')));
},
_getItemRenderHtml: function (item) {
return [
'<li id="',
item._uuid,
'"><a class="view-act-preview" href="javascript:;"><img alt="" src="',
item.url,
'">',
this.readOnly &#63; '' : '<span class="view-act-del" data-uuid="',
item._uuid,
'">删除</span>',
'</a></li>'
].join('');
},
_dealWithSizeLimit: function () {
if (this.sizeLimit) {
var $itemAddLi = this.$element.find('li.view-item-add');
//如果已经达到上传限制的话,就移除上传按钮
if (this.sizeLimit && this.sizeLimit <= this.data.length && $itemAddLi.length) {
this._clearItemAddHtml($itemAddLi);
} else if (!$itemAddLi.length) {
this._setItemAddHtml();
}
}
},
_append: function (data) {
this.$element.append($(this._getItemRenderHtml(data)));
this._dealWithSizeLimit();
},
_delItem: function (data) {
$('#' + data._uuid).remove();
this._dealWithSizeLimit();
}
},
extend: FileUploadBaseView
});
return ImageUploadView;
});

4. 演示说明

演示的项目结构为:

框起来的就是演示的核心代码。其中fileUploadBaserView.js和imageUploadView.js是前面实现的两个核心组件。fileUploader.js是用来模拟上传组件的,它的实例有一个onSuccess的回调,表示上传成功;还有一个openChooseFileWin用来模拟真实的打开选择文件窗口并上传的这个过程:

define(function(require, exports, module) {
return function() {
var imgList = ['../img/1.jpg','../img/2.jpg','../img/3.jpg','../img/4.jpg'], i = 0;
var that = this;
that.onSuccess = function(uploadValue){}
this.openChooseFileWin = function(){
setTimeout(function(){
that.onSuccess(imgList[i++]);
if(i == imgList.length) {
i = 0;
}
},1000);
}
}
});

app/regist.js是演示页面的逻辑代码,关键的部分已用注释进行说明:

define(function (require, exports, module) {
var $ = require('jquery');
var ImageUploadView = require('mod/imageUploadView');
var FileUploader = require('mod/fileUploader');//这是用异步任务模拟的文件上传组件
//$legalPersonIDPic,用来存储已上传的文件信息,上传组件上传成功之后以及ImageUploadView组件删除某个item之后会对$legalPersonIDPic的值产生影响
var $legalPersonIDPic = $('#legalPersonIDPic-input'),
data = JSON.parse($legalPersonIDPic.val() || '[]');//data是初始值,比如当前页面有可能是从数据库加载的,需要用ImageUploadView组件呈现出来
//在文件上传成功之后,将刚上传的文件保存到$legalPersonIDPic的value中
//$legalPersonIDPic以json字符串的形式存储
var appendImageInputValue = function ($input, item) {
var value = JSON.parse($input.val() || '[]');
value.push(item);
$input.val(JSON.stringify(value));
};
//当调用ImageUploadView组件删除某个item之后,要同步把$legalPersonIDPic中已存储的信息清掉
var removeImageInputValue = function ($input, uuid) {
var value = JSON.parse($input.val() || '[]'), index;
value.forEach(function (item, i) {
if (item._uuid === uuid) {
index = i;
}
});
value.splice(index, 1);
$input.val(JSON.stringify(value));
};
var fileUploader = new FileUploader();
fileUploader.onSuccess = function (uploadValue) {
var item = {url: uploadValue};
legalPersonIDPicView.append(item);
appendImageInputValue($legalPersonIDPic, item);
};
var legalPersonIDPicView = new ImageUploadView('#legalPersonIDPic-view', {
data: data,
sizeLimit: 4,
onAppendClick: function () {
//打开选择文件的窗口
fileUploader.openChooseFileWin();
},
onDelItem: function (data) {
removeImageInputValue($legalPersonIDPic, data._uuid);
}
});
});

5. 本文总结

ImageUploadView这个组件最终实现起来并不难,但是我也花了不少时间去琢磨它及其它父类的实现方法,大部分时间都花在对职责分离和行为分离的抽象过程中。我在本文表达的关于这两方面编程思想的观点也只是自己个人的实际体会,因为抽象层面的东西,每个人的思考方式不同最终理解的成果也就不会相同,所以我也不能直接说我的对还是不对,写出来的目的就是为了分享和交流,看看有没有其他有经验的朋友也愿意把自己在这方面的想法拿出来跟大家说一说,相信每个人看多了别人的思路之后,也会对自己的编程思想方面的锻炼带来帮助。

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.