零依赖静态站点生成器caveman:用300行代码对抗依赖地狱

发布时间:2026/10/28 13:26:09

零依赖静态站点生成器caveman:用300行代码对抗依赖地狱 好多年前我带过一个实习生他把一个 200 行的脚本重构成 2000 行的工程然后跑过来问我哥这样是不是更正规我当时没说话。直到最近动手做caveman这个项目我才真正想明白该怎么回答他工具的价值不在工程化在于解决问题。caveman是我刚写完的一个零依赖静态站点生成器全部源码不到 300 行运行只需要 Python 标准库不装任何第三方包。它的用途很朴素——把一批 Markdown 文件变成可以发布的 HTML 页面仅此而已。但正是在刻意削减功能的过程中我重新理解了简化、抽象和取舍也踩了几个很有意思的坑。这篇文章就是这次实验的完整记录适合被依赖地狱和过度设计折磨过、或是正在犹豫要不要也做一个极简工具的开发者。1. 从工具焦虑到caveman一个极简项目是怎么立项的1.1 压垮我的那根构建链稻草事情发生在一个普通的工作日。我需要给内部一个文档目录加个网页入口本来想着十分钟搞定结果发现当前环境里没有现成的文档生成工具于是开始装。先装 Node.js 的包管理器再拉一个静态站点框架然后它告诉我需要额外的插件来支持目录导航装完插件后又要求指定主题版本。等我终于把配置文件调通并且看到一条新的依赖安全告警时已经过去了将近四十分钟而我还没写出任何一行跟业务有关的东西。那种感觉很荒诞我只是想让几个 Markdown 文件能被浏览器打开却引入了一整套需要长期维护的构建生态系统。晚上回家我盯着窗外想了一会儿冒出一个念头如果是一个原始人碰到这个问题他会怎么做原始人不会先搭建火种管理系统他会直接钻木取火。所谓caveman这个名字就是在这一刻定下来的——做一个原始人级别的工具能用最笨的办法解决当下的问题绝不提前为不存在的需求买单。1.2 立项之前我先给自己列了四条原始约束为了避免做的时候又忍不住顺手加个功能我把需求边界写在纸上反复删改后只保留四条零第三方依赖。所有功能只使用编程语言自带的标准库这是硬性要求不接受例外。单一职责。只做Markdown 转 HTML 并生成站点目录不做搜索、不做分页、不做主题系统。源码可全读。任何一个有基本编程经验的人打开代码仓库后半小时内能理解全部逻辑。行为可预测。同样的输入永远产生同样的输出不做缓存、不做增量构建、不做花哨的动态优化。这四条约束现在看很平常但真正执行起来会发现它们会逼你不断做减法。比如不做增量构建意味着每次运行都全量生成当你的内容条目少时这完全没问题可一旦内容多到上千篇单线程全量写文件就会慢得让人怀疑人生。我在第 4 章会专门讲这个取舍带来的问题。1.3 为什么偏偏选静态站点生成器当实验品选这个方向是因为它刚好是复杂度重灾区。市面上的主流静态站点生成器动辄几百上千个依赖包安装体积以百兆计而它解决的核心问题——把文本变成页面——本质上只需要三步读文件、做转换、写文件。这就像两个人去同一个目的地一个人开着改装越野车绕山路另一个人骑自行车走直线最终都能到但后者对路况的敏感度完全不同。我想证明的其实不是极简更好而是很多复杂度到底是需求本身需要还是工具链自己制造出来的。静态站点生成器是一个理想样本它的输入是纯文本输出是纯静态文件中间没有任何必须依赖复杂生态的理由。做完之后我意识到这个实验真正的价值不在工具本身而在它让我看清了默认复杂思维里那些被大家习惯性接受的额外成本。2. 三条铁律我是怎么把复杂度锁在项目门外的2.1 铁律一零依赖不是矫情是可控性的护城河很多人听到零依赖的第一反应是这有什么难的不用第三方库而已。但你只要真的开始做就会发现它带来的约束比你想象的更深刻。首先是功能层面你只能用标准库那就意味着很多现成的东西要自己写——HTML 转义、路径整理、目录遍历全都得用最原始的方式实现。其次是心理层面每当你动念头如果有个库能干这个就好了你就必须停下来问自己这个功能真的需要吗还是我可以换一种更简单的输出形式绕过去举个例子我最初想支持 Markdown 里的表格语法但标准库没有现成解析器自己写一个完整支持 GFM 表格的解析器至少要二三百行。按照零依赖原则我面临两个选择要么写解析器要么放弃表格。我最终选择放弃表格改为建议用户在文档里用列表和代码块表达结构化信息。这个决定让代码量砍了一半而面向的读者几乎没有感知——因为我的目标用户就是我自己和几个同事我们完全可以用更简单的语法写作。依赖带来的另一个问题是被动升级。主流框架每半年发一个大版本接口变化、配置格式调整、插件兼容性问题这些都是你无法控制的。零依赖意味着这个工具在未来五年、十年后打开仍然能跑只要 Python 还在。这种时间上的确定性在我看来比任何功能都值钱。2.2 铁律二标准库能搞定的绝不自己重复造轮子这里有个容易走向极端的误区为了显示极简连标准库都刻意回避。我见过有人不用标准库的glob模块而是自己手写通配符匹配理由是这样更纯粹。这完全是本末倒置。极简的目的是降低总成本不是表演行为艺术。我的原则是反向的只要标准库提供了成熟实现就毫不犹豫地用。目录遍历用os.walk文件路径拼接用pathlib.Path甚至 HTML 转义都可以用html.escape。这些模块经过十几年大规模使用稳定性远超我自己写的替代品。真正需要自己动手的是那些标准库没有、但需求又很核心的部分比如把 Markdown 的行级语法转成 HTML 段落这才是我用几十行代码解决的地方。这条铁律帮我划清了一个重要边界不依赖外部但信任系统自带的能力。这就像原始人虽然不会冶炼金属但知道用石块和木棍大自然给什么就用什么而不是非要去火星挖矿。2.3 铁律三特性支持宁缺毋滥每一项功能都有维护成本功能的维护成本分三层第一层是写代码的时间第二层是测试和修 bug 的时间第三层是用户包括未来的自己理解你的时间。第三层最容易被忽略——每多一个特性使用文档就要多一段别人阅读源码就要多花几秒去理解。而真实情况是绝大多数用户只用到工具功能的百分之二十。caveman最终支持的 Markdown 语法被我砍到只剩三种标题、段落、代码块。注意连列表语法我都没做。为什么因为我发现把列表手写成- 项目再通过正则转换成ulli本质上只是换个格式内容表达并没有得到增强而多一个列表语法就意味着多一套嵌套和转义规则。我的解法是让用户直接在 Markdown 里写原始 HTML 列表反正 HTML 本身也是文本转换器对已含 HTML 标签的行原样透传。这个取舍让我想起一个很妙的类比瑞士军刀功能多但真正在野外求生时一把砍刀往往比一把全是小零件的军刀更可靠。工具数量的增多会稀释你对每一个工具的熟悉程度而极简工具的每一个功能用户都清清楚楚知道它在什么时候触发、什么时候不触发。3. 关键实现拆解caveman的代码骨架与核心流程3.1 目录扫描先用os.walk把文件讲清楚工具的第一步是找到所有 Markdown 文件。这里的核心问题是目录结构应该怎么映射到输出站点的路径结构我采用的策略是最直观的一一映射——输入目录content/docs/下的intro.md对应输出目录site/docs/intro.html子目录自动创建。用os.walk遍历目录把.md结尾的文件收集起来这个逻辑简单到不需要解释。真正需要想清楚的是排序规则。你不可能指望操作系统返回的文件顺序就是你想在导航中展示的顺序所以我额外约定了一个同目录下按文件名排序的规则并在命名时就让它体现顺序比如01-intro.md、02-setup.md。这是一个很原始人的约定但效果出奇地稳定完全不需要配置 TOML 或 YAML 导航文件。from pathlib import Path def collect_markdown_files(content_dir: Path) - list[Path]: files [] for root, dirs, filenames in os.walk(content_dir): for name in filenames: if name.endswith(.md): full Path(root) / name files.append(full) files.sort(keylambda p: str(p).lower()) return files这段代码我在实际写的时候只花了几分钟但它保证了后续所有逻辑都能专注于处理文件而不是找文件。3.2 Markdown 转 HTML只做三种结构但做扎实核心转换逻辑是最值得细看的部分。我没有引入任何现成的 Markdown 解析器因为如果只支持标题、段落、代码块三种结构手写一个行级解析器其实非常可控。处理流程是按行读取核心状态只有一个是否在代码块内的布尔变量。逻辑要点如下遇到以#开头的行根据#数量生成对应的h1到h3遇到空行视为段落分隔遇到切换代码块状态代码块内的所有行原样保留并做 HTML 转义其余非空行作为普通段落包进p标签。这里最关键的一步是转义——正文里如果出现script或者img onerror之类的字符串不转义就会变成活性内容轻则样式错乱重则有安全风险。标准库的html.escape在这里发挥了大作用。import html import re def md_to_html(md_text: str) - str: lines md_text.splitlines() out [] in_code False for line in lines: stripped line.strip() if stripped.startswith(): if in_code: out.append(/code/pre) in_code False else: out.append(precode) in_code True continue if in_code: out.append(html.escape(line)) continue m re.match(r^(#{1,3})\s(.*), line) if m: level len(m.group(1)) out.append(fh{level}{html.escape(m.group(2))}/h{level}) continue if stripped: out.append(fp{html.escape(stripped)}/p) else: out.append() return \n.join(out) \n这段代码只有二十行但我花了大半个晚上调试边界情况行内代码标记code没有处理、图片语法![]()没有处理、引用块没有处理。每次遇到一种常见语法它带来的不是便利而是一连串新的转义和嵌套问题。最后我狠下心说只要内容表达不依赖这些语法就不支持。事实证明用纯文本 少量 HTML 写文档反而让我更注意内容本身的结构。3.3 模板与站点骨架字符串替换就足够了有了 HTML 片段还需要把它放进一个完整的页面模板里带html、head、body结构还要有一个简单的导航栏。这一步我同样选择了最原始的方法——预定义模板字符串用.replace()做占位符替换。模板中我设计了三个占位符!-- TITLE --放页面标题、!-- NAV --放导航列表、!-- CONTENT --放正文 HTML。生成导航的方式是遍历前面的文件列表为每个文件生成一个a链接当前页面加一个classactive。这种模板方案在今天看来确实很史前但它有一个明显的优点没有任何隐藏行为一个模板变量对应一个替换点想加变量就先加占位符逻辑一眼看穿。这里我补充一个实际遇到的细节链接路径统一用正斜杠/即使你在 Windows 上运行输出到浏览器里也必须用正斜杠。pathlib.Path在 Windows 上会给出反斜杠直接拼到 HTML 里虽然浏览器通常能容错但碰到某些严格场景就会出现资源加载失败。我在代码里统一做了str(rel_path).replace(os.sep, /)处理这个细节是实测时被坑过之后才补上的。3.4 监听文件变化没有 watchdog轮询也是一种美德写完转换逻辑后我发现开发时每次手动跑命令太麻烦于是想加一个文件变化自动重建的功能。常规做法是装一个watchdog库监听文件系统事件。但在零依赖的约束下我只能用最古老的轮询方案每隔一秒扫描一次所有文件的修改时间发现变化就重新生成。这个方案的实现非常简单用一个dict记录每个文件的mtime每次扫描对比新旧值变了就触发重建。缺点也很明显对于上千个文件的目录每秒全量扫描一次会带来额外的磁盘 IO好处是无法更简单了只有十几行代码而且跨平台稳定不需要依赖操作系统特有的文件事件机制。import time def watch_and_build(content_dir: Path, build, interval1.0): snapshot {} while True: for f in collect_markdown_files(content_dir): mtime f.stat().st_mtime if snapshot.get(f) ! mtime: snapshot[f] mtime build() break time.sleep(interval)这段轮询代码让我想到一个道理很多高级需求其实都有低级但够用的解法。文件监听用轮询虽然理论上延迟一秒但对编辑文档这种场景完全无感。工具的价值是解决问题不是使用最先进的技术方案。4. 实测中的意外与边界情况简洁不等于没有坑4.1 第一个坑Windows 记事本存出的 UTF-8 带 BOM转换直接乱套我拿到第一批真实文档时有几篇是从 Windows 机器上拷贝过来的。转换后的页面在标题处莫名多了一个\ufeff字符浏览器里显示成一个不可见的零宽空格但 HTML 校验总是报错。排查了半天才发现是 BOM 头。BOM 是 Unicode 编码规范里用来标记字节序的字符UTF-8 本身不需要它但 Windows 记事本默认会加。去掉它的方式很简单读取文本后检查第一个字符是否为\ufeff如果是就切片去除。这个坑很小但如果不处理后续所有基于字符串开头的判断比如正则匹配#标题都会失效标题永远不会被识别成标题而是被包进一个段落里。我把这个处理放在了文件读取函数里保证进入解析器之前文本已经是干净的。def read_text(path: Path) - str: raw path.read_text(encodingutf-8) if raw.startswith(\ufeff): raw raw[1:] return raw4.2 第二个坑中文和空格文件名在链接里必须处理这个项目的内容有不少中文文件名比如开发笔记.md。生成链接时直接拼a href开发笔记.html在本地文件协议下浏览器通常能打开可一旦部署到 Nginx 或托管到对象存储中间经过一次 URL 解析未编码的中文和空格就可能触发 404。标准做法是把链接里的非 ASCII 字符做百分号编码。最初的代码里我没有做编码导致部署后一串链接全部打不开。排查思路是先在浏览器开发者工具里看实际请求的 URL再对比服务器上真实的文件路径才发现是编码问题。解法是用urllib.parse.quote对相对路径做编码同时保留/分隔符。from urllib.parse import quote def href_for(rel_path: str) - str: return quote(rel_path.replace(os.sep, /))4.3 第三个坑用户总会把和直接写进正文站点的读者不仅有开发者还有产品经理和运营同学他们不会记得 Markdown 里需要转义。第一次真实使用时就有人写了一行当 x 10 时需要告警结果你会发现这个被子段p的判断逻辑吞掉了浏览器把它当成一个未闭合标签的开始后续整个页面的结构全部错乱。传统解析器会在这里有自己的纠正逻辑但我的极简解析器直接撞上了。解决方式有两个层次第一层是上面提到的html.escape——这能保证任何裸的和都被转义成实体第二层是约定——在面向非技术用户的使用说明里明确写不要直接使用或用中文的『小于』『大于』。两层同时上才把这类问题压到零。4.4 第四个坑目录越深os.walk的返回顺序越任性在导航排序上我一开始天真地以为os.walk会按字典序返回子目录。实测发现目录较浅时确实基本有序但当嵌套到三层以上顺序开始变得不稳定——这跟底层文件系统的遍历顺序有关。导航里原来靠前的内容目录跑到中间视觉上很错乱。排查过程比我想象的复杂我先是怀疑排序逻辑写错了检查半天没发现问题然后写了个脚本打印所有目录和文件的遍历顺序对比多次运行结果发现顺序本身就有随机性。最终确认是文件系统遍历顺序的问题解决方案是不要在遍历过程中依赖顺序而是在收集完所有文件后把每个文件的相对路径拆分后逐级排序保证导航树按照层级 文件名双键稳定排列。这个修复让我意识到极简工具虽然逻辑简单但它的输入环境文件系统、编码、平台并不简单该防御的地方一个都不能少。5. Caveman 式开发思维的适用边界与我的最终取舍5.1 什么时候我推荐你像原始人一样做工具经过这个项目我总结出了几个适合采用极简方案的判断标准使用者和开发者高度重合。如果这个工具只有你自己和身边两三个同事用那么任何为了工程化而工程化的设计都是负债。需求边界清晰且短期内不会膨胀。文档转 HTML 这类需求未来无非是改改样式、加点导航不太会出现需要插件生态的爆发式增长。运行环境受控。caveman只需要 Python而你的机器上反正有 Python零依赖就等于零安装成本。对长期可维护性有要求。一个十年后还能原样运行的工具远比一个每年要跟随上游升级的工具更省心。我还发现一个有趣的副产品极简工具逼着用户简化自己的工作流。以前我用文档工具时会去搜各种插件、主题、短代码现在因为caveman不支持我反而养成了用朴素 Markdown 直接表达的习惯写文档的速度变快了因为不需要记二十种语法规则。5.2 什么时候千万别学caveman那是自找麻烦极简不是万能的。以下场景我会明确反对原始人方案团队协作且人员流动频繁。别人不熟悉你的约定和限制每换一个人都要重新解释一遍我们不支持什么沟通成本极高。需求真的需要复杂生态。比如你需要全文搜索、多语言国际化、动态路由、权限系统这些不是简单能替代的硬砍功能只会让产品残缺。安全敏感场景。自己写的解析器往往没有经过大规模安全测试XSS 防护、路径穿越、恶意输入这些内容成熟框架的积累远比自己硬写的几十行代码可靠。caveman目前只适配我信任的内容来源绝不建议直接暴露给不可控的互联网输入。这个边界很重要。极简是一种价值观不是一个可以到处套用的万能答案。它适合解决的问题类型是工具而不是平台。5.3 项目收尾后我给自己留了三条经验做完caveman已经有几周了它现在稳定地帮我生成内部文档站点。回看整个过程有三条经验我会长期保留第一条任何功能在加进去之前先写一段如果删掉它会发生什么。大部分时候你会发现删掉之后什么都没发生不更准确地说是你节省了后续所有维护它的时间。第二条限制条件越硬创造力越强。这听起来像鸡汤但当代码里只剩下标准库时你会本能地寻找更简单、更本质的解法而不是第一时间去搜索现成库。这种锻炼对编程能力本身是一种很好的训练。第三条工具的使用体验不只是界面和速度还包括我完全理解你在干什么的安全感。caveman的源码我随时可以打开每个人都能从头读到尾这种透明感是所有黑盒依赖给不了的。最后分享一个小技巧如果你也想做类似的实验不要一上来就写通用框架先拿自己手头一个真实的小需求练手把功能砍到不能再砍用零依赖实现一遍。做完你大概率会发现那些必须用重型工具才能解决的借口有一大半其实是幻觉。
延伸阅读

