DrillLab
第 07 / 08 节LESSON 07 / 08约 13 分钟~13 min

完整答案跑不起来 —— 一个扩展名的事The complete answer does not run — the cause is one file extension

README 说「先运行完整答案熟悉流程」。实测 0 个测试跑起来。The README says to run the complete answer first to get used to the flow. In practice 0 tests start.

2 个练习2 exercisesCab Booking · 第 3 部分Cab Booking · Part 3
这一页有什么On this page6
学完这节你会After this lesson you can
  • 读懂「Failed to parse source for import analysis」这条报错Understand the error message "Failed to parse source for import analysis"
  • 说清 Vite 为什么默认不在 .js 里解析 JSXExplain why Vite does not parse JSX inside .js files by default
  • 在两种修法里选对的那个,并说出为什么Pick the right one of the two fixes, and say why
  • 养成「先跑一次基线」的习惯Build the habit of running the project once as a baseline first
这在考试里考什么What the exam does with this

这是本站主线 ③「脚手架本身也会有问题」的又一个实例,而且这次踩得最狠 —— 不是某个测试失败,是 0 个测试跑起来。真实考试里遇到这种情况,能不能在两分钟内判断出「是环境问题不是我写错了」,直接决定你剩下的时间怎么花。This is another example of theme 3 on this site: the project you are given can be broken itself. This case is the worst one. It is not that one test fails, it is that 0 tests start. In a real exam, being able to decide within two minutes that the setup is at fault and not your code decides how you spend the rest of your time.

这节课要看的真实文件Real files this lesson looks at高亮行 = 需要你动手改的文件Highlighted rows are the files you edit
cab-booking-context/src/context/CabContext.js缺陷本体:.js 扩展名 + 文件里有 JSX

提醒:源项目在磁盘上是做完的版本 —— 下面就是答案。想自己先写一遍的话,现在关上。Heads up: on disk this project is the finished version — what follows is the answer. Close this if you want to write it yourself first.

JavaScriptCabContext.js源项目From source
1import { createContext, useContext, useState } from "react";
2
3const CabContext = createContext();
4
5const CabProvider = ({ children }) => {
6 const [bookedCabDetails, setBookedCabDetails] = useState(null);
7 const [rideHistory, setRideHistory] = useState([]);
8
9 const updateBookedCabDetails = (details) => {
10 setBookedCabDetails(details);
11 setRideHistory([...rideHistory, details]);
12 };
13
14 return (
15 <CabContext.Provider
16 value={{ bookedCabDetails, updateBookedCabDetails, rideHistory }}
17 >
18 {children}
19 </CabContext.Provider>
20 );
21};
22
23const useCabContext = () => {
24 const context = useContext(CabContext);
25
26 if (!context) {
27 throw new Error("useCabContext must be used within a CabProvider");
28 }
29
30 return context;
31};
32
33export { CabProvider, useCabContext };
Source: cab-booking-context/src/context/CabContext.js
cab-booking-context/vite.config.mjs另一种(不推荐的)修法会改这里The other fix, the one not recommended, changes this
JavaScriptvite.config.mjs源项目From source
1import { defineConfig } from "vite";
2import react from "@vitejs/plugin-react";
3
4export default defineConfig({
5 plugins: [react()],
6 test: {
7 environment: "jsdom",
8 setupFiles: "./src/test/setup.js",
9 globals: true,
10 },
11});
Source: cab-booking-context/vite.config.mjs
§01

为什么 .js 里的 JSX 会炸Why JSX inside a .js file fails

esbuild 默认按扩展名决定用哪个 loaderBy default esbuild picks the loader from the file extension

一句话:Vite 用 esbuild 转换文件,esbuild 按扩展名选 loader ——.jsx 用 jsx loader,.js 用 js loader,而 js loader 不认识 <div> 这种语法

报错读法(这条值得逐行拆):

报错里的这一段告诉你什么
Failed to parse source for import analysis解析都没过 —— 不是运行时错误,是编译前就挂了
Plugin: vite:import-analysis是 Vite 的插件报的,不是 React、不是 vitest ——问题在构建层
If you are using JSX, make sure to name the file with the .jsx or .tsx extension.它直接把答案说了 —— 这类报错要读到最后一句
File: …/src/context/CabContext.js:19:27精确到行列。19 行 27 列是 </CabContext.Provider> 的末尾
Test Files 1 failed / Tests no tests「no tests」是关键词 —— 一个测试都没跑,不是跑了但失败

