在数字时代,我们常常被复杂的框架和云服务淹没,但 Derek Sivers 的方法提供了一种回归本质的路径:用单一数据库驱动整个个人应用系统,确保数据完全在自己掌控中。Sivers 在他的 GitHub 仓库中展示了如何用 PostgreSQL 构建一个自给自足的 web 生态,但对于个人规模的任务管理和内容 curation,轻量级的 SQLite 更合适。它无需服务器,数据存于单个文件,支持事务和 SQL 查询,完美契合数据主权原则。本文将探讨如何受 Sivers 启发,构建 minimalist SQLite-driven web apps,使用 vanilla JS 前端和边缘函数,实现零配置部署。
首先,理解 Sivers 的核心理念。他的系统将数据库作为应用的核心,不仅存储数据,还生成 HTML 输出和处理 HTTP 响应。这减少了层级依赖,避免了框架的臃肿。在 Sivers 的仓库中,PostgreSQL 函数直接返回 head 和 body 文本,用于构建完整响应。这种方法强调简洁:代码在数据库中,应用逻辑封装在函数里。对于 SQLite,我们可以类似地设计:用 SQL 函数或简单查询驱动应用,但由于 SQLite 更嵌入式,我们将逻辑移到 JS 层,利用其零配置特性。
为什么选择 SQLite?它自包含、无服务器,文件大小小巧,适合个人使用。相比 Sivers 的 Postgres,SQLite 更易于本地开发和备份——只需复制 .db 文件即可实现数据迁移。这确保了数据主权:你的任务列表或 curation 内容不会被第三方云锁定。根据 SQLite 官方文档,它支持高达 2TB 的数据库大小,足以应对个人需求,且在嵌入式场景中性能出色。Sivers 的哲学在这里适用:优先简单性和可移植性,而不是企业级扩展。
让我们从任务管理 app 开始设计 schema。创建一个 tasks.db 文件,使用 SQLite CLI 或工具如 DB Browser for SQLite 初始化:
CREATE TABLE tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
status TEXT DEFAULT 'pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_status ON tasks(status);
这个 schema 简单明了,支持基本 CRUD 操作。受 Sivers 启发,我们添加触发器自动更新 timestamp:
CREATE TRIGGER update_timestamp
AFTER UPDATE ON tasks
FOR EACH ROW
BEGIN
UPDATE tasks SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;
对于内容 curation,如书签或笔记,添加另一个表:
CREATE TABLE curations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
url TEXT,
content TEXT,
tags TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_tags ON curations(tags);
这些表保持扁平,避免复杂关系,符合 Sivers 的“少即是多”原则。数据完整性通过约束和索引确保,无需额外 ORM。
前端使用 vanilla JS,无需 React 或 Vue。假设我们用一个单页应用 (SPA) 结构:index.html 加载 app.js,连接到 SQLite via sql.js (一个 WebAssembly 端口的 SQLite,允许浏览器直接操作 .db 文件)。但为数据主权,我们优先本地或边缘部署。
在 app.js 中,实现任务列表:
async function initDb() {
const SQL = await initSqlJs({
locateFile: file => `https://sql.js.org/dist/${file}`
});
const response = await fetch('tasks.db');
const buffer = await response.arrayBuffer();
const db = new SQL.Database(new Uint8Array(buffer));
return { SQL, db };
}
async function loadTasks() {
const { db } = await initDb();
const res = db.exec('SELECT * FROM tasks ORDER BY created_at DESC');
const tasks = res[0]?.values || [];
const list = document.getElementById('task-list');
list.innerHTML = tasks.map(row => `
<li>${row[1]} - ${row[2]} (${row[3]})</li>
`).join('');
}
document.getElementById('add-task').addEventListener('click', async () => {
const { db } = await initDb();
const title = document.getElementById('title').value;
db.run('INSERT INTO tasks (title, description) VALUES (?, ?)', [title, '']);
loadTasks();
});
这个代码直接在浏览器中操作,但为持久化,我们用边缘函数处理更新。边缘函数如 Cloudflare Workers 或 Deno Deploy,提供零配置:上传脚本,无需服务器管理。
例如,在 Cloudflare Worker 中,实现 API:
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
if (request.method === 'POST') {
const data = await request.json();
return new Response('Task added', { status: 200 });
}
}
为零配置部署:用 Vercel 或 Netlify 托管静态文件,边缘函数集成。Sivers 的方法在这里闪光:后端逻辑最小化,前端直接查询 DB 文件(via sql.js for read-only)或 API for writes。
参数和清单:
这种方法确保应用轻量:总大小 <1MB,启动即用。Sivers 在他的 tech blog 中写道:“Simplify: move code into database functions”,这启发我们即使在 SQLite,也用视图和函数封装逻辑,如 CREATE VIEW active_tasks AS SELECT * FROM tasks WHERE status != 'done'。
风险:SQLite 单文件锁定可能在高写时瓶颈,但个人 app 罕见。相比云 DB,它避免订阅费和供应商锁。
最后,这种构建方式体现了 Sivers 的精神:拥有你的数据,构建你需要的工具,而非追逐潮流。
资料来源: