第5课:数据库与邮件存储 —— SQLite 怎么存邮件
本课目标
搞懂邮件数据是怎么在数据库里存放和查询的。这是搜索功能和看板数据的基础。
为什么需要数据库?
直接从 Gmail 搜邮件不行吗?也行,但有几个问题:
| 直接查 Gmail | 先存到本地数据库 | |
|---|---|---|
| 速度 | 每次都要网络请求,慢 | 本地查询,毫秒级 |
| 离线 | 断网就完蛋 | 没网也能看历史邮件 |
| 灵活性 | 只支持 Gmail 的搜索语法 | 想怎么查就怎么查 |
| 额外数据 | 不能存自定义字段 | 可以存标签、分类等 |
所以 demo 的策略是:先把邮件同步到本地 SQLite,后续操作都在本地做。
SQLite 是什么?
SQLite 是一个超轻量的数据库——不需要安装任何服务器,整个数据库就是一个文件(emails.db)。
打个比方:MySQL/PostgreSQL 是大型仓库,需要专人管理;SQLite 是你桌上的文件夹,打开就用。
数据库里有哪些表?
位置:database/email-db.ts
表1:emails(邮件主表)
CREATE TABLE emails (
id INTEGER PRIMARY KEY,
messageId TEXT UNIQUE, -- Gmail 唯一标识
threadId TEXT, -- 对话线程
"from" TEXT, -- 发件人
"to" TEXT, -- 收件人
date_sent TEXT, -- 发送时间
date_received TEXT, -- 接收时间
subject TEXT, -- 主题
body_text TEXT, -- 纯文本正文
body_html TEXT, -- HTML 正文
snippet TEXT, -- 摘要
is_read INTEGER DEFAULT 0, -- 已读?
is_starred INTEGER DEFAULT 0, -- 星标?
is_important INTEGER DEFAULT 0, -- 重要?
has_attachments INTEGER DEFAULT 0, -- 有附件?
folder TEXT, -- 文件夹
labels TEXT -- 标签(JSON数组)
);
表2:recipients(收件人明细)
CREATE TABLE recipients (
id INTEGER PRIMARY KEY,
email_id INTEGER, -- 关联到 emails 表
type TEXT, -- 'to', 'cc', 'bcc'
address TEXT, -- 邮箱地址
name TEXT -- 显示名
);
表3:attachments(附件)
CREATE TABLE attachments (
id INTEGER PRIMARY KEY,
email_id INTEGER,
filename TEXT,
content_type TEXT, -- 如 'application/pdf'
size_bytes INTEGER
);
表4:ui_states(看板数据)
CREATE TABLE ui_states (
stateId TEXT PRIMARY KEY, -- 如 'financial-dashboard'
data TEXT, -- JSON 格式的数据
createdAt TEXT,
updatedAt TEXT
);
这个表很特别——它不存邮件,而是存看板的持久数据(财务记录、任务列表等)。后面第10课详讲。
常见的数据库操作
插入新邮件
// database-manager.ts(简化版)
async function insertEmail(email: ParsedEmail) {
db.run(`
INSERT INTO emails (messageId, threadId, "from", "to", subject, body_text, ...)
VALUES (?, ?, ?, ?, ?, ?, ...)
`, [email.messageId, email.threadId, email.from, ...]);
}
搜索邮件
// email-search.ts(简化版)
async function searchEmails(query: string) {
return db.all(`
SELECT * FROM emails
WHERE subject LIKE ? OR body_text LIKE ?
ORDER BY date_sent DESC
LIMIT 20
`, [`%${query}%`, `%${query}%`]);
}
读取/更新 UI State
// ui-state-manager.ts
async function getState(stateId: string) {
const row = db.get('SELECT data FROM ui_states WHERE stateId = ?', stateId);
return JSON.parse(row.data);
}
async function setState(stateId: string, data: any) {
db.run(`
INSERT OR REPLACE INTO ui_states (stateId, data, updatedAt)
VALUES (?, ?, ?)
`, [stateId, JSON.stringify(data), new Date().toISOString()]);
// 关键:更新后通过 WebSocket 广播给前端
broadcastStateUpdate(stateId, data);
}
数据怎么流到前端?
graph TD
A["数据库"]
A -->|"HTTP 接口"| B["后端服务器 (server/endpoints/)"]
B -->|"GET /api/emails/inbox -> 返回邮件列表<br>POST /api/emails/search -> 搜索邮件<br>GET /api/ui-state/:id -> 返回看板数据"| C["前端浏览器"]
B -->|"WebSocket 推送"| C
C -->|"React 组件渲染"| D["你看到的界面"]
HTTP 接口 用于前端主动请求数据(比如打开收件箱时加载邮件列表)。
WebSocket 推送 用于实时更新(比如新邮件到达时自动刷新列表)。
WAL 模式:为什么数据库不卡?
demo 开启了 SQLite 的 WAL(Write-Ahead Logging) 模式:
db.pragma('journal_mode = WAL');
简单说:普通模式下,写数据库时会锁住,其他人不能读。WAL 模式下,读写可以同时进行,互不干扰。
这对邮件助手很重要——你在前端搜邮件(读)的同时,后台可能在同步新邮件(写),WAL 保证两者不冲突。
本课小结
- SQLite 是本地轻量数据库,整个数据库就是一个文件
- 四张表:emails(邮件)、recipients(收件人)、attachments(附件)、ui_states(看板数据)
- HTTP 接口 让前端查询数据库
- WebSocket 让数据变化实时推送到前端
- WAL 模式 保证读写不冲突