webpack 打包CommonJs、ES Module 源码解析

1,272 阅读5分钟

参考链接






IIFE 匿名函数自调用

现代模块实现的基石

数据是私有的, 外部只能通过暴露的方法操作

// module.js文件
// $ 引入依赖
;(function (window, $) {
  // 内部私有的函数
  function otherFun() {
    console.log('otherFun()')
  }

  let data = 'www.baidu.com'
  function foo() {
    console.log(`foo() ${data}`)
    $('body').css('background', 'red')
  }
  function bar() {
    console.log(`bar() ${data}`)
    otherFun() // 内部调用
  }
  // 暴露行为
  window.myModule = { foo, bar }
})(window, jQuery)

引入的js必须有一定顺序

// index.html文件
<script type="text/javascript" src="jquery-1.10.1.js"></script>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
  myModule.foo()
</script>





CJS 与 ESM 原理

基础配置

  • webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const path = require('path')

module.exports = {
  mode: 'development',
  devtool: 'source-map',
  entry: './src/main.js',
  output: {
    filename: 'index.js',
    path: path.resolve(__dirname, 'dist'),
  },
  plugins: [
    new HtmlWebpackPlugin(),
    new CleanWebpackPlugin()
  ]
}
  • src/js/format.js
const dateFormat = date => '2020-12-12'
const prtceFormat = price => '100.00'

module.exports = {
  dateFormat,
  prtceFormat,
}
  • src/js/math.js
export const sum = (a, b) => a + b
export const mul = (a, b) => a * b

CommonJS 原理

// src/main.js 文件
const { dateFormat, priceFormat } = require('./js/format')

console.log(dateFormat('abc'))
console.log(priceFormat('abc'))

webpack打包后的源码

CJS 的特点

  • 运行时加载
  • require() 是同步加载模块
  • 输出的是一个值的拷贝
    • CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存
;(function () {
  var __webpack_modules__ = {
    './src/js/format.js': function (module) {
      const dateFormat = date => {
        return '2020-12-12'
      }

      const priceFormat = price => {
        return '100.00'
      }

      // CommonJS 模块输出的是一个值的拷贝
      module.exports = {
        dateFormat,
        priceFormat,
      }
    },
  }

  // 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
    // 导出 module.exports 对象
    return module.exports
  }

  !(function () {
    // CommonJS 模块是运行时加载
    // CommonJS 模块的 require() 是同步加载模块
    const { dateFormat, priceFormat } =
      __webpack_require__('./src/js/format.js')

    console.log(dateFormat('1213'))
    console.log(priceFormat('1213'))
  })()
})()

ESModule 原理

// src/main.js
import { sum, mul } from './js/math'

console.log(mul(20, 30))
console.log(sum(20, 30))

webpack打包后的源码

ESM 特点

  • 输出的是值的引用
    • 只读属性
  • 是编译时输出接口
    • ES6 模块内部的代码都只是被转换成了一段静态的代码,只有进入到运行时才会被执行
  • 定义模块为 esModule

image.png

;(function () {
  'use strict'
  var __webpack_modules__ = {
    './src/js/math.js': function (
      __unused_webpack_module,
      __webpack_exports__,
      __webpack_require__
    ) {
      __webpack_require__.r(__webpack_exports__)
      // ES6 模块输出的是值的引用
      // ES6 模块是编译时输出接口
      __webpack_require__.d(__webpack_exports__, {
        sum: function () {
          return sum
        },
        mul: function () {
          return mul
        },
      })
      // ES6 模块内部的代码都只是被转换成了一段静态的代码,只有进入到运行时才会被执行
      const sum = (num1, num2) => {
        return num1 + num2
      }

      const mul = (num1, num2) => {
        return num1 * num2
      }
    },
  }

  // 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/define property getters */
  !(function () {
    // define getter functions for harmony exports
    __webpack_require__.d = function (exports, definition) {
      for (var key in definition) {
        if (
          __webpack_require__.o(definition, key) &&
          !__webpack_require__.o(exports, key)
        ) {
          // import 命令输入的变量都是只读的,因为它的本质是输入接口
          Object.defineProperty(exports, key, {
            enumerable: true,
            get: definition[key],
          })
        }
      }
    }
  })()

  /* webpack/runtime/hasOwnProperty shorthand */
  !(function () {
    __webpack_require__.o = function (obj, prop) {
      return Object.prototype.hasOwnProperty.call(obj, prop)
    }
  })()

  /* webpack/runtime/make namespace object */
  !(function () {
    // define __esModule on exports
    // 定义模块为 esModule
    __webpack_require__.r = function (exports) {
      if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
      }
      Object.defineProperty(exports, '__esModule', { value: true })
    }
  })()

  var __webpack_exports__ = {}
  !(function () {
    __webpack_require__.r(__webpack_exports__)
    var _js_math_js__WEBPACK_IMPORTED_MODULE_0__ =
      __webpack_require__('./src/js/math.js')

      console.log(_js_math_js__WEBPACK_IMPORTED_MODULE_0__)

    console.log((0, _js_math_js__WEBPACK_IMPORTED_MODULE_0__.sum)(20, 30))
    console.log((0, _js_math_js__WEBPACK_IMPORTED_MODULE_0__.mul)(20, 30))
  })()
})()

