硬盘坏掉那天,我重学了四件以为早就会的事
我刚开始工作那年改了一周代码,硬盘坏掉,没备份,也没远程仓库,一周的东西全没了。后来我学会用「最终版.docx」「最终版_改.docx」命名,文件名越来越长,心里的不安一点没少。这篇文章写的是那之后我回头补的四件事:文件到底放在哪、一个网址是怎么变成页面的、终端里在做什么、改坏了怎么退回去。路径、DNS 与三次握手、pwd 与 grep、Git 的三区模型和 stash,都是每天要碰的东西。
In my first job I lost a week of code when my hard drive died — no backup, no remote repo. I moved on to naming files `final.docx`, `final_v2.docx`, `final_actually_final.docx`; the names got longer and my confidence did not. This post covers the four things I went back to learn: where files actually live, how a URL becomes a page, what the terminal is really doing, and how to get back to a working version. Absolute and relative paths, DNS and the TCP three-way handshake, pwd and grep, and Git's three-zone model with stash — the parts you touch every day.
硬盘坏掉那天
我刚工作那年的冬天,改了一周的代码没了。硬盘坏掉,没有备份,也没有远程仓库,开机之后只剩一个不认识我项目的系统。
那次之后我学乖了一点,开始用文件名管理版本:最终版.docx、最终版_改.docx、最终版_真的不改了.docx。名字越来越长,心里的不安一点没少,因为我知道这里面只要搞错一步,就再也没人分得清哪个是真的最终版。
后来我慢慢补了几样东西,才发现当时缺的是四件我自以为早就会了的事:文件到底放在哪、一个网址怎么变成页面、终端里到底在做什么、改坏了怎么退回去。
第一件:我说不清我的文件到底放在哪
我前同事是这么形容零基础用户的:「桌面堆满图标,找文件靠搜索,从来没看过 C 盘里到底有什么。」
这话听起来像在说别人,但我第一次在终端里敲 cd src 报 No such file or directory 的时候,就是那个状态。我知道屏幕上有个 src 文件夹,不知道终端眼里的 src 和我看到的不是同一个东西。
路径就是地址,一个个 / 就是一层层的门牌:
/Users/coya/projects/website/src/index.js
└─根─┘└─用户─┘└──项目──┘└库┘└─文件─┘开头那个单独的 / 是根目录,所有路径都从它开始。这么写出来的叫绝对路径,从根出发,到哪台机器上都指向同一个地方。
相对路径是另一回事,它相对于「我现在在哪」:
pwd # 先看看自己在哪
# /Users/coya/projects/website
./src/index.js # 就在我脚下这一层
../public # 回到上一层,再进 public. 是我脚下,.. 是回头一步。这两个符号看着简单,但它们解释了一个我困惑很久的现象:同一个命令,在不同的目录里跑,结果完全不一样。终端里每条命令都带着「你现在在哪」这个隐藏参数。
现在我看到「文件不存在」这类报错,第一反应是 pwd,先确认自己站在哪。十次里有八次是路径问题,不是文件问题。
另一件我当时没想明白的事:VSCode 打开的是一个文件夹,不是一个文件。File → Open Folder 选中的那个目录,就成了项目里所有相对路径的原点。所以 ./src/index.js 能跑,./其他项目的文件 不能,因为项目指的是这棵树。
也正因为这样,代码里写绝对路径总有代价。我在自己的电脑上写 /Users/coya/projects/website/public/logo.png,本地跑得好好的,换到你机器上就是一行报错。相对路径是从项目结构里算出来的,项目搬到哪都成立。
第二件:我讲不清输入网址到页面出现之间发生了什么
工作第二年,一个老同事问我:「从你在浏览器输入 https://example.com,到页面出现在你眼前,中间发生了什么?」
我答得挺溜:请求发出去,服务器返回 HTML,浏览器渲染出来。我觉得这就是答案。
他说:「那请求是怎么找到那台服务器的?」
我卡住了。
后来我把这条链自己拆了一遍,才发现中间至少有五段,每一段都有它自己的失败方式。
第一段,域名变成 IP。 浏览器先查自己的缓存,再问系统,再问路由器,再问运营商,问到根 DNS、顶级域 DNS,最后才到管着 example.com 的那台权威服务器。这一串是为了拿到一个 IP 地址。
第二段,建立连接。 三次握手:
客户端 → SYN → 服务器 "我想连"
客户端 ← SYN + ACK ← 服务器 "好,我也准备好了"
客户端 → ACK → 服务器 "收到,开始吧"为什么不是两次?如果只握两次,服务器发出「我准备好了」之后就以为通了,但它并不知道自己那句话有没有被客户端收到。第三次存在的意义,是让服务器确认自己发出的能力也被验证过。
第三段,TLS 握手。 因为地址是 https。客户端先说自己支持哪些加密算法,服务器挑一个、把证书发过来,双方换密钥。这一段之后,后面的通信才是加密的。
第四段,HTTP 请求和响应。 这一段大概是最多人(包括当时的我)误以为的「全部」:
GET /index.html HTTP/1.1
Host: example.comHTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8Host 这个头值得单独说一句。一台服务器上可能托管着几十个域名,收到请求时它得知道你要的是哪一个。IP 只能定位到机器,Host 才能定位到站点。
第五段,解析和渲染。 HTML 字节变成 DOM 树,CSS 字节变成 CSSOM 树,两棵树合成渲染树,再算布局、再画。
知道这几段之后,排查问题的方向就变了。看到 ERR_NAME_NOT_RESOLVED,我知道请求根本没出去,问题在第一段,跟服务器一点关系没有。看到 502,我知道请求到了服务器,是它后面的应用没接住。在那之前,我看这些报错全靠猜。
第三件:我以为终端是黑客才用的东西
黑窗口、绿色字、要背一堆命令。很多年里我对终端的态度就是敬而远之,图形界面能做的事何必敲字。
改变我想法的是 Claude Code。我开始好奇它到底怎么「动手」,翻了它的运行日志,看到的全是这个:
● Reading file: lib/content/reader.ts
● Running command: npm test它没有截图,没有模拟点鼠标。它读文件、跑命令,全部通过终端。那一刻我意识到,终端其实是机器替人动手的标准接口。你让 AI 帮你做事的每一次,背后都是一串命令。
终端里有个东西图形界面藏起来了:Shell 始终记得「我现在在哪个目录」。pwd 把它问出来:
pwd # 我在哪
ls -la # 这一层有什么(含隐藏文件)
cd src # 进去
cat package.json # 读出内容这四个动作,构成了 AI Agent 干活前的固定仪式。你在 Claude Code 里说「帮我看看这个项目用了哪些依赖」,它大概会先 pwd 确认位置,再 ls 看结构,再 cat package.json。
pwd 是它最频繁的「自我审视」动作,原因很实在:避免「我以为我在 X 目录,其实我在 Y 目录」这种尴尬。同一个坑我也踩过不止一次。
再往下是我真正每天用的那几个:
grep -rn "TODO" src/ # 哪个文件里提到了 TODO
grep -rn "旧域名" . # 全项目找一处待改的字符串
which node # 这个命令到底是哪个
npm ls react # 装的到底是哪个版本grep -rn 是我用得最多的。要找的东西在哪,我不再一个个文件翻,直接问。
后面三个月的实际使用告诉我,终端不需要背 200 条命令。真正每天用到的不到十个,其余都是「知道有这么回事,需要时再查」。
第四件:最终版_真的不改了.docx
回到开头那件事。
我当时的问题是:改动没有历史,只有一个「现在」。所以任何一次误操作都是不可逆的,任何一次「昨天那版还能跑」都无从对照。
Git 解决的正是这件事。它靠的是一个三区模型:
工作区是你正在写的文件,暂存区是「这次要提交哪些改动」的待办清单,版本库是提交之后的历史。中间多出的暂存区一开始让我很别扭,多敲一步图什么。用久了我才懂它的用处:它让我可以「只提交一半」。改了五个文件,其中三个还没写完,那就只把写完的两个放进暂存区,剩下的留在工作区。
入门要用的命令其实很少:
git init # 让这个目录开始有历史
git status # 现在是什么状态
git diff # 具体改了哪几行
git add src/login.ts # 这个文件的改动算作这次
git commit -m "feat(login): 支持手机号验证码登录"
git log --oneline # 看过往git status 会是我一辈子用得最多的命令之一。它回答的是那个最基础的问题:我现在处在什么状态。
写提交信息有个约定:
<type>(<scope>): <subject>
feat(login): 支持手机号验证码登录
fix: 修复首页 500 错误feat 是新功能,fix 是修错误。看起来是形式主义,但半年后你翻历史找「那次登录改动是哪一笔」,能不能一眼认出来就看它。
还有一个新手最容易踩的坑:.gitignore。
node_modules/ # 依赖,几百 MB,别人 npm install 就能重建
dist/ # 构建产物,每次构建都会变
.DS_Store # 系统文件我见过有人把 node_modules 提交上去,仓库一下涨到几百 MB。判据很简单:这份文件能不能由别的文件重新生成? 能,就不进版本库。反过来,package-lock.json 一定要进,因为它记录的是「当时装的确切版本」,那是无法重建的信息。
再往后走,我遇到两个场景,顺手补了两个工具。
一个是「昨天那版能跑,今天的不行」。切到老版本先验证:
git log --oneline # 找那个提交
git checkout a1b2c3d # 切过去
git checkout main # 验证完切回来另一个是「我正在改 A,突然要切去修紧急的 B」。不想提交半成品,又想保住手上的改动:
git stash # 把当前改动打包存起来
git checkout hotfix
# 修完 B,回来
git checkout main
git stash pop # 拆开,继续这两个动作我在最初两个月里用得比 commit 还多,因为它们救的都是「来不及提交但必须马上离开」的时刻。
后来我做多分支并行开发的时候,还用到过 Git Worktree 配合 Claude Code 的子代理:同一个仓库在不同目录里各自 checkout 一个分支,一个代理改 A 分支,另一个修 B 分支,互不干扰。这是 Git 里比较后面才会碰到的部分,但底层还是那三区模型,只是「工作区」多了几个。
回头看
这四件事排在一起才看出它们的顺序。
路径告诉你东西放在哪。网络告诉你它怎么被送到对方那里。终端是让机器替你动手的接口。Git 是改了之后能退回去的保险。
它们都不高级,学校里也不专门讲。但每一样都是每天要碰的,而且每一样缺了都会让你在某个具体的时刻彻底卡住:站在别人的项目里找不到文件、看报错只能靠猜、想自动化却不知道从哪里下手、以及硬盘坏掉那天。
那批「最终版_改_真的不改了」的文件我早就删了,但我不太后悔走过那一段。试过一次分不清哪个是最新版本,才会明白 git log --oneline 那一行行提交记录到底值多少钱。