Files
knowledge-base/4 - Resources/Claude-Code/Claude Code 新项目实操教程 (贯穿示例).md

14 KiB
Raw Blame History

created, type, tags, source
created type tags source
2026-06-27 resource
claude-code
tutorial
hands-on
new-project
walkthrough
example
Anthropic 官方最佳实践 + 实操演练 · 2026-06-27

Claude Code 新项目实操教程 (贯穿示例)

配套理论篇:Claude Code 新项目开发最佳实践 (2026 调研)。 本篇是手把手教程:用一个真实小项目「短链接 API(url-shortener)」从空文件夹 → 第一个 PR 合并, 每一步都给出真实命令、完整文件内容、与 Claude 的真实对话。技术栈用 Node + TypeScript + Vitest(换成你的栈思路一样)。


目标产物

一个能 POST /shorten {url} 返回短码、GET /:code 302 跳转的 HTTP 服务。我们只做第一个垂直切片:POST /shorten,走完整 TDD + 四阶段闭环。

整个过程的目录最终长这样:

url-shortener/
├── .claude/
│   ├── settings.json          # hooks:改完自动跑测试
│   ├── rules/
│   │   └── typescript.md      # path-scoped 规则(只在改 .ts 时加载)
│   └── agents/
│       └── code-reviewer.md   # 自定义子代理(也可用内置)
├── CLAUDE.md                  # 项目记忆:命令 + 约定
├── src/
│   ├── shorten.ts
│   └── server.ts
├── test/
│   └── shorten.test.ts
├── package.json
└── tsconfig.json

步骤 0 — 先建 git,再开 Claude

git 是回滚安全网。先 init 再让 Claude 动手,这样任何一步都能 git checkout . 退回。

mkdir url-shortener && cd url-shortener
git init
npm init -y
npm i -D typescript vitest @types/node tsx
npx tsc --init
git add -A && git commit -m "chore: 初始化空项目"

然后在该目录启动 Claude Code:

claude

步骤 1 — 用 /init 生成起始 CLAUDE.md,再亲手补全

在 Claude 里输入:

/init

它会扫描目录、检测到 TypeScript + Vitest,生成一份起始 CLAUDE.md起始版通常太泛,你要立刻补上「怎么跑测试 / lint / build」——这是验证循环能跑起来的前提。手动把 CLAUDE.md 改成:

# CLAUDE.md

This file provides guidance to Claude Code when working in this repository.

## 命令
- 安装依赖:`npm install`
- 跑全部测试:`npm test`            # 即 vitest run
- 跑单个测试文件:`npx vitest run test/shorten.test.ts`
- 跑单个用例:`npx vitest run -t "rejects an invalid url"`
- 类型检查:`npx tsc --noEmit`
- 启动服务:`npx tsx src/server.ts`

## 约定
- 纯函数放 src/,每个函数配同名 test/*.test.ts
- 不可变:函数返回新对象,绝不原地修改入参
- 短码用 base62、长度 7,生成逻辑必须可单测(不依赖网络/随机时不可测的部分要可注入)
- 错误处理:非法输入抛 `ValidationError`,不静默吞掉

## 测试策略(TDD)
先写失败测试 → 最小实现 → 重构。覆盖率目标 80%+。

⚠️package.json 里把 "test": "vitest run" 配好,否则 npm test 会报错。

把它提交:

git add CLAUDE.md package.json && git commit -m "docs: 添加 CLAUDE.md 与测试命令"

为什么重要:下次 Claude 想跑测试时,会直接读到 npx vitest run -t "..." 而不是瞎猜 jest、瞎猜路径。这一步省掉大量来回。


步骤 2 — 大规则拆进 .claude/rules/(path-scoped)

CLAUDE.md 要 < 200 行。TypeScript 细则不该塞进主文件,而是放 .claude/rules/typescript.md,只在你改 .ts 文件时才按需加载,省上下文。

.claude/rules/typescript.md:

---
paths: ["**/*.ts"]
---

# TypeScript 规则(改 .ts 时加载)

- 开启 strict;不写 `any`,用 `unknown` + 收窄
- 导出函数必须有显式返回类型
-`type` 而非 `interface` 定义数据形状(本项目约定)
- 校验在边界做:HTTP handler 入口校验 body,核心纯函数假设输入已校验
- 命名:函数 camelCase,类型 PascalCase,常量 UPPER_SNAKE_CASE

paths frontmatter 是关键:没有它就变成全局常驻、白占上下文。


步骤 3 — Explore:进 plan mode 只读探索(不写一行码)

Shift+Tab 循环模式,切到 plan mode(界面底部会显示 plan mode on)。plan mode 下 Claude 只能读、不能改,逼它先看懂再动手。

你输入:

我们要做第一个切片:POST /shorten {url} -> {code}。
先只读探索:看现有 src/、CLAUDE.md、.claude/rules/typescript.md,
告诉我你打算怎么拆分文件、纯函数边界在哪、需要哪些测试用例。先别写代码。

