import WxStorage from '../helpers/WxStorage' var factory = function () { "use strict"; var _maxExpireDate = new Date('Fri, 31 Dec 9999 23:59:59 UTC'); var _defaultExpire = _maxExpireDate; // https://github.com/jeromegn/Backbone.localStorage/blob/master/backbone.localStorage.js#L63 var defaultSerializer = { serialize: function (item) { return JSON.stringify(item); }, // fix for "illegal access" error on Android when JSON.parse is // passed null deserialize: function (data) { return data && JSON.parse(data); } }; function _extend (obj, props) { for (var key in props) obj[key] = props[key]; return obj; } /** * https://github.com/gsklee/ngStorage/blob/master/ngStorage.js#L52 * * When Safari (OS X or iOS) is in private browsing mode, it appears as * though localStorage is available, but trying to call .setItem throws an * exception below: "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was * made to add something to storage that exceeded the quota." */ function _isStorageSupported (storage) { return true; var supported = false; if (storage && storage.setItem ) { supported = true; var key = '__' + Math.round(Math.random() * 1e7); try { storage.setItem(key, key); storage.removeItem(key); } catch (err) { supported = false; } } return supported; } // get storage instance function _getStorageInstance (storage) { //调用封装 var wxStorage = new WxStorage; return wxStorage; /* var type = typeof storage; if (type === 'string' && window[storage] instanceof Storage) { return window[storage]; } return storage; */ } function _isValidDate (date) { return Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date.getTime()); } function _getExpiresDate (expires, now) { now = now || new Date(); if (typeof expires === 'number') { expires = expires === Infinity ? _maxExpireDate : new Date(now.getTime() + expires * 1000); } else if (typeof expires === 'string') { expires = new Date(expires.replace(/-/g, "/")); } if (expires && !_isValidDate(expires)) { throw new Error('`expires` parameter cannot be converted to a valid Date instance'); } return expires; } // http://crocodillon.com/blog/always-catch-localstorage-security-and-quota-exceeded-errors function _isQuotaExceeded(e) { var quotaExceeded = false; if (e) { if (e.code) { switch (e.code) { case 22: quotaExceeded = true; break; case 1014: // Firefox if (e.name === 'NS_ERROR_DOM_QUOTA_REACHED') { quotaExceeded = true; } break; } } else if (e.number === -2147024882) { // Internet Explorer 8 quotaExceeded = true; } } return quotaExceeded; } // cache item constructor function CacheItemConstructor (value, exp) { // createTime this.c = (new Date()).getTime(); exp = exp || _defaultExpire; var expires = _getExpiresDate(exp); // expiresTime this.e = expires.getTime(); this.v = value; } function _isCacheItem(item) { if (typeof item !== 'object') { return false; } if(item) { if('c' in item && 'e' in item && 'v' in item) { return true; } } return false; } // check cacheItem If effective function _checkCacheItemIfEffective(cacheItem) { var timeNow = (new Date()).getTime(); return timeNow < cacheItem.e; } function _checkAndWrapKeyAsString(key) { if (typeof key !== 'string') { console.warn(key + ' used as a key, but it is not a string.'); key = String(key); } return key; } // cache api var CacheAPI = { set: function (key, value, options) {}, get: function (key) {}, delete: function (key) {}, // Try the best to clean All expires CacheItem. deleteAllExpires: function() {}, // Clear all keys clear: function () {}, // Add key-value item to memcached, success only when the key is not exists in memcached. add: function (key, options) {}, // Replace the key's data item in cache, success only when the key's data item is exists in cache. replace: function (key, value, options) {}, // Set a new options for an existing key. touch: function (key, exp) {} }; // cache api var CacheAPIImpl = { set: function(key, val, options) { key = _checkAndWrapKeyAsString(key); options = _extend({force: true}, options); if (val === undefined) { return this.delete(key); } var value = defaultSerializer.serialize(val); var cacheItem = new CacheItemConstructor(value, options.exp); try { this.storage.setItem(key, defaultSerializer.serialize(cacheItem)); } catch (e) { if (_isQuotaExceeded(e)) { //data wasn't successfully saved due to quota exceed so throw an error this.quotaExceedHandler(key, value, options, e); } else { console.error(e); } } return val; }, get: function (key) { key = _checkAndWrapKeyAsString(key); var cacheItem = null; try{ cacheItem = defaultSerializer.deserialize(this.storage.getItem(key)); }catch(e){ return null; } if(_isCacheItem(cacheItem)){ if(_checkCacheItemIfEffective(cacheItem)) { var value = cacheItem.v; return defaultSerializer.deserialize(value); } else { this.delete(key); } } return null; }, delete: function (key) { key = _checkAndWrapKeyAsString(key); this.storage.removeItem(key); return key; }, deleteAllExpires: function() { var length = this.storage.length; var deleteKeys = []; var _this = this; for (var i = 0; i < length; i++) { var key = this.storage.key(i); var cacheItem = null; try { cacheItem = defaultSerializer.deserialize(this.storage.getItem(key)); } catch (e) {} if(cacheItem !== null && cacheItem.e !== undefined) { var timeNow = (new Date()).getTime(); if(timeNow >= cacheItem.e) { deleteKeys.push(key); } } } deleteKeys.forEach(function(key) { _this.delete(key); }); return deleteKeys; }, clear: function () { this.storage.clear(); }, add: function (key, value, options) { key = _checkAndWrapKeyAsString(key); options = _extend({force: true}, options); try { var cacheItem = defaultSerializer.deserialize(this.storage.getItem(key)); if (!_isCacheItem(cacheItem) || !_checkCacheItemIfEffective(cacheItem)) { this.set(key, value, options); return true; } } catch (e) { this.set(key, value, options); return true; } return false; }, replace: function (key, value, options) { key = _checkAndWrapKeyAsString(key); var cacheItem = null; try{ cacheItem = defaultSerializer.deserialize(this.storage.getItem(key)); }catch(e){ return false; } if(_isCacheItem(cacheItem)){ if(_checkCacheItemIfEffective(cacheItem)) { this.set(key, value, options); return true; } else { this.delete(key); } } return false; }, touch: function (key, exp) { key = _checkAndWrapKeyAsString(key); var cacheItem = null; try{ cacheItem = defaultSerializer.deserialize(this.storage.getItem(key)); }catch(e){ return false; } if(_isCacheItem(cacheItem)){ if(_checkCacheItemIfEffective(cacheItem)) { this.set(key, this.get(key), {exp: exp}); return true; } else { this.delete(key); } } return false; } }; /** * Cache Constructor */ function CacheConstructor (options) { // default options var defaults = { storage: 'localStorage', exp: Infinity //An expiration time, in seconds. default never . }; var opt = _extend(defaults, options); var expires = opt.exp; if (expires && typeof expires !== 'number' && !_isValidDate(expires)) { throw new Error('Constructor `exp` parameter cannot be converted to a valid Date instance'); } else { _defaultExpire = expires; } var storage = _getStorageInstance(opt.storage); var isSupported = _isStorageSupported(storage); this.isSupported = function () { return isSupported; }; if (isSupported) { this.storage = storage; this.quotaExceedHandler = function (key, val, options, e) { console.warn('Quota exceeded!'); if (options && options.force === true) { var deleteKeys = this.deleteAllExpires(); console.warn('delete all expires CacheItem : [' + deleteKeys + '] and try execute `set` method again!'); try { options.force = false; this.set(key, val, options); } catch (err) { console.warn(err); } } }; } else { // if not support, rewrite all functions without doing anything _extend(this, CacheAPI); } } CacheConstructor.prototype = CacheAPIImpl; return CacheConstructor; } export default factory();