node、TS下CJS模块引入ESM注意事项

908 阅读2分钟

node、TS下CJS模块引入ESM注意事项

使用ESM的条件

  • 模块化检测

    • .mts、.mjs、.d.mts始终表示ESM
    • .cts、.cjs、.d.cts始终表示CJS
    • 最近的package.jsontype: module则所有的.ts、.js、.d.ts都表示ESM,反之为CJS
  • ESM导入CJS时

    • ESM用默认导入可操作CJS的"module.exports" 👍
    • ESM用命名导入CJS时,TS类型推断默认会成功,但可能运行时失败,所以仍然推荐默认导入
  • CJS导入ESM时

    • CJS无法通过import语句导入ESM,因为TS编译后import -> require()
    • CJS只能通过动态导入:import(),去动态异步加载ESM!⚠️
  • "type": "module"添加到您的package.json中。

  • (可选)将package.json中的"main": "index.js"替换为"exports": "./index.js"

  • 将package.json中的"engines"字段更新为"node": ">=16" 。

  • 移除所有"use strict"声明。

  • 全部使用完整的导入路径。

    import x from '.';
    // 替换为如下
    import x from './index.js'; // 即使你要导入的是ts文件,这里也必须写.js,编译代码不会改变后缀
    
  • 使用node包则使用"node:*"导入。

    import fs from "node:fs";
    
  • tsconfig.json设置。

    {
      "module": "node16",
      "moduleResolution": "node16"
    }
    
  • 如果TS类型声明文件有namespace的使用替换为export导出。

详细参考地址: Pure ESM package

默认TS下node的模块化规则

⚠️ 综上所述,TS默认情况下是CJS模块的!!!

强烈推荐

  • 强烈推荐模块化使用ESM,因为ESM可以直接导入CJS/ESM。而CJS只能通过动态导入import()去导入ESM模块

😡 nodejs双模块真该死啊!没办法毕竟ESM才是官方主流啊~ 😡

坑:当你使用动态导入import()报错require() of ES Module

  • 报错
Error [ERR_REQUIRE_ESM]: require() of ES Module /Users/xiaoqinvar/Desktop/project/xqv-solution/node_modules/got/dist/source/index.js not supported.
Instead change the require of index.js in null to a dynamic import() which is available in all CommonJS modules.
  • 发现问题流程:

    1. 最直接的想法:因为网上有太多讨论TS CJS使用动态导入import(),因为TS编译成require()的问题,所以第一步想到的是TS编译的产物。

    2. 我的TS是v 5.2.2(2023-11-11,当时可以说是最新版本了),在TS Playground测试发现TS的编译结果没有问题

      // 代码
      import foo from "./foo";
      console.log(foo);
      
      const foo = await import("./foo")
      
      // 编译产物
      console.log(foo);
      const foo = await import("./foo");
      export {};
      
    3. 也就是不是TS问题,接下来我查看了build构建的产物(因为我项目使用的是nx,内置的使用的是webpack构建)

      // 原始代码
      async function gotLoader() {
        /*webpackIgnore: true*/
        const { got } = await import("got");
        return got;
      }
      
      // build产物
      async function gotLoader() {
          /*webpackIgnore: true*/
          const { got } = await Promise.resolve(/* import() */).then(__webpack_require__.t.bind(__webpack_require__, 52, 23));
          return got;
      }
      
    4. 很显然这里肯定是webpack修改了TS构建产物之后的最终产物,而且__webpack_require__这个变量想都不用想与require()肯定是一致的东西

  • 解决起来相对容易,忽略编译即可

async function gotLoader() {
  const { got } = await import(/*webpackIgnore: true*/ "got"); // 忽略对import()动态导入的编译转换
  return got;
}