easy-word 的核心设计原则是离线优先 :所有学习数据(单词、学习记录、复习计划)以本地 RDB 为事实源,云端同步是可选的增强。断网时功能零降级。
为什么离线优先? 学习类应用的特殊性:
学生可能在学校(无网络)使用
学习数据不能丢失(一次丢数据 = 失去信任)
高频读写(每次翻卡、拼写、答题都产生记录)
云端同步延迟不能阻塞 UI
graph TD
A["用户操作"] --> B["本地 RDB"]
B --> C["UI 更新"]
B --> D{"网络可用?"}
D -->|"是"| E["AGC 云同步"]
D -->|"否"| F["队列缓存"]
F -->|"网络恢复"| E
RDB 表设计 三张核心表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 CREATE TABLE IF NOT EXISTS words ( id INTEGER PRIMARY KEY AUTOINCREMENT, word TEXT NOT NULL , meaning TEXT NOT NULL , phonetic TEXT, grade INTEGER NOT NULL , unit INTEGER NOT NULL , created_at INTEGER NOT NULL ) CREATE TABLE IF NOT EXISTS learn_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, word_id INTEGER NOT NULL , user_id TEXT NOT NULL , status INTEGER NOT NULL , review_count INTEGER DEFAULT 0 , correct_count INTEGER DEFAULT 0 , next_review_at INTEGER NOT NULL , last_review_at INTEGER , FOREIGN KEY (word_id) REFERENCES words(id) ) CREATE TABLE IF NOT EXISTS daily_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL , date TEXT NOT NULL , words_learned INTEGER DEFAULT 0 , words_reviewed INTEGER DEFAULT 0 , accuracy REAL DEFAULT 0.0 , UNIQUE (user_id, date ) )
BaseDao 懒加载模式 鸿蒙的 relationalStore.getRdbStore() 是异步的,但组件渲染是同步的。BaseDao 的懒加载解决了这个时序问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 abstract class BaseDao <T> { private store : relationalStore.RdbStore | null = null protected abstract tableName : string protected async getStore (): Promise <relationalStore.RdbStore > { if (this .store ) return this .store this .store = await relationalStore.getRdbStore (getContext (), { name : 'easy_word.db' , securityLevel : relationalStore.SecurityLevel .S1 }) await this .ensureTable (this .store ) return this .store } protected abstract ensureTable (store : relationalStore.RdbStore ): Promise <void > async insert (data : Partial <T>): Promise <number > { const store = await this .getStore () const bucket = new relationalStore.RdbPredicates (this .tableName ) const rowId = await store.insert (this .tableName , data) return rowId } async query (predicates : relationalStore.RdbPredicates ): Promise <T[]> { const store = await this .getStore () const resultSet = await store.query (predicates, []) return this .mapResultSet (resultSet) } }
第一次调用 getStore() 时初始化数据库并确保表存在,后续调用直接返回缓存。组件的 aboutToAppear() 中调用 DAO 方法,第一次会 await,第二次起就是毫秒级。
分布式同步:同账号设备间同步 鸿蒙的 relationalStore 支持分布式表,同账号的不同设备可以自动同步数据:
1 2 3 4 5 6 7 8 9 10 async enableDistributed ( ) { const store = await this .getStore () await store.setDistributedTables (['learn_records' , 'daily_logs' ]) } async syncToDevice (deviceId ?: string ) { const store = await this .getStore () const predicate = new relationalStore.RdbPredicates ('learn_records' ) await store.sync (relationalStore.SyncMode .SYNC_MODE_PUSH , predicate, deviceId) }
setDistributedTables 标记哪些表参与分布式同步。sync 触发一次推送同步,deviceId 为空时推送到所有设备。
多用户:儿童档案隔离 一个家庭可能有多个孩子使用同一台设备。通过 user_id 字段隔离数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class LearnRecordDao extends BaseDao <LearnRecord > { protected tableName = 'learn_records' async getByUser (userId : string ): Promise <LearnRecord []> { const predicates = new relationalStore.RdbPredicates (this .tableName ) predicates.equalTo ('user_id' , userId) return this .query (predicates) } async getNextReviewWords (userId : string , limit : number ): Promise <LearnRecord []> { const predicates = new relationalStore.RdbPredicates (this .tableName ) predicates.equalTo ('user_id' , userId) predicates.lessThanOrEqualTo ('next_review_at' , Date .now ()) predicates.equalTo ('status' , 1 ) predicates.limit (limit) predicates.orderByAsc ('next_review_at' ) return this .query (predicates) } }
所有查询都带 user_id 过滤,切换档案时只需要切换 currentUserId。
数据安全 securityLevel: relationalStore.SecurityLevel.S1 设置了安全级别。S1 级别表示:
数据加密存储
设备锁定后数据不可访问
适合存储用户学习数据
对于更敏感的数据(如登录凭证),使用 preferences KV 存储,安全级别可以设到 S3。
离线优先不是”网络不重要”,是”网络不可靠”。把本地数据做扎实,网络是锦上添花。