React Native离线级联选择器开发手记:当SQLite遇见小区房号选择

🚀 需求背景:离线时代的"数字寻址"挑战

产品大大甩给我一个看似寻常的级联选择需求------楼栋→单元→楼层→房号。这在常规Web开发中就像吃泡面一样简单,但这次的需求却加了"离线使用"这个魔鬼调料🌶️!

技术选型

技术侦探笔记:离线存储就像给APP装了个随身保险箱,要保证用户即使在地下车库没信号,也能丝滑选择房号。

可用的存储方案

选手 优势 致命弱点 适用场景
AsyncStorage 轻量如燕(6MB) 数据量一大就喘气 简单配置存储
SQLite 老牌劲旅,ACID全能选手 需要写SQL有点费脑细胞 复杂关系数据
MMKV 微信团队的闪电侠⚡ 不支持复杂查询 高频键值读写
Realm 对象存储界的贵公子 学习曲线陡峭 跨设备同步需求
WatermelonDB React Native亲儿子 文档就像迷宫 超大型数据集

决策依据

  1. 数据结构复杂度:地址数据存在层级关系,适合关系型存储
  2. 查询需求:需支持DISTINCT、JOIN等复杂操作
  3. 调试支持:Android Studio提供原生Database Inspector工具
  4. 社区生态:SQLite拥有成熟的开发者社区和工具链

最终选定SQLite方案,其ACID特性可确保数据一致性,且符合SQL-92标准便于长期维护。

💣 避坑指南:那些年我们追过的star陷阱

建议不要使用这个插件

github 一搜索,可能最先出现的是这个插件,我也不意外,选用这个插件,毕竟star比较多。于是遇到了各种兼容性问题,因为插件作者已经4年没有维护该项目,在最新的react-native@0.78.0 版本中,该插件已几乎无法使用。

建议使用的插件

这个插件作者在一直维护,虽然star少,但是我没有遇到什么坑,也感谢作者大佬的付出

编码过程

思维导图

graph TD A[用户打开APP] --> B{网络状态?} B -->|在线| C[从API获取最新数据] B -->|离线| D[读取本地SQLite] C --> E[数据同步到SQLite] D --> F[级联选择器初始化] E --> F F --> G[用户选择房号] G --> H[结束]

程序员的第一课:安装依赖

bash 复制代码
npm i -s @op-engineering/op-sqlite

创建数据库

因为我已经有一份数据库文件,所以我需要依据这个数据库文件创建我的数据库

  1. 将数据库文件放在项目目录下,推荐放在assets目录下
  1. 指定资源目录 在根目录下创建 react-native.config.js 文件
js 复制代码
//指定你的database路径
module.exports = {
  assets: ['./src/assets/database'],
};
  1. 链接你的资源
bash 复制代码
npx react-native-asset@latest

这时候资源就会同步到你的安卓目录下

  1. 执行创建方法
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方法后,数据库就创建成功了

  1. 查看你的数据库
  • 使用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);
};

感谢阅读

如果对你有帮助,希望能给个赞!,你的点赞是我继续更新的动力!

相关推荐
定栓3 小时前
Webpack迁到Vite第七节(四):全家桶移除 — 保留项、脚本与数据收尾
前端·webpack·node.js
数据掘金3 小时前
小程序埋点方案的埋点文档怎么自动生成?
前端
deli0073 小时前
cron 表达式为什么总跑错?10 个场景实测,下次执行时间一眼看懂
前端
代码猎人3 小时前
前端怎么优化页面性能
前端
代码猎人3 小时前
前端怎么配合后端完成数据对接
前端
lexiangqicheng3 小时前
Docker的沙箱机制是怎样的?
前端·javascript
燕落南枝3 小时前
HTML 表格标签与 AI 合并单元格实战指南
前端·人工智能·html
zyseo84 小时前
谷歌SEO 内链地图脚本:Python 画出权重分发,揪出孤岛页
java·前端·python
ClickHouseDB4 小时前
ClickHouse .NET 驱动程序新特性:从 1.0 到 1.3 的演进之路
java·前端·windows