乐趣区

关于typescript:tscbabelwebpack对模块导入导出的处理

问题引入

很多 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.esModuleInteroptrue,报错就隐没了。

要搞清楚这个问题的起因,首先须要晓得 JS 的模块零碎。罕用的 JS 的模块零碎有三个:

  • CommonJS(后文简称 cjs
  • ES module(后文简称 esm
  • UMD

AMD 当初用得比拟少了,故疏忽掉)

babelTS 等编译器更加偏爱 cjs。默认状况下,代码里写的 esm 都会被 babelTS 转成 cjs。这个起因我揣测有以下几点:

  1. cjs 呈现得比 esm 更早,所以已有大量的 npm 库是基于 cjs 的(数量远高于 esm),比方 react
  2. cjs 有着十分成熟、风行、使用率高的 runtime:Node.js,而 esmruntime 目前反对十分无限(浏览器端须要高级浏览器,node 须要一些稀奇古怪的配置和批改文件后缀名)
  3. 有很多 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,因为 reactmodule.exports 中基本就没有 default 和这个属性。所以后续获取 React.createElementReact.Component 天然都会报错。

这个问题引申进去的问题其实是,目前已有的大量的第三方库大多都是用 UMD / cjs 写的(或者说,应用的是他们编译之后的产物,而编译之后的产物个别都为 cjs),但当初前端代码基本上都是用 esm 来写,所以 esmcjs 须要一套规定来兼容。

  • esm 导入 esm

    • 两边都会被转为 cjs
    • 严格依照 esm 的规范写,个别不会呈现问题
  • esm 导入 cjs

    • 援用第三方库时最常见,比方本文举例的 react
    • 兼容问题的产生是因为 esmdefault 这个概念,而 cjs 没有。任何导出的变量在 cjs 看来都是 module.exports 这个对象上的属性,esmdefault 导出也只是 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 会间接读该模块

TSbabelexport 变量的转译规定为:(代码通过简化)

 // 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.exportsdefault 属性上
  • 对于 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。它做的事件是:

  1. 如果指标模块是 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。它做的事件是:

  1. 如果指标模块是 esm,就间接返回指标模块。否则
  2. 将指标模块上所有的除了 default 以外的属性挪到 result
  3. 将指标模块本人挂到 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 的模块解决

个别开发中,babelTS 都会配合 webpack 来应用。个别是以下两种形式:

  • ts-loader
  • babel-loader

如果是应用 ts-loader,那么 webpack 会将源代码先交给 tsc 来编译,而后解决编译后的代码。通过 tsc 编译后,所有的模块都会变成 cjs,所以 babel 也不会解决,间接交给 webpack 来以 cjs 的形式解决模块。ts-loader实际上就是调用了 tsc 命令,所以须要 tsconfig.json 配置文件

如果是应用的 babel-loader,那么 webpack 不会调用 tsctsconfig.json 也会被疏忽掉。而是间接用 babel 去编译 ts 文件。这个编译过程相比调用 tsc 会轻量许多,因为 babel 只会简略的移除所有 ts 相干的代码,不会做类型查看。个别在这种状况下,一个 ts 模块通过 babel@babel/preset-env@babel/preset-typescript 两个 preset 解决。后者做的事件很简略,仅仅去掉所有 ts 相干的代码,不会解决模块,而前者会将 esm 转成 cjsbabel7开始反对编译 ts,这样一来,tsc 的存在就被弱化了。webpackbabel-loader实际上就是调用了 babel 命令,须要 babel.config.js 配置文件

然而 webpackbabel-loader 在调用 babel.transform 时,传了这样一个 caller 选项:

从而导致 babel 保留了 esmimport export

tscbabel能够将 esm 编译成 cjs,然而cjs 只有在 node 环境下能力运行,而 webpack 本人领有一套模块机制,用来解决 cjs esm AMD UMD 等各种各样的模块,并且为模块提供 runtime。因而,须要在浏览器运行的代码最终还须要webpack 进行模块化解决

对于 cjs 援用 esmwebpack 的编译机制比拟特地:

// 代码通过简化
// 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.jsmodule.exports

因为 webpack 为模块提供了一个 runtime,所以 webpack 解决模块对于 webpack 本人而言很自在,在模块闭包里注入代表 module require exports 的变量就能够了

总结:

目前很多罕用的包是基于 cjs / UMD 开发的,而写前端代码个别是写 esm,所以常见的场景是 esm 导入 cjs 的库。然而因为 esmcjs 存在概念上的差别,最大的差别点在于 esmdefault 的概念而 cjs 没有,所以在 default 上会出问题。

TS babel webpack 都有本人的一套解决机制来解决这个兼容问题,核心思想根本都是通过 default 属性的削减和读取

参考

esModuleInterop 到底做了什么?

退出移动版