DrillLab
练习Practice

动手做Get your hands on it

练习跟着课文走 —— 每节课尾都有本课的练习。这一页是全部练习的总库,想集中刷题的时候来。 每个练习都写清了它来自哪一节,卡住了就回去看那一节。Practice follows the lessons — every lesson ends with the exercises for that lesson. This page is the whole library, for when you want to drill in one sitting. Each exercise names the lesson it came from, so you can go back when you stall.

0 / 148个做对过you got right

练习Exercises

筛出 40 个练习(共 148 个) · 第 2 / 4 页。Showing 40 of 148 · page 2 / 4.
来自From 变式三 · fetch 取数:loading、error 与竞态Variation 3 · fetching data: loading, error, and the race between two requests · React 考试React exam
L3Debug LabDebug LabDebug Lab · URL 上是用户 2,界面显示用户 1Debug Lab · the URL says user 2 and the screen shows user 1DrillLab 自出Written by DrillLab

快速点两个用户,界面最后显示的是先点的那个。 慢一点点就没问题。控制台干净。

Click two users quickly and the screen ends up showing the one you clicked first. Click a little slower and it is fine. The console is clean.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
# 没有任何报错。 $ npx vitest run src/UserCard.test.tsx ✕ a slow stale response must not overwrite the newer one(竞态) Expected element to have text content: 用户2 Received: 用户1 Tests 1 failed | 5 passed (6) # 手动复现: # 1. 点用户 1(这个接口慢,200ms) # 2. 立刻点用户 2(这个快,10ms) # 3. 先看到用户 2 —— 对的 # 4. 200ms 后界面自己变成了用户 1 ← 错的,URL 上还是 2# No error at all. $ npx vitest run src/UserCard.test.tsx ✕ a slow stale response must not overwrite the newer one(竞态) Expected element to have text content: 用户2 Received: 用户1 Tests 1 failed | 5 passed (6) # Manual repro: # 1. Click user 1 (that request is slow, 200ms) # 2. Click user 2 right away (that one is fast, 10ms) # 3. User 2 shows up first — correct # 4. 200ms later the view switches itself to user 1 ← wrong, the URL still says 2
TSXsrc/components/UserCard/index.tsx示意Illustrative
1useEffect(() => {
2 setLoading(true);
3 setError(null);
4
5 (async () => {
6 try {
7 const res = await fetch(`/api/users/${userId}`);
8 if (!res.ok) throw new Error(`HTTP ${res.status}`);
9 setUser(await res.json());
10 } catch (e) {
11 setError((e as Error).message);
12 } finally {
13 setLoading(false);
14 }
15 })();
16}, [userId]);
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From 变式四 · 递归读取评论的评论Variation 4 · reading replies to replies with recursion · React 考试React exam
L3写整块Write a block写出树形数据的不可变更新Write an immutable update for tree dataDrillLab 自出Written by DrillLab

这是这道题真正的难点。目标节点可能在任意深度, 要返回一棵新树,而且原树一个字节都不能改

This is the hard part of the question. The target node can be at any depth, you have to return a new tree, and not one byte of the original may change.

要求Requirements
  • 找到 id === parentId 的节点,把 reply 追加到它的 replies 末尾Find the node whose id === parentId and append reply to the end of its replies
  • 返回新数组、新节点对象,不修改原数据Return a new array and new node objects, without changing the original data
  • 目标可能在任意深度,需要递归往下找The target can be at any depth, so recurse downwards to find it
  • 不许用 JSON.parse(JSON.stringify(...)) 深拷贝Do not deep-copy with JSON.parse(JSON.stringify(...))
  • 不许用 push / splice / 直接赋值Do not use push / splice / direct assignment
TypeScriptsrc/components/CommentTree/index.tsx
This check is textual: it looks for the right constructs, it does not run your code
提示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.

看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.

来自From 变式四 · 递归读取评论的评论Variation 4 · reading replies to replies with recursion · React 考试React exam
L3Debug LabDebug LabDebug Lab · 回复加进去了,界面不动Debug Lab · the reply went in and the screen never movedDrillLab 自出Written by DrillLab

给深层评论加回复,console.log 打出来的树里 新回复确实在,但界面没变化。控制台干净。

You add a reply to a deep comment. The tree printed by console.log really does contain the new reply, but the screen does not change. The console is clean.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
# 没有任何报错。 $ npx vitest run src/CommentTree.test.tsx ✕ addReply 挂到深层节点,且不改原树 TypeError: Cannot add property 0, object is not extensible (测试把原树深冻结了,实现试图直接修改它) ✕ 给三层的评论再回复,落在正确的位置 Unable to find an element with the text: 第四层 # 手动复现:点某条评论的 Reply、输入、发送 # console.log(comments) -> 新回复确实在树里 # 屏幕 -> 一点变化都没有# No error at all. $ npx vitest run src/CommentTree.test.tsx ✕ addReply 挂到深层节点,且不改原树 TypeError: Cannot add property 0, object is not extensible (The test deep-froze the original tree; the implementation edits it in place.) ✕ 给三层的评论再回复,落在正确的位置 Unable to find an element with the text: 第四层 # Manual repro: click Reply on a comment, type something, send it # console.log(comments) -> the new reply really is in the tree # the screen -> nothing changes at all
TSXsrc/components/CommentTree/index.tsx示意Illustrative
1function addReply(nodes: Comment[], parentId: number, reply: Comment) {
2 for (const node of nodes) {
3 if (node.id === parentId) {
4 node.replies.push(reply); // 找到就塞进去
5 return nodes;
6 }
7 addReply(node.replies, parentId, reply);
8 }
9 return nodes;
10}
11
12const handleReply = (parentId: number, text: string) => {
13 const reply = { id: Date.now(), author: "我", body: text, replies: [] };
14 setComments(addReply(comments, parentId, reply));
15};
1function addReply(nodes: Comment[], parentId: number, reply: Comment) {
2 for (const node of nodes) {
3 if (node.id === parentId) {
4 node.replies.push(reply); // found it, so push it in
5 return nodes;
6 }
7 addReply(node.replies, parentId, reply);
8 }
9 return nodes;
10}
11
12const handleReply = (parentId: number, text: string) => {
13 const reply = { id: Date.now(), author: "我", body: text, replies: [] };
14 setComments(addReply(comments, parentId, reply));
15};
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From 变式五 · 主题切换:Context 怎么用Variation 5 · theme switching: how to use Context · React 考试React exam
L3写整块Write a block自己写出 ThemeProvider 和 useThemeWrite ThemeProvider and useTheme yourselfDrillLab 自出Written by DrillLab

类型已给好。写出 context、Provider、自定义 hook 三部分。 检查器会查记忆化、函数式更新和守卫。

The types are given. Write all three parts: the context, the Provider, and the custom hook. The checker looks for the memoization, the updater form and the guard.

要求Requirements
  • theme 初始为 'light'theme starts as 'light'
  • toggleTheme 在 light / dark 之间翻转,必须用函数式更新toggleTheme flips between light and dark, using the updater form
  • context value 要记忆化,theme 不变时不产生新对象The context value has to be memoized, so no new object appears while theme is unchanged
  • toggleTheme 引用要稳定(theme 变了它也不变)The reference of toggleTheme has to be stable, unchanged even when theme changes
  • 没套 Provider 就用 useTheme() 时抛出一句能看懂的错误Calling useTheme() with no Provider above it throws a message a person can read
  • 不许把 theme 存到组件外的全局变量里Do not keep theme in a global variable outside the component
TSXsrc/context/ThemeContext.tsx
This check is textual: it looks for the right constructs, it does not run your code
提示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.

看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.

来自From Debug Lab · React 十种典型故障Debug Lab · ten typical React failures · React 考试React exam
L3Debug LabDebug Lab故障 4 · 编辑后列表毫无变化(综合题)Fault 4 · the list does not change after an edit (mixed question)

这一题不告诉你是哪一类。控制台干净,console.log 显示数据是对的。 自己分诊。

This one does not tell you which category it is. The console is clean, and console.log shows the data is correct. Sort it yourself.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
# 没有任何报错。 # 复现:添加 "A"、"B" 两条 → 点 B 的 Edit → 改成 "B2" → 点 Update # 期望:列表变成 A、B2 # 实际:列表还是 A、B # 在 handleSubmitNote 里插了日志: console.log("submitted:", submittedNote); // → submitted: { id: 1785737900978, title: 'B2', content: '...' } ← 数据是对的 console.log("after:", notes); // → after: [ {title:'A'...}, {title:'B2'...} ] ← 数组里也是对的! # 但屏幕上还是 B。 # 测试结果: # ✕ edits a note in place# No error at all. # Repro: add "A" and "B" → click Edit on B → change it to "B2" → click Update # Expected: the list becomes A, B2 # Actual: the list is still A, B # Logs added inside handleSubmitNote: console.log("submitted:", submittedNote); // → submitted: { id: 1785737900978, title: 'B2', content: '...' } ← the data is right console.log("after:", notes); // → after: [ {title:'A'...}, {title:'B2'...} ] ← the array is right too! # But the screen still shows B. # Test result: # ✕ edits a note in place
TSX有问题的 handleSubmitNoteThe handleSubmitNote with the problem示意Illustrative
1const handleSubmitNote = (submittedNote: Note) => {
2 if (noteToEdit) {
3 const i = notes.findIndex((n) => n.id === submittedNote.id);
4 notes[i] = submittedNote;
5 setNotes(notes);
6 setNoteToEdit(null);
7 } else {
8 setNotes((prev) => [...prev, submittedNote]);
9 }
10};
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From TODO 1 · User.ordersTODO 1 · User.orders · Federation 考试Federation exam
L3写整块Write a block不看答案,自己写出 User.ordersWrite User.orders yourself, without looking at the answer

按 TODO 的三条要求写完整实现。检查器会核对方法名、兜底、 错误处理和 correlation id。

Write the full implementation against the three requirements in the TODO. The checker looks at the method name, the fallback, the error handling and the correlation id.

要求Requirements
  • 用 user.id 去取该用户的订单Use user.id to fetch that user's orders
  • 调用数据源上真实存在的方法Call a method that really exists on the data source
  • 绝不返回 null 或 undefined(schema 是 [Order!]!)Never return null or undefined (the schema says [Order!]!)
  • 用 try/catch 包住,失败时抛 GraphQLErrorWrap it in try/catch and throw a GraphQLError on failure
  • 错误的 extensions 里带 code 和 correlationIdPut code and correlationId in the error's extensions
  • 已经是 GraphQLError 的错误要原样往上抛,不要重新包装Rethrow an error that is already a GraphQLError untouched, without rewrapping it
  • 日志里带上 correlationIdInclude correlationId in the log line
JavaScriptsrc/resolvers/orderResolvers.js
This check is textual: it looks for the right constructs, it does not run your code
提示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.

看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.

来自From TODO 3 & 4 · Query.order 与 Query.ordersTODO 3 & 4 · Query.order and Query.orders · Federation 考试Federation exam
L3写整块Write a block不看答案,自己写出两个 Query resolverWrite both Query resolvers yourself, without looking at the answer

两个函数一起写。注意它们的数据来源、兜底策略、 context 解构都不一样。

Write both functions together. They differ in where they read from, what they fall back to, and what they destructure out of context.

要求Requirements
  • Query.order 用 orderLoader 取数据Query.order reads through orderLoader
  • Query.order 找不到时抛带 ORDER_NOT_FOUND code 的 GraphQLErrorWhen Query.order finds nothing, it throws a GraphQLError carrying the ORDER_NOT_FOUND code
  • Query.orders 用 orderDataSource.getOrdersByUserId 取数据Query.orders reads through orderDataSource.getOrdersByUserId
  • Query.orders 校验 userId,非法时抛 INVALID_INPUTQuery.orders validates userId and throws INVALID_INPUT when it is not valid
  • Query.orders 绝不返回 null(schema 是 [Order!]!)Query.orders never returns null (the schema says [Order!]!)
  • 两个都用 try/catch,catch 里先放行已有的 GraphQLErrorBoth use try/catch, and the catch lets an existing GraphQLError through first
  • 两个都在日志里带上 correlationIdBoth include correlationId in their log line
JavaScriptsrc/resolvers/orderResolvers.js
This check is textual: it looks for the right constructs, it does not run your code
提示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.

看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.

来自From 三处埋雷:怎么系统地找出来The three planted bugs: how to find them systematically · Federation 考试Federation exam
L3Debug LabDebug LabDebug Lab · Cannot read properties of undefinedDebug Lab · Cannot read properties of undefined

Mutation.createOrder 的测试挂了。 报错说在读一个 undefined 的属性。自己分诊。

The Mutation.createOrder test fails. The error says something read a property of undefined. Diagnose it yourself.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
● Order Resolvers › Mutation.createOrder resolver › should create a new order successfully GraphQLError: Failed to create order 91 | } catch (error) { 92 | console.error(`[${correlationId}] Error creating order:`, error.message); > 93 | throw new GraphQLError('Failed to create order', { # 往上翻,console.error 打出的原始错误是: console.error [test-correlation-id] Error creating order: Cannot read properties of undefined (reading 'createOrder')● Order Resolvers › Mutation.createOrder resolver › should create a new order successfully GraphQLError: Failed to create order 91 | } catch (error) { 92 | console.error(`[${correlationId}] Error creating order:`, error.message); > 93 | throw new GraphQLError('Failed to create order', { # Scroll up: the raw error that console.error printed is console.error [test-correlation-id] Error creating order: Cannot read properties of undefined (reading 'createOrder')
JavaScriptsrc/resolvers/orderResolvers.js示意Illustrative
1const order = await dataSources.orderAPI.createOrder({ userId, items });
2
3// 参考:index.js 里 context 的 return
4// return {
5// dataSources: { orderDataSource, inventoryDataSource, shippingDataSource },
6// loaders: { shippingInfoLoader, orderLoader },
7// correlationId
8// };
1const order = await dataSources.orderAPI.createOrder({ userId, items });
2
3// For reference: what the context function in index.js returns
4// return {
5// dataSources: { orderDataSource, inventoryDataSource, shippingDataSource },
6// loaders: { shippingInfoLoader, orderLoader },
7// correlationId
8// };
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From 三处埋雷:怎么系统地找出来The three planted bugs: how to find them systematically · Federation 考试Federation exam
L3Debug LabDebug LabDebug Lab · 错误码不对(不报错的那种 bug)Debug Lab · The wrong error code (the kind of bug that throws nothing)

代码跑得通,没有异常。但测试说错误码不对。 这是三处埋雷里最值得理解的一处。

The code runs and raises no exception, but the test says the error code is wrong. Of the three planted bugs, this is the one most worth understanding.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
● Order Resolvers › Error handling › should return structured error for validation failures expect(received).toBe(expected) // Object.is equality Expected: "INVALID_INPUT" Received: "SERVICE_ERROR" # 测试代码: # const input = { userId: '789', items: [] }; ← 空 items,应该被校验拦下 # try { # await resolvers.Mutation.createOrder({}, input, context); # throw new Error('Should have thrown an error'); # } catch (error) { # expect(error.extensions.code).toBe('INVALID_INPUT'); # }● Order Resolvers › Error handling › should return structured error for validation failures expect(received).toBe(expected) // Object.is equality Expected: "INVALID_INPUT" Received: "SERVICE_ERROR" # The test code: # const input = { userId: '789', items: [] }; ← empty items, validation should stop it # try { # await resolvers.Mutation.createOrder({}, input, context); # throw new Error('Should have thrown an error'); # } catch (error) { # expect(error.extensions.code).toBe('INVALID_INPUT'); # }
JavaScriptsrc/resolvers/orderResolvers.js示意Illustrative
1try {
2 if (!userId || !items || items.length === 0) {
3 throw new GraphQLError('Invalid order input', {
4 extensions: { code: ErrorCodes.INVALID_INPUT, correlationId }
5 });
6 }
7 const order = await dataSources.orderDataSource.createOrder(userId, pricedItems);
8 return order;
9} catch (error) {
10 console.error(`[${correlationId}] Error creating order:`, error.message);
11 throw new GraphQLError('Failed to create order', {
12 extensions: { code: ErrorCodes.SERVICE_ERROR, correlationId }
13 });
14}
1try {
2 if (!userId || !items || items.length === 0) {
3 throw new GraphQLError('Invalid order input', {
4 extensions: { code: ErrorCodes.INVALID_INPUT, correlationId }
5 });
6 }
7 const order = await dataSources.orderDataSource.createOrder(userId, pricedItems);
8 return order;
9} catch (error) {
10 console.error(`[${correlationId}] Error creating order:`, error.message);
11 throw new GraphQLError('Failed to create order', {
12 extensions: { code: ErrorCodes.SERVICE_ERROR, correlationId }
13 });
14}
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From 六个端点:状态码就是这道题的全部Six endpoints: the status codes are the whole task · Federation 考试Federation exam
L3写整块Write a block不看答案,自己写出全部六个端点Write all six endpoints yourself, without looking at the answer

六个端点一起写。业务逻辑全部调 orderService, 你负责选对状态码、处理可选参数、转 enum、打日志。

Write all six endpoints. Every piece of business logic goes through orderService; your job is picking the right status codes, handling the optional parameter, converting the enum, and logging.

要求Requirements
  • GET /api/orders:?userId= 传了就按用户过滤,没传返回全部;200GET /api/orders: filter by user when ?userId= is given, otherwise return everything; 200
  • GET /api/orders/{id}:200;不要 try/catch,让 404 由全局处理器给出GET /api/orders/{id}: 200; no try/catch, let the global handler produce the 404
  • GET /api/orders/user/{userId}:200GET /api/orders/user/{userId}: 200
  • POST /api/orders:201 CreatedPOST /api/orders: 201 Created
  • PATCH /api/orders/{id}/status:把 body 里的字符串转成 OrderStatus;缺失或非法值返回 400;成功 200PATCH /api/orders/{id}/status: convert the string in the body into an OrderStatus; return 400 when it is missing or invalid; 200 on success
  • DELETE /api/orders/{id}:204 No ContentDELETE /api/orders/{id}: 204 No Content
  • 六个端点都用 logger.info 打日志,并带上 MDC 里的 correlationIdAll six endpoints log with logger.info and include the correlationId from the MDC
JavaOrderController.java
This check is textual: it looks for the right constructs, it does not run your code
提示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.

看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.

来自From 六个端点:状态码就是这道题的全部Six endpoints: the status codes are the whole task · Federation 考试Federation exam
L3Debug LabDebug LabDebug Lab · 查一个不存在的订单,返回了 200Debug Lab · Asking for an order that does not exist returns 200

五个测试全过。但手动 curl 一个不存在的 id, 得到 200 和一个空 body。期望是 404 加一段 JSON。

All five tests pass. But curl an id that does not exist by hand and you get a 200 with an empty body. It should be a 404 with a piece of JSON.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
$ curl -i -s localhost:8080/api/orders/999 HTTP/1.1 200 Content-Length: 0 # 期望: # HTTP/1.1 404 # { "timestamp": "...", "status": 404, "message": "Order not found with id: 999" } # mvn test:Tests run: 5, Failures: 0 ← 测试全过!$ curl -i -s localhost:8080/api/orders/999 HTTP/1.1 200 Content-Length: 0 # Expected: # HTTP/1.1 404 # { "timestamp": "...", "status": 404, "message": "Order not found with id: 999" } # mvn test: Tests run: 5, Failures: 0 ← every test passes!
Java有问题的实现The broken implementation示意Illustrative
1@GetMapping("/api/orders/{id}")
2public ResponseEntity<Order> getOrderById(@PathVariable Long id) {
3 logger.info("GET /api/orders/{} correlationId={}", id, correlationId());
4 try {
5 return ResponseEntity.ok(orderService.getOrderById(id));
6 } catch (EntityNotFoundException ex) {
7 return null;
8 }
9}
1@GetMapping("/api/orders/{id}")
2public ResponseEntity<Order> getOrderById(@PathVariable Long id) {
3 logger.info("GET /api/orders/{} correlationId={}", id, correlationId());
4 try {
5 return ResponseEntity.ok(orderService.getOrderById(id));
6 } catch (EntityNotFoundException ex) {
7 return null;
8 }
9}
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From 两道书面题:延迟传播与生产配置The two written questions: how delay spreads, and production configuration · Federation 考试Federation exam
L3写整块Write a block写出 actuator 那一条的修正配置Write the corrected configuration for the actuator lineDrillLab 自出Written by DrillLab

针对 management.endpoints.web.exposure.include=*, 写出修正后的配置。至少要做到:白名单、管理端口分离、 health 不泄漏细节、支持 k8s 探针。

Write the corrected configuration for management.endpoints.web.exposure.include=*. At a minimum: an allow list, management on its own port, a health endpoint that leaks no detail, and support for Kubernetes probes.

要求Requirements
  • 用白名单列出需要的端点,不用 *List the endpoints you need in an allow list; do not use *
  • management.server.port 设成与业务端口不同的值Set management.server.port to something other than the business port
  • health 端点不显示详情The health endpoint shows no details
  • 开启 health probes(liveness / readiness)Turn on the health probes (liveness and readiness)
Propertiesapplication-prod.properties
This check is textual: it looks for the right constructs, it does not run your code
提示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.

看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.