完整答案跑不起来 —— 一个扩展名的事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.
这一页有什么On this page6
- 读懂「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
这是本站主线 ③「脚手架本身也会有问题」的又一个实例,而且这次踩得最狠 —— 不是某个测试失败,是 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.
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.
cab-booking-context/src/context/CabContext.jscab-booking-context/vite.config.mjs另一种(不推荐的)修法会改这里The other fix, the one not recommended, changes this
cab-booking-context/vite.config.mjs为什么 .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 error | What it tells you |
|---|---|
Failed to parse source for import analysis | It never got past parsing — not a runtime error, it died before compilation |
Plugin: vite:import-analysis | A 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:27 | Line 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.
两种修法,选哪个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" } } |
| 要改多少 import | 0 个 —— 所有 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 | |
|---|---|---|
| How | mv CabContext.js CabContext.jsx | Add esbuild: { loader: { ".js": "jsx" } } |
| Imports to change | None — every import is from "./context/CabContext", with no extension | None |
| Blast radius | One file | Every .js in the project is now parsed as JSX |
| Cost | None | Hides 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.
两处「测试能过但面试会问」的写法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.
动手做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.
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.cab-booking-context/src/context/CabContext.js这题是多选。More than one answer is correct.
初学者常见的几种写法错误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.
代价有三层:① 以后任何人在
.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.
换一道题也能用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.
- 基线是 0 个测试跑起来 —— 不是某个测试失败,是连收集都没过。The baseline is 0 tests started. It is not that one test failed; collecting the tests never got through.
- 根因:CabContext.js 里有 JSX,但 esbuild 按扩展名选 loader。The cause: CabContext.js contains JSX, but esbuild picks the loader from the file extension.
- 修法是改名 .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.
- 别用 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.
- 两处「能过但可更好」:非函数式更新、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.