/** * 地址数据懒加载器 - 按需加载,大幅减少首次加载时间 * 优化方案: * 1. 首次只加载省份列表(数据量很小,< 1MB) * 2. 选择省份后,按需加载该省份的市数据 * 3. 逐级懒加载,避免一次性加载90M+数据 */ import IndexedDBHelper from '@/common/IndexedDBHelper.js'; class AddressDataLoaderLazy { constructor() { this.dbHelper = null; this.dbName = 'AddressDataLazyDB'; this.storeName = 'addressDataLazy'; this.isInitialized = false; // 缓存配置 this.cacheExpireDays = 7; // 数据源配置 this.baseUrl = 'http://124.243.245.42/ks_cms'; this.fullDataUrl = `${this.baseUrl}/address.json`; // 完整地址数据 this.provinceListUrl = `${this.baseUrl}/address_provinces.json`; // 省份列表(轻量级,可选) this.provinceDetailUrl = `${this.baseUrl}/address_province_{code}.json`; // 省份详情(按需加载,可选) // 是否启用分片接口(如果服务器提供了分片接口,设置为true) this.useSplitApi = false; // 默认false,从完整数据中提取 // 内存缓存(避免重复请求) this.memoryCache = new Map(); } /** * 初始化数据库 */ async init() { if (this.isInitialized && this.dbHelper) { return; } try { this.dbHelper = new IndexedDBHelper(this.dbName, 1); await this.dbHelper.openDB([{ name: this.storeName, keyPath: 'key', indexes: [ { name: 'type', key: 'type', unique: false }, { name: 'updateTime', key: 'updateTime', unique: false } ] }]); this.isInitialized = true; console.log('✅ 地址数据懒加载器初始化成功'); } catch (error) { console.error('❌ 地址数据懒加载器初始化失败:', error); this.isInitialized = true; } } /** * 从缓存获取数据 */ async getCachedData(key) { // 先检查内存缓存 if (this.memoryCache.has(key)) { const cached = this.memoryCache.get(key); if (cached.expireTime > Date.now()) { return cached.data; } else { this.memoryCache.delete(key); } } // 检查 IndexedDB 缓存 try { if (this.dbHelper) { const cached = await this.dbHelper.get(this.storeName, key); if (cached && cached.data) { const updateTime = cached.updateTime || 0; const expireTime = this.cacheExpireDays * 24 * 60 * 60 * 1000; if (Date.now() - updateTime < expireTime) { // 同时更新内存缓存 this.memoryCache.set(key, { data: cached.data, expireTime: Date.now() + expireTime }); return cached.data; } } } } catch (e) { console.warn('从 IndexedDB 读取缓存失败:', e); } // 降级方案:从 uni.storage 读取 try { const cached = uni.getStorageSync(key); if (cached && cached.data) { const updateTime = cached.updateTime || 0; const expireTime = this.cacheExpireDays * 24 * 60 * 60 * 1000; if (Date.now() - updateTime < expireTime) { this.memoryCache.set(key, { data: cached.data, expireTime: Date.now() + expireTime }); return cached.data; } } } catch (e) { console.warn('从 uni.storage 读取缓存失败:', e); } return null; } /** * 保存数据到缓存 */ async saveToCache(key, data, type = 'province') { const cacheData = { key: key, type: type, data: data, updateTime: Date.now() }; // 更新内存缓存(1小时有效期) this.memoryCache.set(key, { data: data, expireTime: Date.now() + 60 * 60 * 1000 }); try { if (this.dbHelper) { try { const existing = await this.dbHelper.get(this.storeName, key); if (existing) { await this.dbHelper.update(this.storeName, cacheData); } else { await this.dbHelper.add(this.storeName, cacheData); } } catch (e) { await this.dbHelper.add(this.storeName, cacheData); } } } catch (e) { console.warn('保存到 IndexedDB 失败,降级到 uni.storage:', e); } // 降级方案:保存到 uni.storage try { uni.setStorageSync(key, cacheData); } catch (e) { if (e.errMsg?.includes('exceed')) { console.warn('存储空间不足,清理部分缓存'); await this.clearOldCache(); } } } /** * 从服务器加载数据 */ async fetchData(url, cacheKey = null) { return new Promise((resolve, reject) => { console.log(`📥 开始加载: ${url}`); const startTime = Date.now(); uni.request({ url: url, method: 'GET', timeout: 60000, // 60秒超时 success: (res) => { const endTime = Date.now(); const duration = ((endTime - startTime) / 1000).toFixed(2); console.log(`✅ 数据加载完成,耗时 ${duration} 秒`); if (res.statusCode === 200) { let data = res.data; if (typeof data === 'string') { try { data = JSON.parse(data); } catch (e) { reject(new Error('JSON解析失败: ' + e.message)); return; } } // 如果提供了缓存key,自动保存 if (cacheKey) { this.saveToCache(cacheKey, data).catch(e => { console.warn('保存缓存失败:', e); }); } resolve(data); } else { reject(new Error(`请求失败,状态码: ${res.statusCode}`)); } }, fail: (err) => { console.error('❌ 数据加载失败:', err); reject(err); } }); }); } /** * 加载省份列表(轻量级,只包含省份基本信息,不包含children) * 这是首次加载,数据量很小(< 1MB) */ async loadProvinceList(forceRefresh = false) { await this.init(); const cacheKey = 'address_provinces_list'; // 如果不是强制刷新,先尝试从缓存加载 if (!forceRefresh) { const cached = await this.getCachedData(cacheKey); if (cached) { console.log('✅ 从缓存加载省份列表'); return cached; } } uni.showLoading({ title: '加载省份列表...', mask: true }); try { let data = null; // 方案1:如果启用了分片接口,尝试从分片接口加载 if (this.useSplitApi) { try { data = await this.fetchData(this.provinceListUrl, cacheKey); if (data && Array.isArray(data) && data.length > 0) { console.log('✅ 从分片接口加载省份列表'); return data; } } catch (error) { console.warn('⚠️ 分片接口不可用,降级到完整数据提取:', error.message); } } // 方案2:从完整数据中提取省份列表(默认方案) // 如果完整数据已缓存,提取会很快;如果未缓存,需要加载完整数据 data = await this.loadProvinceListFromFullData(forceRefresh); return data; } catch (error) { console.error('❌ 加载省份列表失败:', error); // 如果加载失败,尝试使用缓存(即使已过期) if (!forceRefresh) { const cached = await this.getCachedData(cacheKey); if (cached) { console.log('⚠️ 使用过期缓存数据'); return cached; } } throw error; } finally { uni.hideLoading(); } } /** * 从完整数据中提取省份列表(默认方案) * 优化策略: * 1. 优先从缓存获取已提取的省份列表(最快) * 2. 如果未缓存,尝试从缓存的完整数据中提取(快速) * 3. 如果完整数据也未缓存,使用分段加载策略: * - 先尝试只加载前2MB数据来提取省份列表(快速) * - 同时在后台加载完整数据(不阻塞用户) */ async loadProvinceListFromFullData(forceRefresh = false) { const cacheKey = 'address_full_data'; const provinceListCacheKey = 'address_provinces_extracted'; // 先尝试从缓存获取已提取的省份列表 if (!forceRefresh) { const cached = await this.getCachedData(provinceListCacheKey); if (cached) { console.log('✅ 从缓存加载已提取的省份列表'); // 如果完整数据未缓存,在后台预加载 this.preloadFullDataInBackground(); return cached; } } // 尝试从缓存获取完整数据(如果已缓存,提取会很快) let fullData = await this.getCachedData(cacheKey); if (!fullData || forceRefresh) { // 如果完整数据未缓存,使用分段加载策略 console.log('📥 完整数据未缓存,使用分段加载策略...'); // 方案1:尝试分段加载(只加载前2MB来提取省份列表) try { const provinceList = await this.loadProvinceListFromPartialData(); if (provinceList && provinceList.length > 0) { console.log('✅ 从部分数据提取省份列表成功'); // 缓存提取的省份列表 await this.saveToCache(provinceListCacheKey, provinceList, 'province_list'); // 在后台预加载完整数据 this.preloadFullDataInBackground(); return provinceList; } } catch (error) { console.warn('⚠️ 分段加载失败,降级到完整加载:', error.message); } // 方案2:如果分段加载失败,加载完整数据 console.log('📥 开始加载完整数据...'); console.log('💡 提示:首次加载需要一些时间,加载后会缓存,后续使用会很快'); uni.showLoading({ title: '加载地址数据...', mask: true }); try { fullData = await this.fetchData(this.fullDataUrl, cacheKey); console.log('✅ 完整数据加载完成,已缓存'); } finally { uni.hideLoading(); } } else { console.log('✅ 从缓存提取省份列表(快速)'); } // 提取省份列表(只保留基本信息,移除children) let provinceList = []; if (Array.isArray(fullData)) { provinceList = fullData.map(province => ({ code: province.code, name: province.name, // 不包含children,减少数据量 _hasChildren: !!province.children && province.children.length > 0 })); // 缓存提取的省份列表 await this.saveToCache(provinceListCacheKey, provinceList, 'province_list'); } return provinceList; } /** * 尝试从部分数据中提取省份列表(只加载前2MB) * 这样可以快速显示省份列表,而不需要等待90M数据加载完成 */ async loadProvinceListFromPartialData() { // 尝试使用Range请求只加载前2MB数据 const maxBytes = 2 * 1024 * 1024; // 2MB return new Promise((resolve, reject) => { console.log('📥 尝试分段加载(前2MB)...'); // 在H5环境可以使用fetch + Range请求 // #ifdef H5 if (typeof fetch !== 'undefined') { fetch(this.fullDataUrl, { headers: { 'Range': `bytes=0-${maxBytes - 1}` } }) .then(response => { if (response.status === 206 || response.status === 200) { return response.text(); } else { throw new Error(`Range请求失败,状态码: ${response.status}`); } }) .then(text => { // 尝试解析部分JSON const provinceList = this.extractProvincesFromPartialJson(text); if (provinceList && provinceList.length > 0) { console.log(`✅ 从部分数据提取到 ${provinceList.length} 个省份`); resolve(provinceList); } else { reject(new Error('无法从部分数据中提取省份列表')); } }) .catch(error => { reject(error); }); } else { reject(new Error('当前环境不支持Range请求')); } // #endif // #ifndef H5 // 小程序环境不支持Range请求,直接拒绝 reject(new Error('小程序环境不支持Range请求,将使用完整加载')); // #endif }); } /** * 从部分JSON字符串中提取省份列表 * 由于JSON可能不完整,需要智能解析 */ extractProvincesFromPartialJson(partialJson) { try { const provinces = []; const seenCodes = new Set(); // 方法1:使用正则表达式提取所有省份(code和name) // 匹配模式:{"code":"31","name":"上海市",...} 或 "code":"31","name":"上海市" const patterns = [ // 标准格式:{"code":"31","name":"上海市"} /\{\s*"code"\s*:\s*"([^"]+)"\s*,\s*"name"\s*:\s*"([^"]+)"[^}]*\}/g, // 简化格式:"code":"31","name":"上海市" /"code"\s*:\s*"([^"]+)"\s*,\s*"name"\s*:\s*"([^"]+)"/g ]; for (const pattern of patterns) { let match; while ((match = pattern.exec(partialJson)) !== null) { const code = match[1]; const name = match[2]; // 省份code通常是2位数字(如"31"),或者6位数字(如"310000") // 根据实际数据结构,code可能是"31"格式 if (code && name && !seenCodes.has(code)) { // 判断是否为省份code(2位或6位数字) if ((code.length === 2 && /^\d{2}$/.test(code)) || (code.length === 6 && /^\d{6}$/.test(code))) { seenCodes.add(code); provinces.push({ code: code, name: name, _hasChildren: true // 假设都有下级 }); } } } } // 方法2:如果正则提取失败,尝试解析JSON(可能不完整) if (provinces.length === 0) { try { // 尝试修复不完整的JSON let jsonStr = partialJson.trim(); // 如果以[开头,尝试找到所有完整的省份对象 if (jsonStr.startsWith('[')) { // 查找所有省份对象的开始位置 let pos = 1; // 跳过开头的[ while (pos < jsonStr.length) { // 找到下一个{的位置 const objStart = jsonStr.indexOf('{', pos); if (objStart === -1) break; // 尝试找到这个对象的结束位置(简单的匹配) let braceCount = 0; let objEnd = objStart; for (let i = objStart; i < jsonStr.length; i++) { if (jsonStr[i] === '{') braceCount++; if (jsonStr[i] === '}') braceCount--; if (braceCount === 0) { objEnd = i + 1; break; } } if (objEnd > objStart) { const objStr = jsonStr.substring(objStart, objEnd); try { const obj = JSON.parse(objStr); if (obj.code && obj.name && !seenCodes.has(obj.code)) { seenCodes.add(obj.code); provinces.push({ code: obj.code, name: obj.name, _hasChildren: true }); } } catch (e) { // 解析失败,继续下一个 } } pos = objEnd; } } } catch (e) { // 解析失败,忽略 } } // 去重并排序 const uniqueProvinces = Array.from(new Map(provinces.map(p => [p.code, p])).values()); return uniqueProvinces.length > 0 ? uniqueProvinces : null; } catch (error) { console.warn('从部分JSON提取省份列表失败:', error); return null; } } /** * 在后台预加载完整数据(不阻塞用户操作) */ async preloadFullDataInBackground() { const cacheKey = 'address_full_data'; // 检查是否已缓存 const cached = await this.getCachedData(cacheKey); if (cached) { console.log('✅ 完整数据已缓存,无需预加载'); return; } console.log('🔄 开始在后台预加载完整数据...'); // 使用setTimeout让出主线程,避免阻塞 setTimeout(async () => { try { await this.fetchData(this.fullDataUrl, cacheKey); console.log('✅ 后台预加载完成,完整数据已缓存'); } catch (error) { console.warn('⚠️ 后台预加载失败:', error); } }, 100); } /** * 加载省份详情(包含该省份的所有下级数据) * 按需加载,只有用户选择该省份时才加载 */ async loadProvinceDetail(provinceCode, forceRefresh = false) { await this.init(); const cacheKey = `address_province_${provinceCode}`; // 如果不是强制刷新,先尝试从缓存加载 if (!forceRefresh) { const cached = await this.getCachedData(cacheKey); if (cached) { console.log(`✅ 从缓存加载省份详情: ${provinceCode}`); return cached; } } uni.showLoading({ title: '加载城市数据...', mask: true }); try { let data = null; // 方案1:如果启用了分片接口,尝试从分片接口加载 if (this.useSplitApi) { try { const url = this.provinceDetailUrl.replace('{code}', provinceCode); data = await this.fetchData(url, cacheKey); if (data && data.code === provinceCode) { console.log(`✅ 从分片接口加载省份详情: ${provinceCode}`); return data; } } catch (error) { console.warn(`⚠️ 分片接口不可用,降级到完整数据提取: ${error.message}`); } } // 方案2:从完整数据中提取省份详情(默认方案) data = await this.loadProvinceDetailFromFullData(provinceCode, forceRefresh); return data; } catch (error) { console.error(`❌ 加载省份详情失败: ${provinceCode}`, error); // 如果加载失败,尝试使用缓存 if (!forceRefresh) { const cached = await this.getCachedData(cacheKey); if (cached) { console.log('⚠️ 使用过期缓存数据'); return cached; } } throw error; } finally { uni.hideLoading(); } } /** * 从完整数据中提取省份详情(默认方案) * 如果完整数据已缓存,提取会很快 */ async loadProvinceDetailFromFullData(provinceCode, forceRefresh = false) { const cacheKey = 'address_full_data'; // 先尝试从缓存获取完整数据(如果已缓存,提取会很快) let fullData = await this.getCachedData(cacheKey); if (!fullData || forceRefresh) { // 如果完整数据未缓存,需要加载完整数据 console.log(`📥 完整数据未缓存,开始加载完整数据以获取省份 ${provinceCode}...`); uni.showLoading({ title: '加载地址数据...', mask: true }); try { fullData = await this.fetchData(this.fullDataUrl, cacheKey); console.log('✅ 完整数据加载完成,已缓存'); } finally { uni.hideLoading(); } } else { console.log(`✅ 从缓存提取省份详情: ${provinceCode}(快速)`); } // 查找并返回指定省份的完整数据 // 注意:根据实际数据结构,code可能是"31"格式,需要精确匹配 if (Array.isArray(fullData)) { const province = fullData.find(p => p.code === provinceCode || p.code === String(provinceCode)); if (province) { return province; } else { console.warn(`⚠️ 未找到省份: ${provinceCode}`); return null; } } return null; } /** * 清除缓存 */ async clearCache() { await this.init(); try { if (this.dbHelper) { await this.dbHelper.clearStore(this.storeName); } // 清除 uni.storage const keys = uni.getStorageInfoSync().keys; keys.forEach(key => { if (key.startsWith('address_')) { uni.removeStorageSync(key); } }); // 清除内存缓存 this.memoryCache.clear(); console.log('✅ 已清除所有地址数据缓存'); } catch (e) { console.error('清除缓存失败:', e); } } /** * 清除旧缓存 */ async clearOldCache() { try { // 清除7天前的缓存 const expireTime = this.cacheExpireDays * 24 * 60 * 60 * 1000; const now = Date.now(); if (this.dbHelper) { const allData = await this.dbHelper.getAll(this.storeName); for (const item of allData) { if (item.updateTime && (now - item.updateTime) > expireTime) { await this.dbHelper.delete(this.storeName, item.key); } } } } catch (e) { console.warn('清理旧缓存失败:', e); } } } // 单例模式 const addressDataLoaderLazy = new AddressDataLoaderLazy(); export default addressDataLoaderLazy;