Claude 在 plan mode 会回类似(读完文件后):

读完了。现状:src/ 为空,约定要求纯函数可单测、base62 短码长度 7。
拆分建议:
- src/shorten.ts:纯函数 shorten(url, idgen) —— 校验 url、用注入的 idgen 生成 code
- src/server.ts:HTTP 层,调用 shorten,负责 302/JSON 响应(薄)
纯函数边界:把"生成 id"作为参数注入,使 shorten 完全确定、可单测。
建议测试用例:
1. 合法 url -> 返回 7 位 base62 code
2. 非法 url(无 scheme)-> 抛 ValidationError
3. 同一 idgen 下相同输入 -> 相同输出(确定性)
4. code 字符集只含 [0-9a-zA-Z]

关键点:探索阶段产出的是「文件拆分 + 测试清单」,不是代码。idgen 作为参数注入,正是为了让纯函数可测——这种设计决策在探索阶段定下来,远比写完再返工省事。


步骤 4 — Plan:生成详细计划并亲手改

继续在 plan mode,让它把上面落成一份有序计划:

按 TDD 给我一份分步计划:先写哪个测试、再写哪段实现,顺序排好。

Claude 给出计划后,按 Ctrl+G 可在编辑器里直接改这份计划(比如你想调整用例顺序、加一条"超长 url 截断"的用例)。改完确认。一份典型计划:

1. 写 test/shorten.test.ts:用例 1-4(此时无实现,测试会失败)
2. 跑 npx vitest run test/shorten.test.ts → 确认 RED
3. 写 src/shorten.ts 最小实现:base62 编码 + url 校验
4. 跑测试 → GREEN
5. 重构:抽出 isValidUrl、BASE62 常量
6. 写 src/server.ts 薄 HTTP 层
7. tsc --noEmit + 全量测试
8. commit + PR

计划本身就是给「实现阶段」的 prompt —— 见 20260320100300 Plans as Prompts设计模式


步骤 5 — Implement:TDD 红→绿→重构(看真实代码)

退出 plan mode(Shift+Tab 切回普通模式),让 Claude 按计划执行。下面是真实产物。

5a. 先写失败测试(RED)

test/shorten.test.ts:

import { describe, it, expect } from "vitest";
import { shorten, ValidationError } from "../src/shorten";

// 注入一个确定性 idgen,使测试可重复
const fixedIdgen = () => 123456789;

describe("shorten", () => {
  it("returns a 7-char base62 code for a valid url", () => {
    const { code } = shorten("https://example.com", fixedIdgen);
    expect(code).toHaveLength(7);
    expect(code).toMatch(/^[0-9a-zA-Z]+$/);
  });

  it("rejects an invalid url", () => {
    expect(() => shorten("not-a-url", fixedIdgen)).toThrow(ValidationError);
  });

  it("is deterministic for the same idgen", () => {
    const a = shorten("https://example.com", fixedIdgen).code;
    const b = shorten("https://example.com", fixedIdgen).code;
    expect(a).toBe(b);
  });
});

跑一下,确认它失败(这一步不能跳——你要看到 RED):

npx vitest run test/shorten.test.ts
# ❌ FAIL  Cannot find module '../src/shorten'  —— 正是预期的 RED

5b. 最小实现到通过(GREEN)

src/shorten.ts:

export class ValidationError extends Error {}

const BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const CODE_LEN = 7;

function isValidUrl(url: string): boolean {
  try {
    const u = new URL(url);
    return u.protocol === "http:" || u.protocol === "https:";
  } catch {
    return false;
  }
}

function toBase62(n: number): string {
  let code = "";
  let x = n;
  while (code.length < CODE_LEN) {
    code = BASE62[x % 62] + code;
    x = Math.floor(x / 62);
  }
  return code.slice(-CODE_LEN);
}

export function shorten(
  url: string,
  idgen: () => number,
): { url: string; code: string } {
  if (!isValidUrl(url)) {
    throw new ValidationError(`Invalid url: ${url}`);
  }
  return { url, code: toBase62(idgen()) }; // 返回新对象,不改入参
}

再跑:

npx vitest run test/shorten.test.ts
# ✅ PASS  3 passed —— GREEN

5c. 薄 HTTP 层

src/server.ts:

import { createServer } from "node:http";
import { shorten, ValidationError } from "./shorten";

const idgen = () => Date.now() + Math.floor(Math.random() * 1000);

createServer((req, res) => {
  if (req.method === "POST" && req.url === "/shorten") {
    let body = "";
    req.on("data", (c) => (body += c));
    req.on("end", () => {
      try {
        const { url } = JSON.parse(body);              // 边界校验输入
        const result = shorten(url, idgen);
        res.writeHead(200, { "content-type": "application/json" });
        res.end(JSON.stringify(result));
      } catch (e) {
        const status = e instanceof ValidationError ? 400 : 500;
        res.writeHead(status, { "content-type": "application/json" });
        res.end(JSON.stringify({ error: (e as Error).message }));
      }
    });
    return;
  }
  res.writeHead(404).end();
}).listen(3000, () => console.log("listening on :3000"));

手动验一下:

npx tsx src/server.ts &
curl -s -XPOST localhost:3000/shorten -d '{"url":"https://example.com"}'
# {"url":"https://example.com","code":"0bygMqo"}
curl -s -XPOST localhost:3000/shorten -d '{"url":"bad"}'
# {"error":"Invalid url: bad"}
kill %1

步骤 6 — 把验证循环变成 Hook(确定性自动跑测试)

别依赖"记得跑测试"。配一个 PostToolUse hook:每次 Claude 改完 .ts,自动跑测试,结果回灌给它自我纠正。

.claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "if echo \"$CLAUDE_FILE_PATHS\" | grep -q '\\.ts$'; then npx vitest run 2>&1 | tail -20; fi"
          }
        ]
      }
    ]
  }
}

效果:Claude 一改 .ts,测试自动执行,失败输出直接进它的上下文,它会主动修。这就是"自给自足循环"——见 20260627062504 自给自足验证循环配合 TDD20260319120100 Hook驱动优于提示词驱动

想更严:再加一个 PreToolUse 在 git commit 前跑 tsc --noEmit,不过则拦截。


步骤 7 — 用子代理做独立代码审查(隔离上下文)

实现完,派一个子代理用全新上下文只看 diff 做审查——它不被你的实现思路"污染",更容易发现问题。

两种方式:

A. 直接用内置/已装的(最简单)——在主对话里说:

用 code-reviewer 子代理审查刚写的 src/shorten.ts 和 src/server.ts,
重点:不可变性、错误处理、base62 边界(n 极大时是否溢出)。

B. 自定义一个项目子代理 .claude/agents/code-reviewer.md:

---
name: code-reviewer
description: 审查本项目 TS 代码的质量与安全,改完代码后用。聚焦不可变、错误处理、边界。
tools: Read, Grep, Glob, Bash
---

你是资深 TS 评审。只读不改。按严重度(CRITICAL/HIGH/MEDIUM/LOW)输出问题,
每条给文件:行号 + 具体修法。重点:
- 是否原地修改了入参(违反不可变约定)
- 错误是否被静默吞掉
- 数值边界(base62 对超大整数是否丢精度)
- 输入是否在边界校验
最后给一句"可合并 / 需修改"结论。

何时用子代理 vs 主对话、为什么隔离上下文有效:20260320100100 上下文腐烂与全新窗口隔离Claude Code 官方多Agent编排最佳实践 (2026)。 注意:子代理产生大量审查输出但只回摘要,不占用你主线上下文。


步骤 8 — Commit & PR

测试绿、审查过,收尾:

npx tsc --noEmit && npm test        # 最后一道闸
git add -A
git commit -m "feat: 实现 POST /shorten 短链接生成

- shorten() 纯函数 + 注入式 idgen,完全可单测
- base62 7 位短码,http/https 校验,非法输入抛 ValidationError
- 薄 HTTP 层,400/500 区分
- 3 个用例覆盖:合法/非法/确定性"
git push -u origin HEAD
gh pr create --fill

让 Claude 起草 commit/PR 正文时,它会读 git diff 总结改动——这也是四阶段的最后一阶段 Commit。


回看:八步对应四阶段

步骤 四阶段 核心动作
02 (准备) git init、/init、写 CLAUDE.md 命令、拆 path-scoped rules
3 Explore plan mode 只读,定文件拆分 + 测试清单
4 Plan TDD 分步计划,Ctrl+G 手改
5 Implement RED → GREEN → 重构,看真实代码
6 (Implement 加固) PostToolUse hook 自动验证
7 (Implement 把关) 子代理隔离审查
8 Commit tsc+test 闸门、描述性 commit、PR

常见坑(实操踩过的)

  • 跳过 RED:不看测试失败就写实现,等于没验证测试本身有效。务必先跑出红。
  • CLAUDE.md 不写命令:Claude 反复猜测试命令/框架,浪费大量轮次。命令是 CLAUDE.md 的第一优先级内容。
  • rules 不加 paths:细则全局常驻,白烧上下文。能 scope 就 scope。
  • plan mode 忘了退:停在 plan mode 时 Claude 改不了文件,会一直只给建议。Implement 前 Shift+Tab 切回。
  • hook 命令没过滤文件类型:改个 README 也触发全量测试。用 grep '\.ts$' 限定。
  • 把所有事塞一个 agent:复杂流程拆"一任务一代理"更好调试。但多数日常编码留主对话即可,别为了用而用(多代理约 15x token)。

Source