Node12+ 下 axios 包使用报错引发的对 package.json's exports 等属性以及 esm 的探究

发布时间 2023-03-24 22:00:18作者: lessfish

最近碰到一个 case,在一个用 ts 写的 node 项目里,使用 axios,本地开发没问题,但是部署上去报错了,然后使用方式改了一下就没问题了

import axios from 'axios' // 部署上去后报错

// 修改后
import axios from 'axios/dist/node/axios.cjs' // 部署上去后运行 ok

写个最小 demo:

import axios from 'axios'

axios.get('https://jsonplaceholder.typicode.com/todos/1').then(res => {
  console.log(res.data);
})

在 tsconfig.json 中注意设置 "module": "commonjs"

编译成 js 后的结果:

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
var axios_1 = __importDefault(require("axios"));
axios_1.default.get('https://jsonplaceholder.typicode.com/todos/1').then(function (res) {
    console.log(res.data);
});

当然也可以直接用 ts-node 来运行 ts-node index.ts

所以其实我们可以简单用下面代码来复现也可以,本身和 ts 没太大关系:

const axios = require('axios')
axios.get('https://jsonplaceholder.typicode.com/todos/1').then(function (res) {
  console.log(res.data);
});

本地 Node 版本 v16.18.1 下运行正常,线上部署的版本是 v12.13,本地切到这个版本,报错和线上一致:

import axios from './lib/axios.js';
^^^^^^

SyntaxError: Cannot use import statement outside a module
    at Module._compile (internal/modules/cjs/loader.js:895:18)
    at Module._extensions..js (internal/modules/cjs/loader.js:995:10)
    at Object.require.extensions.<computed> [as .js] (/usr/local/lib/node_modules/ts-node/src/index.ts:1608:43)
    at Module.load (internal/modules/cjs/loader.js:815:32)
    at Function.Module._load (internal/modules/cjs/loader.js:727:14)
    at Module.require (internal/modules/cjs/loader.js:852:19)
    at require (internal/modules/cjs/helpers.js:74:18)
    at Object.<anonymous> (/Users/bytedance/fish/dustbin/node-ts/index.ts:1:1)
    at Module._compile (internal/modules/cjs/loader.js:959:30)
    at Module.m._compile (/usr/local/lib/node_modules/ts-node/src/index.ts:1618:23)

这个报错,顾名思义,对于 import axios from './lib/axios.js' 这样 esm 方式的导入,12.13 版本无法识别,确实,Node 13.2 开始才原生支持 esm,那么,v16.18.1 下用的就是 esm 形式的 axios 模块,所以能正常运行?我们将 Node 版本切到 13.2,也报错了

(node:96271) Warning: require() of ES modules is not supported.
require() of /Users/bytedance/fish/dustbin/node-ts/node_modules/axios/index.js from /Users/bytedance/fish/dustbin/node-ts/index.js is an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which defines all .js files in that package scope as ES modules.
Instead rename /Users/bytedance/fish/dustbin/node-ts/node_modules/axios/index.js to end in .cjs, change the requiring code to use import(), or remove "type": "module" from /Users/bytedance/fish/dustbin/node-ts/node_modules/axios/package.json.
internal/modules/cjs/loader.js:1163
      throw new ERR_REQUIRE_ESM(filename);
      ^

Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /Users/bytedance/fish/dustbin/node-ts/node_modules/axios/index.js

这个就要聊到 package.json 中的一些字段了

看下 axios's package.json,几个关键配置项:

{
  "main": "index.js",
  "exports": {
    ".": {
      "types": {
        "require": "./index.d.cts",
        "default": "./index.d.ts"
      },
      "browser": {
        "require": "./dist/browser/axios.cjs",
        "default": "./index.js"
      },
      "default": {
        "require": "./dist/node/axios.cjs",
        "default": "./index.js"
      }
    },
    "./package.json": "./package.json"
  },
  "type": "module",
  "browser": {
    "./lib/adapters/http.js": "./lib/helpers/null.js",
    "./lib/platform/node/index.js": "./lib/platform/browser/index.js",
    "./lib/platform/node/classes/FormData.js": "./lib/helpers/null.js"
  }
}
  • main - 定义了 npm 包入口文件,browser 和 node 环境均可使用
  • module - 定义了 npm 包 esm 规范的入口文件,browser 和 node 环境均可使用
  • browser - 定义了 npm 包在 browser 环境的入口文件

