Appearance
前言
开发中最折磨的就是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.exportstsc新增了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)。处理方式有不少区别
- 对于需要导出的模块如果是esm, 和tsc等相同,将默认导出放到exports.default上,同时添加了exports.__esModule = true在引入时也是取值 exports.default
- 对于需要导出的模块是cjs,而且是未修改module的场景,导出没什么区别,就是简单的require
- 对于需要导出的模块是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.mapwebpack在和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'))。 所以也能实现别名
