“caveman”这个词儿在开发者圈子里其实有两种完全不同的味道。一种是贬义的形容代码写得太原始、不懂用工具另一种则是这几年慢慢流行起来的一种反叛姿态——主动选择“原始”抛弃花花绿绿的现代框架回到最朴素的方式去解决问题。我这篇要聊的就是后者。我一个人维护三四个小项目慢慢发现一件事很多时候让我们寸步难行的根本不是业务复杂度而是那些为了“省事”引进来的现代化工具链本身。依赖冲突、版本升级、构建报错、抽象泄漏……层层叠叠的复杂性像滚雪球一样。于是我开始尝试一种很土很野的做法尽量做一个“caveman”——只穿兽皮、只会钻木取火、只用最基础的工具但照样把活干完。这个过程有意思得很踩的坑也不少今天就完整复盘一下。1. 为什么突然想不开是谁逼我当原始人的先说结论不是我要回归原始是现代工具链先动的手。1.1 一次让我彻底破防的依赖地狱年初接了一个内部工具的小需求逻辑本身两百行能写完。结果npm install之后node_modules占了800多MB光构建就跑了三分钟期间还冒出来两个版本冲突警告、一个deprecated提示。我当时的感受就四个字实属离谱。翻出package-lock.json数了一下顶层依赖一共6个包但传递依赖有1400个包。我写业务逻辑结果三分之二的时间花在跟webpack、babel loader、polyfill配置搏斗上。这合理吗我到底是在写功能还是在给工具链打工1.2 从“用框架”到“写框架”的顿悟后来我在做一个极简网页的时候试着没引任何框架纯HTML加原生JavaScript一个外部依赖都没有。写完之后发现体感竟然出奇地好启动零等待、调试零转换、部署零构建。那一刻我开始认真琢磨一个问题如果我把这种“什么现成工具都不碰”的狠劲推到极致用最原始的方式去实现一个完整的东西到底能干到什么程度会不会遇到一堵墙带着这个好奇心我正式立项了一个自己的“caveman project”。1.3 给这个项目的定义我给这个项目定的规矩很简单也很苛刻不引入任何第三方运行时库标准库除外不引入任何构建打包工具直接用原生模块不使用任何前端框架纯HTML/CSS/原生JS不依赖自动化测试框架用最土的手写断言脚本不用配置框架能用环境变量解决的不写配置文件解析器这使得项目的骨架非常干净整齐。整个项目的目录结构caveman/ ├── public/ # 静态文件一个文件夹里躺着所有东西 │ ├── index.html │ ├── style.css │ └── app.js ├── server.js # 一个文件打天下既是服务端又是API层 ├── db.js # 基于JSON文件的“数据库”零依赖 └── test.js # 手写冒烟测试一眼看到底没有任何花活。接下来我详细拆一下这套“原始组合拳”到底是怎么打出来的。2. 原始工具链的选型逻辑越土的东西越不会骗你用了这么多年现代框架突然回到“石器时代”最核心的转变不是技术栈换了而是思考逻辑完全变了从“我需要什么工具”转变成“如果我连工具都得自己造这个需求还成立吗”。2.1 数据库的返祖JSON文件当存储项目里有一块需要持久化的数据量不大几百条记录但是要求结构灵活。正常思路是引个SQLite或者MongoDB。但我想了想这个体量的数据存文件就好——直接存JSON。// db.js - 用同步读写实现最原始的数据库 const fs require(fs); const path require(path); const DB_FILE path.join(__dirname, data.json); function readDB() { try { return JSON.parse(fs.readFileSync(DB_FILE, utf-8)); } catch (e) { return { tasks: [] }; } } function writeDB(data) { fs.writeFileSync(DB_FILE, JSON.stringify(data, null, 2), utf-8); } function addTask(task) { const db readDB(); db.tasks.push({ id: Date.now(), ...task, createdAt: new Date().toISOString() }); writeDB(db); } function getTasks() { return readDB().tasks; } module.exports { addTask, getTasks };这个设计听起来简陋但它把所有“数据库该干的事”都暴露在明面上没有任何隐藏的锁机制、缓存层、查询优化器。数据量一旦真的上到十万条这个方案会崩会非常惨。但作为一个小工具它够用了而且重启进程数据还在。2.2 为什么同步读写在这里是对的很多人一听到fs.writeFileSync就皱眉头觉得阻塞、不优雅。但在这类极低并发、一个用户自己用的小工具里同步读写反而是最简单可靠的方案。它天然避免了异步条件下常见的写冲突和竞态问题根本不需要引入事务机制。这就是caveman思路的核心不是选最优雅的而是选最匹配场景的。写代码的人心里要有杆秤——知道哪里该讲究、哪里不该讲究。2.3 HTTP服务器Node自带模块一把梭服务端用的是Node内置的http模块不碰Express、不碰Koa。// server.js - 原生http模块实现路由 const http require(http); const fs require(fs); const path require(path); const db require(./db); const MIME_TYPES { .html: text/html, .css: text/css, .js: text/javascript, .json: application/json }; const server http.createServer((req, res) { const url req.url.split(?)[0]; if (url /api/tasks req.method GET) { res.writeHead(200, { Content-Type: application/json }); res.end(JSON.stringify(db.getTasks())); return; } if (url /api/tasks req.method POST) { let body ; req.on(data, chunk { body chunk; }); req.on(end, () { const task JSON.parse(body); db.addTask(task); res.writeHead(201, { Content-Type: application/json }); res.end(JSON.stringify({ ok: true })); }); return; } // 静态文件处理 const filePath path.join(__dirname, public, url / ? index.html : url); const ext path.extname(filePath); fs.readFile(filePath, (err, content) { if (err) { res.writeHead(404); res.end(Not Found); return; } res.writeHead(200, { Content-Type: MIME_TYPES[ext] || text/plain }); res.end(content); }); }); server.listen(3000, () { console.log(caveman server running at http://localhost:3000); });没上Express的原因很简单我的路由一共四个两个API加一个静态文件。用框架带来的收益几乎为零而框架本身的抽象反而让“理解代码到底在干什么”变难了。2.4 前端回归HTML的原始愉悦感前端就更直接了。一份HTML、一份CSS、一份原生JS没有SPA路由、没有状态管理库、没有虚拟DOM。!DOCTYPE html html langzh head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleCaveman Task Manager/title link relstylesheet hrefstyle.css /head body main classcontainer h1Caveman Tasks/h1 form idtaskForm input typetext idtaskInput placeholder写点啥原始人 required button typesubmit加进去/button /form ul idtaskList/ul /main script srcapp.js/script /body /htmlapp.js里的核心逻辑也就二十几行const form document.getElementById(taskForm); const list document.getElementById(taskList); async function loadTasks() { const res await fetch(/api/tasks); const tasks await res.json(); list.innerHTML tasks.map(t li${t.title}/li).join(); } form.addEventListener(submit, async (e) { e.preventDefault(); const title document.getElementById(taskInput).value; await fetch(/api/tasks, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ title }) }); form.reset(); loadTasks(); }); loadTasks();没有脚手架没有组件化代码从头到尾一撸到底。初次体验下来最大的感受是四个字心里踏实。每个变量从哪来到哪去一眼看穿。3. Caveman项目的核心摩擦点没有框架庇护之后的破事如果光说“原始真好”那是不负责任的。真正用兽皮和石头过日子第一周的新鲜感过了之后各种问题就浮出水面了。3.1 原生JS的禁区你以为你没依赖其实你创造了依赖不引第三方库之后我发现自己开始在项目里手写各种工具函数。比如日期格式化、防抖函数、深拷贝……写着写着我猛然意识到这不就是在重新发明轮子吗这些函数虽然没有出现在package.json里但它们本质上就是我自己的私房框架。我同样要维护它、测试它、修补它的边界情况。这跟用第三方库的差别在哪我的判断标准变成了这样考虑维度引库手写代码量少但不可见多但全部可控出bug概率低社区验证过高自己造的轮子不圆维护成本看上游脸色完全自己做主理解成本需要读文档不用读文档后来我的结论是只手写“和业务强相关、而且只有我自己才懂为什么这么写”的逻辑。像深拷贝这种通用问题、边界条件多如牛毛的基础设施老老实实用工具库别硬扛。Caveman不等于蠢更不等于跟标准库以外的所有代码势不两立。3.2 CSS的“伪依赖”样式崩了没人背锅前端没框架了CSS布局只能自己调。以前Flexbox搞不定的地方我打开Bootstrap文档抄一个类名就行。现在不行了所有像素都得自己抠。更扎心的是我发现自己对CSS标准并不熟。平时被框架们保护得太好很多属性只在框架源码里见过。一旦没有框架罩着遇到一个奇怪的垂直居中问题得从头去翻MDN文档。后来走了这么一条路把常用的CSS布局模式自己沉淀成了一套“符合直觉”的手写工具类但再也不引第三方库了自己维护的那套东西放在style.css顶部做就做到彻底。3.3 没有类型系统的夜晚留下了悔恨的泪水这个项目最大的痛苦点不是别的是JavaScript本身的动态类型。写小demo的时候很爽但功能多了之后就不行了。有一次重构API把一个字段的命名从taskName改成title全局搜了一遍漏掉了两处字符串拼接的地方。跑起来不报错就是前端列表上显示undefined。排查了半天才找到。这次教训让我做了一个违背caveman准则的决定在server.js和app.js里加JSDoc注释配合IDE的类型推导。它不是TS但至少能在我悬停变量时给我一点点慰藉能提醒我“注意这个函数可能返回undefined”。这大概就是caveman的弹性原则要有但不是用来殉道的。4. 从零开始的生命周期管理原始人怎么确保代码还活着现代工程体系里有个重要的角色叫“测试”。但作为caveman我连测试框架都不想引。那我为什么还要测试怎么测4.1 手写冒烟测试从钻木取火到保留火种我写了一个test.js用最土的方式把核心逻辑过一遍。不用Jest不用Mocha甚至不用assert模块。// test.js - 手工冒烟测试不合格就退出码非0 const db require(./db); const fs require(fs); const path require(path); // 重置测试数据 const TEST_DB path.join(__dirname, test-data.json); fs.writeFileSync(TEST_DB, JSON.stringify({ tasks: [] }), utf-8); // 手动覆盖DB路径 process.env.DB_FILE TEST_DB; const tests []; function test(name, fn) { try { fn(); console.log(PASS: ${name}); } catch (e) { console.error(FAIL: ${name}); console.error(e.message); process.exitCode 1; } } test(添加任务后列表长度1, () { const before db.getTasks().length; db.addTask({ title: 测试任务 }); const after db.getTasks().length; if (after ! before 1) throw new Error(期望${before1}实际${after}); }); test(任务包含必要字段, () { const tasks db.getTasks(); const last tasks[tasks.length - 1]; if (!last.id || !last.title || !last.createdAt) { throw new Error(任务缺少必要字段); } }); console.log(冒烟测试跑完了退出码, process.exitCode || 0);然后直接在package.json里加一行{ scripts: { test: node test.js } }就这两个脚本覆盖了这个项目80%的“核心风险”。为什么够用因为caveman项目的优势就是代码量少、模块边界清晰手写几个测试就能覆盖绝大多数关键路径。上了一堆测试框架反而给这么小的项目增加依赖负担。注意这个方案的前提是“你确实只有一个核心模块、一条主流程”。如果你的项目有四个模块、互相调用那还是老老实实用正经测试框架吧别把冒烟测试当万能药。4.2 部署的返祖rsync一梭子现代部署方案是容器化、CI/CD流水线、Kubernetes集群。但对我来说这个项目部署只需要一条命令rsync -avz --delete ./public/ usermy-server:/var/www/caveman/再加一条重启服务的命令ssh usermy-server pm2 restart caveman-server没了。没有镜像构建、没有编排文件、没有私有仓库。为什么这么干因为项目的全部产品就是一个静态服务加一个轻量API进程把它塞进容器、走一遍镜像流水线的收益不足以覆盖成本。尤其是在带宽和部署频率都低的情况下一个rsync干完的事儿为什么非要弄出七八个配置文件4.3 配置管理的返祖环境变量裸奔配置文件多的项目我记得早就用过什么etcd、什么Apollo配置中心。但caveman项目的配置就该用环境变量直接裸奔PORT3000 DB_FILE./data.json node server.js没有任何配置文件解析、没有分层配置、没有热更新。原因很简单变量一共两个用配置文件反而在引入“解析”“格式校验”“覆盖逻辑”这些额外复杂度。5. 怎样的项目适合caveman式开发边界条件一手摸清前面说了这么多caveman的好处但咱们得承认原始人在刮骨疗伤现代人也在用抗生素。这个方法论有它天然的适用边界用错地方就是灾难。5.1 试金石三个问题问自己考虑一个项目能不能用caveman方式我建议先问三个问题核心业务逻辑有多复杂如果超过了“两个模块、三条数据流”那手写基础设施的成本就开始高于框架成本了。你会不会长期维护如果项目只需要跑一个月那怎么糙怎么来。如果打算跑三年我得考虑未来接手的人能不能理解我这套石器时代的规矩。业务变化有多频繁变化越快越需要框架提供的可维护性变化慢的稳定小工具反而适合极简方案。5.2 最适合caveman的项目类型根据我这次的实践经验下面这几类项目是caveman方案的主场内部工具、原型验证、一次性脚本个人博客、极简内容展示站数据量小、并发低、无复杂查询的业务学习项目——用它来弄懂底层原理这些项目有一个共同特征可丢弃性高。就算代码写得丑、架构不优雅出问题了大不了重写一遍成本极低。这种项目引入重型工程体系反而是负资产。5.3 千万别caveman的几种情况当然踩过坑才清楚禁忌在哪里。首先涉及多人协作、要长期演进的中大型业务千万别用caveman。团队需要的不是你的个人英雄主义而是约定俗成的工程规范。这时候硬要追求极简会让所有同仁活在你的“独创迷宫”里。其次安全敏感型项目。你敢在一个存用户登录态的项目里自己手写加密算法那就是给自己挖坑。这种领域老老实实用成熟方案别拿caveman当挡箭牌。第三你要用到生态里特别强势的功能比如前端的三维渲染、复杂的图表展示去引库就好。真的别用原始人的石头去画油画。6. 写在最后原始与文明说到底是一种平衡这次caveman项目的体验给的收获倒是意外地丰厚。它让我重新审视了每一天都在用的工具——框架省下的那部分时间我到底用在了哪儿是被更多抽象层吃掉了还是真的省下来写了业务后来我把这次经验的结论沉淀成了四个字适度返祖。该野的时候野该文明的时候文明。更重要的是这次返祖经历把我从“只会用框架堆代码”的状态里拉了出来很多以前觉得理所当然的工程设施我总算搞明白了它到底在解决什么问题又是怎么解决的。如果你也在被自己手头工具链搞得疲惫不堪我真心建议你也做一次“caveman实验”——不用多一个周末的小项目就够。挑个以前想做的点子把一切能砍的依赖全砍了用最原始的方式把它造出来。你会发现两个结果里的至少一个要么工具链确实该死要么原来框架一直在为你负重前行。顺带一提如果你也想复刻我这个caveman任务管理器那份完整代码我整理好了。核心文件不超过五个总共加起来不到两百行。动手改一改往里面加你自己的业务你会发现写代码这回事有时候越土越自在。