Webpack系列-模块加载机制

1,026 阅读5分钟

webpack自己实现了一套基于ES5的模块加载机制,无论是CommonJS模块的require语法还是ES6模块的import语法,都能够被解析并转化成指定环境的可运行代码。

示例代码

index.js

import foo from './foo'
import bar from './bar'

console.log('run => index.js')
console.log(`log => foo.name: ${foo.name}`)
console.log(`log => bar.name: ${bar.name}`)

export default {
    name: 'index'
}

foo.js

import bar from './bar'

console.log('run => foo.js')
console.log(`log => bar.name: ${bar.name}`)

export default {
    name: 'foo'
}

bar.js

console.log('run => bar.js')

export default {
  name: 'bar'
}

webpack.config.js

const path = require('path')

module.exports = {
    entry: './src/index',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js',
    },
    optimization: {
        concatenateModules: false
    }
}

webpack 的模式默认是production,production模式下默认会开启Scope Hoisting(作用域提升),上边的示例代码三个模块会被合为一个模块,所有需要关闭concatenateModules,这样打包后的代码才会有三个模块,便于我们分析。

打包后代码

(function (modules) {
    // webpackBootstrap
    // The module cache
    var installedModules = {}

    // The require function
    function __webpack_require__(moduleId) {
        // Check if module is in cache
        if (installedModules[moduleId]) {
            return installedModules[moduleId].exports
        }
        // Create a new module (and put it into the cache)
        var module = (installedModules[moduleId] = {
            i: moduleId,
            l: false,
            exports: {},
        })

        // Execute the module function
        modules[moduleId].call(
            module.exports,
            module,
            module.exports,
            __webpack_require__
        )

        // Flag the module as loaded
        module.l = true

        // Return the exports of the module
        return module.exports
    }

    // expose the modules object (__webpack_modules__)
    __webpack_require__.m = modules

    // expose the module cache
    __webpack_require__.c = installedModules

    // define getter function for harmony exports
    __webpack_require__.d = function (exports, name, getter) {
        if (!__webpack_require__.o(exports, name)) {
            Object.defineProperty(exports, name, {
                enumerable: true,
                get: getter,
            })
        }
    }

    // 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 })
    }

    // create a fake namespace object
    // mode & 1: value is a module id, require it
    // mode & 2: merge all properties of value into the ns
    // mode & 4: return value when already ns object
    // mode & 8|1: behave like require
    __webpack_require__.t = function (value, mode) {
        if (mode & 1) value = __webpack_require__(value)
        if (mode & 8) return value
        if (mode & 4 && typeof value === 'object' && value && value.__esModule)
            return value
        var ns = Object.create(null)
        __webpack_require__.r(ns)
        Object.defineProperty(ns, 'default', {
            enumerable: true,
            value: value,
        })
        if (mode & 2 && typeof value != 'string')
            for (var key in value)
                __webpack_require__.d(
                    ns,
                    key,
                    function (key) {
                        return value[key]
                    }.bind(null, key)
                )
        return ns
    }

    // getDefaultExport function for compatibility with non-harmony modules
    __webpack_require__.n = function (module) {
        var getter =
            module && module.__esModule
                ? function getDefault() {
                    return module['default']
                }
                : function getModuleExports() {
                    return module
                }
        __webpack_require__.d(getter, 'a', getter)
        return getter
    }

    // Object.prototype.hasOwnProperty.call
    __webpack_require__.o = function (object, property) {
        return Object.prototype.hasOwnProperty.call(object, property)
    }

    // __webpack_public_path__
    __webpack_require__.p = '../'

    // Load entry module and return exports
    console.log('(__webpack_require__.s = 2):', (__webpack_require__.s = 2))
    debugger
    return __webpack_require__((__webpack_require__.s = 2))
})(
    /************************************************************************/
    [
        /* 0 */
        /***/ function (module, __webpack_exports__, __webpack_require__) {
            'use strict'
            console.log('run => bar.js')
            /* harmony default export */ __webpack_exports__['a'] = {
                name: 'bar',
            }
            /***/
        },
        /* 1 */
        /***/ function (module, __webpack_exports__, __webpack_require__) {
            'use strict'
            /* harmony import */ var _bar__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
                0
            )

            console.log('run => foo.js')
            console.log(
                'log => bar.name: '.concat(
                    _bar__WEBPACK_IMPORTED_MODULE_0__[/* default */ 'a'].name
                )
            )
            /* harmony default export */ __webpack_exports__['a'] = {
                name: 'foo',
            }
            /***/
        },
        /* 2 */
        /***/ function (module, __webpack_exports__, __webpack_require__) {
            'use strict'
            __webpack_require__.r(__webpack_exports__)
            /* harmony import */ var _foo__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
                1
            )
            /* harmony import */ var _bar__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(
                0
            )

            console.log('run => index.js')
            console.log(
                'log => foo.name: '.concat(
                    _foo__WEBPACK_IMPORTED_MODULE_0__[/* default */ 'a'].name
                )
            )
            console.log(
                'log => bar.name: '.concat(
                    _bar__WEBPACK_IMPORTED_MODULE_1__[/* default */ 'a'].name
                )
            )
            /* harmony default export */ __webpack_exports__['default'] = {
                name: 'index',
            }
            /***/
        },
    ]
)

