6.3 本地数据存储方案


6.3 本地数据存储方案

本节摘要:本地数据存储让应用"关掉再打开,数据还在"。RN 用 AsyncStorage,Flutter 用 shared_preferences,都是键值对形式的轻量存储。本节讲清两者的用法(存、读、删、清)、JSON 序列化的处理、数据迁移的版本意识,以及"何时用键值存储、何时上数据库"的分层判断。

本节地图

阅读完本节,你应当能够:

  1. 用 AsyncStorage 实现 RN 的数据存、读、删。
  2. 用 shared_preferences 实现 Flutter 的同款操作。
  3. 理解对象数据必须先 JSON 序列化再存储。
  4. 判断"轻量存储还是数据库"的分层标准。

一、问题与直觉

想一想:一个笔记应用,用户写了一条笔记,关掉应用再打开,笔记还在吗?如果不做本地存储,答案是不在——应用一重启,内存里的数据全部清空。本地存储就是解决"数据在应用之外存活"的问题。

一个反直觉的点先讲清楚:本地存储不是万能的。它擅长存"少量、简单、键值对"的数据——用户偏好、登录状态、轻量配置;不擅长存"大量、结构化、需要查询"的数据——那要交给数据库。很多新手把复杂数据硬塞进键值存储,用"自己约定的 JSON 结构"模拟查询,最后维护成一团乱麻。理解这个分层,是本节最重要的认知。

另一个容易被忽略的点是读写的异步性。键值存储的读写都是异步操作(RN 的 Promise、Flutter 的 Future)。这意味着"存完立刻读"要按异步顺序写代码,而不是顺序执行两条语句。新手常在这上面栽跟头:存数据的代码还没执行完,读数据的代码已经开始跑了。养成"存完再读、读结果再渲染"的习惯,存储逻辑就稳了。

二、核心原理

2.1 分层认知:轻量存储 vs 数据库

2.1 分层认知:轻量存储 vs 数据库

这张图的分层一句话:键值存储管"偏好",数据库管"数据"。分清"这只是个偏好"还是"这是用户的数据",存储方案自然选对。

2.1 键值存储的基本操作流

键值存储的操作本身很简单,把"存、读、删"画成流程,一眼就能看清:

2.2 RN 侧:AsyncStorage

AsyncStorage 是基于 Promise 的异步键值存储,底层在 iOS 用 UserDefaults、Android 用 SharedPreferences。

存数据

import AsyncStorage from '@react-native-async-storage/async-storage'; const saveUser = async (id, name) => { try { await AsyncStorage.setItem('user_id', id); await AsyncStorage.setItem('user_name', name); } catch (error) { console.error('保存失败:', error); } };

读数据

const loadUser = async () => { try { const id = await AsyncStorage.getItem('user_id'); const name = await AsyncStorage.getItem('user_name'); return id && name ? { id, name } : null; } catch (error) { console.error('读取失败:', error); return null; } };

删数据

await AsyncStorage.removeItem('user_id');

2.2.1 为什么读取结果要判空

读数据代码里有一句 id && name ? ... : null,这个判空不是可有可无的防御,而是必须的。getItem 对不存在的 key 返回 null,对存在但值为空字符串的 key 返回空串。如果不判空直接使用,id.length 这类调用会直接崩掉。

判空的另一个作用是区分"没存过"与"存了空值"。这是键值存储的常见语义陷阱:判断"用户是否登录过",不能只看 key 是否存在,还要看值是否有效。统一处理方式是在读取后做一次显式校验,返回 null 或默认值。这个习惯在本地存储里叫"防御式读取",是数据可靠性的第一道防线。

2.3 Flutter 侧:shared_preferences

shared_preferences 是 Flutter 官方的轻量存储插件,API 风格更直接:

import 'package:shared_preferences/shared_preferences.dart'; // 存 final prefs = await SharedPreferences.getInstance(); await prefs.setString('user_name', '张三'); // 读 final name = prefs.getString('user_name'); // 删 await prefs.remove('user_name');

注意 shared_preferences 的读写是异步的(Future),但 API 设计比 AsyncStorage 更贴近"直接操作"的直觉。