更多相关文章

2026/10/28 13:26:09

DeepSeek API + Python 接入避坑:版本、密钥与流式输出实战

简介:这是一份面向具备一定 Python 基础、希望快速接入 DeepSeek 语言能力的开发者与研究人员的实战型 API 调用指南,聚焦从账号注册、密钥获取到接口集成的完整上手路径。内容以单个 docx 文档承载,压缩包约 13KB,轻量便于查阅&a…

2026/10/28 13:26:08

Cloudflare Workers 构建的前端运行时契约:轻量级边缘OS实践

1. 项目概述:这不是一个操作系统,而是一套基于 Cloudflare Workers 的前端运行时抽象层“cloudflare-os”这个名称极具迷惑性——它既不是 Linux 发行版,也不提供内核、进程调度或设备驱动,更不替代 Windows、macOS 或 Android。我…

2026/10/28 13:26:08

基于C#与YOLO的工业视觉检测系统设计与优化

1. 项目概述:工业视觉检测的智能升级方案这个项目实现了一套基于C#上位机与YOLO目标检测算法的实时视觉检测系统。作为工业自动化领域常见的解决方案,它完美结合了传统工控软件的稳定性和深度学习算法的智能识别能力。我在汽车零部件质检项目中实际应用过…

2026/10/28 15:31:52

