進一步豐富和簡化表單管理的組件:form.js


上文《簡潔易用的表單數據設置和收集管理組件》介紹了我自己的表單管理的核心內容,本文在上文的基礎上繼續介紹自己關於表單初始值獲取和設置以及表單數據提交等內容方面的做法,上文的組件粒度很小,都是跟單個表單元素相關的某種特定類型的組件,所以內容很多;本文要介紹的內容集中於整個表單組件本身,有點像上文介紹的formMap.js組件,但不同的是在我自己的項目中form.js用的更多,formMap幾乎不用,因為在form的內部就有用到formMap組件的實例來管理表單的數據,之所以這么做,也是為了讓各個組件的功能更加單一,方便今后的維護和重用。form.js的代碼不多,只有200多行,該組件以及我提供的demo頁面的js內都有比較詳細的注釋,方便有興趣的朋友閱讀參考。

form.js的代碼地址:

https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/form.js

demo地址:

新增模式:

http://liuyunzhuge.github.io/blog/form/dist/html/demo2.html?mode=1

編輯模式:

http://liuyunzhuge.github.io/blog/form/dist/html/demo2.html?mode=2&id=1

form.js解決的問題

在我自己以前開發項目的經驗中,在開發一個表單的時候會遇到下列的一些問題:

1)表單各個字段的初始值如何設置?
雖然上文的內容已經解決了如何區分新增模式和編輯模式的初始值,但是還存在的問題是如果某個字段的初始值需要從后台返回該如何處理?傳統項目中我們可以用后台模板來解決,但是假如是一個純前后端分離的項目呢,那就沒有后台模板可以利用了;還有,從后台返回的話,如果是獲取編輯模式的初始值,意味着要后台從數據庫查詢相應的數據,這個時候如何規范傳遞給后台查詢初始值的接口參數?

2)假如通過接口來獲取初始值,編輯模式一定需要ajax,但是新增模式就不一定需要,意味着同樣的一個功能,有的時候可能是異步的,有的時候可能是同步的,這種情況該如何統一?

3)如果新增模式和編輯模式要使用不同的接口來保存該如何處理?

4)一般表單在保存之后都會根據后台返回的響應添加一些交互邏輯,但是每個保存功能的交互邏輯都是不固定的,更別說項目之間的區別了,如何才能讓表單的保存功能更加單一,不受其它功能的影響?

5)提交到后台的數據一般都是按照querystring的格式提交,但有時候為了方便,在php里面會將querystring的參數名都封裝成數組索引的形式,比如有一個參數id=1,就會變成某個model名稱加參數名稱的形式如 User[id]=1,這么做是為了配合后台的數據解析功能;而在java里面,更喜歡直接把整個表單的所有參數合並成一個參數,把所有數據通過json字符串來傳遞,java后台也有好用的工具將數據直接解析成model,要考慮兼容這樣的問題,表單組件在提交數據的時候該如何管理?

這些問題,我考慮的解決方法是:

1)使用以下幾個option來管理通過接口初始值的獲取和設置的功能:

queryUrl: '',//編輯模式時查詢初始值的url
key: '',//編輯模式時使用它作為主鍵的值,跟在queryUrl后面傳遞到后台查詢數據
keyName: '',//編輯模式時使用它作為主鍵的名稱,跟在queryUrl后面傳遞到后台查詢數據
defaultData: {},//新增模式時的默認值,可以是一個object,也可以是一個字符串,是字符串的時候表示一個后台查詢的接口地址

看這部分的代碼就能明白它們的實際作用了:

//獲取表單初始化數據
getInitData: function () {
    var opts = this.options,
        mode = this.mode;

    //這個函數返回的格式,包含三個參數,各個參數的含義如下:
    //valid: true表示有效,false表示無效
    //ajax: true表示data返回的是jquery創建的ajax對象,false表示不是
    //data: 當ajax為true的時候返回jquery創建的ajax對象,否則直接返回一個object實例表示初始化數據

    //新增模式,通過defaultData來獲取初始值
    if (mode == 1) {
        var defaultData = opts.defaultData;
        //如果defaultData是一個字符串,表示它需要從后台加載
        if (typeof(defaultData) == 'string') {
            return {
                valid: true,
                ajax: true,
                data: Ajax.get(defaultData)
            };
        } else {
            return {
                valid: true,
                ajax: false,
                data: defaultData
            };
        }
    } else {
        //非新增模式,通過queryUrl來獲取初始值
        //此模式下必須跟后台傳遞keyName跟key值
        //否則后台無法查詢到要編輯的數據給前端返回
        var url = $.trim(opts.queryUrl),
            keyName = $.trim(opts.keyName),
            key = $.trim(opts.key);

        if (!url || !key || !keyName) {
            //url key keyName 為falsy值時均返回valid:false,表示初始值無效
            return {
                valid: false
            };
        } else {
            var params = {};
            params[keyName] = key;
            return {
                valid: true,
                ajax: true,
                data: Ajax.get(url, params)
            }
        }
    }
}

