ESM 引入三方依赖的转换规则

npm 上,有数以万计的三方包,在开发过程中,不可能所有功能都自己从头写一遍,那样的话工作量实在太大了。

例如业务中需要一个 uuid 函数用于动态生成商品 id,如果自己写一个完全随机的、永不重复的、符合规范的 uuid,难度挺大,甚至可能比你要实现的业务功能还要复杂,因此最佳的解决方案就是搜索 uuid,然后在社区中找到最合适的三方包,引入即可。

大部分的三方包都按照 CommonJS 规范来进行导出,而不是最新的 ESM。npm 发包大神 sindresorhus 提出倡议,所有 npm 包都按照 ESM 的方式进行导出,他自己的很多包都不提供 CommonJS 版本了。

如果你还没听说过 sindresorhus 或用过他写的库,那么你应该不是搞前端的。

ESM 是全新的模块组织方式,在 CommonJS 中是不支持引入 ESM 文件的,如果库作者只提供了 ESM 版本的 package 的话,你的代码也需要用 ESM 来写:

实际上,无论我们写的源码是 ESM 还是 CommonJS,在打包的时候都是转换成 CommonJS 来处理的,例如一个 ESM 的三方库导出如下:

js 复制代码
export const name = 'esm'
export default { age: 12}

那么在打包过程中, Babel 会将其转换为下面的 CommonJS 模块:

js 复制代码
exports.__esModule = true
exports.name = 'esm'
exports.default = { age: 12 }

大家可以点击 Babel REPL 链接进行体验(也可以用 TypeScript 的 playground):

export 语法转换成 CommonJS 的规则是:

  • 设置 module.exports.__esModule 私有属性为 true,表示这是从 ESM 转换过来的
  • 如果 export 一个变量 xxx,就放到 module.exports.xxx 属性上
  • 如果 export default 一个变量,就放到 module.exports.default 属性上

而我们引入三方库的时候,以 React 为例:

js 复制代码
import React from 'react'
console.log(React)

Babel 会转换成:

js 复制代码
var _react = _interopRequireDefault(require('react'))
function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : { default: obj }
}
console.log(_react.default)

大家同样可以前往 Babel 的 REPL 中体验:

其实 import 语法转换成 CommonJS 的规则如下:

  • import * as X from 'Y' 转换为 X = require('Y')
  • import X from 'Y' 转换为 X = require('Y').default
  • import { X } from 'Y' 转换为 X = require('Y').X

能够确保导出的对象有一个 default 属性,从而不会得到 undefined,Babel 在外面又封装了 _interopRequireDefault 函数。

以 React 为例,打开 node_modules/react 目录,可以看到目录结构如下:

React 提供了 cjs 和 umd 两种产物,但是并没有 esm 的产物,打开 cjs/react.development.js 文件,可以看到都是以 exports.xxx 进行导出的:

注意并没有 exports.default 这一项。而在 TypeScript 中,默认情况下我们必须按照 import * 的语法引入 React 才行,下面的方式是拿不到值的:

js 复制代码
import React from 'react'
console.log(React) // undefined

因为 TypeScript 编译器把上面的代码转换成 CommonJS 后,导出的对象里面并没有 default 属性:

js 复制代码
var React = require('react')
console.log(React['default']) // undefined

并且会在代码中看到错误提示:

提示你可以用 allowSyntheticDefaultImports 方式来消除错误提示:

json 复制代码
{
  "compilerOptions": {
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true
  }
}

如果按照提示进行配置之后,发现果然没有报错了!但是千万不要高兴太早,因为官方是这样解释的:

--allowSyntheticDefaultImports: Allow default imports from modules with no default export. This does not affect code emit, just typechecking.

也就是说这个配置项的作用只是告诉编译器不要对 import 进行语法检查,并没有对代码进行兼容,所以拿到的依然是 undefined,编译阶段没问题了,但是代码运行时会报错。

那应该怎么办呢?这就是 esModuleInterop 配置项的作用了,开启之后,TypeScript 的转换规则就不一样了,大家可以对比一下:

  1. 未开启的转换效果
js 复制代码
import * as React from 'react'
console.log(React)
// 转换为
var React = require('react')
console.log(React)