分析

代码的主体其实是一个立即执行函数(IIFE::Immediately-Invoked Function Expression)。

(function (modules) {

})(
    [
        /* 0 */
        function (module, __webpack_exports__, __webpack_require__) {
            //bar.js
        },
        /* 1 */
        function (module, __webpack_exports__, __webpack_require__) {
            //foo.js
        },
        /* 2 */
        function (module, __webpack_exports__, __webpack_require__) {
            //index.js
        },
    ]
)

立即执行函数有一个形参modules,对应的实参是一个数组,数组的每一项是一个函数,每一个函数就是一个模块。把所有的模块封装成一个函数传给了立即执行函数。每个函数拥有module,webpack_exportswebpack_require 三个参数。

webpackBootstrap 启动函数的执行

代码的执行框架搭建好了,那启动函数 webpackBootstrap 内究竟做了什么让模块之间联系提来?模块的三个形参到底是什么? 立即执行函数核心代码:

(function (modules) {
    // webpackBootstrap
    // The module cache
    var installedModules = {}
    function __webpack_require__(moduleId) {
        if (installedModules[moduleId]) {
            return installedModules[moduleId].exports
        }
        // Create a new module (and put it into the cache)
        var module = (installedModules[moduleId] = {
            i: moduleId,
            l: false,
            exports: {},
        })
        // Execute the module function
        modules[moduleId].call(
            module.exports,
            module,
            module.exports,
            __webpack_require__
        )
        module.l = true
        return module.exports
    }
    return __webpack_require__((__webpack_require__.s = 2))
})(
    [
        /* 0 */
        function (module, __webpack_exports__, __webpack_require__) {},
        /* 1 */
        function (module, __webpack_exports__, __webpack_require__) {},
        /* 2 */
        function (module, __webpack_exports__, __webpack_require__) {},
    ]
)

首先定义了一个 installedModules 对象,它的作用是用来缓存已经加载过的模块,然后声明 webpack_require 函数,似曾相识对吧,没错,它就是模块的第三个形参对应的实参。最后返回 webpack_require(2) 执行结果,当前 webpackBootstrap 函数的第二项既是index.js入口文件。

那再看看 webpack_require 函数,顾名思义,它就是用来加载模块的函数,接收一个 moduleId 的形参,也就是模块的 id 值。

首先判断 moduleId 对应的模块是否已被缓存,也就是在 installedModules 对象中能不能找到属性 moduleId 对应的值,如果找到了,则直接返回模块的输出 exports。从这里可以发现,无论被多少个模块所依赖的模块都只会被加载一次,结果相同,因为返回的是同一个对象的引用地址,所以如果某个模块修改了对象内的属性值,则会被同步反应到其它依赖此模块的对象。

继续,当模块没有被加载过的情况下,定义一个模块对象,同步加入 installedModules 对象缓存起来,模块对象包含三个属性,i 表示模块 id 值,l 表示是否被加载,exports 表示模块的输出结果对象。

接着就是模块执行的调用函数,通过 moduleId 从 modules 内找到模块的函数代码块,使用 call 方法绑定函数内的 this 指向 module.exports,传入三个实参 module module.exports webpack_require,与模块函数的形参一一对应。

当模块函数执行完成并返回结果之后,模块标识为已加载状态,最后返回模块的输出对象。

模块函数的执行

目前浏览器还没有完全支持 ES6 模块语法,所以模块内的 import 语法会如何处理?以 foo.js 为例来瞧瞧模块函数代码块内的代码:

/* 1 */
/***/ function (module, __webpack_exports__, __webpack_require__) {
    'use strict'
    /* harmony import */ var _bar__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
        0
    )

    console.log('run => foo.js')
    console.log(
        'log => bar.name: '.concat(
            _bar__WEBPACK_IMPORTED_MODULE_0__[/* default */ 'a'].name
        )
    )
    /* harmony default export */ __webpack_exports__['a'] = {
        name: 'foo',
    }

    /***/
},

原来 import 语法被转换了,模块名称变为一个变量名称,值是使用 webpack_require 函数根据依赖模块 id 值获取的输出结果,并且模块函数内的所有依赖模块名称都被转换成对应的变量名称,模块的输出结果被绑定在 webpack_exports 对象中,这里 module.exports === webpack_exports,等于就是模块的输出。

总结

webpack 的模块机制包含三大要点:

  • 1、modules 保存所有模块
  • 2、webpack_require 函数加载模块
  • 3、installedModules 对象缓存模块

个人理解

  • 1、使用 acorn 将代码解析成 AST(抽象语法树) 从入口(entry)模块开始,以及后续各个依赖模块
  • 2、分析 AST 根据关键词 import``require 加载并确定模块之间的依赖关系、标识 id 值以及其它
  • 3、生成输出内容的 AST 并将模块的 AST 根据 id 值顺序插入 modules 的 AST 输出内容是打包后输出的文件内容,模块 AST 会被包裹之后插入(原本的模块代码会被包裹进函数内)。
  • 4、修改 AST 将各模块引入依赖模块的语法进行转换并将模块内所有的依赖标识进行对应替换 import 语法转换和模块标识替换,上文有描述。
  • 输出结果