2) 利用$.Deffered將同步的功能變成異步的功能,但是由於異步的功能里面調用異步回調傳遞的參數是$.ajax返回的對象,所以調用resolve方法時也必須傳遞同樣的格式,才能保證功能的健壯:

//設置初始值
//方法名起的不好...
//因為這是一個帶返回值的設置型函數
//而且為了將初始化數據的設置統一成異步任務
//用到了$.Deferred()
setInitData: function () {
    var $defer = $.Deferred(),
        initData = this.getInitData(),
        opts = this.options;

    if (!initData.valid) {
        setTimeout(function () {
            $defer.resolve({});
        }, 0);
    } else if (initData.ajax) {
        initData.data.done(function (res) {
            var data = opts.parseInitResponse(res);
            $defer.resolve(data);
        }).fail(function () {
            $defer.resolve({});
        });
    } else {
        setTimeout(function () {
            $defer.resolve(initData.data);
        }, 0);
    }

    return $.when($defer);
}

3)將表單保存的接口分成兩個,一個postUrl用於新增模式新增,一個putUrl用於編輯模式新增,結合mode參數,通過下面的接口來返回實際發起ajax請求時的地址:

//獲取表單保存時調用的接口地址
getSaveUrl: function () {
    var url = '',
        opts = this.options,
        mode = this.mode;

    //mode=1時用postUrl
    if (opts.postUrl && mode == 1) {
        url = opts.postUrl;
    }

    //mode!=1時用putUrl
    if (opts.putUrl && mode != 1) {
        url = opts.putUrl;
    }

    return url;
}

4)保存的方法返回的時候直接返回$.ajax創建的對象,不考慮對外部提供任何的保存后的回調,目的就是為了讓保存方法更加簡單和單一:

//表單保存邏輯
save: function () {
    if (this.mode > 2) return false;

    var opts = this.options,
        formData = this.getData(),
        event;

    //觸發beforeSave事件
    this.trigger((event = $.Event('beforeSave')), formData);

    //方便外部對formData進行一些額外的處理
    formData = opts.parseSubmitData(formData);

    //如果beforeSave事件默認行為被阻止,則直接返回
    if (event.isDefaultPrevented()) {
        return false;
    }

    var url = this.getSaveUrl();

    //發ajax請求保存,同時把Ajax組件創建的實例返回
    //方便外部根據實際情況添加自己的回調
    return Ajax[opts.ajaxMethod](url, formData);
}

5)通過parseSubmitData回調來解決以何種結構傳遞數據到后台的問題,在上面的save方法的代碼中,發起ajax請求前有一個調用parseSubmitData的代碼,這個回調需返回一個有效的對象作為實際要傳遞的數據。formData在調用這個回調前是一個object實例,假如后台是php,我們可以把parseSubmitData定義成:

function (data) {
    var hasOwn = Object.prototype.hasOwnProperty;

    var ret = {};
    for (var i  in data) {
        if (hasOwn.call(data, i)) {
            ret['User[' + i + ']'] = data[i];
        }
    }

    return ret;
}

假如formData默認傳遞時是這樣的結構:

image

調用parseSubmitData后將會是這樣的結構:

image

從我自身的經驗來說,一個表單管理的整體組件能夠把以上問題解決,基本上功能就夠了,因為表單管理的功能本身是比較單一的,當我想要往這個組件添加功能的時候,我總是想兩個問題:

1. 是否違背單一原則,加東西會不會讓這個組件今后的改動更不穩定

2. 是否能夠添加新的組件來解決要添加的功能。

比如說,我原來想把表單校驗的功能集成到form.js里面,后面我就發現這是個明顯地違背單一原則的決定,最后將表單校驗的功能單獨出來形成了一個新的組件,這樣做最大的好處就是兩邊的功能沒有任何關聯影響;而且分開之后,從代碼印象上都感覺代碼質量跟原來明顯不同。

form.js的整體功能

首先它定義的option如下:

