分離與繼承的思想實(shí)現(xiàn)圖片上傳后的預(yù)覽功能:ImageUploadView
本文要介紹的是網(wǎng)頁中常見的圖片上傳后直接在頁面生成小圖預(yù)覽的實(shí)現(xiàn)思路,考慮到該功能有一定的適用性,于是把相關(guān)的邏輯封裝成了一個(gè)ImageUploadView組件,實(shí)際使用效果可查看下一段的git效果圖。在實(shí)現(xiàn)這個(gè)組件的過程中,有用到前面幾篇博客介紹的相關(guān)內(nèi)容,比如繼承庫class.js,任意組件的事件管理庫eventBase.js,同時(shí)包含進(jìn)了自己對(duì)職責(zé)分離,表現(xiàn)與行為分離這兩方面的一些思考,歡迎閱讀與交流。
演示效果:
注:由于演示的代碼都是靜態(tài)的,所以文件上傳的組件是用setTimeout模擬的,不過它的調(diào)用方式跟我自己在實(shí)際工作中用上傳組件完全相同,所以演示效果的代碼實(shí)現(xiàn)完全符合真實(shí)的功能需求。
按照我以前博客的思路,先來介紹下這個(gè)上傳預(yù)覽功能的需求。
1. 需求分析
根據(jù)前面的演示效果圖,分析需求如下:
1)初始時(shí)上傳區(qū)域只顯示一個(gè)可點(diǎn)擊上傳的按鈕,當(dāng)點(diǎn)擊該按鈕后,將上傳成功的圖片顯示在后面的預(yù)覽區(qū)域
2)上傳的圖片添加到預(yù)覽區(qū)域以后,可以通過刪除按鈕來移除
3)當(dāng)已上傳的圖片總數(shù)達(dá)到一定限制之后,比如演示中已上傳的限制為4,就把上傳按鈕給移除掉;
4)當(dāng)已上傳的圖片總數(shù)達(dá)到一定限制之時(shí),如果通過刪除操作移除了某張圖片,還得再把上傳按鈕給顯示出來。
以上需求是看的見的,根據(jù)經(jīng)驗(yàn),還可以分析得出的需求如下:
1)如果頁面是編輯狀態(tài),也就是從數(shù)據(jù)庫中查詢出來的狀態(tài),只要圖片列表不為空,初始時(shí)還得把圖片顯示出來;而且還要根據(jù)查出來的圖片列表的長(zhǎng)度跟上傳限制去控制上傳按鈕是否顯示;
2)如果當(dāng)前頁面是一種只能看不能改的狀態(tài),那么在初始時(shí)一定要把上傳按鈕和刪除按鈕移除掉。
需求分析完畢,接下來說明一下我的實(shí)現(xiàn)思路。
2. 實(shí)現(xiàn)思路
由于這是個(gè)表單頁面,所以圖片上傳完以后如果要提交到后臺(tái),肯定得需要一個(gè)文本域,所以我在做靜態(tài)頁面的時(shí)候就把這個(gè)文本域考慮進(jìn)去了,當(dāng)上傳完新的圖片以及刪除了圖片之后都得去修改這個(gè)文本域的值。當(dāng)時(shí)做靜態(tài)頁時(shí)這部分的結(jié)構(gòu)如下:
<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="點(diǎn)擊上傳">+</a> </li> </ul> <p class="img-upload-msg"> 請(qǐng)確保圖片清晰,文字可辨 <a href="#" title="查看示例"><i class="fa fa-question-circle"></i> 查看示例</a> </p> </div> </div>
從這個(gè)結(jié)構(gòu)還可以看出,我把整個(gè)上傳區(qū)域都放在一個(gè)ul里面,然后把ul的第一個(gè)li作為上傳按鈕來使用。為了完成這個(gè)功能,我們主要的任務(wù)是:上傳及上傳后的回調(diào),新增或刪除圖片預(yù)覽以及文本域值的管理。從這一點(diǎn),結(jié)合職責(zé)分離的思想,這個(gè)功能至少需要三個(gè)組件,一個(gè)負(fù)責(zé)文件上傳,一個(gè)負(fù)責(zé)圖片預(yù)覽的管理,一個(gè)負(fù)責(zé)文本域值的管理。千萬不能把這三個(gè)功能,兩兩或者全部都封裝在一起,那樣的話功能耦合太強(qiáng),寫出來的組件可擴(kuò)展性可重用性不高。如果這三個(gè)組件之間需要交互,我們只要借助回調(diào)函數(shù)或者發(fā)布-訂閱模式定義它們給外部調(diào)用的接口即可。
不過文本域值的管理本身就很簡(jiǎn)單,寫不寫成組件都關(guān)系不大,但是至少函數(shù)級(jí)別的封裝是得有的;文件上傳組件雖然不是本文的重點(diǎn),但是網(wǎng)上有很多現(xiàn)成的開源插件,比如webuploader,不管是直接用還是做二次封裝都可以應(yīng)用進(jìn)來;圖片預(yù)覽的功能是本文的核心內(nèi)容,ImageUploadView這個(gè)組件就是對(duì)它的封裝,從需求來看,這個(gè)組件有語義的實(shí)例方法無非就是三個(gè),分別是render, append, delItem,其中render用來在初始化完成之后顯示初始的預(yù)覽列表,append用來在上傳成功后添加新的圖片預(yù)覽,delItem用來刪除已有的圖片預(yù)覽,按照這個(gè)基本思路,我們只需要再結(jié)合需求和組件開發(fā)的經(jīng)驗(yàn)為它設(shè)計(jì)好options和事件即可。
從前面的需求我們發(fā)現(xiàn),這個(gè)ImageUploadView組件的render會(huì)受到頁面狀態(tài)的影響,當(dāng)頁面為查看模式時(shí),這個(gè)組件不能做上傳和刪除的操作,所以可以考慮給它加一個(gè)readonly的option。同時(shí)它的上傳和刪除操作還會(huì)影響到上傳按鈕的UI邏輯,這個(gè)跟上傳限制有關(guān)系,為了靈活性,也得把上傳限制作為一個(gè)option。從前一段提到的三個(gè)實(shí)例方法來說,按照自己以前定義事件的經(jīng)驗(yàn),一般一個(gè)實(shí)例方法會(huì)定義一對(duì)事件,就像bootstrap的插件的做法一樣,比如render方法,可以定義一個(gè)render.before,這個(gè)事件在render的主要邏輯執(zhí)行前觸發(fā),如果外部監(jiān)聽器調(diào)用了這個(gè)事件的preventDefault()方法,那么render的主要邏輯都不會(huì)執(zhí)行;還有一個(gè)render.after事件,這個(gè)事件在render的主要邏輯執(zhí)行后觸發(fā)。這種成對(duì)定義事件的好處是,既給外部提供擴(kuò)展組件功能的方法,又能增加組件默認(rèn)行為的管理。
最后從我之前的工作經(jīng)驗(yàn)來說,除了有上傳圖片進(jìn)行預(yù)覽這樣的功能,我曾經(jīng)還做過上傳視頻,上傳音頻,上傳普通文檔等類似的,所以這一次碰到這個(gè)功能的時(shí)候我就覺得應(yīng)該把這些功能里面相似的東西抽取出來,作為一個(gè)基類,圖片上傳,視頻上傳等分別繼承這個(gè)基類去實(shí)現(xiàn)各自的邏輯。這個(gè)基類還有一個(gè)好處,就是能夠讓那些通用的邏輯完全與HTML結(jié)構(gòu)分離,在這個(gè)基類里面只做一些通用的事情,比如options與組件行為(render, append, delItem)的定義,以及通用事件的監(jiān)聽和觸發(fā),它只要留有固定的接口留給子類來實(shí)現(xiàn)即可。在后面的實(shí)現(xiàn)中,我定義了一個(gè)FileUploadBaseView組件來完成這個(gè)基類的功能,這個(gè)基類不包含任何html或css處理的邏輯,它只是抽象了我們要完成的功能,不處理任何業(yè)務(wù)邏輯。根據(jù)業(yè)務(wù)邏輯實(shí)現(xiàn)的子類會(huì)受html結(jié)構(gòu)的限制,所以子類的適用范圍小;而基類因?yàn)樽龅搅伺chtml結(jié)構(gòu)完全分離,所以有更大的適用范圍。
3. 實(shí)現(xiàn)細(xì)節(jié)
從第2部分的實(shí)現(xiàn)思路,要實(shí)現(xiàn)的類有:FileUploadBaseView和ImageUploadView,前者是后者的基類。同時(shí)考慮到要給組件提供事件管理的功能,所以要用到上一篇博客的eventBase.js,F(xiàn)ileUploadBaseView得繼承該庫的EventBase組件;考慮到要有類的定義和繼承,還要用到之前寫的繼承庫class.js來定義組件以及組件的繼承關(guān)系。相關(guān)組件的繼承關(guān)系為:ImageUploadView extend FileUploadBaseView extend EventBase。
(注:以下相關(guān)代碼中模塊化用的是seajs。)
FileUploadBaseView所做的事情有:
1)定義通用的option以及通用的事件管理
在該組件的DEFAULTS配置中可以看到所有的通用option和通用事件的定義:
var DEFAULTS = { data: [], //要展示的數(shù)據(jù)列表,列表元素必須是object類型的,如[{url: 'xxx.png'},{url: 'yyyy.png'}] sizeLimit: 0, //用來限制BaseView中的展示的元素個(gè)數(shù),為0表示不限制 readonly: false, //用來控制BaseView中的元素是否允許增加和刪除 onBeforeRender: $.noop, //對(duì)應(yīng)render.before事件,在render方法調(diào)用前觸發(fā) onRender: $.noop, //對(duì)應(yīng)render.after事件,在render方法調(diào)用后觸發(fā) onBeforeAppend: $.noop, //對(duì)應(yīng)append.before事件,在append方法調(diào)用前觸發(fā) onAppend: $.noop, //對(duì)應(yīng)append.after事件,在append方法調(diào)用后觸發(fā) onBeforeDelItem: $.noop, //對(duì)應(yīng)delItem.before事件,在delItem方法調(diào)用前觸發(fā) onDelItem: $.noop //對(duì)應(yīng)delItem.after事件,在delItem方法調(diào)用后觸發(fā) };
在該組件的init方法中可以看到對(duì)通用option和事件管理的初始化邏輯:
init: function (element, options) { //通過this.base調(diào)用父類EventBase的init方法 this.base(element); //實(shí)例屬性 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)定義組件的行為,預(yù)留可供子類實(shí)現(xiàn)的接口:
render: function () { /** * render是一個(gè)模板,子類不需要重寫render方法,只需要重寫_render方法 * 當(dāng)調(diào)用子類的render方法時(shí)調(diào)用的是父類的render方法 * 但是執(zhí)行到_render方法時(shí),調(diào)用的是子類的_render方法 * 這樣就能把before跟after事件的觸發(fā)操作統(tǒng)一起來 */ var e; this.trigger(e = $.Event('render.before')); if (e.isDefaultPrevented()) return; this._render(); this.trigger($.Event('render.after')); }, //子類需實(shí)現(xiàn)_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); }, //子類需實(shí)現(xiàn)_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); }, //子類需實(shí)現(xiàn)_delItem方法 _delItem: function (data) { }
為了統(tǒng)一處理行為前后的事件派發(fā)邏輯,將render, append ,delItem的主要邏輯抽出來成為需被子類實(shí)現(xiàn)的方法_render, _append和_delItem。當(dāng)調(diào)用子類的render方法時(shí),調(diào)用的實(shí)際上父類的方法,但是當(dāng)父類執(zhí)行到_render方法時(shí),執(zhí)行的就是子類的方法,另外兩個(gè)方法也是類似的處理。需要注意的是子類不能去覆蓋render, append ,delItem三個(gè)方法,否則就得自己去處理相關(guān)事件的觸發(fā)邏輯。
FileUploadBaseView整體實(shí)現(xiàn)如下:
define(function (require, exports, module) { var $ = require('jquery'); var Class = require('mod/class'); var EventBase = require('mod/eventBase'); var DEFAULTS = { data: [], //要展示的數(shù)據(jù)列表,列表元素必須是object類型的,如[{url: 'xxx.png'},{url: 'yyyy.png'}] sizeLimit: 0, //用來限制BaseView中的展示的元素個(gè)數(shù),為0表示不限制 readonly: false, //用來控制BaseView中的元素是否允許增加和刪除 onBeforeRender: $.noop, //對(duì)應(yīng)render.before事件,在render方法調(diào)用前觸發(fā) onRender: $.noop, //對(duì)應(yīng)render.after事件,在render方法調(diào)用后觸發(fā) onBeforeAppend: $.noop, //對(duì)應(yīng)append.before事件,在append方法調(diào)用前觸發(fā) onAppend: $.noop, //對(duì)應(yīng)append.after事件,在append方法調(diào)用后觸發(fā) onBeforeDelItem: $.noop, //對(duì)應(yīng)delItem.before事件,在delItem方法調(diào)用前觸發(fā) onDelItem: $.noop //對(duì)應(yīng)delItem.after事件,在delItem方法調(diào)用后觸發(fā) }; /** * 數(shù)據(jù)處理,給data的每條記錄都添加一個(gè)_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調(diào)用父類EventBase的init方法 this.base(element); //實(shí)例屬性 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) { //根據(jù)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是一個(gè)模板,子類不需要重寫render方法,只需要重寫_render方法 * 當(dāng)調(diào)用子類的render方法時(shí)調(diào)用的是父類的render方法 * 但是執(zhí)行到_render方法時(shí),調(diào)用的是子類的_render方法 * 這樣就能把before跟after事件的觸發(fā)操作統(tǒng)一起來 */ var e; this.trigger(e = $.Event('render.before')); if (e.isDefaultPrevented()) return; this._render(); this.trigger($.Event('render.after')); }, //子類需實(shí)現(xiàn)_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); }, //子類需實(shí)現(xiàn)_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); }, //子類需實(shí)現(xiàn)_delItem方法 _delItem: function (data) { } }, extend: EventBase, staticMembers: { DEFAULTS: DEFAULTS } }); return FileUploadBaseView; });
ImageUploadView 的實(shí)現(xiàn)就比較簡(jiǎn)單了,跟填空差不多,有幾個(gè)點(diǎn)需要說明一下:
1)這個(gè)類的DEFAULTS需要擴(kuò)展父類的DEFAULTS,以便添加這個(gè)子類的默認(rèn)options,同時(shí)還保留父類默認(rèn)options的定義;根據(jù)靜態(tài)頁面結(jié)構(gòu),新增了一個(gè)onAppendClick事件,外部可在這個(gè)事件中調(diào)用文件上傳組件的相關(guān)方法:
//繼承并擴(kuò)展父類的默認(rèn)DEFAULTS var DEFAULTS = $.extend({}, FileUploadBaseView.DEFAULTS, { onAppendClick: $.noop //點(diǎn)擊上傳按鈕時(shí)候的回調(diào) });
2)在init方法中,需要調(diào)用父類的init方法,才能完成那些通用的邏輯處理;同時(shí)在init的最后還得手動(dòng)調(diào)用一下render方法,以便在組件實(shí)例化之后就能看到效果:
其它實(shí)現(xiàn)純粹是業(yè)務(wù)邏輯實(shí)現(xiàn),跟第2部分的需求密切相關(guān)。
ImageUploadView的整體實(shí)現(xiàn)如下:
define(function (require, exports, module) { var $ = require('jquery'); var Class = require('mod/class'); var FileUploadBaseView = require('mod/fileUploadBaseView'); //繼承并擴(kuò)展父類的默認(rèn)DEFAULTS var DEFAULTS = $.extend({}, FileUploadBaseView.DEFAULTS, { onAppendClick: $.noop //點(diǎn)擊上傳按鈕時(shí)候的回調(diào) }); var ImageUploadView = Class({ instanceMembers: { init: function (element, options) { var $element = this.$element = $(element); var opts = this.getOptions(options); //調(diào)用父類的init方法完成options獲取,data解析以及通用事件的監(jiān)聽處理 this.base(this.$element, options); //添加上傳和刪除的監(jiān)聽器及觸發(fā)處理 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="點(diǎn)擊上傳">+</a></li>')); }, _clearItemAddHtml: function ($itemAddLi) { $itemAddLi.remove(); }, _render: function () { var html = [], that = this; //如果不是只讀的狀態(tài),并且還沒有達(dá)到上傳限制的話,就添加上傳按鈕 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 ? '' : '<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'); //如果已經(jīng)達(dá)到上傳限制的話,就移除上傳按鈕 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. 演示說明
演示的項(xiàng)目結(jié)構(gòu)為:
框起來的就是演示的核心代碼。其中fileUploadBaserView.js和imageUploadView.js是前面實(shí)現(xiàn)的兩個(gè)核心組件。fileUploader.js是用來模擬上傳組件的,它的實(shí)例有一個(gè)onSuccess的回調(diào),表示上傳成功;還有一個(gè)openChooseFileWin用來模擬真實(shí)的打開選擇文件窗口并上傳的這個(gè)過程:
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是演示頁面的邏輯代碼,關(guān)鍵的部分已用注釋進(jìn)行說明:
define(function (require, exports, module) { var $ = require('jquery'); var ImageUploadView = require('mod/imageUploadView'); var FileUploader = require('mod/fileUploader');//這是用異步任務(wù)模擬的文件上傳組件 //$legalPersonIDPic,用來存儲(chǔ)已上傳的文件信息,上傳組件上傳成功之后以及ImageUploadView組件刪除某個(gè)item之后會(huì)對(duì)$legalPersonIDPic的值產(chǎn)生影響 var $legalPersonIDPic = $('#legalPersonIDPic-input'), data = JSON.parse($legalPersonIDPic.val() || '[]');//data是初始值,比如當(dāng)前頁面有可能是從數(shù)據(jù)庫加載的,需要用ImageUploadView組件呈現(xiàn)出來 //在文件上傳成功之后,將剛上傳的文件保存到$legalPersonIDPic的value中 //$legalPersonIDPic以json字符串的形式存儲(chǔ) var appendImageInputValue = function ($input, item) { var value = JSON.parse($input.val() || '[]'); value.push(item); $input.val(JSON.stringify(value)); }; //當(dāng)調(diào)用ImageUploadView組件刪除某個(gè)item之后,要同步把$legalPersonIDPic中已存儲(chǔ)的信息清掉 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. 本文總結(jié)
ImageUploadView這個(gè)組件最終實(shí)現(xiàn)起來并不難,但是我也花了不少時(shí)間去琢磨它及其它父類的實(shí)現(xiàn)方法,大部分時(shí)間都花在對(duì)職責(zé)分離和行為分離的抽象過程中。我在本文表達(dá)的關(guān)于這兩方面編程思想的觀點(diǎn)也只是自己個(gè)人的實(shí)際體會(huì),因?yàn)槌橄髮用娴臇|西,每個(gè)人的思考方式不同最終理解的成果也就不會(huì)相同,所以我也不能直接說我的對(duì)還是不對(duì),寫出來的目的就是為了分享和交流,看看有沒有其他有經(jīng)驗(yàn)的朋友也愿意把自己在這方面的想法拿出來跟大家說一說,相信每個(gè)人看多了別人的思路之后,也會(huì)對(duì)自己的編程思想方面的鍛煉帶來幫助。
- 圖片上傳之前檢查大小、尺寸、格式并預(yù)覽的js代碼
- javascript 圖片上傳預(yù)覽-兼容標(biāo)準(zhǔn)
- jquery 圖片上傳按比例預(yù)覽插件集合
- php圖片上傳存儲(chǔ)源碼并且可以預(yù)覽
- 兼容IE和FF的圖片上傳前預(yù)覽js代碼
- Asp.net圖片上傳實(shí)現(xiàn)預(yù)覽效果的簡(jiǎn)單代碼
- Asp.net FileUpload+Image制作頭像效果示例代碼
- 模擬QQ心情圖片上傳預(yù)覽示例
- jquery實(shí)現(xiàn)兼容瀏覽器的圖片上傳本地預(yù)覽功能
- ie8本地圖片上傳預(yù)覽示例代碼
- Javascript圖片上傳前的本地預(yù)覽實(shí)例
相關(guān)文章
D3.js實(shí)現(xiàn)力向?qū)D的繪制教程詳解
力向?qū)D是繪圖的一種算法,實(shí)現(xiàn)了用以模擬粒子物理運(yùn)動(dòng)的?velocity?Verlet?數(shù)值積分器。本文將利用D3.js實(shí)現(xiàn)力向?qū)D的繪制,需要的可以參考一下2022-11-11JS和css實(shí)現(xiàn)檢測(cè)移動(dòng)設(shè)備方向的變化并判斷橫豎屏幕
這篇文章主要介紹了JS和css實(shí)現(xiàn)檢測(cè)移動(dòng)設(shè)備方向的變化并判斷橫豎屏幕,本文分別給出實(shí)現(xiàn)代碼,需要的朋友可以參考下2015-05-05JavaScript自動(dòng)點(diǎn)擊鏈接 防止繞過瀏覽器訪問的方法
下面小編就為大家?guī)硪黄狫avaScript自動(dòng)點(diǎn)擊鏈接 防止繞過瀏覽器訪問的方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-01-01js 右鍵菜單,支持不同對(duì)象不同菜單(兼容IE、Firefox)
版本雖然很老也不符合標(biāo)準(zhǔn)了,不過代碼是值得參考的,需要右鍵菜單的朋友可以參考下。2010-01-01WebStorm中如何將自己的代碼上傳到github示例詳解
這篇文章主要介紹了WebStorm中如何將自己的代碼上傳到github,本文分步驟通過圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-10-10JavaScript實(shí)現(xiàn)微信飛機(jī)大戰(zhàn)游戲
這篇文章主要為大家詳細(xì)介紹了JavaScript實(shí)現(xiàn)微信飛機(jī)大戰(zhàn)游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05微信小程序picker組件兩列關(guān)聯(lián)使用方式
這篇文章主要介紹了微信小程序picker組件兩列關(guān)聯(lián)使用方式,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-10-10