如何回答cjs和esm差异

1,324 阅读3分钟

 作者提肛

  • webpack里 esm 如何和 cjs 兼容

  • 在ts中 cjs 如何和 esm 兼容

  • ts中cjs为什么能用import

什么是cjs和ESM

ESM 全称是 ESModuleweb 系统的模块化,ESModule 输出的是 值的引用

cjs 全称 CommonJSnodejs 的模块化,CommonJS输出的是 值的拷贝

这里不做过多的介绍,更多介绍可以自己从掘金上搜索相关差异

webpac里esm是如何做向下兼容cjs

(1) webpack是如何打包cjs方式

先看下webpack的打包出来的关键内容

var __webpack_modules__ = {
  './src/index.js': ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 
    eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _esm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./esm */ \"./src/esm.js\");\nconst v  = __webpack_require__(/*! ./cjs */ \"./src/cjs.js\");\n\nv()\n;(0,_esm__WEBPACK_IMPORTED_MODULE_0__[\"default\"])()\n\n//# sourceURL=webpack://webpack/./src/index.js?");
  }),
  './src/esm.js': ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
    eval("__webpack_require__.r(__webpack_exports__); __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\nfunction m (){\n  console.log('>>><><><')\n}\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (m);\n\n//# sourceURL=webpack://webpack/./src/esm.js?");
  }),
  './src/cjs.js': ((module) => {
    eval("function v() {\n  console.log('>>>>>>>')\n}\nmodule.exports = v\n\n//# sourceURL=webpack://webpack/./src/cjs.js?");
  })
}

function __webpack_require__(moduleId){
    var module = {
        exports: {}
    }

    __webpack_modules__[moduleId](module, module.exports, __webpack_require__)

   return module.exports

}

(()=>{
    __webpack_require__.d = () => {
        __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_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
    __webpack_require__.r = (exports) => {
        Object.defineProperty(exports, '__esModule', { value: true });
    }
})()
var __webpack_exports__ = __webpack_require__("./src/index.js");

PS: 这里对webpack打包机制有问题的同学,可以找下webpack相关知识

其实 webpack 都是通过 module.exports 做的兼容处理。

当处理cjs时候。

模块化的代码最后会打包成一个对象结构,然后通过路径key(./src/index)找到对应的string代码块,然后会执行 __webpack_require__ 函数

这里 cjs 模块化分为两种方式一种是 module.exportsexports

接下来看下 __webpack_require__ 是如何执行 module.exports 方式

var __webpack_modules__ = {
    './src/cjs.js': (module) => {
        let v = 1
        module.exports = v
    }
}

function __webpack_require__(src){

let module = {
    exports:{}
   }
__webpack_modules__[src](module)return module.exports}
__webpack_require__('./src/cjs.js'

执行 __webpack_require__ 的时候会把module对象传进去,类似module.exports = v,最后return module.exports

接下来看下 __webpack_require__ 是如何执行 exports 方式

var __webpack_modules__ = {
    './src/cjs.js': (module, exports) => {
        let v = 1
        exports = v
    }
}

function __webpack_require__(src){

let module = {
    exports:{}
   }

__webpack_modules__[src](module, module.exports)
return module.exports
}
__webpack_require__('./src/cjs.js'

当 exports 方式时,执行 __webpack_require__ 函数时会多传入一个参数,第二个参数会直接传入 module.exports

其实就是 v = exports = module.exports

(2) webpack如何打包esm

先展示下代码

var __webpack_modules__ = {
    './src/cjs.js': (module, exports, __webpack_require__) => {        
        __webpack_require__.r(exports)
        __webpack_require__(__webpack_require__.n(exports: {
            default: 1
        }))
    }
}

function __webpack_require__(src){

let module = {
    exports:{}
   }

__webpack_modules__[src](module, module.exports, __webpack_require__)

return module.exports
}


__webpack_require__.r = (exports) => {
  Object.defineProperty(exports, '__esModule', { value: true })
}
__webpack_require__.n = (exports) => {
exports.__esModule ? exports['default'] : exports}__webpack_require__('./src/cjs.js'

我这里做了非常大的简化,

其实说白了就是当检测是__esModule 时,就会从对象中再多找一层 default

总结:在webpack中如果是cjs的就会返回module.exports 当遇到检测到是esm时,就会返回

module.exports.default 。其实就是多了一次default

在ts中cjs如何向上兼容esm

咋们还是先上代码

var __importDefault = function (mod) {    
    return (mod && mod.__esModule) ? mod : { "default": mod };
};

Object.defineProperty(exports, "__esModule", { value: true });

const cjs = require("cjs.js");const esm = __importDefault(require("esm.js"));

总结:ts会把 import方式最后打包成require方式 ,ts遇到当前是esm模式的就会把require方法放到**__importDefault** 函数里面,其实本质也是多找一层defualt

那ts是如何做到cjs支持import方式导入的呢

ts中cjs为什么能用import

简单看下ast解析cjs和esm的结果

import importCjs from './cjs.js';

const requireCjs = require('./cjs.js')

其实在ast里面,我是指将import cjs from './cjs'转换成const cjs = require('cjs')

我们看下具体的ast代码实现

const plugin = declare((api) => {
  return {    
        visitor: {      
            ImportDeclaration(path, state) {        
                const specifiers = path.get('specifiers.0')        
                const source = path.get('source').toString();        
                const name = specifiers.get('local').toString()        
                const v = api.template.ast(`const ${name} = require(${source})`)
                path.replaceWith(v)      
    }        
    }  
    }})
module.exports = plugin

总结:找到****cjs,然后找到path路径,通过api.template直接转换为require就好了