🚀 需求背景:离线时代的"数字寻址"挑战
产品大大甩给我一个看似寻常的级联选择需求------楼栋→单元→楼层→房号。这在常规Web开发中就像吃泡面一样简单,但这次的需求却加了"离线使用"这个魔鬼调料🌶️!
技术选型
技术侦探笔记:离线存储就像给APP装了个随身保险箱,要保证用户即使在地下车库没信号,也能丝滑选择房号。
可用的存储方案
选手 | 优势 | 致命弱点 | 适用场景 |
---|---|---|---|
AsyncStorage | 轻量如燕(6MB) | 数据量一大就喘气 | 简单配置存储 |
SQLite | 老牌劲旅,ACID全能选手 | 需要写SQL有点费脑细胞 | 复杂关系数据 |
MMKV | 微信团队的闪电侠⚡ | 不支持复杂查询 | 高频键值读写 |
Realm | 对象存储界的贵公子 | 学习曲线陡峭 | 跨设备同步需求 |
WatermelonDB | React Native亲儿子 | 文档就像迷宫 | 超大型数据集 |
决策依据
- 数据结构复杂度:地址数据存在层级关系,适合关系型存储
- 查询需求:需支持DISTINCT、JOIN等复杂操作
- 调试支持:Android Studio提供原生Database Inspector工具
- 社区生态:SQLite拥有成熟的开发者社区和工具链
最终选定SQLite方案,其ACID特性可确保数据一致性,且符合SQL-92标准便于长期维护。
💣 避坑指南:那些年我们追过的star陷阱
建议不要使用这个插件
github 一搜索,可能最先出现的是这个插件,我也不意外,选用这个插件,毕竟star比较多。于是遇到了各种兼容性问题,因为插件作者已经4年没有维护该项目,在最新的[email protected] 版本中,该插件已几乎无法使用。
建议使用的插件
这个插件作者在一直维护,虽然star少,但是我没有遇到什么坑,也感谢作者大佬的付出
编码过程
思维导图
程序员的第一课:安装依赖
bash
npm i -s @op-engineering/op-sqlite
创建数据库
因为我已经有一份数据库文件,所以我需要依据这个数据库文件创建我的数据库
- 将数据库文件放在项目目录下,推荐放在assets目录下
- 指定资源目录 在根目录下创建 react-native.config.js 文件
js
//指定你的database路径
module.exports = {
assets: ['./src/assets/database'],
};
- 链接你的资源
bash
npx react-native-asset@latest
这时候资源就会同步到你的安卓目录下
- 执行创建方法
ts
//database.ts
import {DB, moveAssetsDatabase, open} from '@op-engineering/op-sqlite';
let db: DB | undefined;
export const init = async () => {
const moved = await moveAssetsDatabase({filename: 'trash_classification.db'});
if (!moved) {
throw new Error('Could not move assets database');
}
};
执行init方法后,数据库就创建成功了
- 查看你的数据库
- 使用android studio
打开tools->app Inspection -> Database Inspector 就能看到你的数据库
- 使用其他工具
如果你没有指定其他存储路径的话,database文件一般会存在在默认的项目目录下。
你可以使用方法获取文件地址
ts
export const getDBPath = () => {
console.log(getDB().getDbPath());
};
也可以直接打开手机的文件目录查找数据库文件导出,然后用三方工具查看
通常在 data/data/com.xxx/database目录下
比如我导出后使用DB Browser for SQLite查看就是这样的
同步数据库
基于我们app的需求我需要让app一打开就同步数据库
tsx
//LoadDatabse.tsx
//...其他实现
/** 更新数据库 */
const {loading} = useRequest(services.main.initDatabase, {
retryCount: -1,
retryInterval: 3000,
onError(err) {
setError(true);
console.log('数据库同步失败!', err.message);
},
/** 完成时的回调 */
onSuccess(data) {
if (data.code === 200) {
syncDatabase(data.data).then(success => {
if (success) {
navigation.replace('Home');
} else {
setError(true);
}
});
}
},
});
};
export default LoadDatabase;
主要为同步数据库,如果同步失败就间隔3s继续同步, 同时提供用户直接放弃同步前往主页的入口,同步成功就直接前往主页
然后实现syncDatabase函数
ts
//database.tsx
import {DB, moveAssetsDatabase, open} from '@op-engineering/op-sqlite';
let db: DB | undefined;
/** 避免重复链接数据库 */
export const getDB = () => {
if (!db) {
db = open({name: 'trash_classification.db'});
}
return db;
};
/** 插入站点数据 */
export const insertStation = async (station: API.StationDTO) => {
console.log(station);
const result = await getDB().execute(
'INSERT INTO device_station (id,hazardous_bins_quantity,kitchen_bins_quantity, other_bins_quantity, recyclable_bins_quantity,residential_quarters_id, station_code, station_name, description, status, deletion_flag, create_time, update_time, creator) VALUES (?,?,?,?,?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[
station.id,
station.otherBinsQuantity,
station.hazardousBinsQuantity,
station.kitchenBinsQuantity,
station.recyclableBinsQuantity,
station.residentialQuartersId,
station.stationCode,
station.stationName,
station.description,
station.status,
station.deletionFlag,
station.createTime,
station.updateTime,
station.creator,
],
);
return result;
};
//... 其他插入数据实现
export const syncDatabase = async (data: API.DatabaseDTO) => {
const newDb = getDB();
try {
await newDb.transaction(async tx => {
// 先来个大扫除
await tx.execute('DELETE FROM device_station');
// ...其他表清理
// 新数据入住仪式
await insertStation(data.station);
await insertCommunity(data.community);
// 楼栋房间的俄罗斯套娃
for (const building of data.buildingList) {
await insertBuilding(building);
for (const room of building.roomList) {
await insertRoom(room);
}
}
});
return true;
} catch (error) {
console.error('数据库同步失败:', error);
return false;
}
};
到这里数据库就同步完成了!
查询数据库
有了数据库自然要查询。我们继续补充database.ts 文件,添加查询语句
ts
//database.ts
/** 获取所有楼栋 */
export const getBuildings = async () => {
try {
// 去重查询
const result = await getDB().execute(
'SELECT DISTINCT building_number as id, building_number as name FROM building_info WHERE deleted = 0 ORDER BY sort',
);
return result.rows as Array<{id: string; name: string}>;
} catch (error) {
console.error('Get buildings failed:', error);
return [];
}
};
//... 其他查询实现
核心实现:级联选择的舞蹈编排
tsx
//Login.tsx
// 选择逻辑就像跳格子游戏
const handleItemSelect = async (item: {id: string; name: string}) => {
let newPath = [...selectedPath];
switch(currentLevel) {
case 'building':
newPath = [{...item, type: 'building'}];
currentItems = await getUnits(item.id); // 召唤单元列表
break;
case 'unit':
newPath = [selectedPath[0], {...item, type: 'unit'}];
currentItems = await getFloors(item.id); // 召唤楼层列表
break;
// ...其他case就像俄罗斯套娃
}
setSelectedPath(newPath);
setCurrentItems(currentItems);
};
感谢阅读
如果对你有帮助,希望能给个赞!,你的点赞是我继续更新的动力!