“写笔记”支持四种格式——Word 文档、Excel 表格、Markdown、纯文本,起稿或二次编辑时都能随时切换,同一篇笔记想用哪种形态来记,都由你说了算。
md、txt、csv、json 这类纯文本则原样载入,不做多余加工。拿一张现成的表倒进来、改几笔、再导出去,等于白用一台免费的格式转换器。
要带走就在右上角点“下载”,可导出 PDF、Word、Markdown、Excel、TXT 等格式;列表卡片“⋯”菜单里,也有同样的下载入口。
在“工具”页点“+ 上传工具”即可发布:填好名称与链接,再用 Markdown 把使用方法写清楚——能解决什么问题、怎么装、怎么用,比堆介绍实在。
要分发安装包就一并上传压缩包(ZIP、RAR、7Z、TAR.GZ,最大 35MB),别人在详情页一键下载;只放链接不带附件也可以。
工具按大家的收藏热度排序,好用的自然会被顶上来。发布后可在详情页或卡片菜单里编辑、下架。
写笔记时勾上“隐藏”,这篇就只存在于你自己的账号里:不进列表、不进搜索、不上首页精选,也不会出现在任何公开的页面,链接发给别人同样打不开。
适合放密码、草稿、日记这类只给自己看的内容;想公开,去“发布”打开它,把“隐藏”的勾去掉再保存,之后编辑会默认保持原状态,不会悄悄变回公开。
你的内容会同时保存在多个副本上,系统定期做备份与完整性校验,再配合异地容灾机制:就算某台机器出问题,数据也不会丢,可以长期放心存放;特别重要的资料,仍建议你另外再留一份备份。
全站跑在容器化、模块化的现代架构上,更新、部署、回滚都很快,扩展性和稳定性都按长期运营的标准来设计(Built for reliability, designed to scale)。
这个网站最早只是一个人的笔记仓库,后来慢慢长成现在的知识中枢。设计上很克制——没有广告、没有追踪、没有推荐算法,只是干干净净地存放一些东西;既然做好了,就公开出来,万一有人用得上呢。
不做大而全,不做平台梦,保持简单、保持克制、保持好奇。所有内容都由用户贡献、由用户维护:不会突然冒出付费墙,不会在角落塞广告位,也不会把你的数据卖给第三方。
产品会持续迭代,站内日志页记录着每一次改动,改了什么都有迹可循;想了解这个站是怎么一步步走到今天的,翻翻日志就能看到来龙去脉。
如果在这里看到涉嫌违规的内容,点对应卡片右侧的“举报”按钮就能提交,我们会尽快核实处理;也谢谢你花一点时间,一起把这里维护干净。
在 GitHub Pages 上托管 SQLite 数据库:让静态网站拥有真正的查询能力
通过将 SQLite 编译为 WebAssembly 并利用 HTTP Range 请求实现虚拟文件系统,让静态网站也能承载并高效查询超大数据库,无需后端服务器。
引言:一个反复出现的痛点
作者在开发一个小工具网站时,发现了一个自己反复遇到的模式:想写一个查询数据库并以图表或表格形式展示结果的网页小工具。但在这个需求面前,传统方案总有令人遗憾的缺陷:
写一个后端服务器:需要持续的托管和维护。麻烦之处在于,这些小型副项目往往会上线后就被遗忘。某天外部 API 挂掉、密钥过期,或者因为不再关注而停止了 VPS 的续费。几年后回访时,发现服务早已消失,悔恨自己当初依赖了外部服务,也高估了自己长期维护的意愿。
把整个数据集下载到浏览器:对于超过 10MB 的数据集,这变得不可接受——加载缓慢、内存占用巨大,且无法利用索引进行高效检索。
而维护一个静态网站要简单得多:GitHub Pages、GitLab Pages、Netlify 等提供了大量且免费可靠的选择,流量几乎无限扩展而无需任何运维成本。那么,有没有可能把“真正的 SQL 数据库”塞进静态托管里?
答案是肯定的。作者开发了一个名为 sql.js-httpvfs 的工具,实现了这个想法。
核心理念:SQLite 编译为 WebAssembly + HTTP Range 请求
技术方案的核心思路并不复杂,由两部分组成:
SQLite → WebAssembly:SQLite 本身是用 C 语言编写的,它可以无修改地通过 Emscripten 编译为 WebAssembly 代码。
sql.js库就是封装这套 wasm 代码的轻量 JS 层。但sql.js只允许在内存中完全创建和读取数据库——这显然不够。虚拟文件系统——按需获取数据库分块:作者实现了一个自定义的虚拟文件系统层。从 SQLite 的角度看,它感觉自己运行在一台正常的电脑上,文件系统里除了一个它只能读取、不能写入的文件外空空如也。但底层原理是:当 SQLite 尝试从文件系统读取某个偏移量的数据时,这个虚拟文件系统会发起一个带
Range头的 HTTP 请求,从服务器上获取对应字节范围的数据块。
用 SQL 术语来说:数据库文件永远不用被完整下载到浏览器,你只下载查询所必需的那几个页面。
分块策略:寻找请求数与带宽之间的平衡
HTTP 请求本身的往返开销相当大(握手、头信息等),所以必须批量获取数据。十分幸运的是,SQLite 本身就把数据组织成固定大小的“页”(用户可自定义页大小,默认为 4 KiB)。
为了在单次请求中能传输更多有用数据,作者将演示数据库(World Development Indicators,共 6 张表,超过 800 万行,总大小 670 MiB)的页大小压缩到了 1 KiB。这样每次请求获取的数据更精确,避免浪费带宽。
来看看实际运行效果。查询 wdi_country 表的前 3 行,只需获取 1KB 数据:
sql
select country_code, long_name from wdi_country limit 3;
这是一个完整的 SQLite 查询引擎。这意味着 SQLite 的所有原生能力都能用,包括 JSON 处理:
sql
select json_extract(arr.value, '$.foo.bar') as bar from json_each('[{"foo": {"bar": 123}}, {"foo": {"bar": "baz"}}]') as arr;
甚至可以注册 JavaScript 函数让 SQLite 在查询时调用。比如将国家代码转换为对应的旗帜 emoji:
js
function getFlag(country_code) {
// 只需一些 Unicode 魔法
return String.fromCodePoint(...Array.from(country_code || "").map(c => 127397 + c.codePointAt()));
}
await db.create_function("get_flag", getFlag);
return await db.query( select long_name, get_flag("2-alpha_code") as flag from wdi_country where region is not null and currency_unit = 'Euro';);
索引查询的底层逻辑:B-Tree 的逐页读取
深入观察一个简单的通过索引进行的查找查询:
sql
select indicator_code, long_definition
from wdi_series
where indicator_name = 'Literacy rate, youth total (% of people ages 15-24)';
通过页面读取日志,可以看到 SQLite 为此进行了 7 次页面读取:
- 3 次页码读取:仅用于获取 Schema 元信息(这些通常会被缓存)。
- 2 次索引查找:在
wdi_series(indicator_name)索引的 B-Tree 中进行查找。 - 2 次表数据读取:第一次根据主键找到行值,第二次从溢出页获取长文本数据。
索引和表读取本质上都是在 B-Tree 中逐层下降。由于每层的 B-Tree 页面的位置未必相邻,如果按 1 KiB 一页去请求,那确实需要多次随机 HTTP 请求。
更复杂的查询示例:自 2010 年后,基于最新数据,女性青年识字率最低的国家是哪些?
sql
with newest_datapoints as (
select country_code, indicator_code, max(year) as year
from wdi_data
join wdi_series using (indicator_code)
where indicator_name = 'Literacy rate, youth total (% of people ages 15-24)'
and year > 2010
group by country_code
)
select c.short_name as country, printf('%.1f %%', value) as "Youth Literacy Rate"
from wdi_data
join wdi_country c using (country_code)
join newest_datapoints using (indicator_code, country_code, year)
order by value asc
limit 10;
此查询预计会发送 1020 个 GET 请求,下载约 130270 KiB。为什么不会达到 270 次请求(如果 1 KiB 分批请求的理论最大值)?
预取系统:识别访问模式,指数化扩大请求
这便是精妙之处——作者实现了一个 预取系统,它会检测访问模式。系统使用了三个独立的虚拟“读头”,对于顺序读取(例如 B-Tree 叶节点的扫描),会指数级扩大请求大小。
也就是说:索引扫描或表扫描读取超过几 KiB 的数据时,发出的请求数量会与扫描总字节数成对数级别。第一次读 1 KiB,第二次读 2 KiB,然后 4 KiB、8 KiB,以此类推,这样可以以极少数请求拉取大段连续数据。这极大地缓和了网络延迟对性能的影响。
索引设计至关重要
这个方法只在一个前提下才高效:数据库中拥有匹配查询模式的正确索引。
作为反例,如果一个查询需要对大量数据点进行单独取值,而索引不包含该value列,SQLite 就必须为每个匹配行再做一次“随机访问”读盘——本地还能接受,但在 HTTP 环境下这将带来一场灾难性的大量请求。
此外,索引中列的顺序也非常讲究。例如:
INDEX ON wdi_data (indicator_code, country_code, year, value)能使“查询某个指标所有国家的数据”过快;- 但如果索引顺序改为
(country_code, indicator_code, ...),就可以快速拿到一个国家的所有指标,却无法高效地查询某个指标的值。
写这篇博客时所用的 World Development Indicators 数据库,正是仔细考虑了索引设计(wdi_data 表上的索引为 (indicator_code, country_code, year, value)),才能在页面如此高效的运作。
文本搜索:基于 FTS5 模块
这个方案与 SQLite 的全文搜索(FTS5)模块结合同样出色。在演示数据库中,有超过 1000 个文本量较大的人类发展指标描述,indicator_search 表利用了 FTS5 提供模糊且快速的全文检索:
sql
select * from indicator_search
where indicator_search match 'educatio* femal*'
order by rank
limit 10;
该 FTS 表的总数据量大约 8 MB,而上面这条查询只需要获取约 70 KiB。FTS5 内部的索引结构同样利用了 B-Tree,所以这里的预取逻辑同样有效。
实际效果:一个交互式图表案例
为了让读者认识到实用性,作者展示了完整的图表交互功能:选择国家(如美国、德国、印度、中国、韩国)和任意指标(如“使用互联网的个人人口百分比”),可以渲染随时间变化的曲线图。相关指标的描述(Indicator Code、Long definition 等)也预先存放在数据库中,可以即时查询。重要的是,这一切都发生在静态托管中,没有后端请求。
扩展阅读和演示源码可在仓库中找到。
总结:为什么值得尝试
- 零维护:静态文件托管是完全免费的,并且有可靠的服务提供商(如 GitHub Pages,它为此项目提供 100% 的静态托管服务)。
- 高可扩展性:不需要动一根手指,你的数据库就能服务无限量的访问者。
- 真正的 SQL 能力:不受限于像 localStorage 这样的键值存储,它可以拥有索引、事务、视图、触发器、FTS5 全文检索、JSON 处理等全部 SQLite 特性。
- 远超 10MB 的数据:通过智能分块,几百兆的数据库也能被浏览器轻松地在快速查询下提供流畅体验,初始加载只有几十 KB。
如果你也曾在静态网站中苦于无法进行复杂数据筛选,这个方案值得一试。当你需要存储不断增长的真实数据并希望用户随时可查时,静态托管 + SQLite WASM + HTTP Range 是一套极为优雅的组合。
原文链接:https://phiresky.github.io/blog/2021/hosting-sqlite-databases-on-github-pages/