2017年1月6日 星期五

(js)JSONP的流程


////////////////////////////////////////////////////////////////////////////////
/**
 *
 *
 * 使用方式 JSONP.send(options);
 *
 *
 * <options 主要對外參數>
 *
 * timeout(int) => 設定過時
 * url(string, not null) => 要連往後端的網址
 * data(string, {}) => 要送出的參數
 * jsonp(string) => url參數(request('callBack') = function_name)
 * jsonpCallBack(string) => 手動設定callBack 函式的名稱(尚無此功能,主要是自動給予)
 *
 * error(function) => 若執行錯誤要執行的
 * beforeSend(function) => 在整個執行前要執行的函式
 * complete(function) => success完,數據都讀完處理完要做的事
 * success(function) => 執行成功要執行的函式
 *
 */
////////////////////////////////////////////////////////////////////////////////


function $JSONP_(option) {
    'use strict';
    // debugger;

    var self = this;

    // 物件的建構函式
    this.fn = this.constructor;

    /* --------------------------------------------- */
    this.haveCall = false; // 是否在傳輸階段
    this.haveLoad = false; // script是否正在load
    /* --------------------------------------------- */
    this.errorMsg_array = [];

    // head-tag
    this.headTag;
    /* --------------------------------------------- */

    /**
     * timeout: 設定過時
     * url: 要連往後端的網址
     * data: 要送出的參數(string|{})
     * jsonp: url參數(傳遞給後端的key => 此 key 內容主要的功能在告訴回呼函式的名稱)
     * jsonpCallBack: 手動設定callBack 函式的名稱(尚無此功能,主要是自動給予)
     *
     * async: 網頁運作是否要等 script loading完
     * defer:是否要等整網頁都 load完才執行
     *
     * error: 若執行錯誤要執行的函式
     * beforeSend: 在整個執行前要執行的函式
     * complete:
     * success: 執行成功要執行的函式
     */
    this.options = {
        timeout: 6000,
        url: '',
        data: null,
        async: true,
        defer: false,
        jsonp: 'callBack', // (request('callBack') = function_name)
        error: null,
        beforeSend: null,
        complete: null,
        success: null
    };

    /* -------------------------------------------- */
    this.id;
    this.fun_name = ''; // (callBack)的名稱
    this.script_id = '';
    /* -------------------------------------------- */

    this.src = ''; // 要引入 json 數據的 script.src
    this.arg = ''; // 要引入 json 數據的網址參數

    /* ====================================================================== */

    /**
     * 初始化(建構式)
     */
    this.__construct = function() {
        'use strict';
        // debugger;
        if (!(this.headTag = this.fn.prototype.headTag)) {
            this.headTag = this.fn.prototype.headTag = document.querySelector('head');
        }

        // 登錄所有 option
        this.setOption(option);
        /* ---------------------------------------------- */

        // 創建所需要的(id)
        this.id = this.fn.prototype.guid++;

        this.script_id = '$JSONP_' + this.id;

        // (callBack)的名稱
        this.fun_name = 'fun_' + this.id;
        /* ---------------------------------------------- */
        // 處理要傳送的參數(this.arg)
        this.setData();

        this.setArg();
        /* ---------------------------------------------- */
        // (script)的網址
        this.src = this.options.url + '?' + this.arg;;
    };
    /* ====================================================================== */

    this.__construct();
}
/* ========================================================================== */
/**
 * 共有參數
 */
(function() {
    this.headTag;
    this.guid = 1; // 編碼

}).call($JSONP_.prototype);


////////////////////////////////////////////////////////////////////////////////
/**
 * prototype
 *
 */

