Skip to content

前言

开发中最折磨的就是esm和cjs的相互引用。esm因为default的存在,和cjs兼容始终有大大小小的问题。

js
// esm.js
export default {
    a: 1
}
export const b = { b: 1 }
export const c = { c: 1 }

// cjs.js   cjs导入
const esm = require('./esm.js') // 这个到底是导入的 export default 还是导入的 export的各种const。没办法兼容
// esm.a === ?   esm.b === ?

这个到底是导入的 export default 还是导入的 export的各种const。没办法兼容。但是反过来cjs转esm能行,只要esm不导入默认。

tsc和babel

tsc通过module配置能将ts代码转换成esm或者cjs兼容,其中还增加esModuleInterop支持。

js
// before
export default React
export const CreateElement = xx
// after
module.exports = {
    __esModule: true
    default: React,   // 恰恰也能兼容,因为没有export const default = xxx 或者 export { xxx as default }
    CreateElement
}

// before
import React from 'react'
import { CreateElement } from 'react'
import * as React from 'react'
// after
const React = require('react').default    // 一般用cjs写代码不会有default这个属性。所以这玩意儿基本都报错。
const CreateElement = require('react').CreateElement   // 没毛病
const React = require('react')  // 对应的是module.exports

tsc新增了esModuleInterop来兼容旧的模块、这个选项会新增两个helper函数用于import(为啥不是export,因为没办法去控制第三方的export)

js
const React = _importDefault(require('react')).default // 对于默认导入使用这个, 也能和原来的保持兼容
const CreateElement = require('react').CreateElement   // 不变化
const React =  _importStar(require('react')) // 对于 * 导入使用这个函数

function _importDefault(mod) {
    return (mod && mod.__esModule) ? mod : { default: mod }  // 
}

function _importStar(mod) {
    if(mod && mod.esModule) {
        return mod // 无需处理
    }

    // 第三方模块,将default属性添加到自身。
    const result = {};     // 添加这个为了防止污染
    for(key in mod) {
        if(key !== 'default' && mod.hasOwnProperty(key)) {
            result[key] = mod[key]
        }
    }

    result['default'] = mod
}

对于tsc编译的模块总是会有__esModule 和 default 属性,能满足正常的调用,对于第三方模块因为没有exports.default 通过这个{ default: mod } 来补全。 对于星导入,实际就是在exports 上面添加了default属性等于自身

babel

babel和esm实现原理差不多,都是实现以上helper函数。@babel/preset-env 中modules配置编译后代码,默认是auto(交给babel-loader等工具来配置),也有cjs等选项,或者是false使用esm。

webpack

webpack也能实现esm转cjs(不能实现esm => esm)。处理方式有不少区别

  1. 对于需要导出的模块如果是esm, 和tsc等相同,将默认导出放到exports.default上,同时添加了exports.__esModule = true在引入时也是取值 exports.default
  2. 对于需要导出的模块是cjs,而且是未修改module的场景,导出没什么区别,就是简单的require
  3. 对于需要导出的模块是cjs,而且是修改module的场景(module.exports),会将module.exports.default 变成一个函数返回,exports其他的属性变成函数的属性,恰恰能兼容,举例如下。
js
// webpack 第三点伪代码
// a.js
module.exports = {
    b: 3
}

//  b.js
import B, { b } from './a.js'
// 会解析成如下 
const B = importDefaultHelper(require('./a.js'))();
const b = require('./a.js').b

function importDefaultHelper(exports) {
    const getter = () => exports
    return getter
}

最终webpack编译的源码不是很多,不难如下