var DEFAULTS = {
    mode: 1, //同FormFieldBase的mod
    postUrl: '',//編輯時保存的url
    putUrl: '',//新增時保存的url
    queryUrl: '',//編輯模式時查詢初始值的url
    key: '',//編輯模式時使用它作為主鍵的值,跟在queryUrl后面傳遞到后台查詢數據
    keyName: '',//編輯模式時使用它作為主鍵的名稱,跟在queryUrl后面傳遞到后台查詢數據
    defaultData: {},//新增模式時的默認值,可以是一個object,也可以是一個字符串,是字符串的時候表示一個后台查詢的接口地址
    ajaxMethod: 'post',//發ajax請求的時候用的方法
    fieldOptions: {},//各個字段的選項
    parseData: $.noop,//獲取初始化數據時,通過這個回調來解析初始化數據
    parseSubmitData: function (data) {
        //保存提交數據到后台之前,可以通過這個回調對要提交的數據做些額外的處理
        return data;
    },
    parseInitResponse: function (res) {
        //使用這個回調來解析獲取初始化數據時ajax返回的響應
        if (res.code == 200) {
            return res.data;
        } else {
            return {};
        }
    },
    onInit: $.noop,//表單初始化完成后的事件回調
    onBeforeSave: $.noop//表單保存接口調用前觸發的回調
};

要說明的是form.js內部使用了formMap組件來管理表單元素的實例,所以以上option中的mode跟fieldOptions的用法跟上文中的一模一樣。

form.js提供了以下api方法在實際工作中可以經常用到:

getMode(): 返回表單的模式:1 2 3

getData(): 獲取表單數據

reset(): 表單重置

save(): 保存。

在源碼中有一部分可能還需要解釋一下:

//設置初始值
this.setInitData().always(function (data) {
    opts.parseData(data);

    var fields = {};
    for (var i in data) {
        if (hasOwn.call(data, i)) {
            fields[i] = '';
        }
    }

    for (var i in opts.fieldOptions) {
        if (hasOwn.call(opts.fieldOptions, i)) {
            fields[i] = '';
        }
    }

    //解析字段的初始值
    var fieldOptions = {};
    for (var i in fields) {
        if (hasOwn.call(fields, i)) {
            fieldOptions[i] = (i in opts.fieldOptions) && opts.fieldOptions[i] || {};
            (i in data ) && (fieldOptions[i][that.mode == 1 ? 'defaultValue' : 'value'] = data[i]);
        }
    }

    //初始值可能是異步獲取的,所以必須在初始數據獲取完畢之后再初始化formMap組件
    that.formMap = new FormMap($element, {
        mode: that.mode,
        fieldOptions: fieldOptions
    });

    //告訴外部初始化完成
    that.trigger('formInit');
});

1)注意always這個方法使用,跟前面介紹的setInitData()的返回值有關系;

2)以上代碼中的三個循環,前面2個是為了找出fieldOptions和initData中所有的字段,第三個是為了將字段的option跟initData中的值合並起來,以便最后實例化formMap的時候,直接把fieldOptions傳遞進去,這樣里面的每個表單元素組件在實例化的時候就能得到外部表單組件獲取的初始值。

3)還有一種做法:不在表單獲取完initData后再去初始化formMap,而是之前就初始化好,然后當initData獲取完以后再通過formMap的setData方法來設置初始值,這樣有兩個問題:
a. formMap提前初始化,各個表單元素組件的初始值都是空的,當form調用reset的時候,不會重置成form獲取的值,而是reset成空值;
b. setData方法如果管理不好,會導致在初始化調用的時候觸發各個表單元素實例的change事件,這對於初始化過程來說,是不應該的,因為那個時候的change事件不符合語義。

form.js的注意事項

form.js的使用方式可參考demo中的demo2.js。

http://liuyunzhuge.github.io/blog/form/dist/js/app/demo2.js

由於formMap的初始化以及form的init事件觸發都是異步的,所以如果外部有些邏輯依賴formMap的話,要考慮把那些邏輯放到form的onInit事件回調里面去做,否則即使不報undefined錯誤,也達不到想要的功能。

本文小結

本文提供了一個代碼跟功能都很簡單的表單組件,它跟上文的那些組件一起,構成了我自己在工作中做表單開發的全部內容,由於它們跟我自身的開發經驗有很大的關系,所以我也不敢保證這些東西對每個人來說都一定是好用的,但是至少啟發作用還是有的,我寫這些東西就是受曾經公司開發平台的啟發以及后來的項目實際情況的影響,也許有人看到了這些,會寫出更符合自己使用習慣的另一套組件出來,那樣的話,對自己或者對工作,都會有很大的價值。

下一篇介紹如何自定義jquery.validation,來實現好看的帶tooltip的表單校驗,敬請關注:)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM