在 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 的转换规则就不一样了,大家可以对比一下:
- 未开启的转换效果
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)
- 开启后的转换效果:
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)
可以看到,区别在于增加了 importStar
和 importDefault
两个函数,这两个函数其实就是起到一层封装的作用:
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 上发包的时候,就可以这么做兼容。