ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Caveman风格:零依赖手写HTML,回归原始的极简网页搭建

Caveman风格:零依赖手写HTML,回归原始的极简网页搭建

"Caveman",这个词第一次被程序员圈子当成褒义词,大概是从"Caveman Debugging"这个梗开始的——不用断点、不用调试器、不用任何花哨工具,就在代码里塞几个print(),一遍遍把变量打出来看。听起来很原始,对吧?但我在实际项目里待得越久,越发现这种"穴居人式"的思维方式,恰恰是现在最稀缺的东西。

今天想聊的"caveman",不只是那个调试梗。它代表的是一整套"退回最原始工具链、却依然把事办成"的思路。这篇文章里,我会先从穴居人调试法说起,然后带你把一个零依赖、零框架、零构建步骤的"Caveman"风格网页从头到尾手搓出来,再聊聊部署和性能收益,最后把同一种哲学延伸到日常协作里。适合被复杂工具链折磨得头疼的前端新手,也适合想给自己日常开发做减法、但一直没找到落点的人。

1. "Caveman"不只是一个梗:我为什么决定退回最原始的工具链

先说结论:我手头一个长期维护的个人项目,从一堆框架、依赖、构建脚本,最后被我精简成了一个纯静态目录,整个站点加起来不到 30KB。速度、稳定性、可维护性全部变好了,唯一"变差"的是说出去不够炫。

这个项目一开始不是这样的。它用过主流的单页应用框架,配了路由、组件、状态管理,接了 CI 流水线,光node_modules就有三四百兆。结果呢?每次改个文案都要走一遍构建、走一遍缓存刷新,时不时还要处理依赖升级带来的破坏性变更。最离谱的一次,因为某个传递依赖的小版本更新,整个构建流程挂了半天。

后来我做了个很"caveman"的决定:把这个项目重写成一堆纯 HTML 文件,每个页面公共部分用脚本生成,CSS 全部手写,JavaScript 降到最低限度。做完之后整个维护体验完全变了。

为什么"原始"反而更好?这里有个特别容易被忽视的道理:工具的复杂度是会复利的。每加一层依赖,你就要为这一层持续支付认知成本——它怎么升级、它跟别的库怎么兼容、它有没有安全漏洞、它在新版 Node 上还能不能跑。这些成本平时不显眼,但一出问题就是连锁反应。Caveman 风格不是"不用工具",而是只保留能自己完全理解的工具。如果哪天它出了问题,我打开源码就能看懂,不用去翻三层抽象之外的文档。

这个项目让我重新审视了一个词:原始。原始不是简陋,是"零件足够少,坏了你自己修得了"。就像一台老式机械手表和一个智能手表比功能完全比不上,但它的每一个齿轮你都看得懂,拆得开。

说回主题。这篇文章后面要动手做的这个"Caveman"风格主页,就是这种哲学的直接体现。它不用任何框架、不经过任何构建工具、不用图片字体图标库,一个小编辑器和一台能跑静态服务器的电脑就能开始。

2. Caveman Debugging:Printf 大法为什么到今天依然能打

"穴居人调试法"听起来像在骂人,但它其实是我见过的最稳定的调试策略。所谓 Caveman Debugging,就是在代码关键路径上插入输出语句(比如 Python 的print、JavaScript 的console.log、C 的printf),把中间变量值、函数调用顺序、分支走向全部打到终端里,靠人眼看逻辑在哪一步断了。

我第一次深刻理解这个方法,是几年前排查一个诡异的内存相关问题。那个平台上的调试器支持断点、支持表达式求值,理论上一应俱全,但我加断点之后发现:只要暂停在某个线程上,程序的时序就会被改变,问题就不复现了。一去掉断点,问题又出现。折腾了一整天,最后是同事默默在旁边加了几行日志,把缓存命中、状态切换顺序打出来,跑了三分钟就定位到了问题。

从那以后我形成了一个习惯:遇到诡异问题,先往关键路径上写输出语句,而不是急着开调试器。

这个方法论有个很实在的优势:它在任何环境里都能用。嵌入式设备上远程调试器可能连不上,生产环境你不能随便挂断点(断了进程就死了),微服务之间的问题你总不能跑到对方节点上去断点。但日志不一样,日志是零侵入的——它只在当前进程打几行字,不断线程、不改时序,大多数情况下问题该复现还是复现,只是你多了几双看流程的"眼睛"。

当然,Caveman Debugging 不是万能药。如果业务逻辑极其复杂、状态非常多,纯靠打印变量会非常低效,这时候断点调试、录制重放这些高级手段还是更合适。我的实用标准是这样的:

场景推荐方式理由
确定在哪段代码、但想知道它经过了哪些分支输出语句直接看流程,成本最低
问题只在特定数据、特定时序下触发输出语句不改变时序,容易复现
生产环境、不可随意重启服务结构化日志只能在现有日志框架下加信息
无从下手、不知道问题跟哪段代码有关断点调试可以逐步探路
需要查看调用栈和局部变量断点调试输出语句做不到

有一点需要提醒你:用输出语句调试,千万别随手把日志留在生产代码里。我见过有人排查完问题,明明删了临时日志,但没重启服务,结果生产环境在关键路径上打了一整天文本日志,把磁盘塞满了。所以我的做法是:临时调试日志里必须带上明显前缀,比如DEBUG_TMP >>>,排查结束全局搜这个前缀,确认全部清干净再提交。

3. 手搓一个纯 HTML 的"Caveman"风格主页:零框架、零构建、零依赖

下面进入正题。我们要做的这个"穴居人风格"主页,不是说故意把页面做得像 1991 年那样只有黑底白字(虽然不是不行),而是说开发方式回到最原始的状态:一个文件就是一个页面,没有打包、没有编译、没有路由,浏览器打开什么你就看到什么。

3.1 先定规矩:这种开发风格的约束条件

既然要走 Caveman 路线,那就给自己立几条军规,后面所有代码都在这几条规矩下写:

  1. 不引入任何前端框架。你要会原生 HTML、CSS、JavaScript 的基础知识就够。
  2. 不引入任何第三方 UI 库、图标库、CSS 框架。字体只用系统字体栈,图标能找到 Unicode 字符就用字符,找不到就不放。
  3. 不配置构建工具。本地不需要 Node 环境跑开发服务器,不需要 webpack/vite 做热更新,文件改完刷新浏览器就能看到效果。
  4. 网络请求越少越好,理想情况是一个页面只发一次 HTML 请求,CSS 可以内嵌也可以外链,但不外链任何公共 CDN 资源。因为一旦某个 CDN 挂了,你的页面也残了。

这个约束的核心思想是:每个字节你都认识,每个文件你都知道它是干嘛的。

3.2 页面骨架:只用一个文件就能跑起来

我先把完整的 HTML 文件贴出来,这个文件没有任何外部依赖:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>刀耕火种小站</title> <style> /* 这里写 CSS,后面会展开 */ </style> </head> <body> <header> <nav> <a href="#about">关于</a> <a href="#projects">项目</a> <a href="#reading">阅读</a> <a href="#contact">联系</a> </nav> </header> <main> <section id="about"> <h1>Caveman Style Personal Site</h1> <p>这是一个刻意做旧的个人主页。没有框架打包,没有 CDN 依赖,没有花哨动画。</p> </section> <section id="projects"> <h2>做过的项目</h2> <ul> <li>极简书签管理脚本</li> <li>纯 CSS 图标集</li> <li>离线版 SQLite 速查手册</li> </ul> </section> <section id="reading"> <h2>最近在读</h2> <p>这类区块完全靠手写 HTML 维护,增加的条目也只是复制一行 <code>&lt;li&gt;</code> 而已。</p> </section> <section id="contact"> <h2>怎么联系我</h2> <p>邮件是传统但可靠的方式。在社交平台上找人也行,但回得慢别怪。</p> </section> </main> <footer> <p>手工构建,更新于 2026 年。</p> </footer> <script> // 这里写极少量 JavaScript,后面解释 </script> </body> </html>

注意这个文件没有引入任何外部资源,CSS 直接写在<style>标签里,JavaScript 直接写在<script>标签里。

你可能觉得"这我也会写"。对,这就是重点。等页面多了,我并不需要每次都手写全部骨架,而是做一个小的生成脚本。但生成脚本是我的脚手架,不是线上依赖——最终部署给用户的,就是这一堆平铺直叙的 HTML 文件。这样即使生成脚本某天坏了,我用手工也能修页面。

3.3 样式部分:没有 CSS 框架的排版怎么做

接下来写 CSS。没有框架的情况下,我推荐你先把排版调好再谈美化。所谓排版,就是留白、字体、行高、间距这些"主心骨"。

我的做法是先定义一个最小的设计变量,但这次写在这个单文件里,所以要限定在 CSS 自定义属性里:

:root { --bg: #f4f0e8; --fg: #2b2b2b; --accent: #8c4a2f; --line: #cfc8bc; --font: "Helvetica Neue", Arial, "PingFang SC", "Microsoft YaHei", sans-serif; --mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace; --max-width: 680px; } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: var(--font); background: var(--bg); color: var(--fg); line-height: 1.75; padding: 2rem 1rem; } main { max-width: var(--max-width); margin: 0 auto; } header { max-width: var(--max-width); margin: 0 auto 3rem auto; } nav { display: flex; gap: 1.5rem; flex-wrap: wrap; border-bottom: 1px solid var(--line); padding-bottom: 0.8rem; } nav a { color: var(--fg); text-decoration: none; font-size: 0.95rem; } nav a:hover { color: var(--accent); text-decoration: underline; } section { margin-bottom: 3.5rem; } h1 { font-size: 2rem; margin-bottom: 0.8rem; letter-spacing: -0.01em; } h2 { font-size: 1.3rem; margin-bottom: 1rem; padding-left: 0.6rem; border-left: 3px solid var(--accent); } p { margin-bottom: 1rem; } ul { margin: 1.2rem 0; padding-left: 1.5rem; } li { margin-bottom: 0.4rem; } code { font-family: var(--mono); font-size: 0.9em; background: #e8e2d8; padding: 0.15em 0.4em; border-radius: 3px; } footer { max-width: var(--max-width); margin: 4rem auto 0; padding-top: 1.5rem; border-top: 1px solid var(--line); font-size: 0.85rem; color: #6a655e; }

有几个地方值得展开说说为什么这么写:

第一,max-width: 680px。这是从纯文本阅读体验里学来的。一行文字太长,眼睛会累;太短,视线来回跳也很累。680 到 720px 是我试下来最舒服的区间。没有用栅格系统、没有用分栏,整个页面就是单列上下滚动——这种方式在移动端和桌面端的适配都特别省心。

第二,系统字体栈。"PingFang SC", "Microsoft YaHei"是给中文用户准备的,优先用苹果和微软系统中文字体;sans-serif兜底。这套字体栈不用从服务器下载任何字体文件,每个用户看到的都是自己设备上渲染最好、最熟悉的字体,加载速度天然快。

第三,用边框代替装饰图。h2左侧那条 3 像素的装饰线,是自己给自己划分视觉层次的笨办法。我特意把它做成血橙色,因为 Caveman 风格虽然"原始",但不是"无聊"。你完全可以根据自己的喜好改这个强调色。

如果你想在这个阶段做点更有个性的效果,我建议玩一个"复古浏览器"技巧:给 HTML 加一个 CRT 屏幕的纹理遮罩,用 CSS 背景渐变模拟扫描线。但我要泼一盆冷水:这种效果在移动端和高刷新率屏幕上容易让人觉得头晕,第一次做玩玩可以,长期用真不建议。

3.4 一点克制的 JavaScript:只在真需要交互的时候动

刚才的页面里我留了一个空的<script>标签,现在来聊 JavaScript 该不该加。很多做过项目的人有个习惯:先引入一堆工具函数或交互库,再想这些交互是不是真的需要。Caveman 风格的思路正好反过来:先想想哪个页面没有 JavaScript 还能不能用,再用最原始的 DOM 操作补上它必需的部分。

如果这个页面几乎不需要交互,我的选择是:不要 JavaScript。静态 HTML 文件又不是不能活。但如果页面有"页头导航在移动端太长、要一个展开收起按钮"这种需求,那就用原生 JS 写个十几行的折叠效果:

<nav id="site-nav"> <button id="nav-toggle" aria-expanded="false" aria-controls="nav-menu">菜单</button> <ul id="nav-menu" hidden> <li><a href="#about">关于</a></li> <li><a href="#projects">项目</a></li> <li><a href="#reading">阅读</a></li> <li><a href="#contact">联系</a></li> </ul> </nav> <script> const toggle = document.getElementById('nav-toggle'); const menu = document.getElementById('nav-menu'); toggle.addEventListener('click', () => { const expanded = toggle.getAttribute('aria-expanded') === 'true'; toggle.setAttribute('aria-expanded', String(!expanded)); menu.hidden = expanded; // expanded 为 true 时隐藏 }); </script>

这段代码没有任何依赖,运行效率也够,并且保持了无障碍的基本规范——按钮上的aria-expanded属性可以让屏幕阅读器正确读出菜单状态。这么多就够用了,不要再加滑动动画、折叠动画、页面切换动画。如果你实在想要平滑过渡,加一行 CSS 的transition: opacity 0.15s ease,意思到位就行。

我把 JavaScript 的使用原则总结成三条:

  1. 能不用就不用。这个页面是内容展示型,用户来看的是文字和链接,不是交互。
  2. 用也只用到getElementById/addEventListener这种原生方法,不引第三方封装库。
  3. 所有 JavaScript 必须在页面拿到内容后渐进增强,如果脚本加载失败或者被浏览器禁用,页面本身依然是完整可读的。这其实用的是"渐进增强"思路,我在折腾好几个项目后才真正体会到它的价值。

3.5 在不同设备和浏览器上的表现

写完这个页面,我通常会在本地起一个静态服务器快速看一眼效果。不需要构建服务器,只需要把工作目录作为静态资源根目录,一条命令就行:

python3 -m http.server 8080

然后浏览器打开http://localhost:8080。在 Python 没有装或者用的是 Windows 环境的情况下,可以用npx serve,不过那个会拉一点依赖——Caveman 风格要求比较高的话,建议直接装一个 Python。

我实测下来,这个页面在以下环境的表现基本一致:

  • 新版 Chrome、Edge、Firefox、Safari 桌面版都不需要加前缀就能正常渲染。
  • 手机上的 Chrome 和 Safari 也能自适应,因为设置了viewport,单列布局天然适配小屏幕。
  • 我甚至把它丢到一个老掉牙的 Android WebView 里测试过,依然能正常打开渲染,因为用到的都是十几年甚至二十年前就存在的 HTML/CSS 特性,谈不上兼容性适配。

这种兼容性不是巧合,而是我选择的特性都足够"老":flex布局、::before伪元素、CSS 变量(这个稍微新一点,但它已经普及近十年了)、hidden属性、原生 DOM 事件。这些技术经受住了时间的检验,也扛得住"尽量少的 bug"这个朴素要求。

4. 部署、缓存与性能:极简页面能带来多实在的收益

很多人小看了纯静态站点的收益,觉得"不就几个 HTML 嘛"。等你真的布上去做对比测试,才知道省了多少事。

4.1 部署到哪:静态托管是最省心的路

纯静态页面的部署选择非常多,这里我只说两种我实际用过的:

**方式一:对象存储托管。**把整个目录拖进云端对象存储,开启"静态网站托管"功能,一个桶就是一个站点。这种方式的优点是自带 CDN 和 HTTPS,缺点是控制台操作有点繁琐,不过设置一次之后,以后每次更新就是"上传覆盖",几十秒搞定。

**方式二:一台小内存服务器跑 Nginx。**如果你手头已经有一台虚拟机,那就更简单了,把 HTML 目录放到指定 web 根目录,在 Nginx 里加一个 server 块:

server { listen 80; server_name caveman.example; root /var/www/caveman; index index.html; location / { try_files $uri $uri/ =404; } }

这个配置不到十行就完成了站点服务。反观我以前用单页应用框架时的 Nginx 配置,又要处理历史路由回退、又要配缓存策略、还得注意静态资源 hash 文件的位置,复杂度根本不在一个量级。

4.2 缓存策略:没有构建也能精准过期

静态站点最让我满意的是缓存控制非常简单。HTML 文件不希望被缓存太久,因为内容更新靠它;CSS 和图片(如果以后有额外资源)就放在另一个路径下,用长缓存。

在没有构建工具的前提下,我通常用 Nginx 这么配:

location ~* \.(css|js|png|jpg|woff2)$ { expires 30d; add_header Cache-Control "public, max-age=2592000"; } location / { expires 1h; add_header Cache-Control "public, max-age=3600"; }

没有文件名 hash 也没关系——因为 CSS 文件里内容更新时,我只需手动改下文件名(从style.css改成style-v2.css),然后更新 HTML 里的引用。虽然这个操作听起来原始,但它绝对有效,而且完全可预测,这就是"控制感"的来源。

4.3 实际性能数据:小占有、秒开不是玄学

我拿一个真实站点做了一次对比测试:改造之前,那个站点的首页要加载五十几个请求,总共 2.3MB 大小,首屏完全渲染需要约 2.8 秒;改造为 Caveman 风格之后,首页只有一个 HTML 文件带内嵌样式和脚本,请求数为 1,大小约 28KB,性能测试工具的加载时间稳定在 0.2 秒以内。

每次看到这个对比,我都觉得大部分页面压根不需要那些资源。它们只是被"业界标配"卷进来的——图标库、UI 库、动画库、前端框架、状态管理,层层叠加,最后真正为用户带来的价值可能和三十年前用表格布局做出来的页面没有质变。

我不是说你所有项目都得退回原始时代。需要复杂交互的产品,框架是合理的。但作为一个内容型、文档型、个人工具型的站点,Caveman 风格的收益非常具体:请求少、体积小、加载快、部署简单、没有框架升级的连锁爆炸,不用担心某个依赖库被黑客投毒导致上游供应链问题,因为你根本不从上游拉任何东西。

5. 从网页到工作方式:把"Caveman"哲学用到日常项目里

这种退回原始工具链的思路,一旦尝到甜头,你会忍不住把它用到别的地方。我个人实践下来最有价值的,是把 Caveman 哲学移植到了项目管理、文档维护和团队协作三个场景。

5.1 项目管理:用纯文本清单代替复杂看板

有一次我需要管理一个只有三个人参与的小项目。按理说上个在线协作工具很正常,但我发现那段时间维护工具比维护项目本身还累——按钮不知道被谁移动了,自动化规则要写各种公式。后来我一怒之下建了一个纯文本文件,叫todo.md:

# 项目清单(2026-03 更新) ## 待办 - [ ] 首页文案第二版 - [ ] 接口超时重试机制 ## 进行中 - [ ] 用户反馈收集页 ## 已完成 - [x] 域名迁移 - [x] 基础缓存配置

就靠这个文件加一个 Git 仓库,三个人都能维护,改起来还特别顺手。Markdown 本身就是一种"原始"格式,没有服务端、没有权限控制、没有通知风暴,但它的透明度和可控性,对一个微型团队来说已经完全够用。

5.2 文档维护:手动更新比自动生成更可靠

很多项目文档都上了一些自动化文档生成框架,输入代码注释,自动输出在线文档。听起来很先进,但实际用下来常常是:注释的格式改一次,文档的目录结构就得跟着变;某条接口改了参数,生成脚本没跑,文档和代码就悄悄不一致了。

我后来在一部分项目里回到"手写文档"的路子:每个模块一个 Markdown 文件,版本是 Git 历史,格式是最简单的标题加表格。不扯自动化,反而减少了"生成流程出错"这个新的错误源,也更符合 Caveman 哲学里"只有完全理解每层才会用"的原则。

5.3 团队协作:先开小会,再写文档

有句老话叫"能用一句话说清的事,不需要一段文档;能用一段文档说清的事,不需要一场会"。在一个追求极简的团队里,沟通方式也应该这样排序:

  1. 当面或者语音说一句,解决百分之六十的误会。
  2. 在聊天工具里发一段文字,留个可搜索的记录。
  3. 写进共享文档,作为正式记录和长期参考。
  4. 最后才开会,而且开会时必须有结论、有下一步动作。

这个顺序看起来简单,但执行起来非常难,因为人天然倾向于把事情复杂化。Caveman 哲学在这里的作用是逼你问一句:这个动作,真的必须这么重吗?

5.4 这种哲学的代价:我主动放弃的那些东西

讲完甜头,也必须讲代价。Caveman 风格不是免费的,它要求你自己搞定那些框架替你搞定的事。

  • 没有脚手架,新建一个页面的重复劳动变多,你需要靠自己的脚本或者模板来减轻。
  • 没有状态管理,页面一多,跨页面的状态同步全都得自己设计,一般就是 URL 参数或者本地存储,灵活度有限。
  • 没有组件库,一个日期选择器都要从零写,写错交互的例子比比皆是。

所以我现在有一个判断标准:**复杂度该放在哪里,不取决于技术潮流,而取决于项目的本质。**如果你的核心复杂度来自业务逻辑,那使用框架帮你管理 UI 状态是合理的;如果你的核心复杂度只是展示一些内容,那么框架带来的额外复杂度就是你纯纯的负担,你只是"为了用而用"在付账。

我还发现,在"Caveman 风格"里待久了的人,会慢慢获得一种选型直觉:遇到一个新的"效率神器"时,先不去想它能帮我做多少,而是去想它坏了谁来修、我能不能读懂它、它引入了多少看不见的依赖。这个直觉帮我躲开了很多不必要的坑,也让我写代码时更有底气。

最后再分享一个小技巧:不管你是不是真的打算长期使用"Caveman"风格,我都建议你偶尔用一个纯文本编辑器、不借助任何 AI 自动补全地去完成一个小页面,或者抛开脚手架去写一个原型。这个练习的目的不是回到过去,而是帮你重新校准那些被你当成理所当然的"便利"到底是真正需要,还是仅仅因为大家都在用。等做完这件事,你回过头再看自己手头那些"非用不可"的工具,会发现至少有一半其实可以扔进岩洞里。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进