Java后端跨域问题解决方案与实战配置

1. 跨域问题本质解析第一次接触跨域问题的Java开发者往往会感到困惑:明明前端代码和后端服务都正常运行,为什么浏览器就是报错?这得从浏览器的同源策略说起。同源策略是浏览器最基本的安全机制,它要求协议、域名、端口三者完全一致…

2026/10/28 15:31:52

Node.js实时编程直播工具CC Live开发解析

1. 项目概述:CC Live 是什么?CC Live 是一个基于 Node.js 开发的实时编程直播工具,核心功能是让开发者可以围观他人使用 Claude Code 进行 AI 编程的全过程。这个工具解决了编程学习中的几个痛点:实时性:不同于录屏回放…

2026/10/28 15:31:52

金属3D打印在航空航天制造中的核心技术与应用

1. 金属3D打印如何重塑航空航天制造格局在波音787梦想客机的发动机舱铰链上,传统制造需要30个零件组装,而采用金属3D打印技术后,这个数字直接降到了1。这不是魔法,而是增材制造技术给航空航天工业带来的真实变革。作为从业12年的航…

2026/10/28 15:31:52

ArcGIS字段自动编号实战:从Field Calculator到arcpy生产级工具

简介:ARCGIS编号小工具是一款面向GIS从业者与ArcGIS初/中级用户的实用型桌面插件,专为解决mdb、gdb及shp三类主流地理数据格式的要素顺序编号难题而设计,显著提升空间数据标准化处理效率,适用于地图制图、数据质检、专题图要素标识…

