彻底理解模块化

451 阅读4分钟

这篇文章主要从以下几个方面讲解模块化从使用到实现:

  1. ESM的使用和实现
  2. CommonJS的使用和实现
  3. ESM和CommonJS有什么区别
  4. 项目中如何共用ESM和CommonJS
  5. UMD是如何实现的

ESM的使用和实现

ES6模块基本语法

从其他模块导出
1. export * from './module'
2. export { name1, name2 } from './module'
3. export { v1 as name1, v2 as name2 } from './module'
4. export { default as name } from './module'
导出自己模块的属性
1. export const NAME = 'leo'
2. export { NAME }
3. export { v1 as NAME }
4. export default {}
5. export { v1 as default, v2 }

default是个啥?跟export的其他属性什么关系?

default情况webpack打包情况

写一个简单的demo:

<!-- 1.js -->
import m from './2'
console.log(m)

<!-- 2.js -->
export default { name: 'leo' }

来看下webpack对这两个文件打包之后变成啥样?👇

<!-- 1.js -->
var _2_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./2.js");
console.log(_2_js__WEBPACK_IMPORTED_MODULE_0__["default"])


<!-- 2.js -->
__webpack_require__.r(__webpack_exports__);
__webpack_exports__["default"] = ({
  name: 'leo'
});

就这样完事了。一个设置了default,一个拿default来用

属性导出情况webpack打包情况

下面看看导出属性是怎样👇:

<!-- 1.js -->
var _2_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./2.js");
console.log(_2_js__WEBPACK_IMPORTED_MODULE_0__["name"])


<!-- 2.js -->
__webpack_require__.d(
  __webpack_exports__,
  "name",
  function() { return name; }
);
const name = 'leo'

可以看出default是赋值到__webpack_exports__上的,而单个属性是使用defineProperty定义getter的,这么做是为什么?
ES6导出的是值引用,default是引用类型,而一般变量是基本类型,所以基本类型用getter
这么做因为采用了闭包的原理所以name改变之后,通过getter获取仍是最新值,那么对于default的情况仍然适用吗?

如果export default { name }, 那么又是如何处理的?
<!-- 稍微修改下2.js -->
let name = 'leo'
export default { name }
name = 'hello'

<!-- webpack打包之后,1.js未变,2.js如下 -->
let name = 'leo'
__webpack_exports__["default"] = ({ name: name });
name = 'hello'

试了下输出leo,而不是hello,说明export default的模式导出的是default引用,而其中属性改变是无法侦测到的。

export的是函数webpack是怎么处理的?
__webpack_require__.d(__webpack_exports__, "name", function() { return name; });
__webpack_require__.d(__webpack_exports__, "changeName", function() { return changeName; });
let name = 'leo'
const changeName = function () { name = 'hello' }

可以看出函数和变量是一样处理的

webpack是如何实现ESM的?

(function (modules) {
  let _cache = {}
  function __webpack_require__ (moduleId) {
    if (_cache[moduleId]) {
      return _cache[moduleId]
    }
    let module = _cache[moduleId] = {
      id: moduleId,
      l: false,
      exports: {}
    }
    modules[moduleId](module, module.exports, __webpack_require__)
    return module.exports
  }
  __webpack_require__.d = function (obj, prop, getter) {
    Object.defineProperty(obj, prop, { get: getter })
  }

  return __webpack_require__('./1.js')
})({
  './1.js': (function(module, __webpack_exports__, __webpack_require__) {
    const module_2 = __webpack_require__('./2.js')
    console.log(module_2['name'])
  }),
  './2.js': (function(module, __webpack_exports__, __webpack_require__) {
    __webpack_require__.d(
      __webpack_exports__,
      'name',
      function () {
        return name
      }
    )
    const name = 'leo111'
  })
});

执行IIFE函数时,最后要执行__webpack_require__('./1.js'),执行入口函数进行启动

CommonJS的使用和实现

CommonJS模块基本语法

module.exports = {
  name: 'leo'
}

exports.name = 'leo'

exports = { name: leo } ❌ 这是不行的,因为切断了exportsmodule.exports之间的引用

CommonJS的自定义实现

