# 用 React Native 与 Expo 设计离线笔记

原文: https://yishto.com/learn/application-platforms/expo-offline-notebook/

作者: [Yishto](https://yishto.com/about/)

设计React Native与Expo读书笔记的数据表和同步队列，处理持久保存、重复请求、版本冲突与平台权限。

首次发布: 2026-11-10
内容更新: 2026-11-10

读书笔记可以先在设备保存，再同步到服务端。网络断开时仍能编辑，恢复连接后又不会把同一次编辑写两遍，需要本地存储、操作身份和服务器确认共同配合。React 的页面状态只负责其中一层。

本课用合成笔记 `N-1` 和操作 `OP-1` 设计流程。第一版支持列表、编辑与显式同步，不请求相机或通讯录权限，也不承诺后台始终运行。以后添加附件时再申请所需权限。

## React Native 与 Expo 的边界

React Native 通过 React 组件描述界面，并使用平台组件与能力。`View`、`Text` 等不是网页 DOM 元素，浏览器里的样式和文件接口不能原样套用。Expo 提供工程、模块和开发工具，但目标系统的权限、原生配置和产物仍需分别验证。

Expo Go 与 development build 支持的原生模块范围不同。添加数据库、文件或其他模块前，先查所选 SDK 的文档并确认测试环境支持。不能因为模块在 JavaScript 中可以 import，就假定真机安装包包含了对应原生实现。

创建工程时使用当前官方推荐命令，并保留 SDK 条件。截至本次来源核对，创建页处于 SDK 58 过渡说明，未指定模板和显式指定模板可能产生不同版本。读者应按当时页面选择兼容设备的模板，记录生成版本，不把课程中的数字当长期固定要求。

## 本地数据表先决定恢复能力

笔记记录包含 ID、正文、本地修改序号、服务器基准版本和更新时间。同步操作另存 ID、笔记 ID、提交内容、基准版本与状态。操作内容应是发送时的快照，不能只引用不断变化的页面变量，否则重试可能发送用户后来改过的内容。

本地保存与写入队列最好处于同一事务边界。保存成功才显示本地已保存，队列存在才显示等待同步。列表由持久仓库加载，`useState` 或组件生命周期不会替应用完成跨进程恢复。

选择存储时按数据特点分开。官方存储页将 SQLite 用于持久数据库，将 Async Storage 描述为未加密的异步键值存储；后者适合非敏感的小型偏好，不能当秘密保险箱。小型凭证可评估 SecureStore，正文数据库与凭证存储分开管理。

## 把工程按数据职责接起来

在已选定版本的 Expo 工程中建立笔记模型、仓库、列表页和编辑页。使用 `TextInput` 接收正文，显示保存状态与同步状态。先通过内存仓库实现创建和修改，再按官方模块安装说明加入持久存储，保留所需原生配置。

创建数据库表并设置 schema 版本。启动时打开数据库、完成迁移，再查询笔记；读取失败显示错误，不自动清空数据库。编辑过程中按明确节奏保存，处理快速切换笔记时的待保存内容；离开前的提示应依据是否仍有未落盘编辑。

随后建立受控服务端端点。它校验账户、笔记归属、字段大小和基准版本，返回操作 ID 与确认版本。客户端提交待办，成功后只清理对应操作，不能把整张队列标为成功。先做主动同步，再评估系统允许的后台机制。

## 重试需要稳定操作 ID

超时可能发生在服务器已保存之后。客户端重试同一操作时沿用原 ID，服务器识别重复请求并返回已有结果。仅在客户端按钮上禁止连点，不能处理响应丢失或进程重启。

下面 Python 示例用集合记录处理过的操作。它验证同一 `OP-1` 不会再次更新笔记，没有数据库事务、网络、认证或真实 Expo 运行环境。

```python
server = {}
seen = set()
def sync(operation):
    if operation["id"] in seen:
        return False
    server[operation["note"]] = operation["text"]
    seen.add(operation["id"])
    return True
operation = {"id": "OP-1", "note": "N-1", "text": "draft"}
assert sync(operation)
assert not sync(operation)
assert server["N-1"] == "draft"
print(server)
```

输出 `{'N-1': 'draft'}`。第一次返回真，第二次返回假，服务端字典保留一次写入。真实服务必须将幂等记录与业务更新放在合适事务中；这里的内存集合会随进程结束消失，不能用于生产去重。

## 冲突不能用重试抹掉

客户端提交的基准版本落后时，服务器应报告冲突，附上当前版本或提供读取方式。界面保留本地草稿，显示远端内容并让用户明确选择。重试只解决暂时失败，不能让旧基准忽然变成有效版本。

用户合并后创建新操作，以最新确认版本为基准。旧操作进入已处理或已替代状态，不能继续在队列里无限重试。若前一个操作确认时用户已经继续编辑，只更新其确认信息，不把新编辑误标为已同步。

不要仅以手机更新时间决定覆盖顺序。设备时钟可能不同，正文也可能同时包含需要保留的信息。删除作为有身份的同步操作处理，否则离线设备重新上传旧记录可能让已删除笔记再次出现。

## 权限、错误和重启恢复

只有功能确实需要时才请求系统权限，并提供拒绝后的替代路径。普通正文输入无需读取通讯录。附件选择应由用户主动发起，描述上传内容与用途，不把授权当作一次性获得所有设备数据。

启动后枚举待同步操作，将上次中断的发送恢复为可重试。认证失败要求恢复会话，不在日志打印令牌；本地空间不足时保留未保存提示，不能宣称成功。卸载、系统存储处理和备份行为需按目标平台核对，本地保存不等于永远不会丢失。

编辑器还要处理中文输入法与快速切换笔记。不要在每个输入事件后重置光标；保存任务记录对应笔记ID和内容快照，切换后晚到的确认只能修改原笔记状态。恢复时展示最后落盘内容，并区分仍在编辑器内的未保存输入。

为同步队列设定容量和失败策略。大量操作不应一次并行发送；按照笔记版本依赖顺序处理，暂时失败可以延后，永久字段错误等待用户修正。可合并的连续编辑要保证已发送操作的身份不变，并保留删除等具有独立语义的动作。先用两条依赖操作验证，再增加批量优化。

## 练习：验证两个设备版本的分歧

先在一个测试设备创建笔记、断网编辑、终止应用并重开，正文和队列应恢复。模拟服务器已处理但响应丢失，重复发送相同操作后仍只有一次有效更新。

再模拟另一客户端修改了服务器版本。当前设备应进入冲突，保留草稿并能生成合并后的新操作。分别在 Android 和 iOS 检查键盘、退出恢复与模块配置，标记未验证项目。验收对照页面、数据库和服务器确认，不把 Python 去重示例当成完整同步应用。

## 资料来源与延伸阅读

- [Create an Expo application](https://docs.expo.dev/get-started/create-a-project/)
- [React Native core components](https://reactnative.dev/docs/intro-react-native-components)
- [Store data in an Expo application](https://docs.expo.dev/develop/user-interface/store-data/)

资料核验：2026-11-10。教学示例不代表线上实测。

## 相关文章

- [页面、路由和状态应该放在哪里](https://yishto.com/learn/web-interface/state-routing-and-frontend-architecture/)
- [文件、索引和记录需要不同的存储策略](https://yishto.com/learn/service-data/storage-search-and-data-models/)
