当前位置:   article > 正文

typescript的tsconfig中esModuleInterop的到底做了什么?

esmoduleinterop

问题引入

很多 react 使用者在从 JS 迁移到 TS 时,可能会遇到这样一个问题:

JS 引入 react 是这样的:

  1. // js
  2. import React from 'react'

而 TS 却是这样的:

  1. // ts
  2. 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。这个原因我推测有以下几点:

  1. cjs 出现得比 esm 更早,所以已有大量的 npm 库是基于 cjs 的(数量远高于 esm),比如 react
  2. cjs 有着非常成熟、流行、使用率高的 runtime:Node.js,而 esm 的 runtime 目前支持非常有限(浏览器端需要高级浏览器,node 需要一些稀奇古怪的配置和修改文件后缀名)
  3. 有很多 npm 库是基于 UMD 的,UMD 兼容 cjs,但因为 esm 是静态的,UMD 无法兼容 esm

回到上面那个问题。打开 react 库的 index.js:

可以看到 react 是基于 cjs的,相当于:

  1. module.exports = {
  2. Children: Children,
  3. Component: Component
  4. }

而在 index.ts 中,写一段

  1. import React from "react";
  2. console.log(React);

默认情况下,经过 tsc 编译后的代码为:

  1. "use strict";
  2. exports.__esModule = true;
  3. var react_1 = require("react");
  4. 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 变量的转译规则为:

  1. // before
  2. import React from 'react';
  3. console.log(React)
  4. // after
  5. var React = require('react');
  6. console.log(React['default'])
  7. // before
  8. import {Component} from 'react';
  9. console.log(Component);
  10. // after
  11. var React = require('react');
  12. console.log(React.Component)
  13. // before
  14. import * as React from 'react';
  15. console.log(React);
  16. // after
  17. var React = require('react');
  18. console.log(React);

可以看到:

  • 对于 import 导入默认导出的模块,TS 在读这个模块的时候会去读取上面的 default 属性
  • 对于 import 导入非默认导出的变量,TS 会去读这个模块上面对应的属性
  • 对于 import *,TS 会直接读该模块

TS、babel 对 export 变量的转译规则为:(代码经过简化)

  1. // before
  2. export const name = "esm";
  3. export default {
  4. name: "esm default",
  5. };
  6. // after
  7. exports.__esModule = true;
  8. exports.name = "esm";
  9. exports["default"] = {
  10. name: "esm default"
  11. }

可以看到:

  • 对于 export default 的变量,TS 会将其放在 module.exports 的 default 属性上
  • 对于 export 的变量,TS 会将其放在 module.exports 对应变量名的属性上
  • 额外给 module.exports 增加一个 __esModule: true 的属性,用来告诉编译器,这本来是一个 esm 模块

TS 开启 esModuleInterop 后的编译规则

回到标题上,esModuleInterop 这个属性默认为 false。改成 true 之后,TS 对于 import 的转译规则会发生一些变化(export 的规则不会变):

  1. // before
  2. import React from 'react';
  3. console.log(React);
  4. // after 代码经过简化
  5. var react = __importDefault(require('react'));
  6. console.log(react['default']);
  7. // before
  8. import {Component} from 'react';
  9. console.log(Component);
  10. // after 代码经过简化
  11. var react = require('react');
  12. console.log(react.Component);
  13. // before
  14. import * as React from 'react';
  15. console.log(React);
  16. // after 代码经过简化
  17. var react = _importStar(require('react'));
  18. console.log(react);

可以看到,对于默认导入和 namespace(*)导入,TS 使用了两个 helper 函数来帮忙

  1. // 代码经过简化
  2. var __importDefault = function (mod) {
  3. return mod && mod.__esModule ? mod : { default: mod };
  4. };
  5. var __importStar = function (mod) {
  6. if (mod && mod.__esModule) {
  7. return mod;
  8. }
  9. var result = {};
  10. for (var k in mod) {
  11. if (k !== "default" && mod.hasOwnProperty(k)) {
  12. result[k] = mod[k]
  13. }
  14. }
  15. result["default"] = mod;
  16. return result;
  17. };

首先看__importDefault。它做的事情是:

  1. 如果目标模块是 esm,就直接返回目标模块;否则将目标模块挂在一个对象的 defalut 上,返回该对象。

比如上面的

  1. import React from 'react';
  2. // ------
  3. console.log(React);

编译后再层层翻译:

  1. // TS 编译
  2. const React = __importDefault(require('react'));
  3. // 翻译 require
  4. const React = __importDefault( {Children:Children,Component:Component} );
  5. // 翻译 __importDefault
  6. const React = { default: {Children:Children,Component:Component} };
  7. // -------
  8. // 读取 React:
  9. console.log(React.default);
  10. // 最后一步翻译:
  11. console.log({Children:Children,Component:Component})

这样就成功获取了 react 模块的 modue.exports。

再看 __importStar。它做的事情是:

  1. 如果目标模块是 esm,就直接返回目标模块。否则
  2. 将目标模块上所有的除了 default 以外的属性挪到 result 上
  3. 将目标模块自己挂到 result.default 上

(类似上面 __importDefault 一样层层翻译分析过程略过)

babel 的规则

babel 默认的转译规则和 TS 开启 esModuleInterop 的情况差不多,也是通过两个 helper 函数来处理的

_interopRequireDefault 类似 __importDefault

_interopRequireWildcard 类似 __importStar

特殊的 webpack

一般开发中,babel 和 TS 都会配合 webpack 来使用。而 TS 和 webpack 的结合有两种方式:

  • ts-loader
  • @babel/preset-typescript

如果是使用 ts-loader,那么 webpack 会将源代码先交给 tsc 来编译,然后处理编译后的代码。经过 tsc 编译后,所有的模块都会变成 cjs,所以 babel 也不会处理,直接交给 webpack 来以 cjs 的方式处理模块。

如果是使用的 @babel/preset-typescript,那么 webpack 不会调用 tsc,tsconfig.json 也会被忽略掉。而是直接用 babel 去编译 ts 文件。这个编译过程相比调用 tsc 会轻量许多,因为 babel 只会简单的移除所有 ts 相关的代码,不会做类型检查。一般在这种情况下,一个 ts 模块经过 babel 的 @babel/preset-env 和 @babel/preset-typescript 两个 preset 处理。后者做的事情很简单,仅仅去掉所有 ts 相关的代码,不会处理模块,而前者会将 esm 转成 cjs。然而 webpack 的 babel-loader 在调用 babel.transform 时,传了这样一个 caller 选项:

从而导致 babel 保留了 esm 的 import export

因为 webpack 自己有一套模块机制,用来处理 cjs esm AMD UMD 等各种各样的模块。webpack 希望自己直接来处理用户写的模块,而不是让 babel 处理一遍再交给自己处理一遍。

对于 cjs 引用 esm,webpack 的编译机制比较特别:

  1. // 代码经过简化
  2. // before
  3. import cjs from "./cjs";
  4. console.log(cjs);
  5. // after
  6. var cjs = __webpack_require__("./src/cjs.js");
  7. var cjsdefault = __webpack_require__.n(cjs);
  8. console.log(cjsdefault.a);
  9. // before
  10. import esm from "./esm";
  11. console.log(esm);
  12. // after
  13. var esm = __webpack_require__("./src/esm.js");
  14. 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 属性的增添和读取

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/903732
推荐阅读
相关标签
  

闽ICP备14008679号