(function() {
    /**
     * start here
     */
    this.main = function() {
        var self = this;

        if (typeof this.options.beforeSend == 'function') {
            this.options.beforeSend();
        };
        /* ---------------------------------------------------- */

        // 檢查(options)
        this.checkOptions();

        // 創建臨時需要的(callBack)
        this.createCallBackFun();

        // 定時檢查
        setTimeout(timeOut.bind(this), this.options.timeout);

        // 創建要引入資料的(script)
        this.createJSONDataScript();
    };
    /* ===================================================================== */
    /**
     * 當傳輸過時
     *
     */
    function timeOut() {
        debugger;

        if (!this.haveCall) {
            // 時間到了,(callBack)卻未執行過

            var scriptDom = document.getElementById(this.script_id);;

            /* ---------------------------------- */
            // 錯誤訊息
            this.errorMsg_array.push('timeout');

            if (scriptDom) {
                this.errorMsg_array.push(scriptDom.textContent);
            }
            /* ---------------------------------- */
            // reset

            if (this.haveLoad) {
                this.reset_1();
            } else {
                // (script)還在(load)中
                this.reset_2();
            }

            /* ---------------------------------- */
            // 發出錯誤
            this.error();
        }
    };
}).call($JSONP_.prototype);
/* ========================================================================== */
(function() {
    /**
     * 創建要被(script)呼叫的(callBack)
     *
     * @return {[type]} [description]
     */
    this.createCallBackFun = function() {
        // debugger;

        var self = this;

        this.fn[this.fun_name] = callBack;

        /* ---------------------------------- */
        /**
         * 被呼叫的地方
         *
         */
        function callBack(data) {
            alert('callBack');
            debugger;
            // 已執行(callBack)
            self.haveCall = true;

            var success = self.options.success;
            self.options.success = undefined;

            if (typeof success == 'function') {
                success(data);
            }
        };
    };
    /* ====================================================================== */
    // 創建要引入 json 數據的 script
    this.createJSONDataScript = function() {
        debugger;

        var self = this;

        var script = document.createElement('script');

        script.setAttribute('src', this.src);
        script.setAttribute('id', this.script_id);
        /* ----------------------------------------- */
        /**
         * (script)的同步設定
         *
         * (defer)比(async)優先
         *
         * defer => 在網頁(load)完才執行
         * async => 異步處理
         *
         */

        if (this.options.defer) {
            this.options.async = false;
            script.defer = true;
        } else if (this.options.async) {
            script.async = true;
        }

        /* ----------------------------------------- */
        script.onerror = function(e) {
            self.haveLoad = true; // 已嘗試建立過(script)

            self.errorMsg_array.push('script 無法建立');
            self.error();
        };

        /* ----------------------------------------- */
        /**
         * 最後步驟
         *
         * (script)已經(load)完,要執行的命令也執行完
         */
        script.onload = function(e) {
            self.haveLoad = true; // 已嘗試建立過(script)

            var complete = self.options.complete;
            self.options.complete = undefined;

            (typeof complete == 'function') && (complete());
            /* ---------------------------------- */

            if (self.haveCall == true) {
                // callBack已執行過,script已讀完
                self.reset_1();
            }
        };

        /* ----------------------------------------- */

        // debugger;

        this.headTag.appendChild(script);
    };

    /* ====================================================================== */

    /**
     * 立即移除<script>標籤
     *
     * 立即移除(callBack)在(jsonp)中的紀錄
     */
    this.reset_1 = function() {

        var self = this;

        // 移除(callback)
        if (this.fn[this.fun_name] != null) {
            delete self.fn[this.fun_name];
        }
        /* ---------------------------------------- */
        // 移除(script)
        var scriptNode = document.getElementById(this.script_id);
        (scriptNode) && (this.headTag.removeChild(scriptNode));
    };
    /**
     * 移除(script)標籤
     *
     * 延時移除(callback)
     */
    this.reset_2 = function() {

        var self = this;

        // 移除(callback)
        if (this.fn[this.fun_name] != null) {
            // 正常執行的話(this.fn[this.fun_name])應該已經刪除了
            setTimeout(function() {
                // body
                delete self.fn[this.fun_name];
            }, 100000);
        }
        /* ---------------------------------------- */
        // 移除(script)
        var scriptNode = document.getElementById(this.script_id);
        (scriptNode) && (this.headTag.removeChild(scriptNode));
    };



    /* ====================================================================== */
    /**
     * 發出錯誤
     */
    this.error = function() {
        var error = this.options.error;
        this.options.error = undefined;

        if (typeof error == 'function') {
            var error_msg = this.errorMsg_array.join(', ');
            error(error_msg);
        }
    };

}).call($JSONP_.prototype);