2026/10/28 15:31:52

Agent-Reach:多智能体通信基础设施的设计与排坑实录

最近大半年我一直在捣鼓多智能体系统,demo 阶段大家都很兴奋,等到真往业务上接的时候,整个团队几乎被一个看起来最不起眼的问题卡住了:Agent 之间互相找不到、调不动、调了也没法确认结果是谁产生的。最后我们内部搭了一个叫 Agen…

2026/10/28 15:26:51

CRC校验总失败?统一全段计算避免校验和漏字节

校验和对不上,串口助手上一屏红字,日志里只有一句CRC ERROR,然后就没有然后了。这种场景我遇到过不止一次,最难受的地方不在于错误本身,而在于错在哪儿根本看不出来——数据看着是对的,长度看着是对的&…

2026/10/28 14:42:12

全国计算机二级C语言真题PDF高效刷题指南

简介:这份PDF是全国计算机二级C语言考试的试题与答案解析,专为备考二级C语言的考生准备。内容涵盖C语言特点与设计理念、程序基本组成单位、main函数执行流程、函数嵌套调用规则、标识符命名规范、整型实型字符型数据类型、八进制十六进制数合法形式、运…

2026/10/28 15:28:56

requests+BeautifulSoup:爬虫抓取h1标题的入门实战指南