const fs = require('fs')
const vm = require('vm')
const path = require('path')
const Module = {
  wrapper: [
    '(function (require, module, exports, __filename, __dirname) {',
    '})'
  ],
  _cache: {}
}

function customRequire (filepath) {
  const filename = path.resolve(__dirname, filepath)
  if (Module._cache[filename]) {
    return Module._cache[filename]
  }
  let module = Module._cache[filename] = {
    id: filename,
    l: false,
    exports: {}
  }
  const content = fs.readFileSync(filename)
  const func = new vm.Script(Module.wrapper[0] + content + Module.wrapper[1]).runInThisContext()
  func(customRequire, module, module.exports, filename, path.dirname(filename))
  return module.exports
}

ESM和CommonJS有什么区别

解答

  1. ESM导出的是值的引用,CommonJS导出的是值的拷贝
  2. 循环引用两者的处理有区别
  3. CommonJS是运行时加载,ESM是编译时输出接口
导出的区别

ESM导出的时候,👇:

export const name = 'leo'
👇 会被解析为:
__webpack_require__.d(
  __webpack_exports__,
  'name',
  function () {
    return name
  }
)

其实是使用defineProperty的方法把name的getter设置到__webpack_exports__上面,但是实际取的还是外部的name,这里用了闭包特性

CommonJS导出的时候却是这样实现的👇:

const content = fs.readFileSync(filename)
new vm.Script(Module.wrapper[0] + content + Module.wrapper[1]).runInThisContext()

从两者的实现可以看出:ESM导出值的引用,而CommonJs导出值的拷贝

循环引用

循环引用指的是:a引用b,b又引用a

先存缓存的重要性

在实现ESM和CommonJS的过程中,都可以发现还未执行模块,就先把模块存入了缓存:

let module = Module._cache[filename] = {
  if: filename,
  l: false,
  exports: {}
}

这是因为如果不先存入缓存,循环引用会导致内存溢出,先存入缓存之后取直接取缓存返回

输出已执行部分

因为ESM做得比较好,不能重复导出相同属性,所以这里拿CommonJS来实验:

<!-- 1.js -->
exports.name1 = 'leo1'
const m2 = require('./2.js')
exports.name1 = 'leo11'
console.log(m2.name2)

<!-- 2.js -->
const m1 = require('./1.js')
console.log(m1.name1)
exports.name2 = 'leo2'

这里1.js和2.js循环引用,在导入2.js之前name1为leo1,导入2.js之后name1为leo11。可以发现2.js中输出的name1为leo1
CommonJS 模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值

循环引用中使用了未定义的变量,什么反应?

如果把1.js中的name='leo1'删除,可以发现直接是undefined了,对两个文件稍作加工👇:

<!-- 1.js -->
const m2 = require('./2.js')
exports.name1 = function () {
  return 'leo11'
}
console.log(m2.name2)

<!-- 2.js -->
const m1 = require('./1.js')
console.log(m1.name1())
exports.name2 = 'leo2'

CommonJS直接报错:m1.name1 is not a function

换成ESM试下:

<!-- 1.js -->
import { name2 } from './2.js'
export function name1 () {
  return 'leo11'
}
console.log(name2)

<!-- 2.js -->
import { name1 } from './1.js'
console.log(name1())
export const name2 = 'leo2'

发现输出了leo11 leo2,说明可以,因为函数提升

可以想象:函数表达式也是不行的,因为虽然可以变量提升,但是变量仍是undefined

项目中如何共用ESM和CommonJS

node如何跑ESM模块的代码呢?

node --experimental-modules 1.mjs

两个条件:

  1. --experimental-modules
  2. mjs后缀

开源模块

module

package.json里面的module

{
  type: 'module',
  module: './1.js'
}

import { name1 } from 'test'

设置了type: 'module'说明是ESM,没设置说明是CommonJS模块

exports

exports只有兼容ESM的Node才支持,版本13.2.0以上

"exports": {
  ".": {
    "require": "./main.cjs",
    "default": "./main.js"
  }
}

UMD是如何实现的

(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ?
    module.exports = factory()
    : (
        typeof define === 'function' && define.amd ?
          define(factory)
          : (global.libName = factory())
      )
})(this, function () { 'use strict' })