require(x) vs import x:为什么这是TypeScript开发者的永恒痛点
\n\n如果你曾经写过TypeScript,并看到过类似Cannot use import statement outside a module或require is not defined这样的错误——你并不孤单。在Stack Overflow上,"The difference between 'require(x)' and 'import x'"这个问题已经获得了数万次浏览。这并非偶然:两种模块系统之间的混淆是即使是经验丰富的开发者也会陷入的最常见陷阱之一。
在本教程中,我们不仅仅会列出差异。我们将走完从完全困惑到自信选择正确语法的全过程。你将了解TypeScript如何编译你的代码,为什么require仍然存在,以及何时应该使用import。
1. 历史背景:两种系统的由来
\n\n要理解差异,我们需要回顾JavaScript的历史。最初,Node.js没有内置的模块系统。开发者发明了CommonJS——一种通过require()同步加载模块的方式。这种方法在服务器端运行良好,但不适用于浏览器——浏览器需要异步加载。
后来,出现了ES Modules (ESM)标准——ECMAScript的官方模块语法。TypeScript作为JS的超集,支持这两种方式。但正是在这里,混淆开始了:你写import,而编译器却将其转换为require——或者相反。
2. 语法:主要区别
\n\n让我们详细分解这两种方法的样子。
\n\nCommonJS (require)
\n\n// 导入整个模块\nconst fs = require('fs');\n\n// 导入模块的一部分(解构)\nconst { readFile, writeFile } = require('fs');\n\n// 导出\nmodule.exports = { myFunction, myVariable };\nexports.myFunction = () => { ... };\n\nES Modules (import)
\n\n// 导入整个模块\nimport fs from 'fs';\n\n// 导入命名导出\nimport { readFile, writeFile } from 'fs';\n\n// 将所有内容作为对象导入\nimport * as fs from 'fs';\n\n// 导出\nexport const myVariable = 42;\nexport function myFunction() { ... }\nexport default class MyClass { }\n\n乍一看,区别只在于语法。但让我们深入挖掘。
\n\n3. 静态导入 vs 动态导入
\n\n这是影响代码运行方式的关键区别。
\n\n- \n
require— 动态的。你可以在任何地方调用它,在条件语句或函数内部。模块在调用时加载。 \n import— 静态的。导入在编译阶段解析,在代码执行之前。它们必须始终位于文件的顶层。 \n
动态require的示例:
\n\nlet myModule;\nif (process.env.NODE_ENV === 'production') {\n myModule = require('./production-config');\n} else {\n myModule = require('./dev-config');\n}\n\n这种技巧对import行不通——会导致语法错误。但为此有动态import()(返回Promise):
if (condition) {\n const module = await import('./some-module');\n module.doSomething();\n}\n\n4. TypeScript如何编译模块
\n\n这完全取决于tsconfig.json中的module设置。以下是主要选项:
- \n
"module": "commonjs"— 你的import将被转换为require。代码无需额外配置即可在Node.js中运行。 \n "module": "esnext"—import保持不变。适用于现代浏览器或打包工具(Webpack, Vite)。 \n "module": "node16"或"nodenext"— TypeScript会自动