CommonJS & ESModule 混合原理

// src/main.js

// es module导出内容, CommonJS导入内容
const { sum, mul } = require('./js/math')
// CommonJS导出内容, es module导入内容
import { dateFormat, priceFormat } from './js/format'
console.log(sum(20, 30))
console.log(mul(20, 30))
console.log(dateFormat('aaa'))
console.log(priceFormat('bbb'))

webpack打包源码

;(function () {
  var __webpack_modules__ = {
    './src/js/format.js': function (module) {
      const dateFormat = date => {
        return '2020-12-12'
      }

      const priceFormat = price => {
        return '100.00'
      }

      module.exports = {
        dateFormat,
        priceFormat,
      }
    },

    './src/js/math.js': function (
      __unused_webpack_module,
      __webpack_exports__,
      __webpack_require__
    ) {
      'use strict'
      __webpack_require__.r(__webpack_exports__)
      __webpack_require__.d(__webpack_exports__, {
        sum: function () {
          return sum
        },
        mul: function () {
          return mul
        },
      })
      const sum = (num1, num2) => {
        return num1 + num2
      }

      const mul = (num1, num2) => {
        return num1 * num2
      }
    },
  }

  // 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 */
  !(function () {
    // getDefaultExport function for compatibility with non-harmony modules
    __webpack_require__.n = function (module) {
      var getter =
        module && module.__esModule
          ? function () {
              return module['default']
            }
          : function () {
              return module
            }
      __webpack_require__.d(getter, { a: getter })
      return getter
    }
  })()

  /* webpack/runtime/define property getters */
  !(function () {
    // define getter functions for harmony exports
    __webpack_require__.d = function (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 */
  !(function () {
    __webpack_require__.o = function (obj, prop) {
      return Object.prototype.hasOwnProperty.call(obj, prop)
    }
  })()

  /* webpack/runtime/make namespace object */
  !(function () {
    // define __esModule on exports
    __webpack_require__.r = function (exports) {
      if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
      }
      Object.defineProperty(exports, '__esModule', { value: true })
    }
  })()

  var __webpack_exports__ = {}

  !(function () {
    'use strict'
    __webpack_require__.r(__webpack_exports__)
    var _js_format__WEBPACK_IMPORTED_MODULE_0__ =
      __webpack_require__('./src/js/format.js')
    var _js_format__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(
      _js_format__WEBPACK_IMPORTED_MODULE_0__
    )
    // es module 导出,CommonJs 导入
    const { sum, mul } = __webpack_require__('./src/js/math.js')
    // CommonJs 导出,es module 导入
    console.log(sum(20, 30))
    console.log(mul(20, 30))

    console.log((0, _js_format__WEBPACK_IMPORTED_MODULE_0__.dateFormat)('1213'))
    console.log(
      (0, _js_format__WEBPACK_IMPORTED_MODULE_0__.priceFormat)('1213')
    )
  })()
})()

多chunk加载的原理解析

1. 多个打包入口

生成多个script标签,按顺序导入

2. 分离公共依赖

  1. 最先加载 venders.js image.png

  2. 将第三方模块放入全局变量 webpackChunkwebpacktest 中 image.png

  3. 将对应模块加入到 main 作用域的__webpack_modules__image.png

3. import() 异步打包原理

// src/main.js
import('./js/math').then(({ mul, sum }) => {
  console.log(mul(20, 30))
  console.log(sum(20, 30))
})

webpack 打包后的产物

image.png

总结过程

  1. 执行入口文件 index.js 代码。import()会被替换成webpack的api
  1. 执行 __webpack_require__.e() 返回一个 Promise 对象,状态是pending
  1. 执行 webpack_require.l 生成script标签挂载到页面document.head.appendChild(script)
  2. script标签下载完成执行触发self['webpackChunkwebpacktest'].push()
  3. push()方法被改写,将触发 webpackJsonpCallback 执行
    1. 懒加载的模块被加入到main的__webpack_modules__
    2. 把 Promise 的状态从 pending 改成fulfilled执行后续代码