likes
comments
collection
share

webpack深入浅出:模块依赖和转换的秘密

作者站长头像
站长
· 阅读数 31

webpack是一个前端打包工具,它可以将多个模块的代码和资源进行合并和转换,生成适合浏览器运行的文件。webpack的核心功能之一是模块的导入和导出,它可以让我们使用不同的模块规范,如CommonJS和ES Module,来组织我们的代码,提高我们的开发效率和代码质量。但是同时也会产生以下几个问题

  1. webpack是如何实现模块的导入和导出的?
  2. webpack是如何处理不同的模块规范的?
  3. webpack是如何将模块的代码和依赖转换为一个自执行函数的?

如果你对这些问题感兴趣,本文将深入浅出地介绍webpack的模块依赖和转换的原理和流程,帮助你更好地理解和使用webpack。本文的内容将分为以下几个部分:

  • 前置了解的方法:介绍一些webpack的基本概念和方法,如Symbol.toStringTag和Object.defineProperty等,为后面的内容做准备。
  • 基本配置:介绍webpack的基本配置文件,如入口,出口,模式,插件等,为后面的分析提供一个简单的示例。
  • 转化流程总结:总结webpack的模块依赖和转换的整个流程,从入口文件开始,到输出文件结束,分析webpack是如何处理模块的导入和导出的。

本文能够了解到webpack的同步模块依赖和转换的秘密让我们开始吧!

一、前置了解的方法

下面的两个方法将会作为webpack打包时候的工具方法:

1. Symbol.toStringTag

(一) 作用

Symbol.toStringTag的主要作用是允许开发人员自定义对象在被转换为字符串时的描述。默认情况下,Object.prototype.toString()方法返回的字符串描述是"[object Object]",对于大多数内置的JavaScript对象类型都是如此。通过使用Symbol.toStringTag属性,我们可以更改这个默认描述,使其更具有可读性。 在webpack中这个方法主要是为了标记当前导入的模块是一个es模块,也就是给一个对象标注信息;

(二) 使用
let person = { name: "zs" };
Object.defineProperty(person, Symbol.toStringTag, {value: "Person"});
console.log(Object.prototype.toString.call(person));  // [object Person]
const obj = {
  [Symbol.toStringTag]: 'MyObject'
};

console.log(obj.toString()); // 输出:[object MyObject]

2. Object.defineProperty

(一) 作用

会直接在一个对象上定义个新属性,或者修改一个对象的现有属性,并返回这个对象; webapck中用这个方法来定义es模块中添加变量的访问逻辑(后面有详细说明)

(二) 使用
let obj = {};
let obj1 = Object.defineProperty(obj, "name", {value: 1, writable: true});
console.log(obj1 === obj); // true

二、基本配置

webpack深入浅出:模块依赖和转换的秘密

后文会根据这些简单的配置来分析webpack是如何进行同步打包和模块转化的;

// webpack.config.js

const path = require("path");
const HtmlWebpackPlugin =  require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

module.exports = {
    mode: "development",
    devtool: "source-map",
    entry: "./src/index.js",
    output: {
        path: path.resolve(__dirname, "./bundle"),
        filename: "[name]_bundle.js"
    },
    module: {},
    plugins: [
        new HtmlWebpackPlugin({
            template: "./src/index.html",
            filename: "index.html"
        }),
        new CleanWebpackPlugin()
    ]
}

三、转化流程总结

假设入口和模块代码如下

// header.js
module.exports = "header-com"

// index.js 入口
const header = require("./header.js")
console.log(header);

1. 打包后的文件分析

打包后的JS文件是一个自执行函数,根据职责划分可以分为4个部分;

(() => {
    // 模块1: 模块依赖关系
    let modules = ({
        "./header.js": (module) => {
            module.exports = "header-com"
        }
    })

    // 模块2: 依赖记录
    let modules_cache = {};


    // 模块3: require函数
    function require(moduleId) {
        let cacheModule = modules_cache[moduleId];
        if (cacheModule !== undefined) {
            return cacheModule;
        }
        let module = modules_cache[moduleId] = {
            exports: {}
        }
        modules[moduleId](module, module.exports, require);
        return  module.exports;
    }


    //  模块4:入口函数执行
    (() => {
        const header = require("./header.js");
        console.log(header);
    })();

})();

2. modules模块依赖关系创建

  1. webapack在打包的过程中会查看模块代码的内容,然后会把模块的内容转化为一个函数并且和相对路径结合起来进行记录(webpack自动创建的),这样打包后的JS文件里面就会记录着各个其他模块的执行信息;
  2. 模块规则处理:如果扫描到使用了import或者export进行导出,就说明此模块是EsModule,会进行特殊处理,如果不是则直接使用模块内容,最终都会把所有的模块值保存为 { exports: { ... } } 形式进行缓存,还是CommonJS规范;

3. require函数

创建一个module对象,然后结合模块依赖关系的函数来进行赋值,其实就是使用了闭包。

步骤分析require('./header.js')执行流程,它并非真的去引入了一个js文件,而是使用了模块依赖关系函数;

  1. 查看缓存中是否有./header.js这个值,有则直接返回;
  2. 创建model对象{ exports: {} },将这个对象挂载到缓存中;
  3. 将model对象传入到 ./header.js模块依赖对应的函数中进行赋值操作
  4. 返回model.exports值,也就是模块返回值;

4. 图示

webpack深入浅出:模块依赖和转换的秘密

四、CommonJS引入CommonJS分析

1. commonjs规范特点

在 CommonJS 中,每个模块都有一个名为 module 的对象,它是模块的一个内部变量。这个 module 对象有一个 exports 属性,它用于指定模块的默认导出内容。

如果一个模块没有显式地给 module.exports 赋值,它的默认导出将为一个空对象 {}。

2. 代码

// header.js中
module.exports = "header-model";

// index.js
const header = require('./header.js');
console.log(header);

3. 执行打包后代码分析

其实就是在浏览器实现了一套CommonJS规范;

(一) __webpack_modules__依赖关系
  1. 存储映射关系:webpakc执行打包命令后会根据入口文件对所有引入依赖进行分析,这个变量用来存储模块的映射关系的。
  2. 模块转为函数:当使用 Webpack 打包一个项目时,Webpack 会将每个模块转换为一个独立的函数,并将这些函数存储在__webpack_modules__中。这些模块函数包含了模块代码以及模块的导出,使得其他模块可以通过引用这些函数来加载和使用模块。
  3. 模块ID:不管是通过import还是require,最终的模块ID统一会变成相对根目录的一个相对路径,此时的模块ID就是./src/header.js
(二) __webpack_module_cache__模块缓存

如果之前有获取过这个模块的值,则下次使用的时候直接返回上次的结果

(三) __webpack_require__加载模块函数
  1. 创建一个空对象module
  2. 将这个module作为参数传入到模块ID ./src/header.js 对应的函数中进行赋值
  3. 返回修改后module对象
(() => { // webpackBootstrap
    var __webpack_modules__ = ({
      	// 模块ID
        "./src/header.js": 
          // 模块函数
            ((module) => {
                module.exports = "header-model";
            })
    });

    // 模块缓存
    var __webpack_module_cache__ = {};

    // 加载函数
    function __webpack_require__(moduleId) {
        var cachedModule = __webpack_module_cache__[moduleId];
        if (cachedModule !== undefined) {
            return cachedModule.exports;
        }

        // commonjs特点有一个module变量并且有一个exports属性
        // 执行的时候还需要赋值给缓存模块
        var module = __webpack_module_cache__[moduleId] = {
            exports: {}
        };

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

    }

    // 执行index.js入口代码
    (() => {
        const header = __webpack_require__("./src/header.js");
        console.log(header);
    })();
})();

五、CommonJS引入ESModule分析

1. esModule特点

通过export default 或者 export 进行导出,通过其他模块引入后会是一个对象,默认导出的属性会挂载到default对象中;

2. 代码和输出结果

// header.js
export default "header-default";
export const header = "header"

// index.js
let header = require("./header.js");
console.log(header);

webpack深入浅出:模块依赖和转换的秘密

3. 重点

通过输出结果来看针对esModule导出的变量会进行包装处理,实际还是将esmodule转化为了Commonjs规范

  1. 通过Symbol.toStringTag标记成Module模块
  2. 把最终导出的module.export变量设置上getter(Object.defineProperty方法);
// es模块
export const header = "header";
export default "header-default";

// 通过__webpack_require__.d转化后为两个获取值的get函数
let module = {
  exports: {
    default: get,
    header: get
  }
}
// es模块编译后的依赖结构
var __webpack_modules__ = ({
        // 模块ID
        "./src/header.js":
        // webpack将模块转化为的独立函数
            ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
                "use strict";
                // 标记为esModule模块
                __webpack_require__.r(__webpack_exports__);
                // 给访问的变量设置上getter
                __webpack_require__.d(__webpack_exports__, {
                    "default": () => (__WEBPACK_DEFAULT_EXPORT__),
                    header: () => (header)
                });
                const __WEBPACK_DEFAULT_EXPORT__ = ("header-default");
                const header = "header"
        	})
});

4. 打包后代码

