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 上发包的时候,就可以这么做兼容。

相关推荐
shmily_ke12 分钟前
如何将vue2使用npm run build打包好的文件上传到服务器
服务器·前端·npm
陈随易39 分钟前
薪资跳动,VSCode实时显示今日打工收入
前端·后端·程序员
七灵微43 分钟前
【前端】SPA v.s. MPA
前端
fqq31 小时前
CSS级联样式(基础知识)备忘录
前端·css
前端小巷子1 小时前
JS深拷贝与浅拷贝
前端·javascript·面试
用户21411832636021 小时前
N8N教程-手把手教你搭建 N8N 自动化工作流:从安装到云部署全流程实战
前端·vue.js
Mintopia1 小时前
Three.js 环境贴图:给你的 3D 世界加个梦幻滤镜
前端·javascript·three.js
Mintopia1 小时前
JavaScript 里的光影魔术师:光线投射
前端·javascript·计算机图形学
呆呆的心2 小时前
深入探索 JavaScript 字符串处理:从基础到高阶 🚀
前端·javascript
zhangbao90s2 小时前
react-window:学习如何高效地渲染大型列表
前端·javascript·react.js