DrillLab
第 21 / 21 节LESSON 21 / 21约 60 分钟~60 min

从零重写:空文件夹到 4 个测试全过Write it again yourself: from an empty folder to 4 passing tests

不给答案。给需求、文件清单、验证命令和四级提示。这一关是分界线。No answer is given. You get the requirements, the file list, the commands to check your work, and hints in four levels. This lesson is the dividing line.

2 个练习2 exercisesReact · 第 6 部分React · Part 6
这一页有什么On this page4
学完这节你会After this lesson you can
  • 在没有参考代码的情况下从空文件建出整个项目Build the whole project from empty files, with no code to copy from
  • 自己把 React 项目的构建与测试配置搭起来Set up the build and test configuration of a React project yourself
  • 独立实现 Q1 三个任务和 Q2 调度器Implement the three Q1 tasks and the Q2 task runner without help
  • 用测试和手动场景验证自己的实现Check your implementation with the tests and by trying it by hand
这在考试里考什么What the exam does with this

填空和跟写只能证明你「看懂了」。真正的考试是打开一个空编辑器。这一关就是模拟那个时刻 —— 而且它比真实考试更难,因为连脚手架都要你自己搭。Filling in blanks and copying along only proves you followed the explanation. The real exam starts with an empty editor. This lesson recreates that moment, and it is harder than the real exam, because here you also set up the project yourself.

这节课要看的真实文件Real files this lesson looks at1 项 · 1 个可以展开看原文1 items · 1 can be opened
react-notes-app/参考项目 —— 做完之后再对照,不要提前看The reference project — compare against it after you finish; do not look early
Textreact-notes-app/ · tree源项目From source
1react-notes-app/
2├── q2/
3│ ├── demo.ts
4│ └── taskRunner.ts
5├── src/
6│ ├── components/
7│ │ ├── NoteForm/
8│ │ │ └── index.tsx
9│ │ ├── NoteItem/
10│ │ │ └── index.tsx
11│ │ ├── NoteManager/
12│ │ │ └── index.tsx
13│ │ └── NoteTable/
14│ │ └── index.tsx
15│ ├── types/
16│ │ └── Note.ts
17│ ├── App.tsx
18│ ├── index.css
19│ ├── main.tsx
20│ └── NoteManager.test.tsx
21├── index.html
22├── package-lock.json
23├── package.json
24├── README.md
25├── tsconfig.json
26├── vite.config.ts
27└── vitest.setup.ts
Source: react-notes-app/
§01

为什么必须做这一关Why you have to do this lesson

读代码用的是识别能力,写代码用的是生成能力。两者不是一回事。Reading code means recognising it. Writing code means producing it. These are two different abilities.

看着答案点头「嗯,这里用 map」很容易。 面对空文件想起「我该建几个文件、state 放哪、 useEffect 的依赖写什么」,是完全不同的一件事。

前面几节课里,你已经在 L3 练习里分别写过handleSubmitNotehandleDeleterunTasks。这一关是把它们放回一个完整项目里, 加上你自己搭的配置、你自己划分的组件。

不要跳过这一关直接看参考答案。答案就在那个门后面,跑不掉。先自己撞一遍墙 —— 撞墙的地方才是你真正的薄弱点。

Nodding at an answer and thinking “right, map goes here” is easy. Facing an empty file and recalling how many files to create, where the state goes, what the useEffect dependency should be — that is an entirely different thing.

In the earlier lessons you already wrote handleSubmitNote, handleDelete and runTasks in the L3 exercises. This stage puts them back into a whole project, on top of config you set up and components you divided yourself.

Do not skip this stage and jump to the reference answer. The answer is behind that door and it is not going anywhere. Walk into the wall yourself first — where you hit the wall is where you are actually weak.

§02

建议的做法A suggested way to work through it

  1. 新建一个目录,不要在 react-notes-app 里改。比如 ~/Downloads/my-notes-manager。 源项目要留着最后对照。
  2. 先把项目跑起来再写业务。npm init → 装依赖 → 建index.html / main.tsx /App.tsxnpm run dev看到「Hello」再往下走。先让空架子能跑,是所有项目的正确起手式。
  3. 把测试文件先抄进去。它是你的判卷器。有它在,你随时知道离终点多远。
  4. 一个测试一个测试地攻。先让第 1 个过(Add),再第 2 个(disabled), 依次推进。别想一次写完所有东西。
  5. 卡住超过 15 分钟再看提示。提示是四级递进的,从「想什么方向」到「给局部代码」。
  6. 最后做手动三场景验证。测试全过之后,加三条同名笔记、删中间、编辑中间 —— 这一步才是真正证明你做对了。
  1. Make a fresh directory. Do not edit inside react-notes-app. Something like ~/Downloads/my-notes-manager. Keep the source project intact so you can compare at the end.
  2. Get the project running before writing any features.npm init → install dependencies → create index.html / main.tsx / App.tsxnpm run dev and only move on once you see “Hello”. Getting an empty shell to run first is the correct opening move for any project.
  3. Copy the test file in first. It is your grader. With it in place you always know how far the finish line is.
  4. Attack one test at a time. Get the first one green (Add), then the second (disabled), and keep going. Do not try to write everything at once.
  5. Only open a hint after 15 minutes stuck. The hints escalate through four levels, from “which direction to think” to “here is part of the code”.
  6. Finish with the three manual scenarios. Once the tests are all green, add three same-named notes, delete the middle one, edit the middle one — that step is what actually proves you got it right.
练习Practice

动手做Get your hands on it

填空只是过渡。真正掌握的标准,是在没有答案的时候从头写出来 —— 所以做完 L2 之后一定要往 L3、L4 走。Filling blanks is a stepping stone. The real bar is writing it from nothing, so once L2 is comfortable, push on to L3 and L4.

L4从零重写Rebuild from scratch从零重建 Q1 · Notes ManagerRebuild Q1 · Notes Manager

空目录开始,建出一个 React + TypeScript + Vite 项目, 实现 Notes Manager 的增删改,让下面那四个测试全过。不要打开 react-notes-app 参考。

Starting from an empty directory, build a React + TypeScript + Vite project. Implement add, delete and edit in Notes Manager, and make all four tests below pass. Do not open react-notes-app to look.

需求(不给代码,自己实现)Requirements — no code given, implement it yourself
  • 页面上方是表单:Title 输入框、Content 文本域、一个提交按钮The form sits at the top of the page: a Title input, a Content textarea, and one submit button
  • 页面下方是表格:表头 Title / Content / Edit / Delete,每条笔记一行The table sits below: the header is Title / Content / Edit / Delete, with one row per note
  • 两个输入框都必须是受控的(value + onChange)Both inputs must be controlled (value + onChange)
  • 标题或内容为空(含只有空格)时,提交按钮 disabledWhen the title or the content is empty (including only spaces), the submit button is disabled
  • Task 1 Add:提交后新笔记出现在表格末尾,原有的都还在Task 1 Add: after submit the new note appears at the end of the table, and every existing note is still there
  • Task 2 Delete:点某行的 Delete,该行按 id 被移除(同名笔记只删对的那条)Task 2 Delete: clicking Delete on a row removes that row by id (with notes of the same name, only the right one goes)
  • Task 3 Edit:点某行的 Edit → 内容回填进表单、按钮文字变成 UpdateTask 3 Edit: clicking Edit on a row fills its content back into the form, and the button text becomes Update
  • Task 3 提交后:该笔记在原位置被更新(顺序不变),然后退出编辑模式(表单清空、按钮回到 Add)Task 3 after submit: the note is updated in place (the order does not change), and edit mode ends (the form clears, the button goes back to Add)
  • 必须带上这些 data-testid:note-manager / note-form / form-input / form-textarea / form-submit-button / notes-listThese data-testid values are required: note-manager / note-form / form-input / form-textarea / form-submit-button / notes-list
  • 行内按钮的文字必须正好是 Edit 和 DeleteThe text on the row buttons must be exactly Edit and Delete
  • Note 的类型是 { id: number; title: string; content: string }The type of Note is { id: number; title: string; content: string }
你需要自己建的文件Files you create yourself
文件清单File list
package.json自己写 scripts 与依赖(react / react-dom / vite / @vitejs/plugin-react / typescript / vitest / jsdom / @testing-library/*)You write the scripts and dependencies (react / react-dom / vite / @vitejs/plugin-react / typescript / vitest / jsdom / @testing-library/*)
index.html一个 <div id="root"> 加一行 module scriptOne <div id="root"> plus one module script line
tsconfig.jsonstrict、jsx: react-jsx、moduleResolution: bundlerstrict, jsx: react-jsx, moduleResolution: bundler
vite.config.tsReact 插件 + 内联 vitest 配置(environment: jsdom、globals、setupFiles)The React plugin plus an inline vitest config (environment: jsdom, globals, setupFiles)
vitest.setup.tsimport "@testing-library/jest-dom"
src/main.tsxcreateRoot().render(<App />)
src/App.tsx渲染顶层组件Renders the top-level component
src/types/Note.tsNote 类型The Note type
src/components/NoteManager/index.tsx★ 状态所有者:notes + noteToEdit + 三个 handler★ The state owner: notes + noteToEdit + three handlers
src/components/NoteForm/index.tsx★ 受控表单、编辑回填、Add/Update 切换、提交时 id 的取舍★ The controlled form, filling values back for an edit, switching Add and Update, and choosing the id on submit
src/components/NoteTable/index.tsx表格骨架 + map + notes-list 的 testidThe table skeleton, the map, and the notes-list data-testid
src/components/NoteItem/index.tsx单行 + Edit / Delete 按钮One row plus the Edit and Delete buttons
src/NoteManager.test.tsx把四个测试抄进来当判卷器(见下方参考答案区)Copy the four tests in and let them grade you (see the reference answer area below)
写完后在本机这样验证Verify it locally like this
npm install
装完依赖,node_modules 与 package-lock.json 出现The dependencies install, and node_modules and package-lock.json appear
npm run dev
打开提示的 localhost 地址,能看到表单和空表格Open the localhost address it prints, and you see the form and an empty table
npx vitest run
Test Files 1 passed (1) / Tests 4 passed (4)
npm run dev
手动验证三件事:① 加三条同名笔记,删中间那条,只消失一条 ② 编辑中间那条,它还在第二行 ③ 更新完按钮回到 Add、表单清空Check three things by hand: ① add three notes with the same name, delete the middle one, and only one disappears ② edit the middle one and it is still on the second row ③ after the update the button goes back to Add and the form clears
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

这一关的意义就在于「没有答案也能写出来」。请确认你已经在本机建好文件、跑过验证命令,再打开参考答案对照。The whole point of this level is writing it with no answer in front of you. Create the files on your machine and run the verification commands first, then come back and compare.

Tick this once you have written it locally and the verification commands give the expected output.
L4从零重写Rebuild from scratch从零重建 Q2 · 并发任务调度器Rebuild Q2 · the concurrent task runner

只给类型定义和三条要求。自己写出 runTasks, 并自己写一个验证台来证明它对。

You get only the type definitions and three requirements. Write runTasks yourself, and write your own check harness to show that it is right.

需求(不给代码,自己实现)Requirements — no code given, implement it yourself
  • runTasks(tasks, limit) 接收一个「函数数组」,每个函数被调用后返回 PromiserunTasks(tasks, limit) takes an array of functions, and each function returns a Promise when it is called
  • 同一时刻最多 limit 个任务在运行;某个结束后立刻启动下一个At most limit tasks run at the same time; as soon as one finishes, start the next
  • 任何任务失败都不能让 runTasks 抛错No failing task may make runTasks throw
  • 返回数组顺序必须与 tasks 一致The order of the returned array must match tasks
  • 成功写 { status: "fulfilled", value },失败写 { status: "rejected", reason }On success write { status: "fulfilled", value }; on failure write { status: "rejected", reason }
  • 自己写一个 demo:6 个任务(其中至少 1 个 reject)、limit = 2,打印实时并发数与最终结果Write your own demo: 6 tasks (at least 1 of which rejects), limit = 2, printing how many run at each moment and the final results
你需要自己建的文件Files you create yourself
文件清单File list
package.json装 tsx 和 typescript,加一条跑 demo 的 scriptInstall tsx and typescript, and add one script that runs the demo
tsconfig.jsonstrict: true 就够了strict: true is enough
q2/taskRunner.ts★ Task / SettledResult 类型 + runTasks 实现★ The Task / SettledResult types plus the runTasks implementation
q2/demo.ts★ 自己写验证台:一个 running 计数器 + 6 个任务 + 打印★ Write the check harness yourself: one running counter, 6 tasks, and the printing
写完后在本机这样验证Verify it locally like this
npm install
装好 tsx 和 typescripttsx and typescript are installed
npm run q2
输出里 running now 从不超过 2;最终 6 条结果顺序与输入一致;reject 的那条是 { status: 'rejected', reason: Error }running now never goes above 2 in the output; the final 6 results are in the same order as the input; the rejected one is { status: 'rejected', reason: Error }
npx tsc --noEmit
没有类型错误No type errors
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

这一关的意义就在于「没有答案也能写出来」。请确认你已经在本机建好文件、跑过验证命令,再打开参考答案对照。The whole point of this level is writing it with no answer in front of you. Create the files on your machine and run the verification commands first, then come back and compare.

Tick this once you have written it locally and the verification commands give the expected output.
迁移Transfer

换一道题也能用Works on other problems too

考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.

拿到空目录You are handed an empty folder
先让空架子能跑起来,再写业务Get the empty project running first, then write the features
有测试文件There is a test file
先抄进来当判卷器,一个一个攻Copy it in first and use it as your grader, then take one test at a time
不知道 state 放哪You do not know where to put the state
画组件树,找需要它的组件的共同祖先Draw the component tree and find the closest shared parent of the components that need it
写完了You think you are finished
跑测试 + 手动跑测试覆盖不到的场景Run the tests, then try the cases the tests do not cover by hand
这节的要点What to take away
  1. 识别能力和生成能力是两回事 —— 只有从空文件写过,才算真会。Recognising code and producing code are two different abilities. You only really know it once you have written it from empty files.
  2. 起手式:先让空架子跑起来(能看到 Hello),再写业务逻辑。Opening move: get the empty project running first, so you can see Hello on screen, then write the features.
  3. 有测试就先抄进来,它是你唯一客观的进度条。If there is a test, copy it in first. It is your only objective progress bar.
  4. 卡住 15 分钟再看提示,提示是四级递进的。Stay stuck for 15 minutes before opening a hint. The hints go in four levels, each one more specific.
  5. 最后一定要手动验证「按 id」「原位置」「退出编辑模式」这三条。At the end, check three things by hand: it edits by id, the note stays in place, and edit mode closes.

接下来What next

  1. 把这一节的练习做掉Do this lesson’s exercises2 个,就在这一页上面 —— 别攒着最后一起做2 of them, further up this page — do not save them for later
    回到练习 ↑Back up to them ↑
  2. 这一门读完了 —— 去验收Course finished — go get checked考场:空文件夹、计时、没有提示按钮The arena: an empty folder, a clock, no hint button
    去考场To the arena
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: Debug Lab · React 十种典型故障Debug Lab · ten typical React failures