简介:一份以实战为导向的Python爬虫入门资源,面向具备基本编程基础、希望快速掌握网络数据抓取的初学者,可解决从零编写简单爬虫的常见困惑。实例以requests库发送HTTP请求、BeautifulSoup解析HTML文档,完整演示从网页中提取 标题…

2026/10/28 3:32:29

递推最小二乘参数辨识MATLAB编程:二阶系统与渐消记忆实现

简介:这是一篇面向控制系统、信号处理与机器学习等领域学习者的最小二乘参数辨识程序说明文档。资源共包含一个docx格式文档,压缩包大小七十八KB,内容围绕递推最小二乘算法和渐消记忆最小二乘算法展开:先构造一个极点位于单位圆内…

2026/10/28 0:02:13

Python股票自动交易系统源码解析:从行情获取到风控的完整链路

简介:这份资源是面向高校计算机、金融相关专业毕业设计场景的完整项目源码包,主题为基于Python的股票自动交易系统,适合需要完成毕设选题、课程设计或自学金融量化交易开发的学生与开发者参考。压缩包共收录687个文件,整体约14.93…

2026/10/28 0:02:13

视酷酷信IM源码部署指南:Java 17+MySQL 8.0.33实战配置

简介:这是一套2024年亲测可部署的视酷酷信IM即时通讯系统源码,面向具备Java、OC、C#及Vue开发能力的技术人员,解决私有化即时通讯平台快速搭建与二次定制需求,尤其适用于需集成商城、保障数据主权的中小型企业或独立开发者。资源共…

2026/10/28 0:02:13

Python爬取百度迁徙数据:接口解析、批量采集与避坑指南

简介:这份资源是一个用Python爬取百度迁徙数据的脚本压缩包,面向具备Python基础、希望入门网络爬虫与数据解析的开发者,解决按日期获取全国热门城市人口迁徙数据的问题。包内共1个文件,为单个py脚本,压缩包约1KB&#…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