ESM:import / export 与那些莫名其妙的报错ESM: import / export, and the errors that look strange at first
为什么 subgraph 里 import 要写 .js 后缀,为什么 jest 要加一个实验性参数。Why an import in the subgraph needs the .js ending, and why jest needs an experimental flag.
这一页有什么On this page6
- 01 default 和 named:一个模块只能有一个 defaultdefault and named: a module can have only one default
- 02 ESM 里相对路径必须带 .js —— 哪怕源文件是 .tsIn ESM a relative path must end in .js, even when the source file is .ts
- 03 import type:只要类型,不要运行时代码import type: take the type only, not any code that runs
- 04 为什么 subgraph 的 test script 那么长Why the test script of the subgraph is so long
- 练习 · 动手做Practice
- 迁移模式Transfer
- 分清 default export 和 named export,以及各自怎么 importTell a default export from a named export, and import each one correctly
- 知道 ESM 里相对路径必须带扩展名Know that a relative path in ESM must include the file ending
- 看懂 import type 是干什么的Understand what import type is for
- 认出「模块系统不匹配」这一类报错Recognise the errors that mean two module systems do not match
两个项目都是 ESM。subgraph 的 import 少一个 .js 就跑不起来;React 项目里 import type 用错会让构建失败。这类错误的报错信息通常很不友好。Both projects use ESM. One missing .js in a subgraph import and nothing runs. In the React project, a wrong import type makes the build fail. The messages these errors print are usually hard to read.
default 和 named:一个模块只能有一个 defaultdefault and named: a module can have only one default
export default X 是「这个模块的主角是 X」, 一个文件只能有一个。import 它的时候名字随你起:
export const A / export function B是具名导出,可以有很多个,import 时名字必须对上,而且要用花括号。
两个考试的组件都是 default 导出(export default NoteManager), 而 subgraph 的 resolver 是具名导出 (export const resolvers)—— 所以 import 的写法不同。
export default X says “X is the main character of this module”, and a file only gets one. When you import it, you pick whatever name you like:
export const A / export function B are named exports. There can be many of them, and on import the name has to line up and go inside curly braces.
Components in both exams are default exports (export default NoteManager), while the subgraph resolvers are named exports (export const resolvers) — so the import lines look different.
react-notes-app/src/App.tsx 与各组件graphql-federation-practice/node-subgraph/src/index.jsESM 里相对路径必须带 .js —— 哪怕源文件是 .tsIn ESM a relative path must end in .js, even when the source file is .ts
这是 Node 原生 ESM 的硬规定,不是可选风格。This is a fixed rule of native ESM in Node, not a matter of style.
注意上面那行:from './resolvers/orderResolvers.js'。带了 .js。
在 CommonJS(require)时代,Node 会帮你猜: 你写 ./foo,它会依次试 ./foo.js、./foo/index.js。原生 ESM 取消了这个猜测, 路径必须写完整。
漏了会得到这个报错 —— 而它长得像「文件不存在」, 容易让人去怀疑路径打错了:
而 React 那个项目里 import App from "./App"没写后缀却能跑,因为它经过 Vite —— 打包器有自己的解析规则,会帮你补。结论:走打包器可以省,直接给 Node 跑就必须写。
Look at that line above again: from './resolvers/orderResolvers.js'. It carries the .js.
Back in the CommonJS (require) era, Node guessed for you: write ./foo and it would try ./foo.js, then ./foo/index.js. Native ESM dropped the guessing, so the path has to be written out in full.
Leave it off and you get this error — which reads like “the file does not exist” and sends people off doubting their path:
Meanwhile import App from "./App" in the React project has no extension and runs fine, because it goes through Vite — a bundler has its own resolution rules and fills the extension in for you. Conclusion: with a bundler you can skip it; handing the file straight to Node, you must write it.
import type:只要类型,不要运行时代码import type: take the type only, not any code that runs
import type { Note } from "../../types/Note"这个写法在告诉编译器:「我只需要 Note 这个类型, 编译完请把这行整个删掉」。
为什么要区分?因为 type Note = {...} 这种类型别名在运行时根本不存在 —— TypeScript 编译后类型全被擦掉了。 如果用普通 import,某些配置下打包器会保留这行 import, 然后在运行时去找一个不存在的导出。
实用规则:导入的是类型就写 import type, 导入的是能跑的东西(函数、组件、常量)就写普通 import。真实项目里两种都有:
import type { Note } from "../../types/Note" tells the compiler: “all I need is Note as a type, so delete this whole line once you have compiled”.
Why draw the distinction? Because a type alias like type Note = {...} does not exist at runtime at all — TypeScript erases every type when it compiles. With a plain import, some setups keep that import line and then go looking at runtime for an export that is not there.
Practical rule: importing a type, write import type; importing something that runs (a function, a component, a constant), write a plain import. Real projects carry both:
react-notes-app/src/components/NoteForm/index.tsx为什么 subgraph 的 test script 那么长Why the test script of the subgraph is so long
回头看那条 script:
Jest 诞生于 CommonJS 时代,原生 ESM 支持至今还是实验性的。NODE_OPTIONS=--experimental-vm-modules 就是打开那个开关。 少了它,jest 一遇到 import 就报Cannot use import statement outside a module。
另外 package.json 里内嵌的 jest 配置有"transform": {} —— 空对象, 意思是「不做任何转译」。因为源码本来就是标准 ESM, 不需要 Babel 把它转成 CommonJS。
这些不需要你去改。但需要你认出来 —— 看到这条 script 就知道「这个项目是 ESM,测试是 jest,别照 CommonJS 的思路改东西」。
Look at that script again:
Jest was born in the CommonJS era, and its native ESM support is still experimental to this day. NODE_OPTIONS=--experimental-vm-modules is the switch that turns it on. Without it, jest hits an import and reports Cannot use import statement outside a module.
The jest config embedded in package.json also has "transform": {} — an empty object, meaning “transpile nothing”. The source is already standard ESM, so Babel is not needed to turn it into CommonJS.
None of this needs changing by you. It needs recognising — see this script and you know “this project is ESM, the tests are jest, do not go editing things with a CommonJS mindset”.
graphql-federation-practice/node-subgraph/package.json动手做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.
你在 node-subgraph/ 里跑 npm start, 服务器起不来。报错很长,但关键信息只有两行。
You run npm start inside node-subgraph/ and the server does not come up. The error is long, but only two lines of it matter.
换一道题也能用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.
- default 导出一个文件只能有一个,import 时名字随意、不加花括号。A file can have only one default export. When you import it you may pick any name, and you use no curly braces.
- 具名导出可以多个,import 时名字必须一致、要加花括号。A file can have many named exports. When you import one, the name must match, and you use curly braces.
- 原生 ESM 里相对路径必须带 .js;走 Vite 这类打包器时可以省。In native ESM a relative path must end in .js. With a bundler such as Vite you may leave it out.
- import type 只借类型,编译后整行消失。import type borrows the type only. The whole line disappears after compiling.
- subgraph 那条长 test script 是为了让 jest 能跑 ESM,不需要改但要认得。That long test script in the subgraph is there so jest can run ESM. You do not need to change it, but you should recognise it.