Source: adm.jquery.js

/**
 * @file 全局数据模型 model
 * 提供数据的读取、保存/缓存、删除、更新等操作。各模块 model 可继承该模型,以进行模块范围内的数据存取操作。
 * @module adm
 * @author lzwy0820@qq.com
 * @since 2016-03-31
 *
 * @example
 * import adm from 'ajax-data-model';
 * const upsModel = $.extend(true, {}, adm, {aa: 'ccc', restapi: {task_type: '/rest/task/type'}});
 * // 支持的方法:upsModel.get、upsModel.save、upsModel.delete、upsModel.clear
 * // 配置了 url,则都返回 Promise 对象,不管是否缓存
 * upsModel.get({url: '/rest/xxx'}).done().fail().then();
 * // 保存数据到 localStorage 中
 * upsModel.save('appList', [{music: 'test'}], 'localStorage');
 * // 通过名字获取,返回存储的数据或者 undefined
 * upsModel.get('appList');
 * upsModel.get('appList', 'localStorage');
 *
 * @example
 * // 获取 task_type 数据
 * const data = {type: 10};
 * adm.get({
 *     url: upsModel.restapi.task_type,
 *     data: data,
 *     cache: 'sessionStorage',             // 缓存到 sessionStorage
 *     fromCache: 'sessionStorage',         // 获取时优先从 sessionStorage 读取
 *     cacheName: 'task_type_' + data.type, // 缓存、从缓存读取时使用的名称
 *     expires: 1000 * 60 * 5,              // 数据有效时间为 5 分钟
 * }).then((result) => {
 *     let taskTypeList = result.value || [];
 *     console.log(taskTypeList);
 * }, (err) {
 *     console.log(err);
 * });
 */
'use strict';

// import $ from 'jquery';

import settings from './common/settings';
import {
    getCacheStor,
    deleteCacheDataByName,
    getCacheDataByName,
    saveTOCache,
    isString,
    getPromise
} from './common/cache-helper';

/**
 * ajax 请求通用方法
 * @param {Object}   config - 请求参数配置
 * @param {String}   config.url - ajax url,必须存在,`config.ajaxParam` 中配置此参数无效
 * @param {Object}   config.ajaxParam - ajax 额外参数扩展,如涉及文件上传等
 * @param {Object}   config.data - ajax 请求的参数
 * @param {Object}   config.waiting - 用于传递给 settings.fnWaiting 方法使用的参数配置
 * @param {Object}   config.tipConfig[true] - ajax 出错时的提示配置。配置为 false 时,禁用全局的系统提示,包括 成功/出错/404/50x 等
 * @param {Object}   config.errAlert[true] - ajax error 时是否给出提示
 * @param {Function} callback - ajax 请求成功时回调
 * @param {Function} errCallback - ajax 请求失败或 code !== 200 时回调
 * @param {Object}   param - 传递给 ajax 请求的额外参数
 * @param {Function} fnCB - 请求到数据之后的立即回调方法,用于请求成功后需要前置处理的情况
 * @return {Promise}  用于自定义回调处理。
 *                    注意:ajax 请求的 done/fail 回调,与 callback/errCallback 可能有区别,具体取决于 fnAjaxDone 与 fnAjaxFail 回调的实现
 */