为什么 esbuild 不干脆都按 JSX 解析:因为 JSX 语法和普通 JS 有冲突。 最典型的是类型断言的尖括号< 作为比较运算符的情况 ——a < b > c 在 JS 里是两次比较, 按 JSX 解析就可能被当成标签。所以只能靠扩展名声明「这个文件里有 JSX」。

会追问:「Create React App 里 .js 写 JSX 就没事啊?」—— 对,CRA 用 Babel 且配置成对所有 .js都跑 JSX 转换。这是工具链的选择,不是语言规定。Vite 选了「按扩展名」, 所以从 CRA 迁到 Vite 的项目经常一片红,全是这个原因。

In one line: Vite transforms files with esbuild, and esbuild picks its loader from the extension .jsx gets the jsx loader, .js gets the js loader, and the js loader does not understand syntax like <div>.

How to read the error (worth taking line by line):

This part of the errorWhat it tells you
Failed to parse source for import analysisIt never got past parsing — not a runtime error, it died before compilation
Plugin: vite:import-analysisA Vite plugin reported it, not React and not vitest — the problem is in the build layer
If you are using JSX, make sure to name the file with the .jsx or .tsx extension.It states the answer outright — read this kind of error all the way to the last sentence
File: …/src/context/CabContext.js:19:27Line and column. 19:27 is the end of </CabContext.Provider>
Test Files 1 failed / Tests no tests“no tests” is the keyword — not one test ran, as opposed to running and failing

Why esbuild does not just parse everything as JSX: because JSX syntax collides with plain JS. The classic cases are angle brackets in type assertions and < as a comparison operator — a < b > c is two comparisons in JS but could be read as a tag under JSX rules. So the extension is how you declare “this file contains JSX”.

Follow-up: “But JSX in .js works fine in Create React App?” — right; CRA uses Babel and is configured to run the JSX transform on every .js file. That is a toolchain choice, not a language rule. Vite chose extension-based, which is why projects migrating from CRA to Vite often light up red for exactly this reason.

Textnpx vitest run 的真实输出(本机实测,路径已改短)The real output of npx vitest run (measured here, with paths shortened)示意Illustrative
1 RUN v2.1.8 /Users/you/cab-booking-context
2
3 ❯ src/test/App.test.jsx (0 test)
4
5⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯
6
7 FAIL src/test/App.test.jsx [ src/test/App.test.jsx ]
8Error: Failed to parse source for import analysis because the content contains invalid JS syntax. If you are using JSX, make sure to name the file with the .jsx or .tsx extension.
9 Plugin: vite:import-analysis
10 File: /Users/you/cab-booking-context/src/context/CabContext.js:19:27
11 17 | >
12 18 | {children}
13 19 | </CabContext.Provider>
14 | ^
15 20 | );
16 21 | };
17
18 Test Files 1 failed (1)
19 Tests no tests
「Tests no tests」这五个字是最重要的信号 —— 一个测试都没跑起来。这时候去改组件代码是白费功夫。The words "Tests no tests" are the signal that matters most: not one test even started. Editing the component code at this point is wasted effort.
§02

两种修法,选哪个Two ways to fix it, and which one to pick

改扩展名,还是改构建配置Change the file extension, or change the build configuration

一句话:改扩展名。改构建配置能让它跑起来,但代价不对。

① 改名 .js → .jsx② 改 vite.config.mjs
怎么做mv CabContext.js CabContext.jsx esbuild: { loader: { ".js": "jsx" } }
要改多少 import0 个 —— 所有 import 都是from "./context/CabContext", 没写扩展名0 个
影响范围一个文件整个项目所有 .js都按 JSX 解析
代价没有掩盖了问题本身;以后再有人在 .js 里写 JSX 也不会被发现;换个构建工具(Jest / Next / tsc) 又会炸

为什么改名不用动 import:ES 模块的路径在浏览器里必须写全扩展名, 但打包器会做「扩展名解析」 —— 看到 ./context/CabContext就依次试 .js / .jsx / .ts / .tsx / .json。 所以只要 import 没写扩展名,改名就是无痛的。这也是为什么「import 不写扩展名」在打包项目里是好习惯。

更一般的判断标准:能改一个文件解决的,别去改全局配置。全局配置的每一行都是「以后所有人都要遵守的规则」, 而这里真正的问题只是一个文件的名字起错了

会追问:「那真实项目里什么时候该改配置?」—— 当「不合规」的文件多到改不动的时候 (比如从 CRA 迁过来的几百个 .js)。那时改配置是过渡手段, 通常配一条 lint 规则 + 计划分批改名。一个文件的时候改配置,是拿长期换短期。

In one line: rename the file. Changing the build config also makes it run, but the price is wrong.