为什么一个 main 不够用?main 一般是编译成 commonjs 的代码(当然也可以直接用 main 指定 esm 导出),但是 esm 出来后,我们希望直接使用 esm 的代码(tree shaking 等),这样就出现了 module,而 browser 的话可以更加方便客户端使用

当我们用 import xx from xx 来引用一个包的时候,引的到底是哪个文件?以上三个字段就是用来定义不同环境下引用到的文件

一般 browser 中使用,通过构建工具构建上述代码后,默认的模块加载顺序为:(webpack 可以通过 resolve.mainFields 配置)

browser+mjs > module > browser+cjs > main

以 webpack 为例,在 target:web 配置下,mainFields 的默认值是:

mainFields: ['browser', 'module', 'main']

但是如果指定了 exports 字段,以上三个字段会被忽略(exports 在 Node 13.7+ 才真正支持),导出文件就得看 exports 中的配置了

另外还有个 type 字段,来表明当前包是 commonjs 导出还是 esm,默认 commonjs。如果设置为 module,则表明 esm 身份,所有 .js 文件加载都用 esm 方式来加载,如果要用 commonjs 则需要将文件后缀改成 .cjs,反之,如果 type 为默认的 commonjs,如果要使用 esm 模块,则需要将文件后缀名改成 .mjs

我们再来看开头提到的 case。首先我们的编译产物是用 require 去引 axios 的,那么其实目标比较明确,要引 axios 的 commonjs 版本

在 v16+ 版本,exports 生效,拿到 "./dist/node/axios.cjs",运行 ok

在 v12.13 版本,此时 exports 不生效,拿到的是 "main": "index.js" 作为入口文件,此时很明显 esm 模块无法使用,所以报错(两个原因,一个是 12.13 版本自身还不支持 esm 模块,另一个原因是本身代码是用 require 来引 axios 的,只能引 commonjs)所以这里报错 Cannot use import statement outside a module 其实 module 指的是 commonjs module,它以为加载的是 commonjs 模块,但是用了 import,提示 import 使用姿势有误

在 v13.2 版本中,报错 require() of ES modules is not supported,此时已经支持 esm 了,报错原因是本身用 require 来引,需要 commonjs 的导出,提示你 require 不能引 esm 模块

那么我们在 tsconfig.json 配置中进行修改:(目标是在 Node 中用 esm 来引 axios)

"module": "es2015", 
"moduleResolution": "nodenext",

打包产物和源码没啥区别:

import axios from 'axios';
axios.get('https://jsonplaceholder.typicode.com/todos/1').then(function (res) {
  console.log(res.data);
});

然后运行它

先是一个报错:

Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.

我们将文件后缀改成 .mjs 再来运行,还是报错:

(node:99855) ExperimentalWarning: The ESM module loader is experimental.
file:///Users/bytedance/fish/dustbin/node-ts/node_modules/axios/lib/adapters/http.js:7
import {getProxyForUrl} from 'proxy-from-env';
        ^^^^^^^^^^^^^^
SyntaxError: The requested module 'proxy-from-env' does not provide an export named 'getProxyForUrl'
    at ModuleJob._instantiate (internal/modules/esm/module_job.js:91:21)
    at async ModuleJob.run (internal/modules/esm/module_job.js:106:20)
    at async Loader.import (internal/modules/esm/loader.js:132:24)

拿到 proxy-from-env's package.json,这就是一个 commonjs 的包,不提供 esm 导出,所以报错了

所以如果要用 esm 的方式来跑 Node,得保证它的所有依赖,以及依赖的依赖也都提供了 esm 加载的导出(感觉甚至可以去混个 pr?)这太难了,你不可能期望所有的包都能改造兼容 esm,所以 ts 的编译结果还是设置成 commonjs 为好

总结:

  1. 这个 case 本身是通过 require 来加载 axios,所以和 Node 支不支持 esm 其实没有关系,所以 13.2 版本前后都是报错(13.2 开始 Node 原生支持 esm)
  2. 本身核心问题是 require 的入口文件没找对,而 axios 看起来已经打算完全拥抱 esm,main 中的导出都是 esm,commonjs 模块只在 exports 里配置了,而 exports 在 Node 13.7 才正式支持,所以在 13.7 以下版本,需要手动用 axios/dist/node/axios.cjs 来引