import React from 'react'
console.log(React)
// 转换为
var React = require('react')
console.log(React['default'])


import { Component } from 'react'
console.log(Component)
// 转换为
var React = require('react')
console.log(React.Component)
  1. 开启后的转换效果:
js 复制代码
 import * as React from 'react'
 console.log(React)
 // 转换为
 var react = _importStar(require('react'))
 console.log(react)

 import React from 'react'
 console.log(React)
 // 转换为
 var react = __importDefault(require('react'))
 console.log(react['default'])


 import { Component } from 'react'
 console.log(Component)
 // 转换为
 var react = require('react')
 console.log(react.Component)

可以看到,区别在于增加了 importStarimportDefault 两个函数,这两个函数其实就是起到一层封装的作用:

js 复制代码
var __importDefault = function (mod) {
  return mod && mod.__esModule ? mod : { default: mod }
}

var __importStar = function (mod) {
  if (mod && mod.__esModule) return mod
  var result = {}
  for (var k in mod) {
    if (k !== 'default' && mod.hasOwnProperty(k)) {
      result[k] = mod[k]
    }
  }
  result['default'] = mod
  return result
}

首先把 import 导出的对象封装了一层, 放到了 default 属性里面,这样就肯定能够从 default 里面取到值了,这和 Babel 的转换规则是一样的:

js 复制代码
const f = () => console.log('hello')
module.exports = f

// 未处理前,导出的是函数,import 拿到 f.default 即 undefined
// 处理后,导出的是对象,import 拿到的是 f 函数

其次把 import * 导出的对象也封装了一层

js 复制代码
const f = () => console.log('hello')
module.exports = f

// 未处理前,导出的是函数,import * 拿到的是函数
// 处理后,导出的是对象,import * 拿到的是 { default: f }

因此,如果设置了 esModuleInterop,最好不要用 import * 语法了,因为可能拿不到正确的值,例如下面的写法,moment 就是一个对象,而不是函数了:

js 复制代码
import * as moment from 'moment'

当然,如果库作者本身导出的就是一个对象的话,是没问题的,因为:

js 复制代码
const f = () => console.log('hello')
module.exports = { f }

// 未处理前,导出的是函数,import * 拿到的是 { f } 对象
// 处理后,导出的是对象,import * 拿到的是 { default: f, f } 对象

有些库的作者,例如 classname 库,为了兼容上面两种 import 语法,在导出的对象中包含了 default 属性,就是它自己:

因此无论用户用下面哪种方法引入都可以:

js 复制代码
import classnames from 'classnames'
import * as classnames from 'classnames'

但我们不能要求库作者必须为大家增加默认导出,因为每个三方库的作者都有自己的编码习惯,我们自己在 npm 上发包的时候,就可以这么做兼容。

相关推荐
唐家小妹几秒前
【flex-grow】计算 flex弹性盒子的子元素的宽度大小
前端·javascript·css·html
涔溪2 分钟前
uni-app环境搭建
前端·uni-app
安冬的码畜日常6 分钟前
【CSS in Depth 2 精译_032】5.4 Grid 网格布局的显示网格与隐式网格(上)
前端·css·css3·html5·网格布局·grid布局·css网格布局
洛千陨7 分钟前
element-plus弹窗内分页表格保留勾选项
前端·javascript·vue.js
小小19928 分钟前
elementui 单元格添加样式的两种方法
前端·javascript·elementui
前端没钱28 分钟前
若依Nodejs后台、实现90%以上接口,附体验地址、源码、拓展特色功能
前端·javascript·vue.js·node.js
爱喝水的小鼠34 分钟前
AJAX(一)HTTP协议(请求响应报文),AJAX发送请求,请求问题处理
前端·http·ajax
叫我:松哥1 小时前
基于机器学习的癌症数据分析与预测系统实现,有三种算法,bootstrap前端+flask
前端·python·随机森林·机器学习·数据分析·flask·bootstrap
让开,我要吃人了1 小时前
HarmonyOS鸿蒙开发实战(5.0)网格元素拖动交换案例实践
前端·华为·程序员·移动开发·harmonyos·鸿蒙·鸿蒙开发
谢尔登1 小时前
Webpack 和 Vite 的区别
前端·webpack·node.js