Webpack5学习 --- 模块化原理

423 阅读4分钟

mode

Mode配置选项,可以告知webpack使用响应模式的内置优化:

默认值是production

可选值有:'none' | 'development' | 'production'

module.exports = {
  mode: 'development', // mode是顶层配置选项

  entry: './src/index.js',

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/main.js',
  },

  plugins: [
    new CleanWebpackPlugin(),

    new HtmlWebpakcPlugin()
  ]
}

模块化实现原理

# 以下所有模块化实现原理的版本如下: (版本不同,实现模块化方式可能会存在差异)
$ webpack --version
# webpack 5.46.0
# webpack-cli 4.7.2

webpack.config.js

module.exports = {
 mode: 'development',
 // 在开始模块化调试的时候,需先将devtool设置为'source-map'
 // 因为mode设置为development后,devtool的默认值就会被设置为eval
 // 此时编译后的代码其实是不利于我们进行阅读的
 devtool: 'source-map',
}

ES Module模块化

// 模块
export const sum = (num1, num2) => num1 + num2

// 主模块
import { sum } from './js/math'
console.log(sum(3, 5))
// 打包后的js文件 ---- 是一个使用了严格模式的 IIFE --- 在加载后会立即执行
// 为了便于阅读,部分变量进行了变量名的替换
(function() {
  "use strict";
  // webpack_modules是一个对象,key是模块的路径,value是函数,函数体为模块中之前定义的代码
  var webpack_modules = {
    "./src/js/math.js":
    // __unused_webpack_module就是传入的module对象 --- {export: {...}}
    // 但是这个对象是没有使用的,只是占位,所以取名叫__unused_ ...
    (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {

    // 给导出对象加上标识
    __webpack_require__.r(__webpack_exports__)

    // 在导出对象上,挂载sum函数的函数体
    __webpack_require__.d(__webpack_exports__, {
      "sum": function() { return sum }
    })

    // 之前在math.js中定义的代码
    const sum = (num1, num2) => num1 + num2
    })
  }

  // 一个空对象,用于对模块数据进行缓存
  var cache = {}

  // 一个函数,解析模块
  function __webpack_require__(moduleId) {
    // 如果有缓存,直接取缓存中的值
    var cachedModule = cache[moduleId]
    if (cachedModule !== undefined) {
      return cachedModule.exports
    }

    // 没有缓存,使用连续赋值,使module和chche[moduleId]指向同一个对象
    // { exports: {} } --- 存放导出数据的对象
    var module = cache[moduleId] = {
      exports: {}
    }

    // 取出模块中定义的diam并执行
    webpack_modules[moduleId](module, module.exports, __webpack_require__)

    // 将导出的数据返回
    return module.exports
  }

  // 在存储导出数据的对象上挂载需要导出的数据
  !function() {
    /*
      参数1: 存放导出数据的对象
      参数2: 需要导出的数据
    */
    __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] })
        }
      }
    }
  }()

  // 传入一个属性,判断该属性是不是传入对象的自身属性
  !function() {
    __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop) }
  }()


  // IIFE 给导出的对象加上标识,表明这个对象是Webpack解析后,用于存放模块导出内容的对象
  !function() {
    __webpack_require__.r = function(exports) {
      // 如果浏览器支持Symbol,那么调用(导出对象).toString()的时候输出 [object Module]
      if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
      }
      // 每一个导出模块都有一个属性 __esModule, 值为true
      Object.defineProperty(exports, '__esModule', { value: true })
    }
  }()

  var __webpack_exports__ = {}

  // IIFE 主模块
  !function() {
  // 给模块打上标识
  __webpack_require__.r(__webpack_exports__)

  // 引入模块,并存储导出的数据
  var mathModuleObj_ = __webpack_require__("./src/js/math.js")

  console.log(mathModuleObj_.sum(3, 5))
  }()
})();

将上述的代码进行简化后如下

!function() {
  // webpack_modules是模块映射,key是模块的路径,value是函数,函数体为模块中之前定义的代码
  var module_map = {
    "./src/js/math.js":
    /*
      参数1: module对象
      参数2: 存放导出数据的对象
      参数3: 自定义解析导入模块的函数,(在模块中又引入了别的模块的时候使用)
    */
    (function(__exports, __require) {
      addTag()
      
      // 之前在math.js中定义的代码
      const sum = (num1, num2) => num1 + num2
      // 挂载需要暴露的数据
      __exports.sum = sum
    })
  }

  // 一个空对象,用于对模块数据进行缓存
  var cache = {}

  // 一个函数,解析模块
  function __require(moduleId) {
    // 如果有缓存,直接取缓存中的值
    if (cache[moduleId]) {
      return cache[moduleId].exports
    }

    // 没有缓存,使用连续赋值,使module和chche[moduleId]指向同一个对象
    // { exports: {} } --- 存放导出数据的对象
    var module = cache[moduleId] = {
      exports: {}
    }

    // 执行模块,将导出数据存放到预先定义的对象中
    module_map[moduleId](module.exports, __require)

    // 将导出的数据返回
    return module.exports
  }
  
  // 打上ESM的标记,以便于区分ESM模块和CJS模块
  function addTag() {
    if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
    }

    // 加上属性__esModule 值为true
    Object.defineProperty(exports, '__esModule', { value: true })
  }

  // 执行主模块
  !function() {
    addTag()
    var { sum } = __require("./src/js/math.js")
    console.log(sum(3, 5))
  }()
}()

Common JS模块化

// 模块
const sum = (num1, num2) => num1 + num2
module.exports.sum = sum

// 主模块
const { sum } = require('./js/math')
console.log(sum(3, 5))
// 编译后的js文件 --- 依旧是一个IIFE
(function() {
  // 模块映射 key是模块路径, value是函数 函数体是模块对应代码体
  var __webpack_modules__ = ({
    "./src/js/math.js":
    (function(module) {
      const sum = (num1, num2) => num1 + num2
      module.exports.sum = sum
    })
  });

  // 缓存
  var __webpack_module_cache__ = {};

  // 自定义require方法 --- 和ESM的自定义require方法原理一致
  function __webpack_require__(moduleId) {
    var cachedModule = __webpack_module_cache__[moduleId];
    if (cachedModule !== undefined) {
      return cachedModule.exports;
    }
    var module = __webpack_module_cache__[moduleId] = {
      exports: {}
    };

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

    return module.exports;
  }


  var __webpack_exports__ = {};

  // IIFE 执行主模块
  !function() {
  const { sum } = __webpack_require__("./src/js/math.js")

  console.log(sum(3, 5))
  }();
})();

ESM 和 CJS之间互相调用

从ESM的模块化原理和CJS的模块化原理可以看出,在webpack中,ESM和CJS的模块化转换方式基本是一致的

所以在webpack中支持ESM和CJS互相调用

// esm模块导出
module.exports.format = () =>console.log('format')

// cjs模块导出
export const sum = (num1, num2) => num1 + num2

// 主模块

// CJS导出,ESM引入
import { format } from './js/format'
// ESM导出,CJS引入
const { sum } = require('./js/math')

// 函数调用
format()
console.log(sum(2,5))