function requestAjax(config, callback, errCallback, fnCB) {
    const $p = getPromise(settings.isJquery);

    if (!config.url || typeof config.url !== 'string') {
        console.trace('请求 URL API 不存在,或格式不对:', config.url);
        return $p.reject('请求 URL API 不存在,或格式不对:', config.url);
    }

    // data.btnWaiting 的兼容,应使用 config.waiting 参数
    if (config.data && config.data.btnWaiting) {
        config.waiting = config.waiting || config.data.btnWaiting;
        delete config.data.btnWaiting;
    }

    // jsonp 兼容
    let dataType = 'json';

    if (/^https?:\/\//.test(config.url) && config.url.search(window.location.host) === -1) {
        dataType = 'jsonp';
    }

    // 请求前回调,可以引用方式修改 config
    if (settings.fnBeforeAjax) {
        settings.fnBeforeAjax(config);
    }

    // 格式化 config.data
    let item;

    if ('object' === typeof config.data) {
        for (item in config.data) {
            if ('string' !== typeof config.data[item]) {
                config.data[item] = JSON.stringify(config.data[item]);
            }
        }
    }

    // ajax 请求前处理,与请求后处理呼应
    settings.fnWaiting(config);

    const startTime = new Date();

    return $.ajax($.extend(true, {
        type: 'GET',
        dataType
    }, config.ajaxParam, {
        url: config.url,
        data: config.data
    })).then((result) => {
        const success = settings.fnAjaxDone(result, (res) => {
            if (fnCB instanceof Function) {
                fnCB(result);
            }

            if (callback instanceof Function) {
                callback(res);
            }
        }, errCallback, config);

        // 为 false,设为失败回调
        if (!success) {
            return $p.reject(result);
        }

        // 为 true
        if (true === success) {
            return $p.resolve(result);
        }

        // 为 Promise 风格回调
        if ('function' === typeof success.then) {
            // $p = success;
            // return $p;
            return success;
        }

        // 为其它类型,返回 success 内容
        return $p.resolve(success);
    }, (err) => {
        settings.fnAjaxFail(err, config);

        if (errCallback instanceof Function) {
            errCallback(err);
        }

        return $p.reject(err);
    }).always(() => {
        // ajax 完成后处理
        settings.fnWaiting(config, new Date() - startTime);
    });

    // return $p;
}

// 获取缓存数据的名称 key
function getCacheName(config) {
    // 第一个参数为字符串,则为名称,直接返回 config 作为缓存名称
    if (isString(config) || !config) {
        return config;
    }

    let cacheName = config.cacheName;
    const data = config.data;

    if (!cacheName) {
        cacheName = config.url;

        if (typeof data === 'object') {
            cacheName += JSON.stringify(data);
        }
    }

    return cacheName;
}

/**
 * 全局数据模型 model
 * @alias module:adm
 * @type {Object}
 */
