共计 7023 个字符,预计需要花费 18 分钟才能阅读完成。
问题引入
很多 react
使用者在从 JS
迁徙到 TS
时,可能会遇到这样一个问题:
JS
引入 react
是这样的:
// js
import React from 'react'
而 TS
却是这样的:
// ts
import * as React from 'react'
如果间接在 TS
里改成 JS
一样的写法,在装置了 @types/react
的状况下,编辑器会抛出一个谬误:此模块是应用 “export
=” 申明的,在应用 “esModuleInterop
” 标记时只能与默认导入一起应用。
依据提醒,在 tsconfig.json
中设置 compilerOptions.esModuleInterop
为 true
,报错就隐没了。
要搞清楚这个问题的起因,首先须要晓得 JS
的模块零碎。罕用的 JS
的模块零碎有三个:
CommonJS
(后文简称cjs
)ES module
(后文简称esm
)UMD
(AMD
当初用得比拟少了,故疏忽掉)
babel
、TS
等编译器更加偏爱 cjs
。默认状况下,代码里写的 esm
都会被 babel
、TS
转成 cjs
。这个起因我揣测有以下几点:
cjs
呈现得比esm
更早,所以已有大量的npm
库是基于cjs
的(数量远高于esm
),比方react
cjs
有着十分成熟、风行、使用率高的runtime:Node.js
,而esm
的runtime
目前反对十分无限(浏览器端须要高级浏览器,node
须要一些稀奇古怪的配置和批改文件后缀名)- 有很多
npm
库是基于UMD
的,UMD
兼容cjs
,但因为esm
是动态的,UMD
无奈兼容esm
回到下面那个问题。关上 react
库的 index.js
:
能够看到 react
是基于 cjs
的,相当于:
module.exports = {
Children: Children,
Component: Component
}
而在 index.ts
中,写一段
import React from "react";
console.log(React);
默认状况下,通过 tsc
编译后的代码为:
"use strict";
exports.__esModule = true;
var react_1 = require("react");
console.log(react_1["default"]);
显然,打印进去的后果为 undefined
,因为 react
的 module.exports
中基本就没有 default
和这个属性。所以后续获取 React.createElement
、React.Component
天然都会报错。
这个问题引申进去的问题其实是,目前已有的大量的第三方库大多都是用 UMD / cjs
写的(或者说,应用的是他们编译之后的产物,而编译之后的产物个别都为 cjs
),但当初前端代码基本上都是用 esm
来写,所以 esm
与 cjs
须要一套规定来兼容。
-
esm
导入esm
- 两边都会被转为
cjs
- 严格依照
esm
的规范写,个别不会呈现问题
- 两边都会被转为
-
esm
导入cjs
- 援用第三方库时最常见,比方本文举例的
react
- 兼容问题的产生是因为
esm
有default
这个概念,而cjs
没有。任何导出的变量在cjs
看来都是module.exports
这个对象上的属性,esm
的default
导出也只是cjs
上的module.exports.default
属性而已 - 导入方
esm
会被转为cjs
- 援用第三方库时最常见,比方本文举例的
cjs
导入esm
(个别不会这样应用)-
cjs
导入cjs
- 不会被编译器解决
- 严格依照
cjs
的规范写,不会呈现问题
TS 默认编译规定
TS
对于 import
变量的转译规定为:
// before
import React from 'react';
console.log(React)
// after
var React = require('react');
console.log(React['default'])
// before
import {Component} from 'react';
console.log(Component);
// after
var React = require('react');
console.log(React.Component)
// before
import * as React from 'react';
console.log(React);
// after
var React = require('react');
console.log(React);
能够看到:
- 对于
import
导入默认导出的模块,TS
在读这个模块的时候会去读取下面的default
属性 - 对于
import
导入非默认导出的变量,TS
会去读这个模块下面对应的属性 - 对于
import *
,TS
会间接读该模块
TS
、babel
对 export
变量的转译规定为:(代码通过简化)
// before
export const name = "esm";
export default {name: "esm default",};
// after
exports.__esModule = true;
exports.name = "esm";
exports["default"] = {name: "esm default"}
能够看到:
- 对于
export default
的变量,TS
会将其放在module.exports
的default
属性上 - 对于
export
的变量,TS
会将其放在module.exports
对应变量名的属性上 - 额定给
module.exports
减少一个__esModule: true
的属性,用来通知编译器,这原本是一个esm
模块
TS 开启 esModuleInterop
后的编译规定
回到题目上,esModuleInterop
这个属性默认为 false
。改成 true
之后,TS
对于 import
的转译规定会产生一些变动(export
的规定不会变):
// before
import React from 'react';
console.log(React);
// after 代码通过简化
var react = __importDefault(require('react'));
console.log(react['default']);
// before
import {Component} from 'react';
console.log(Component);
// after 代码通过简化
var react = require('react');
console.log(react.Component);
// before
import * as React from 'react';
console.log(React);
// after 代码通过简化
var react = _importStar(require('react'));
console.log(react);
能够看到,对于默认导入和 namespace(*)
导入,TS
应用了两个 helper
函数来帮忙
// 代码通过简化
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;
};
首先看__importDefault
。它做的事件是:
- 如果指标模块是
esm
,就间接返回指标模块;否则将指标模块挂在一个对象的defalut
上,返回该对象。
比方下面的
import React from 'react';
// ------
console.log(React);
编译后再层层翻译:
// TS 编译
const React = __importDefault(require('react'));
// 翻译 require
const React = __importDefault({ Children: Children, Component: Component} );
// 翻译 __importDefault
const React = {default: { Children: Children, Component: Component} };
// -------
// 读取 React:console.log(React.default);
// 最初一步翻译:console.log({Children: Children, Component: Component})
这样就胜利获取了 react
模块的 modue.exports
。
再看 __importStar
。它做的事件是:
- 如果指标模块是
esm
,就间接返回指标模块。否则 - 将指标模块上所有的除了
default
以外的属性挪到result
上 - 将指标模块本人挂到
result.default
上
(相似下面 __importDefault
一样层层翻译剖析过程略过)
babel 编译的规定
babel
默认的转译规定和 TS
开启 esModuleInterop
的状况差不多,也是通过两个 helper
函数来解决的
// before
import config from 'config';
console.log(config);
// after
"use strict";
var _config = _interopRequireDefault(require("config"));
function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { "default": obj}; }
console.log(_config["default"]);
// before
import * as config from 'config';
console.log(config);
// after
"use strict";
function _typeof(obj) {"@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) {return typeof obj;} : function (obj) {return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;}, _typeof(obj); }
var config = _interopRequireWildcard(require("config"));
function _getRequireWildcardCache(nodeInterop) {if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) {return nodeInterop ? cacheNodeInterop : cacheBabelInterop;})(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) {if (!nodeInterop && obj && obj.__esModule) {return obj;} if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") {return { "default": obj}; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) {return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) {if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) {Object.defineProperty(newObj, key, desc); } else {newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) {cache.set(obj, newObj); } return newObj; }
console.log(config);
_interopRequireDefault
相似 __importDefault
_interopRequireWildcard
相似 __importStar
webpack
的模块解决
个别开发中,babel
和 TS
都会配合 webpack
来应用。个别是以下两种形式:
ts-loader
babel-loader
如果是应用 ts-loader
,那么 webpack
会将源代码先交给 tsc
来编译,而后解决编译后的代码。通过 tsc
编译后,所有的模块都会变成 cjs
,所以 babel
也不会解决,间接交给 webpack
来以 cjs
的形式解决模块。ts-loader
实际上就是调用了 tsc
命令,所以须要 tsconfig.json
配置文件
如果是应用的 babel-loader
,那么 webpack
不会调用 tsc
,tsconfig.json
也会被疏忽掉。而是间接用 babel
去编译 ts
文件。这个编译过程相比调用 tsc
会轻量许多,因为 babel
只会简略的移除所有 ts
相干的代码,不会做类型查看。个别在这种状况下,一个 ts
模块通过 babel
的 @babel/preset-env
和 @babel/preset-typescript
两个 preset
解决。后者做的事件很简略,仅仅去掉所有 ts
相干的代码,不会解决模块,而前者会将 esm
转成 cjs
。babel7
开始反对编译 ts
,这样一来,tsc
的存在就被弱化了。webpack
的 babel-loader
实际上就是调用了 babel
命令,须要 babel.config.js
配置文件
然而 webpack
的 babel-loader
在调用 babel.transform
时,传了这样一个 caller
选项:
从而导致 babel
保留了 esm
的 import export
tsc
、babel
能够将 esm
编译成 cjs
,然而cjs
只有在 node
环境下能力运行,而 webpack
本人领有一套模块机制,用来解决 cjs
esm
AMD
UMD
等各种各样的模块,并且为模块提供 runtime
。因而,须要在浏览器运行的代码最终还须要webpack
进行模块化解决
对于 cjs
援用 esm
,webpack
的编译机制比拟特地:
// 代码通过简化
// before
import cjs from "./cjs";
console.log(cjs);
// after
var cjs = __webpack_require__("./src/cjs.js");
var cjsdefault = __webpack_require__.n(cjs);
console.log(cjsdefault.a);
// before
import esm from "./esm";
console.log(esm);
// after
var esm = __webpack_require__("./src/esm.js");
console.log(esm["default"]);
其中 __webpack_require__
相似于 require
,返回指标模块的 module.exports
对象。__webpack_require__.n
这个函数接管一个参数对象,返回一个对象,该返回对象的 a
属性(我也不晓得为什么属性名叫 a
)会被设为参数对象。所以下面源代码的 console.log(cjs)
会打印出 cjs.js
的 module.exports
因为 webpack
为模块提供了一个 runtime
,所以 webpack
解决模块对于 webpack
本人而言很自在,在模块闭包里注入代表 module
require
exports
的变量就能够了
总结:
目前很多罕用的包是基于 cjs / UMD
开发的,而写前端代码个别是写 esm
,所以常见的场景是 esm
导入 cjs
的库。然而因为 esm
和 cjs
存在概念上的差别,最大的差别点在于 esm
有 default
的概念而 cjs
没有,所以在 default
上会出问题。
TS
babel
webpack
都有本人的一套解决机制来解决这个兼容问题,核心思想根本都是通过 default
属性的削减和读取
参考
esModuleInterop 到底做了什么?