/* ========================================================================== */
/**
 * prototype
 */
(function() {

    /**
     * 處理傳送的參數,轉換 data格式
     */
    this.setData = function() {
        // debugger;

        if (this.options.data == null) {
            this.options.data = {};
        } else if (typeof this.options.data == 'string') {
            // 將文字參數轉為物件
            var arg_array = this.options.data.split('&');

            this.options.data = {};

            for (var i in arg_array) {
                var tmp_array = arg_array[i].split('=');

                var key = tmp_array[0] || '';
                var value = tmp_array[1] || '';

                this.options.data[key] = value;
            }
        }
        /* ------------------------------------------------------- */
        // 處理(callBack)
        var date = new Date();

        // 預設是(callBack)
        var jsonpName = this.options.jsonp;

        // 告訴後端要呼叫那個(callBack)
        this.options.data[jsonpName] = '$JSONP_.' + this.fun_name;
        this.options.data.timeStamp = date.getTime(); // 防止網頁快取
    };
    /* ====================================================================== */
    /**
     * 設定網址參數
     */
    this.setArg = function() {
        var tmpArray = [];
        for (var j in this.options.data) {
            var str = j + '=' + this.options.data[j];
            tmpArray.push(str);
        }

        this.arg = tmpArray.join('&');
    };

    /* ====================================================================== */
    /**
     * 處理選項
     */
    this.setOption = function(option) {
        // debugger;

        var optionType = Object.prototype.toString.call(option);

        if (!/object\]\s*$/gi.test(optionType)) {
            throw new TypeError('input option must be {}');
        }

        for (var key in option) {
            if (this.options.hasOwnProperty(key)) {
                this.options[key] = option[key];
            }
        }

    };

}).call($JSONP_.prototype);
////////////////////////////////////////////////////////////////////////////////
(function() {
    this.___check = function() {};
    /* ======================================================================== */
    /**
     * 輸入檢查
     */
    this.checkOptions = function() {
        // debugger;
        var errorMsg_array = [];
        var toString_fun = Object.prototype.toString;

        if (!this.headTag || this.headTag.tagName.toLowerCase() !== 'head') {
            errorMsg_array.push('no catch head tag');
        }

        if (typeof this.arg !== 'string') {
            errorMsg_array.push('網址參數格式有問題');
        }
        // options.data
        if (this.options.data != null) {
            var type = toString_fun.call(this.options.data);
            if (typeof this.options.data !== 'string' &&
                !/object\]\s*$/gi.test(type)) {
                errorMsg_array.push('data參數格式有問題');
            }
        }
        // options.url
        if (typeof this.options.url !== 'string' || this.options.url.length == 0) {
            errorMsg_array.push('網址格式有問題');
        }

        // options.timeout
        if (typeof this.options.timeout != 'number') {
            errorMsg_array.push('timeout must be number');
        }

        // options.error
        if (this.options.error && (typeof this.options.error !== 'function')) {
            errorMsg_array.push('error must be function');
        }

        // options.beforeSend
        if (this.options.beforeSend && (typeof this.options.beforeSend !== 'function')) {
            errorMsg_array.push('beforeSend must be function');
        }

        // options.complete
        if (this.options.complete && (typeof this.options.complete !== 'function')) {
            errorMsg_array.push('complete must be function');
        }

        // options.success
        if (this.options.success && (typeof this.options.success !== 'function')) {
            errorMsg_array.push('success must be function');
        }

        // this.src
        if (typeof this.src != 'string' || this.src.length == 0) {
            errorMsg_array.push('script no set link');
        }

        if (errorMsg_array.length > 0) {
            throw new Error(errorMsg_array.join(" | "));
        }
    };
    /* ====================================================================== */
}).call($JSONP_.prototype);

////////////////////////////////////////////////////////////////////////////////
/**
 * 類別方法
 */
(function() {
    var self = this;

    /* ====================================================================== */
    // (API)start here
    this.send = function(options) {
        // debugger;

        var jsonp = new $JSONP_(options);

        jsonp.main();

        return jsonp;
    };
    /* ====================================================================== */


}).call($JSONP_);