export default {
    /**
     * 数据获取,可为远程url、缓存等
     * @param {Object} config 为字符串时,从缓存中读取数据,否则为从远程获取数据,参数如下:
     * ```js
     * {
     *     url: '',          // API url 地址,可为空。为空时应存在 cacheName,此时为从缓存中读取数据
     *     data: {},         // url 请求参数
     *     cache: false,     // 配置了 url 获取数据时,是否缓存数据。可取值:`false/true/sessionStorage/localStorage`
     *     fromCache: false, // 配置了 url,是否首先尝试从缓存中读取数据。可取值:`false/true/sessionStorage/localStorage`
     *     cacheName: '',    // 配置了 url 并且 cache 为 true,配置缓存数据的名称,不配置则取值 url (/ 会替换为 . 作为深度路径)
     *     expires: 0,       // 如果 cache 为 true,设置缓存数据的有效期,可为 毫秒数,或 Date 类型日期
     *     tipConfig: {delay: 2000} // ajax 出错时的提示配置。配置为 false 时,禁用全局的系统提示,包括 成功/出错/404/50x 等
     *     errAlert: true    // ajax error 时是否给出全局提示,优先级高于 settings.errAlert
     *     waiting: {}       // 按钮等待等配置,用于传递给 settings.fnWaiting 方法
     *     ajaxParam: null   // ajax 额外参数扩展,如涉及文件上传等,需要修改部分参数。其中 url 参数无效,应当使用 config.url
     * }
     * ```
     * @param {Object} callback 成功回调方法
     * @param {Object} errCallback 从 url 获取时,失败后需要做一些处理的回调方法
     * }
     */
    get(config, callback, errCallback) {
        if (!config) {
            return undefined;
        }

        let cacheData;
        const $promise = getPromise(settings.isJquery);
        const cacheName = getCacheName(config);

        // 配置了 url,从 url 中获取
        if (config.url) {
            cacheData = getCacheDataByName(cacheName, config.fromCache);

            // fromCache 为 true,尝试从缓存中获取数据
            if (config.fromCache && cacheData) {
                if (typeof callback === 'function') {
                    callback(cacheData);
                }

                $promise.resolve(cacheData);
                // return cacheData; // 返回数据
                return $promise; // 这里改了后不兼容旧的调用,应该注意 bug 的出现!
            }

            config.ajaxParam = $.extend(config.ajaxParam, {
                type: 'GET'
            });

            return requestAjax(config, callback, errCallback, (result) => {
                // cache 为 true,缓存数据
                if (config.cache) {
                    this.save(cacheName, result, config);
                }
            });
        } else if (config.hasOwnProperty('url')) { // 配置了 url,但 url 值为空
            console.trace('配置了 URL 参数,但值为空:', config);
            $promise.reject('配置了 URL 参数,但值为空', config);
        } else {
            // 未配置 url,则必须配置 config.cacheName,或者 config 为字符串(作为cacheName),此时为从缓存中取得数据
            cacheData = getCacheDataByName(cacheName, config.fromCache || callback);

            if (callback instanceof Function) {
                callback(cacheData);
            }

            return cacheData;
        }

        return $promise;
    },
    /**
     * 设置/存储数据
     * @param {Object|String} config - 配置信息。也可以为字符串,则为需存储的数据名称。与 {@link module:adm~get} 的 config 参数相同
     * @param {Function|Object} callback - 存储成功后回调方法。当 config 为字符串时,为需存储的数据,或方法执行后返回要存储的数据
     * @param {Function|String} errCallback - 从 url 获取时,失败后需要做一些处理的回调方法。config 为字符串时,为配置信息,如 {cacheType, expires}
     * @example
     * // 存储数据到 localStorage,名称为 testdataName
     * adm.save('testdataName', {test: 1}, 'localStorage');
     * @example
     * // 存储数据到远程,同时存储到 sessionStorage
     * adm.save({url: '/rest/dd', data: {test: 1}, cache: 'sessionStorage'});
     */
    save(config, callback, errCallback) {
        if (!config) {
            return '';
        }

        let cacheData;
        const $promise = getPromise(settings.isJquery);
        const cacheName = getCacheName(config);

        if (isString(config)) { // config 为字符串,则作为cacheName
            if (callback instanceof Function) { // 可以存储为回调方法执行后的结果
                saveTOCache(cacheName, callback(), errCallback);
            } else {
                saveTOCache(cacheName, callback, errCallback);
            }
            $promise.resolve(cacheName);
        } else if (config.url) { // 配置了 url,将数据存储到远程
            cacheData = getCacheDataByName(cacheName, config.fromCache);

            // fromCache 为 true,尝试从缓存中获取数据
            if (config.fromCache && cacheData) {
                if (callback instanceof Function) {
                    callback(cacheData);
                }

                $promise.resolve(cacheData);
                // return cacheData; // 返回数据
                return $promise; // 这里改了后不兼容旧的调用,应该注意 bug 的出现!
            }

            config.ajaxParam = $.extend({
                type: 'POST'
            }, config.ajaxParam);

            return requestAjax(config, callback, errCallback, (result) => {
                if (config.cache) {
                    // 远程存储成功了,本地也需缓存数据时
                    saveTOCache(cacheName, result, config);
                }
            });
        } else if (config.hasOwnProperty('url')) { // 配置了url,但 url 值为空
            console.trace('配置了 URL 参数,但值为空:', config);
            $promise.reject('配置了 URL 参数,但值为空', config);
        } else if (cacheName) { // 没有设置 url,但设置了 config.cacheName(此时 cacheName=config.cachename),则保存数据到本地
            saveTOCache(cacheName, config.data, config);

            if (callback instanceof Function) {
                callback(cacheData);
            }
            $promise.resolve(config.data);
        }

        return $promise;
    },
    /**
     * 删除一个数据
     * @param {Object} config - 为字符串时,作为 cacheName 尝试从缓存中删除数据。否则格式如下:
     * ```js
     * {
     *     url: '',       // 配置了 url,从远程删除数据,否则从缓存中删除
     *     cache: false,  // 配置了 url,是否还尝试从缓存中删除数据。可取值:false/true/sessionStorage/localStorage
     *     cacheName: ''  // 从缓存中删除数据时,提供其名称。
     * }
     * ```
     */
    delete(config, callback, errCallback) {
        if (!config) {
            return '';
        }

        const $promise = getPromise(settings.isJquery);
        const cacheName = getCacheName(config);

        if (isString(config) || config instanceof RegExp) {
            // 第一个参数为字符串或正则,callback 就是 cacheType
            deleteCacheDataByName(config, callback);
            // 删除完成都返回执行成功
            $promise.resolve();
        } else if (config.url) {
            // 配置了 url,从远程删除数据
            return requestAjax(config, callback, errCallback, {
                type: 'DELETE'
            }, () => {
                if (config.cache) {
                    // 远程删除成功了,本地也需清空时
                    deleteCacheDataByName(cacheName, config.cache);
                }
            });
        } else if (config.hasOwnProperty('url')) { // 配置了url,但 url 值为空
            console.trace('配置了 URL 参数,但值为空:', config);
            $promise.reject('配置了 URL 参数,但值为空', config);
        } else if (cacheName) {
            deleteCacheDataByName(cacheName, config.cache);
            $promise.resolve();
        }

        return $promise;
    },
    /**
     * 返回所有存储中的所有数据
     * @param  {String} cacheType 存储的类型:sessionStorage、localStorage 或 memory
     * @return {Object}
     */
    getAll(cacheType) {
        const cacheStor = getCacheStor(cacheType);
        const _cache = {};
        const len = cacheStor.length;
        let i;
        let item, key;

        for (i = 0; i < len; i++) {
            item = cacheStor.key(i);

            if (!item || 0 !== item.indexOf(settings.cachePrefix)) {
                continue;
            }

            key = item.replace(settings.cachePrefix, '');
            try {
                _cache[key] = JSON.parse(cacheStor.getItem(item));
            } catch (e) {
                _cache[key] = cacheStor.getItem(item);
            }
        }

        return _cache;
    },
    /**
     * {@link module:dataModel.get} 的 ajax 快捷方法
     * @see  module:dataModel.get
     * @param  {String}   url         url 地址
     * @param  {Object}  data        要传递的参数,可省略
     * @param  {Function} callback    成功回调
     * @param  {Function}   errCallback 失败回调
     * @returns {Promise}
     */
    getJSON(url, data = {}, callback, errCallback) {
        // data 参数可以省略
        if (data instanceof Function) {
            errCallback = callback;
            callback = data;
            data = void 0;
        }

        return this.get({
            url,
            data
        }, callback, errCallback);
    },
    /**
     * {@link module:dataModel.save} 的 ajax 快捷方法
     * @see  module:dataModel.save
     * @param  {String}   url         url 地址
     * @param  {Object}  data        要传递的参数
     * @param  {Function} callback    成功回调
     * @param  {Function}   errCallback 失败回调
     * @returns {Promise}
     */
    post(url, data, callback, errCallback) {
        return this.save({
            url,
            data
        }, callback, errCallback);
    },
    /**
     * 根据存储类型清空存储的所有数据
     * @param  {String} cacheType
     * @return {scope} this
     */
    clear(cacheType) {
        deleteCacheDataByName(new RegExp('.*'), cacheType);

        return this;
    },
    /**
     * 修改缓存数据的前缀
     * @param {String} prefix           以下划线开头,由字母、数字、或下划线组成
     * @param {Boolean} clear[=true]    修改前缀前,是否移除已有的数据
     */
    setCachePrefix(prefix, clear = true) {
        if (!/^_[_a-zA-Z]*_$/.test(prefix)) {
            console.warn('以下划线开头和结尾,由字母、数字、或下划线组成');
            return this;
        }

        if (clear) {
            this.clear('sessionStorage');
            this.clear('localStorage');
            this.clear();
        }

        settings.cachePrefix = prefix;

        return this;
    },
    /**
     * 设置配置项
     * @param {Object} setting
     */
    setSettings(setting) {
        let item;

        for (item in setting) {
            if ('cachePrefix' === item) {
                this.setCachePrefix(setting[item], false);
            } else if (settings.hasOwnProperty(item)) {
                settings[item] = setting[item];
            }
        }

        return settings;
    }
};