7

problem introduction

Many react users may encounter such a problem when migrating from JS to TS :

JS introduces react like this:

// js
import React from 'react'

And TS is like this:

// ts
import * as React from 'react'

If you directly change JS TS the same way, when @types/react is installed, the editor will throw an error: This module is declared with " export =" and can only be used with " 06204b7fd69e00 " when using the " esModuleInterop " flag. Used with default imports.

According to the prompt, set compilerOptions.esModuleInterop to true in tsconfig.json , and the error will disappear.

To figure out the cause of this problem, you first need to know the module system of JS . There are three commonly used module systems of JS :

  • CommonJS (hereinafter referred to as cjs )
  • ES module (hereinafter referred to as esm )
  • UMD

( AMD is used less now, so ignore it)

Compilers such as babel and TS prefer cjs . By default, esm written in the code will be converted to cjs by babel and TS . I'm guessing the reasons for this are as follows:

  1. cjs appeared earlier than esm , so there are already a lot of npm libraries based on cjs (the number is much higher than esm ), such as react
  2. cjs has a very mature, popular and highly used runtime:Node.js , while esm of runtime currently supports very limited (the browser side requires an advanced browser, and node requires some weird configuration and modification of the file suffix)
  3. There are many npm libraries based on UMD , UMD is compatible with cjs , but because esm is static, UMD cannot be compatible with esm

Back to the question above. Open react of the index.js library:

img

You can see that react is based on cjs , which is equivalent to:

module.exports = {
  Children: Children,
  Component: Component
}

And in index.ts , write a paragraph

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

By default, the code compiled with tsc is:

"use strict";
exports.__esModule = true;
var react_1 = require("react");
console.log(react_1["default"]);

Obviously, the printed result is undefined , because there is no default and this attribute in react of module.exports . Therefore, subsequent acquisitions of React.createElement and React.Component will naturally report errors.

The problem derived from this question is actually that most of the existing third-party libraries are mostly written with UMD / cjs (or, they use their compiled products, and the compiled products are generally cjs ), but Now the front-end code is basically written with esm , so esm and cjs need a set of rules to be compatible.

  • esm import esm

    • Both sides will be converted to cjs
    • Write in strict accordance with the standard of esm , generally there will be no problems
  • esm import cjs

    • It is most common to refer to third-party libraries, such as react in the example in this article
    • compatibility problem arises because esm has the concept of default , but cjs does not. Any exported variable appears to cjs as an attribute on the object module.exports , and the esm export of default is only the cjs attribute on module.exports.default
    • The importer esm will be converted to cjs
  • cjs import esm (generally not used like this)
  • cjs import cjs

    • will not be processed by the compiler
    • Write strictly according to the standard of cjs , there will be no problem

TS default compilation rules

The translation rules of TS for import variables are:

 // 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);

can be seen:

  • For import importing the default exported module, TS will read the above default attribute when reading this module
  • For import importing non-default exported variables, TS will read the corresponding properties of this module
  • For import * , TS will directly read the module

The translation rules of TS and babel to export variables are: (the code is simplified)

 // before
 export const name = "esm";
 export default {
   name: "esm default",
 };

 // after
 exports.__esModule = true;
 exports.name = "esm";
 exports["default"] = {
   name: "esm default"
 }

can be seen:

  • For a variable of export default , TS will put it on the module.exports property of default
  • For the variable of export , TS will put it on the property of module.exports corresponding to the variable name
  • Add an additional attribute of __esModule: true to module.exports to tell the compiler that this is originally a esm module

Compilation rules after TS opens esModuleInterop

Back to the title, the property esModuleInterop defaults to false . After changing to true , the translation rules of TS for import will undergo some changes (the rules of export will not change):

 // 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);

As you can see, for the default import and the namespace(*) import, TS uses two helper functions to help

// 代码经过简化
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;
};

first look at __importDefault . What it does is:

  1. If the target module is esm , return the target module directly; otherwise, hang the target module on defalut of an object, and return the object.

such as the above

import React from 'react';

// ------

console.log(React);

After compiling, translate layer by layer:

// 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 })

In this way, the modue.exports of the react module has been successfully obtained.

__importStar again. What it does is:

  1. If the target module is esm , return the target module directly. otherwise
  2. Move all attributes except default on the target module to result
  3. Hang the target module itself on result.default

(Similar to the above __importDefault , the layer-by-layer translation analysis process is skipped)

Rules for babel compilation

The default translation rule of babel is similar to the case of TS opening esModuleInterop , and it is also handled by two helper functions.

// 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 similar to __importDefault

_interopRequireWildcard similar to __importStar

webpack module handling

In general development, babel and TS will be used together with webpack . Generally there are two ways:

  • ts-loader
  • babel-loader

If ts-loader is used, then webpack will hand over the source code to tsc for compilation, and then process the compiled code. After compiling with tsc , all modules will become cjs , so babel will not be processed, and directly handed over to webpack to process modules in the way of cjs . ts-loader actually calls the tsc command, so the tsconfig.json configuration file is required

If babel-loader is used, then webpack will not call tsc , and tsconfig.json will be ignored. Instead, use babel to compile ts file directly. This compilation process is much lighter than calling tsc , because babel will simply remove all ts -related code without type checking. Generally in this case, a ts module is processed by two babel of @babel/preset-env and @babel/preset-typescript of preset . What the latter does is very simple, it just removes all ts related codes and does not process modules, while the former converts esm to cjs . babel7 began to support compiling ts , so the existence of tsc was weakened. webpack of babel-loader actually calls the babel command, which requires the babel.config.js configuration file

However, when webpack of babel-loader called babel.transform , it passed such a caller option:

img

resulting in babel retaining esm of import export

tsc , babel can esm compiled into cjs , but cjs only node to run under the environment, and webpack own set of modules mechanism to deal with cjs esm AMD UMD other kinds of modules and provides a module runtime . Therefore, the code that needs to run in the browser eventually needs webpack for modular processing

For cjs to refer to esm , the compilation mechanism of webpack is special:

// 代码经过简化
// 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"]);

where __webpack_require__ is similar to require and returns the module.exports object of the target module. __webpack_require__.n This function receives a parameter object and returns an object. The a property of the returned object (I don't know why the property is named a ) will be set as the parameter object. So console.log(cjs) of the above source code will print cjs.js of module.exports

Since webpack provides a runtime for the module, the webpack processing module is very free for webpack itself, and it is enough to inject a variable representing module require exports into the module closure

Summarize:

At present, many commonly used packages are developed based on cjs / UMD , and the front-end code is generally written by esm , so the common scenario is that esm imports the library of cjs . However, due to the conceptual difference between esm and cjs , the biggest difference is that esm has the concept of default and cjs does not, so there will be problems on default .

TS babel webpack have their own set of processing mechanisms to deal with this compatibility problem. The core idea is basically to add and read the attributes of default .

refer to

What exactly does esModuleInterop do?


记得要微笑
1.9k 声望4.5k 粉丝

知不足而奋进,望远山而前行,卯足劲,不减热爱。