Files
ks-app-employment-service/utils/addressDataLoaderLazy.js
2025-11-10 15:27:34 +08:00

689 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 地址数据懒加载器 - 按需加载,大幅减少首次加载时间
* 优化方案:
* 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)) {
// 判断是否为省份code2位或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;