2.3.1 getInstance 与多类型 API

shared_preferences 有个新手容易忽略的点:getInstance 是异步获取实例,每次读写前都要拿到这个单例。而且它按数据类型提供了多组 API:setString、setInt、setDouble、setBool、setStringList。选对类型,读取时不用手动转换。

final prefs = await SharedPreferences.getInstance(); // 按类型存 await prefs.setBool('is_logged_in', true); await prefs.setInt('visit_count', 3); await prefs.setStringList('recent_searches', ['flutter', 'dart']); // 按类型读 final loggedIn = prefs.getBool('is_logged_in');

这组类型化 API 是键值存储"便捷性"的体现:你声明"我要存一个布尔",框架就保证读回来还是布尔,类型转换由库替你完成。相比之下 AsyncStorage 只有字符串一个通道,对象转换必须自己处理。两套 API 风格不同,但底层的"键值对"本质一致。

2.4 对象数据必须序列化

键值存储只能存基本类型。要存对象或数组,必须先用 JSON 序列化:

// RN:对象转字符串存储 const saveTodos = async (todos) => { await AsyncStorage.setItem('todos', JSON.stringify(todos)); }; const loadTodos = async () => { const raw = await AsyncStorage.getItem('todos'); return raw ? JSON.parse(raw) : []; };
// Flutter:同款逻辑 final prefs = await SharedPreferences.getInstance(); await prefs.setString('todos', jsonEncode(todos)); final raw = prefs.getString('todos'); final todos = raw != null ? jsonDecode(raw) : [];

序列化是键值存储处理复杂数据的标准方式。记住"存字符串、取回来解析"这对操作,就掌握了键值存储的全部诀窍。

2.4.1 数据迁移:键值存储的版本问题

存储的键名一旦定下来,后续改动有版本问题。比如字段名从 user_name 改成 username,旧数据还在旧键下,新代码读不到——用户的数据"丢了"。这是键值存储的经典坑。

解决思路是版本化存储:存一个版本号,读取时先看版本,按版本决定数据迁移逻辑。版本号可以这样设计:存 schema_version 键,值从 1 递增;升级数据格式时,旧版本读到先迁移再使用。

对初学者,这个问题的现实意义是:上线前想清楚键名与结构,别频繁改名。一旦应用发布、用户产生数据,改键名就要写迁移逻辑。设计阶段多花一分钟想结构,上线后少写十段迁移代码。

三、工程实践要点

3.1 双框架对照

操作 React Native Flutter
setItem(key, value) setString / setInt 等
getItem(key) getString / getInt 等
removeItem(key) remove(key)
清空 clear() clear()
对象存取 JSON.stringify / parse jsonEncode / jsonDecode

3.2 高频坑

坑一:忘序列化直接存对象。AsyncStorage 只接受字符串,传对象会静默失败或存成无效值。对象必须先 stringify。

坑二:读取结果没判空。key 不存在时 getItem 返回 null、getString 返回 null。不判空直接使用会崩。

坑三:存储体积无节制。键值存储适合小数据,塞大列表会拖慢读写。数据变大的信号出现时,考虑数据库。

⚠️ 常见坑:把登录凭证存进 AsyncStorage 或 shared_preferences 明文存储。这些是普通存储,没有加密,敏感信息应使用安全存储方案(如 iOS 的 Keychain、Android 的 Keystore 封装)。
💡 关键直觉:把键值存储想成"应用的口袋"——放得下的小东西随身带,放不下的大东西用背包(数据库)。口袋塞不下时换背包,而不是把背包塞进口袋。

3.3 动手验证:让待办应用持久化

给待办应用加持久化:添加待办时存进本地,启动应用时读出来恢复列表。做完后关掉应用再打开,待办列表还在——这正是本地存储最直观的体验。如果后续数据变复杂(比如加搜索、排序),再考虑升级数据库方案。

FAQ:本地存储的常见疑问

问:什么时候该从键值存储升级到数据库? 出现三个信号时考虑升级:数据量超过几百条且持续增长、需要按条件查询或排序、数据有明确的关联结构(如用户与订单)。升级是"需求驱动"的,不是"提前规划"的——小项目用键值存储足够了,别过早引入数据库。