(() => { // webpackBootstrap
    var __webpack_modules__ = ({
        // 模块ID
        "./src/header.js":
        // webpack将模块转化为的独立函数
            ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
                "use strict";
                __webpack_require__.r(__webpack_exports__);
                __webpack_require__.d(__webpack_exports__, {
                    "default": () => (__WEBPACK_DEFAULT_EXPORT__),
                    header: () => (header)
                });
                const __WEBPACK_DEFAULT_EXPORT__ = ("header-default");
                const header = "header"
            })
    });

    var __webpack_module_cache__ = {};

    function __webpack_require__(moduleId) {
        var cachedModule = __webpack_module_cache__[moduleId];
        if (cachedModule !== undefined) {
            return cachedModule.exports;
        }
      	// 最终还是使用的这个module变量,还是尊循了commonjs规范
      	var module = __webpack_module_cache__[moduleId] = {
            exports: {}
        };

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

        return module.exports;
    }

    // 将es模块导出的属性设置上getter
    (() => {
        __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))
    })();


    // 对对象进行标识是一个esModule模块
    (() => {
        __webpack_require__.r = (exports) => {
            if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
                Object.defineProperty(exports, Symbol.toStringTag, {value: 'Module'});
            }
            Object.defineProperty(exports, '__esModule', {value: true});
        };
    })();


    (() => {
        let header = __webpack_require__("./src/header.js");
        console.log(header);
    })();
})();

六、ESModule引入CommonJS分析

1. 代码

// header
module.exports = {
    header: "header",
    body: "body"
}

// index
import header from "./header";
console.log(header);

2. 分析

此时入口文件为EsModule规范,但是模块为CommonJS规范,所以再依赖分析中header.js模块不会改变,主要在运行代码时候需要转化EsModule,并且标注入口文件也是一个EsModule

 var __webpack_modules__ = ({
        "./src/header.js":
            ((module) => {
                module.exports = {
                    header: "header",
                    body: "body"
                }
            })
});

var __webpack_exports__ = {};
// 执行逻辑
(() => {
    "use strict";
    // 将当前入口模块标记为esModule
    __webpack_require__.r(__webpack_exports__);
    var _header__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/header.js");
    // 重点理解n方法
    var _header__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(_header__WEBPACK_IMPORTED_MODULE_0__);

    // 执行n方法返回获取module.exports对象
    console.log((_header__WEBPACK_IMPORTED_MODULE_0___default()));
})();

n方法解析:

判断当前模块是否是EsModule,如果是则返回default属性,如果不是则返回module.exports对象

 (() => {
        __webpack_require__.n = (module) => {
            // 判断模块类型
            var getter = module && module.__esModule ?
                () => (module['default']) :
                () => (module);
            // 后面为了异步引入
            __webpack_require__.d(getter, {a: getter});
            return getter;
        };
})();

3. 代码

(() => { // webpackBootstrap
    var __webpack_modules__ = ({

        "./src/header.js":
            ((module) => {
                module.exports = {
                    header: "header",
                    body: "body"
                }
            })

    });

    var __webpack_module_cache__ = {};

    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;
    }


    (() => {
      // 根据当前模块类型获取模块的值,如果是esModule则获取default属性
        __webpack_require__.n = (module) => {
            var getter = module && module.__esModule ?
                () => (module['default']) :
                () => (module);
            __webpack_require__.d(getter, {a: getter});
            return getter;
        };
    })();


    (() => {
        __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) => {
            if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
                Object.defineProperty(exports, Symbol.toStringTag, {value: 'Module'});
            }
            Object.defineProperty(exports, '__esModule', {value: true});
        };
    })();

    var __webpack_exports__ = {};

    (() => {
        "use strict";
        __webpack_require__.r(__webpack_exports__);
        var _header__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/header.js");
        var _header__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(_header__WEBPACK_IMPORTED_MODULE_0__);

        console.log((_header__WEBPACK_IMPORTED_MODULE_0___default()));
    })();

})();

七、EsModule引入EsModule分析

1. 代码

// header.js
export default "header-default";
export const name = "header";

// index.js
import headerDefault, { name } from "./header";

console.log(headerDefault);
console.log(name);

2. 分析

如果模块是es模块则需要将它转化为一个对象,所以此时模块依赖关系的独立函数需要进行处理

var __webpack_modules__ = ({
    "./src/header.js":
        ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
            __webpack_require__.r(__webpack_exports__);
            __webpack_require__.d(__webpack_exports__, {
                "default": () => (__WEBPACK_DEFAULT_EXPORT__),
                name: () => (name)
            });
            const __WEBPACK_DEFAULT_EXPORT__ = ("header-default");
            const name = "header";
        })
});

入口文件也是es模块所以执行的时候也需要处理

 var __webpack_exports__ = {};
(() => {
    __webpack_require__.r(__webpack_exports__);
    var _header__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/header.js");


    console.log(_header__WEBPACK_IMPORTED_MODULE_0__["default"]);
    console.log(_header__WEBPACK_IMPORTED_MODULE_0__.name);

})();