(1) Rename .js → .jsx(2) Edit vite.config.mjs
Howmv CabContext.js CabContext.jsxAdd esbuild: { loader: { ".js": "jsx" } }
Imports to changeNone — every import is from "./context/CabContext", with no extensionNone
Blast radiusOne fileEvery .js in the project is now parsed as JSX
CostNoneHides the actual problem; the next person writing JSX in a .js file will not be caught; and it breaks again under a different toolchain (Jest, Next, tsc)

Why renaming touches no imports: ES module paths need full extensions in a browser, but bundlers perform extension resolution — given ./context/CabContext they try .js / .jsx / .ts / .tsx / .json in turn. So as long as imports omit the extension, renaming is painless. Which is also why omitting extensions in imports is a good habit in a bundled project.

The more general rule: if one file fixes it, do not reach for the global config. Every line of global config is a rule everyone has to live with afterwards, and the real problem here is just one file with the wrong name.

Follow-up: “When is editing the config the right call in a real project?” — when there are too many offending files to rename (say hundreds of .js files migrated from CRA). Then the config change is a transition measure, usually paired with a lint rule and a plan to rename in batches. Reaching for it over a single file trades the long term for the short term.

§03

两处「测试能过但面试会问」的写法Two places that pass the tests but an interviewer will ask about

不是 bug。但你得知道它们的边界在哪They are not bugs. But you need to know where their limits are

一句话:源项目有两处写法在这个应用里完全正确, 但换个场景就会出问题 —— 面试官很爱问这种。

updateBookedCabDetails用的是非函数式更新

setRideHistory([...rideHistory, details])读的是闭包里那个 rideHistory, 也就是「本次渲染时的值」。
什么时候会出错:同一个事件里连调两次 —— 第二次读到的还是第一次之前的值,结果只追加了一条
为什么测试撞不到:每次订车之间都隔着完整的页面切换和重渲染,rideHistory 每次都是最新的。
函数式写法:setRideHistory((prev) => [...prev, details]) ——prev 是 React 给的最新值, 跟闭包无关。只要更新依赖旧值,就用函数式。

② Provider 的 value 没有记忆化

value={{ bookedCabDetails, updateBookedCabDetails, rideHistory }}是一个字面量对象 —— Provider 每次渲染都造一个新的。
后果:所有 useContext的消费者都会重渲染,即使它们用的那部分数据没变。 而且 React.memo 挡不住这个 —— context 变化会穿透 memo。
为什么这里无所谓:只有三个消费者,渲染的东西都很小。而且 Provider 重渲染的唯一原因就是它自己的 state 变了 —— 那时消费者本来就该更新。
标准答案:useCallback 稳住函数 + useMemo 稳住 value 对象。 本站 /code「主题切换(Context + value 记忆化)」那道题专门练这个 —— 实测删掉 useMemo 之后功能测试全绿,只有那一条专门查记忆化的挂

面试时怎么说才漂亮:别说「这里写错了」——它没写错。 说「这个写法在当前规模下没问题, 因为消费者少且 Provider 只在 state 变化时重渲染; 如果 Provider 上面加了会频繁重渲染的父组件, 就该上 useMemo / useCallback」能说出「什么条件下会变成问题」, 比背出 useMemo 强得多。

In one line: the source project has two patterns that are entirely correct in this app but break in a different setting — interviewers love asking about exactly this.

(1) updateBookedCabDetails does not use a functional update

setRideHistory([...rideHistory, details]) reads the rideHistory from its closure, i.e. the value as of this render.
When that goes wrong: two calls inside one event — the second still sees the pre-first value, so only one entry gets appended.
Why the tests never hit it: every booking is separated by a full page switch and re-render, so rideHistory is always current.
The functional form: setRideHistory((prev) => [...prev, details]) prev is the latest value, handed to you by React, with no closure involved. Whenever an update depends on the old value, go functional.

(2) The Provider’s value is not memoised

value={{ bookedCabDetails, updateBookedCabDetails, rideHistory }} is an object literal, so the Provider builds a fresh one on every render.
Consequence: every useContext consumer re-renders, even ones whose slice of the data did not change. And React.memo does not stop it — context changes go straight through memo.
Why it does not matter here: there are three consumers and they all render very little. More to the point, the only reason this Provider re-renders is its own state changing — and then the consumers should update anyway.
The textbook answer: useCallback to stabilise the function plus useMemo to stabilise the value object. The “theme toggle (Context + memoised value)” problem in /code drills precisely this — measured, removing the useMemo leaves every functional test green and only the memoisation test red.

How to say it well in an interview: do not say “this is wrong” — it is not. Say “this is fine at the current scale, because there are few consumers and the Provider only re-renders when its own state changes; add a frequently re-rendering parent above the Provider and you would want useMemo and useCallback”. Naming the condition under which it becomes a problem beats reciting useMemo.

JSX两处改法(示意 —— 不是源项目代码)Two ways to change it (illustration — not source project code)示意Illustrative
1// 源项目的写法 —— 在这个应用里正确
2const updateBookedCabDetails = (details) => {
3 setBookedCabDetails(details);
4 setRideHistory([...rideHistory, details]); // 读闭包里的值
5};
6
7// 更稳的写法 —— 更新依赖旧值时一律这样
8const updateBookedCabDetails = useCallback((details) => {
9 setBookedCabDetails(details);
10 setRideHistory((prev) => [...prev, details]); // React 给你最新值
11}, []); // 依赖空数组:函数身份永远稳定
12
13// value 记忆化
14const value = useMemo(
15 () => ({ bookedCabDetails, updateBookedCabDetails, rideHistory }),
16 [bookedCabDetails, updateBookedCabDetails, rideHistory],
17);
18// 注意:updateBookedCabDetails 必须先被 useCallback 稳住,
19// 否则它每次都是新函数,useMemo 的依赖每次都变 —— 记忆化等于没做。
1// what the source project writes — correct in this app
2const updateBookedCabDetails = (details) => {
3 setBookedCabDetails(details);
4 setRideHistory([...rideHistory, details]); // reads the value in the closure
5};
6
7// the safer form — always use this when the update depends on the old value
8const updateBookedCabDetails = useCallback((details) => {
9 setBookedCabDetails(details);
10 setRideHistory((prev) => [...prev, details]); // React hands you the latest value
11}, []); // empty deps: the function identity never changes
12
13// memoising value
14const value = useMemo(
15 () => ({ bookedCabDetails, updateBookedCabDetails, rideHistory }),
16 [bookedCabDetails, updateBookedCabDetails, rideHistory],
17);
18// note: updateBookedCabDetails has to be held steady by useCallback first,
19// or it is a new function every time, the useMemo deps change every time, and memoising does nothing.
最后那句注释是这一组最容易踩的坑:useMemo 的依赖里放了一个每次都新建的函数,等于白写。useCallback 和 useMemo 通常成对出现,就是这个原因。That last comment is the easiest trap in this pair: put a function that is rebuilt every time into the useMemo dependencies and the memoising achieves nothing. That is why useCallback and useMemo usually appear together.
练习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.

L2Debug LabDebug LabDebug Lab:0 个测试跑起来Debug Lab: zero tests run
README 说「先运行完整答案熟悉流程」。npm install 成功,npx vitest run 却是下面这个输出。注意最后一行的「no tests」。The README says to run the finished answer first to get used to the flow. npm install succeeds, and npx vitest run gives the output below. Look at the “no tests” on the last line.
第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
RUN v2.1.8 /Users/you/cab-booking-context ❯ src/test/App.test.jsx (0 test) ⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯ FAIL src/test/App.test.jsx [ src/test/App.test.jsx ] Error: Failed to parse source for import analysis because the content contains invalid JS syntax. If you are using JSX, make sure to name the file with the .jsx or .tsx extension. Plugin: vite:import-analysis File: /Users/you/cab-booking-context/src/context/CabContext.js:19:27 17 | > 18 | {children} 19 | </CabContext.Provider> | ^ 20 | ); 21 | }; ❯ TransformPluginContext._formatError node_modules/vite/dist/node/chunks/dep-CB_7IfJ-.js:49255:41 ❯ TransformPluginContext.error node_modules/vite/dist/node/chunks/dep-CB_7IfJ-.js:49250:16 Test Files 1 failed (1) Tests no tests
JSXsrc/context/CabContext.js ← 注意这个扩展名src/context/CabContext.js ← look at that extension源项目From source
1import { createContext, useContext, useState } from "react";
2
3const CabContext = createContext();
4
5const CabProvider = ({ children }) => {
6 const [bookedCabDetails, setBookedCabDetails] = useState(null);
7 const [rideHistory, setRideHistory] = useState([]);
8
9 const updateBookedCabDetails = (details) => {
10 setBookedCabDetails(details);
11 setRideHistory([...rideHistory, details]);
12 };
13
14 return (
15 <CabContext.Provider
16 value={{ bookedCabDetails, updateBookedCabDetails, rideHistory }}
17 >
18 {children}
19 </CabContext.Provider>
20 );
21};
22
23const useCabContext = () => {
24 const context = useContext(CabContext);
25
26 if (!context) {
27 throw new Error("useCabContext must be used within a CabProvider");
28 }
29
30 return context;
31};
32
33export { CabProvider, useCabContext };
Source: cab-booking-context/src/context/CabContext.js
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
L1认出来Spot it下面哪些说法是对的?(多选)Which of these statements are correct? (more than one)
关于源项目那两处「能过但可以更好」的写法。About the two places in the source project that pass the tests but could be better.

这题是多选。More than one answer is correct.

先选一个选项Pick an option first
错例Wrong

初学者常见的几种写法错误Mistakes beginners actually make

下面每一段都是「能编译、但结果不对」或者「一跑就炸」的真实写法。先自己看出问题在哪,再看解释。Every snippet below either compiles and gives the wrong answer, or blows up on the first run. Spot the problem yourself before reading the explanation.

JavaScript不推荐的修法The fix we do not recommend示意Illustrative
1// ✕ 用全局配置掩盖一个文件的问题
2// vite.config.mjs
3export default defineConfig({
4 plugins: [react()],
5 esbuild: {
6 loader: { ".js": "jsx" }, // 让所有 .js 都按 JSX 解析
7 },
8 test: { environment: "jsdom", setupFiles: "./src/test/setup.js", globals: true },
9});
1// ✕ hiding one file's problem behind a global setting
2// vite.config.mjs
3export default defineConfig({
4 plugins: [react()],
5 esbuild: {
6 loader: { ".js": "jsx" }, // parse every .js file as JSX
7 },
8 test: { environment: "jsdom", setupFiles: "./src/test/setup.js", globals: true },
9});
它确实能让测试跑起来 —— 这也是它危险的地方。
代价有三层: 以后任何人在 .js 里写 JSX 都不会被发现,问题会扩散; 换构建工具就再炸一次 (Jest、Next、单独跑 tsc 都不看这个配置); 一个新人打开CabContext.js 看到 JSX, 会以为「原来 .js 里可以写 JSX」,学到一个错的结论
判断标准很简单:改一个文件能解决的,别动全局配置。
It does make the tests run — and that is exactly what makes it dangerous.
The price comes in three parts: from now on nobody notices when JSX is written in a .js file, so the problem spreads; the next change of build tool breaks it again (Jest, Next, and a separate run of tsc all ignore this configuration); a newcomer who opens CabContext.js and sees JSX will believe that JSX is allowed in .js files, and learns something that is wrong.
The rule is simple: when changing one file solves it, leave the global configuration alone.
迁移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.

报错里出现「Tests no tests」/「0 test」The output says "Tests no tests" or "0 test"
挂在收集阶段,别改业务代码 —— 去看构建/转换层It failed while collecting the tests: leave the feature code alone and look at the build and transform layer
「Failed to parse source for import analysis」"Failed to parse source for import analysis"
十有八九是 .js 里写了 JSX,改扩展名Almost always JSX written in a .js file: change the extension
报错最后一句给了具体建议The last line of the error gives a concrete suggestion
先照着做 —— 这类工具报错常常直接给答案Follow it first; errors from tools like this often state the answer
想加一条全局配置来救一个文件You are about to add a global setting to rescue one file
先问「改那个文件行不行」Ask first whether changing that one file is enough
面试问「这段代码有什么问题」An interviewer asks what is wrong with a piece of code
先说清它在什么条件下是对的,再说什么条件下会坏Say under which conditions it is correct first, then under which conditions it breaks
这节的要点What to take away
  1. 基线是 0 个测试跑起来 —— 不是某个测试失败,是连收集都没过。The baseline is 0 tests started. It is not that one test failed; collecting the tests never got through.
  2. 根因:CabContext.js 里有 JSX,但 esbuild 按扩展名选 loader。The cause: CabContext.js contains JSX, but esbuild picks the loader from the file extension.
  3. 修法是改名 .jsx,一处 import 都不用改(import 都没写扩展名)。The fix is to rename it to .jsx, and not one import has to change, because none of them write the extension.
  4. 别用 vite.config 的 loader 覆盖来救一个文件 —— 那是拿长期换短期。Do not use a loader override in vite.config to rescue a single file. That buys a short-term gain with a long-term cost.
  5. 两处「能过但可更好」:非函数式更新、value 未记忆化。它们不是 bug,要说清边界条件。Two places that pass but could be better: an update that is not written as a function, and a value that is not memoised. They are not bugs, so be ready to state the conditions where they matter.

接下来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. 接着看下一节Continue to the next lesson从零重写:空文件夹里做出来Rewrite it: build the whole app in an empty folder
    下一节Next lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: 历史与确认页:两个小而致命的细节The history and confirmation pages: two small details that decide pass or fail