问:AsyncStorage 和 shared_preferences 数据会丢吗? 正常情况下不会,但要理解它们的存储位置:iOS 用 UserDefaults、Android 用 SharedPreferences,都是系统级持久化。卸载应用会清除数据;系统清理缓存一般不触碰这类存储。真正的风险是误用——比如把大体积数据塞进去导致写入失败。

问:存图片、音频这类大文件怎么办? 键值存储不适合存大文件。大文件应该用文件系统:RN 与 Flutter 都有文件操作库(路径获取、读写文件)。存储选型是个光谱:小数据用键值、中数据用文件、大数据用数据库。理解这条光谱,按数据规模选方案即可。

问:多设备同步数据用什么? 本地存储是"单设备"的。多设备同步需要云服务(后端 API 加数据库),本地存储只做离线缓存。设计时要分清"哪些数据属于设备本地"(偏好设置)、"哪些属于用户云端"(业务数据)。这个边界想清楚,架构才清晰。

3.4 一个存储设计的思考框架

设计本地存储时,用"数据生命周期"来思考比"数据存在哪"更有用。一份数据有三种生命周期:会话级(应用运行期间,用内存)、设备级(关应用还在,用键值存储或文件)、云端级(多设备同步,用服务器数据库)。先判断数据属于哪个级别,再选存储方案,顺序不会错。最常见的错误是"拿设备级存储存会话数据"或"拿云端数据库解决一切"——前者浪费资源,后者过度设计。这个框架在真实项目中能帮你快速定位存储需求。

3.5 数据读取与启动的配合

本地存储与应用的启动流程有个常见配合:应用启动时读取本地数据,用于"恢复上次状态"。这个流程要处理好"读取是异步的"——启动时界面先渲染,数据到了再更新。RN 里用 useEffect 加异步读取、Flutter 里在 initState 发起读取、数据到达后 setState。常见错误是在启动时就同步等待数据,导致白屏。正确做法是界面先用默认值渲染,数据到达后无缝更新。这个"先渲染后填充"的模式,与第 6.2 节网络加载的三态管理是同一个思路,两处可以互相印证。

3.6 存储与后端的关系

本地存储与后端数据不是"二选一",而是"分工合作"。典型模式是缓存回退:优先展示本地缓存(快),同时请求后端(新),数据到达后更新界面并刷新缓存。这个模式让应用在弱网或离线时仍可用,联网后自动同步。实现时要注意版本一致性与冲突处理——本地与云端数据不一致时以谁为准。入门阶段先把"本地存、本地读"做扎实,缓存回退是进阶方向。但知道这个模式存在,你对"为什么既要本地又要后端"的理解会更完整。

3.7 一个关于数据安全的提醒

本地存储的数据都在用户设备上,凡是涉及隐私(登录凭证、支付信息、个人资料),都不能用明文键值存储。正确的做法是使用系统安全存储:iOS 的钥匙串、Android 的加密存储,或使用封装了安全存储的库。判断"这条数据要不要加密"有个简单标准:如果它泄露出去会让用户受损,就加密。业务数据(待办、偏好)明文存储问题不大,但凭证类数据必须走安全通道。这个红线从一开始就守住,能避免很多事后补课。

重点提炼

  • 分层认知:键值存储管偏好,数据库管数据,按需选择。
  • RN 操作:setItem、getItem、removeItem 三件套。
  • Flutter 操作:setString、getString、remove 对应实现。
  • 类型化 API:shared_preferences 按类型存读,RN 只有字符串通道。
  • 对象序列化:存字符串、取回解析,是键值存储的通用诀窍。
  • 判空习惯:key 不存在返回 null,读取结果先判空。
  • 异步读写:存储读写是异步操作,按顺序等待再使用。
  • 版本意识:键名改动要版本化迁移,上线前想清楚结构。
  • 敏感数据:登录凭证别明文存普通存储,用安全存储。
  • 扩容信号:数据变大、需要查询时,考虑数据库方案。

数据能落地了,下一步让界面"能玩"——手势与用户交互,从点击到滑动拖拽。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U