js
/******/ (() => { // webpackBootstrap
/******/ 	var __webpack_modules__ = ({

/***/ "./src/b.js":
/*!******************!*\
  !*** ./src/b.js ***!
  \******************/
/***/ ((module) => {

module.exports = {
    b: 1
}

/***/ }),

/***/ "./src/c.js":
/*!******************!*\
  !*** ./src/c.js ***!
  \******************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

"use strict";
__webpack_require__.r(__webpack_exports__);  // 赋值__esModule
/* harmony export */ __webpack_require__.d(__webpack_exports__, {  // __webpack_exports__ 初始化是空的module的初始化流程,这一步将esm模块组装好
/* harmony export */   "c": () => (/* binding */ c),
/* harmony export */   "default": () => (__WEBPACK_DEFAULT_EXPORT__)
/* harmony export */ });
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (3);
const c = 3

/***/ }),

/***/ "./src/d.js":
/*!******************!*\
  !*** ./src/d.js ***!
  \******************/
/***/ ((__unused_webpack_module, exports) => {

exports.b = 1

/***/ })

/******/ 	});
/************************************************************************/
/******/ 	// The module cache
/******/ 	var __webpack_module_cache__ = {};
/******/ 	
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/ 		// Check if module is in cache
/******/ 		var cachedModule = __webpack_module_cache__[moduleId];
/******/ 		if (cachedModule !== undefined) {
/******/ 			return cachedModule.exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = __webpack_module_cache__[moduleId] = {
/******/ 			// no module.id needed
/******/ 			// no module.loaded needed
/******/ 			exports: {}
/******/ 		};
/******/ 	
/******/ 		// Execute the module function
/******/ 		__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/ 	
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/ 	
/************************************************************************/
/******/ 	/* webpack/runtime/compat get default export */
/******/ 	(() => {
/******/ 		// getDefaultExport function for compatibility with non-harmony modules
/******/ 		__webpack_require__.n = (module) => {
/******/ 			var getter = module && module.__esModule ?
/******/ 				() => (module['default']) :
/******/ 				() => (module);
/******/ 			__webpack_require__.d(getter, { a: getter });
/******/ 			return getter;
/******/ 		};
/******/ 	})();
/******/ 	
/******/ 	/* webpack/runtime/define property getters */
/******/ 	(() => {
/******/ 		// define getter functions for harmony exports
/******/ 		__webpack_require__.d = (exports, definition) => {
/******/ 			for(var key in definition) {
/******/ 				if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ 					Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ 				}
/******/ 			}
/******/ 		};
/******/ 	})();
/******/ 	
/******/ 	/* webpack/runtime/hasOwnProperty shorthand */
/******/ 	(() => {
/******/ 		__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ 	})();
/******/ 	
/******/ 	/* webpack/runtime/make namespace object */
/******/ 	(() => {
/******/ 		// define __esModule on exports
/******/ 		__webpack_require__.r = (exports) => {
/******/ 			if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ 				Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ 			}
/******/ 			Object.defineProperty(exports, '__esModule', { value: true });
/******/ 		};
/******/ 	})();
/******/ 	
/************************************************************************/
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be in strict mode.
(() => {
"use strict";
/*!******************!*\
  !*** ./src/a.js ***!
  \******************/
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _b__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./b */ "./src/b.js");
/* harmony import */ var _b__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_b__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var _d__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./d */ "./src/d.js");
/* harmony import */ var _c__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./c */ "./src/c.js");

console.log((_b__WEBPACK_IMPORTED_MODULE_0___default()), _b__WEBPACK_IMPORTED_MODULE_0__.b, _c__WEBPACK_IMPORTED_MODULE_2__.c, _d__WEBPACK_IMPORTED_MODULE_1__, _d__WEBPACK_IMPORTED_MODULE_1__.d)
})();

/******/ })()
;
//# sourceMappingURL=main.js.map

webpack在和tsc和babel配合时,流程如下。webpack能够分析模块依赖,当查询到依赖的模块是ts时,调用ts-loader或者babel-loader对单个模块进行解析。这个时候tsc或babel本身用不用处理模块的依赖? 当然ts总是esm兼容的,如果处理依赖成esm。最终webpack处理的就是esm模块,如果处理成cjs(就会添加之前那些helper函数,不添加会怎么样),这时候webpack会当做cjs来处理(webpack本质就是处理cjs)。

不添加会怎么样? webpack只会处理cjs,模块间引用都是tsc处理好扔给webpack,比如也会添加 tsc独到的require('xxx').default,不会想webpack一样使用default()。 所以最好的就是处理esm,让webpack来处理模块间的引用关系。包括babel也是这个逻辑。

回来来看,发现各种loader还处理了模块间的引用关系,这也是为什么ts代码中不支持webpack别名的原因(tsc import时还需要判断类型,tsc并不识别webpack中的别名,)。 使用babel能识别别名吗,当然能,babel是直接将ts中的ts部分移除掉。不会处理任何的模块相关的 import "@/xxx" 原封不动传给webpack

再来看看如果babel处理js遇到别名怎么办,首先helper函数是不管目的模块的格式的,不管是esm还是cjs,统统加上_importDefault(require('@/b'))。 所以也能实现别名

rollup