分離與繼承的思想實現圖片上傳後的預覽功能:ImageUploadView

来源:http://www.cnblogs.com/lyzg/archive/2016/04/07/5361314.html
-Advertisement-
Play Games

本文要介紹的是網頁中常見的圖片上傳後直接在頁面生成小圖預覽的實現思路,考慮到該功能有一定的適用性,於是把相關的邏輯封裝成了一個ImageUploadView組件,實際使用效果可查看下一段的git效果圖。在實現這個組件的過程中,有用到前面幾篇博客介紹的相關內容,比如繼承庫class.js,任意組件的事... ...


本文要介紹的是網頁中常見的圖片上傳後直接在頁面生成小圖預覽的實現思路,考慮到該功能有一定的適用性,於是把相關的邏輯封裝成了一個ImageUploadView組件,實際使用效果可查看下一段的git效果圖。在實現這個組件的過程中,有用到前面幾篇博客介紹的相關內容,比如繼承庫class.js,任意組件的事件管理庫eventBase.js,同時包含進了自己對職責分離,表現與行為分離這兩方面的一些思考,歡迎閱讀與交流。

演示效果(代碼下載):
2
註:由於演示的代碼都是靜態的,所以文件上傳的組件是用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方法,以便在組件實例化之後就能看到效果:
image

其它實現純粹是業務邏輯實現,跟第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 ? '' : '<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. 演示說明

演示的項目結構為:
image
框起來的就是演示的核心代碼。其中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這個組件最終實現起來並不難,但是我也花了不少時間去琢磨它及其它父類的實現方法,大部分時間都花在對職責分離和行為分離的抽象過程中。我在本文表達的關於這兩方面編程思想的觀點也只是自己個人的實際體會,因為抽象層面的東西,每個人的思考方式不同最終理解的成果也就不會相同,所以我也不能直接說我的對還是不對,寫出來的目的就是為了分享和交流,看看有沒有其他有經驗的朋友也願意把自己在這方面的想法拿出來跟大家說一說,相信每個人看多了別人的思路之後,也會對自己的編程思想方面的鍛煉帶來幫助。

最後,希望本文的理論和實踐,都能給大家帶來實際的工作價值,謝謝閱讀:)


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 現在記錄下Gridview的相關內容,也是強迫症犯了,Yii2自帶的Gridview雖然不錯,但是過濾欄如果一些欄位用不著,不會自動合併成一行,當然也可以過濾欄不用,而是在最上方自己寫一些需要檢索的數據,但是這樣很麻煩,還要自己去規劃樣式,寫檢索什麼的。正好在搜索將檢索欄和標題合併時,看到了kart ...
  • 一、IDE介紹 ① 什麼是IDE? 1、 IDE全稱是”Integrated Development Environment”,中文稱為“集成開發環境”; 2、是用於提供程式開發環境的應用程式,一般包括代碼編輯器、編譯器、調試器和圖形用戶界面工具。就是集成了代碼編寫功能、分析功能、編譯功能、調試功能 ...
  • 一.攔截器與過濾器的區別: 1.filter基於回調函數,我們需要實現的filter介面中doFilter方法就是回調函數,而interceptor則基於Java本身的反射機制,這是兩者最本質的區別。 2.filter是依賴於servlet容器的,即只能在servlet容器中執行,很顯然沒有serv ...
  • 獲取【下載地址】 QQ: 313596790 【免費支持更新】三大資料庫 mysql oracle sqlsever 更專業、更強悍、適合不同用戶群體【新錄針對本系統的視頻教程,手把手教開發一個模塊,快速掌握本系統】A 集成代碼生成器(開發利器); 技術:313596790 增刪改查的處理類,ser ...
  • 適配器模式(Adapter) 適配器模式有兩種形式:類的適配器模式和對象的適配器模式。 一、類的適配器模式 類圖 描述 類的適配器模式: Target,目標介面,可以是具體的或抽象的類,也可以是介面; Adaptee,需要適配的類; Adapter,適配器類,把源介面轉換成目標介面;Adapter類 ...
  • 我們可以試驗一下,JS類的繼承 children.constructor==father 返回的是true,而原型繼承children.constructor==father 返回的是false; ...
  • 1.解決HTML頁面中的中文問題: 為了使HTML頁面很好地支持中文,就必須在每個HTML頁面的頭部增加如下代碼: <HEAD> ... <META http-equiv=Content-Type content="text/html;charset=gb2312"> ... <HEAD> 2.解決 ...
  • 本文內容 Angular JS 實現模式對話框。基於 AngularJS v1.5.3 和 Bootstrap v3.3.6。 項目結構 圖 1 項目結構 運行結果 圖 1 運行結果:大模態 index.html Angul... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...