webpack的模块化处理(一)

1,821 阅读10分钟

webpack的模块化处理(一)

前言及准备工作:

  • 本系列文章致力于让读者了解 webpack 模块化相关知识;

    • 我们会从打包结果来探索 webpack 对模块化的具体处理方式;但是不会涉及 webpack 的具体编译细节(如 acorn 进行 AST 转换等)和底层构建方式;

    • 本篇文章采用当前最新的 webpack(5.66.0);在阅读本篇文章之前,建议先 clone 代码后参照文章学习,这样理解起来效果会更好:

  • 代码仓库地址

  • 我们知道,webpack 支持各种模块语法风格,包括 ES6,CommonJS 和 AMD 等;

    • 那么webpack究竟有什么魔法做到兼容各种模块化风格的呢?
    • 在不支持 es Module 的浏览器中,也能实现 import 函数异步加载,它又是如何处理的呢? 带着这些疑问开启我们的 webpack 探索之旅吧。

在了解webpack的模块化方式之前,我们先看我们的文件结构:

image.png

红框所标记的,分别为异步加载模块化,commonJs同步加载模块化,esModule同步加载模块化的入口文件(*_index)及被引用的文件。

再看一下webpack的配置:

我们的webpack.config.js配置:

const pathLib = require("path");
const HtmlWebpackPlugin = require('html-webpack-plugin');
const options = {
    mode:"development",
    entry: {
        commonJs_sync_index: pathLib.resolve(__dirname, "./src/commonJs_sync_index.js"),
        es_sync_index: pathLib.resolve(__dirname, "./src/es_sync_index.js"),
        async_index: pathLib.resolve(__dirname, "./src/async_index.js")
    },
    output: {
        path: pathLib.resolve(__dirname, "./dist"),  //出口位置
        publicPath: '',
        //initial chunk命名
        filename: 'js/[name].initial.js',
        //no-initial chunk命名
        chunkFilename: 'js/async/[name].chunk.[id].[contenthash].js',
        clean: true,
    },
    watch: true,
    watchOptions: {
        poll: 1000, // 每秒询问多少次
        aggregateTimeout: 500,  //防抖 多少毫秒后再次触发
        ignored: /node_modules/ //忽略实时监听
    },
    plugins: [
        new HtmlWebpackPlugin({
            title: '分析 webpack 模块 : CommonJs Sync 模式',
            filename: 'commonJs_sync_index.html',
            template: 'index.html',
            chunks: ['commonJs_sync_index'],
            minify: {
                removeComments: true,   // 删除注释
                collapseWhitespace: false,  // 取消去除空格
                removeAttributeQuotes: true // 去除属性引号
            }  
        }),
        new HtmlWebpackPlugin({
            title: '分析 webpack 模块 : Es Sync 模式',
            filename: 'es_sync_index.html',
            template: 'index.html',
            chunks: ['es_sync_index'],
            minify: {
                removeComments: true,
                collapseWhitespace: false,
                removeAttributeQuotes: true
            }  
        }),
        new HtmlWebpackPlugin({
            title: '分析 webpack 模块 : import() 模式',
            filename: 'async_index.html',
            template: 'index.html',
            chunks: ['async_index'],
            minify: {
                removeComments: true,
                collapseWhitespace: false,
                removeAttributeQuotes: true
            }  
        })
    ]
}
module.exports = options;

在这里我们配置了多入口(三个),分别对应刚才说的三种模块化方式,方便区分和调试。我们首先去探索CommonJs的模块化方式

对CommonJs的模块化的处理(对应 commonJs_sync)

我们先了解对于CommonJs的模块化webpack是如何处理的; 首先我们按照CommonJs的模块化的方式如下编写我们代码: 在commonJs_sync_index.js中:

const sync = require('./commonJs_sync');;
console.log('commonJs_sync', sync);

在commonJs_sync.js中:

module.exports = 'sync';

经过我们的webpack编译我们会发现我们dist/js的文件夹下中生成了commonJs_sync_index.initial.js文件(这是我们的编译后的分发代码,是最终运行在浏览器的代码),它的内容如下:

/*
 * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
 * This devtool is neither made for production nor for readable output files.
 * It uses "eval()" calls to create a separate source file in the browser devtools.
 * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
 * or disable the default devtool with "devtool: false".
 * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
 */
/******/ (() => { // webpackBootstrap
/******/ 	var __webpack_modules__ = ({

/***/ "./src/commonJs_sync.js":
/*!******************************!*\
  !*** ./src/commonJs_sync.js ***!
  \******************************/
/***/ ((module) => {

eval("module.exports = 'sync';\n\n//# sourceURL=webpack://webpack_module_analysis/./src/commonJs_sync.js?");

/***/ }),

/***/ "./src/commonJs_sync_index.js":
/*!************************************!*\
  !*** ./src/commonJs_sync_index.js ***!
  \************************************/
/***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {

eval("const sync = __webpack_require__(/*! ./commonJs_sync */ \"./src/commonJs_sync.js\");;\nconsole.log('commonJs_sync', sync);\n\n//# sourceURL=webpack://webpack_module_analysis/./src/commonJs_sync_index.js?");

/***/ })

/******/ 	});
/************************************************************************/
/******/ 	// 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;
/******/ 	}
/******/ 	
/************************************************************************/
/******/ 	
/******/ 	// startup
/******/ 	// Load entry module and return exports
/******/ 	// This entry module can't be inlined because the eval devtool is used.
/******/ 	var __webpack_exports__ = __webpack_require__("./src/commonJs_sync_index.js");
/******/ 	
/******/ })()
;

我们可以很清楚的看到,生成的内容是一个立即执行函数(IIFE),这样做的好处有很多,主要目的是不暴露私有属性(如__webpack_require__等私有属性),封装模块业务逻辑等。我们先不着急细致的去看,我们先分析一下打包结果的大致结构:

image.png

对这四个部分,也引出了我们要提出与之对应的四个问题:

  1. 到底什么是__webpack__modules__?我们编写的源码哪去了?所谓的__webpack__modules__与我的源码有什么关系?
  2. __webpack_module_cache__是什么?有什么作用?
  3. __webpack_require__函数是干什么用的?有什么作用?
  4. 我们在运行打包后的文件会立即从加载入口文件开始加载执行,这个入口是如何确认的?

接下来我们带着我们的疑问一步步的进行分析: 首先我们先解决第一个疑问,__webpack__modules__是什么,我们先分析一下__webpack__modules__。

image.png

  • 我们的打包结果包含两个模块(函数):
    • 一个是入口文件对应的模块(函数) ,模块id为./src/commonJs_sync.js(在开发模式下,id名字为该文件的相对路径);
    • 一个是入口同步引用文件("./commonJs_sync.js")对应的模id为 ./src/commonJs_sync_index.js的模块(函数); 一般来说模块与源文件一一对应;而__webpack_modules__对象就保存了所有的模块。这些模块(函数)是通过入口文件开始,遍历引用的文件,生成模块(函数)。

webpack模块其实就是一个个经过webpack处理后的函数;之所以把它封装成函数,就是利用函数特点来模拟模块,隔离上下文,创建私有的变量和方法,这样不会破坏全局的命名空间。最重要的是可以利用调用函数的方式来模拟调用模块;做到只有调用该模块时,才会真正去执行。

解答问题一到底什么是__webpack__modules__?我们编写的源码哪去了?所谓的__webpack__modules__与我的源码有什么关系?

解答:__webpack_modules__ 就是缓存当前所有模块(函数)的对象。 我们如何调用模块的呢?利用的就是__webpack_require__;我们接着分析调用__webpack_require__我们做了什么?

image.png

如图所见,我们就是运用了一个缓存代理(__webpack_module_cache__),保存了各模块的导出;调用__webpack_reqruie__经历以下过程:

  1. 在__webpack_module_cache__中查找,如果有缓存的模块则直接返回缓存结果;

  2. 没有缓存的模块则生成新的模块(对象)并缓存在__webpack_module_cache__,注意这里的模块(对象)不同于上文提到的模块(函数);这里的模块(对象)指的是该模块(函数)执行后的的导出结果;

  3. 在__webpack_modules__找到对应的模块函数并执行模块函数

  4. 返回模块的导出;

可以看到__webpack_require__很类似CommonJs中require中的作用,其实这很类似commonJs的模块化方式。

解答问题二__webpack_module_cache__是什么?有什么作用?

解答:__webpack_module_cache__ 就是用来缓存模块加载后的返回结果。

解答问题三__webpack_require__函数是干什么用的?有什么作用?

解答:__wepback_require__是webpack用于加载模块的方法(类似require的作用)。

至于webpack是如何确定入口的?这个问题其实很好解决,还记得我们在webpack.config.js中配置了entry吗?我们有如过如下的配置:

commonJs_sync_index: pathLib.resolve(__dirname, "./src/commonJs_sync_index.js")

解答问题四我们在运行打包后的文件会立即从加载入口文件开始加载执行,这个入口是如何确认的?

wepback就是靠entry配置来确定入口的。

Chunk

当然webpack在构建产出我们的最终代码的过程中,有一个阶段这里并没有提及,就是构建chunk,所谓的chunk,是webpack内部运行时中的一个概念;它是一系列模块的集合;它通常与最终的bundle一一对应,但是也并不一定,比如你配置了source map会产出两个最终文件,它分为以下三类:

  1. 入口文件:由入口文件及其同步加载的依赖构成(本章的这种情况);
  2. 异步加载:由异步加载进来的模块构成(比如用import()异步加载;下一章节会详细说明);
  3. 代码分割:依靠optimization.splitChunks配置生产。

以下是webpack打包的过程;

image.png 到这里webpack对于CommonJs的模块化处理的讲解就到此结束了

对esModule的模块化的处理(对应es_sync)

这时你可能会有疑问,如果是webpack是实现了类似commonJs模块化的方式,是如何支持esModule方式的模块化的:

  1. webpack是如何做到原始值变了,import加载的值也会跟着变的?
  2. 做到import输入的变量不允许改写的呢?

接下来我们带着这两个疑问,开始我们的代码分析,首先我们先用esModule的模块化风格编写我们的代码

在commonJs_sync_index.js中:

import {sync} from './es_sync';
console.log('es_sync', sync);

setTimeout(() => {
    console.log('es_sync_change', sync);
}, 3000);

在es_sync.js.js中:

setTimeout(() => {
    sync = 'sync_change'
}, 1000);
export let sync = 'sync';

经过我们的webpack编译我们会发现我们dist/js的文件夹下中生成了es_sync_index.initial.js文件,它的内容如下:

/*
 * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
 * This devtool is neither made for production nor for readable output files.
 * It uses "eval()" calls to create a separate source file in the browser devtools.
 * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
 * or disable the default devtool with "devtool: false".
 * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
 */
/******/ (() => { // webpackBootstrap
/******/ 	"use strict";
/******/ 	var __webpack_modules__ = ({

/***/ "./src/es_sync.js":
/*!************************!*\
  !*** ./src/es_sync.js ***!
  \************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   \"sync\": () => (/* binding */ sync)\n/* harmony export */ });\nsetTimeout(() => {\n    sync = 'sync_change'\n}, 1000);\nlet sync = 'sync';\n\n\n//# sourceURL=webpack://webpack_module_analysis/./src/es_sync.js?");

/***/ }),

/***/ "./src/es_sync_index.js":
/*!******************************!*\
  !*** ./src/es_sync_index.js ***!
  \******************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _es_sync__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./es_sync */ \"./src/es_sync.js\");\n\nconsole.log('es_sync', _es_sync__WEBPACK_IMPORTED_MODULE_0__.sync);\n\nsetTimeout(() => {\n    console.log('es_sync_change', _es_sync__WEBPACK_IMPORTED_MODULE_0__.sync);\n}, 3000);\n\n//# sourceURL=webpack://webpack_module_analysis/./src/es_sync_index.js?");

/***/ })

/******/ 	});
/************************************************************************/
/******/ 	// 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 */
/******/ 	(() => {
/******/ 		// define getter functions for harmony exports
/******/ 		__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/runtime/hasOwnProperty shorthand */
/******/ 	(() => {
/******/ 		__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ 	})();
/******/ 	
/******/ 	/* webpack/runtime/make namespace object */
/******/ 	(() => {
/******/ 		// define __esModule on exports
/******/ 		__webpack_require__.r = (exports) => {
/******/ 			if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ 				Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ 			}
/******/ 			Object.defineProperty(exports, '__esModule', { value: true });
/******/ 		};
/******/ 	})();
/******/ 	
/************************************************************************/
/******/ 	
/******/ 	// startup
/******/ 	// Load entry module and return exports
/******/ 	// This entry module can't be inlined because the eval devtool is used.
/******/ 	var __webpack_exports__ = __webpack_require__("./src/es_sync_index.js");
/******/ 	
/******/ })()
;

我们对比刚才 CommonJS 风格编译后的代码;发现增加了 __webpack_require__,__webpack_require__.o,__webpack_require__.r方法;并且我们导出模块的方式有所不同。我们先看下这三个方法是干什么的:

__webpack_require__.d: d 是 definePropertyGetters 的缩写;用来定义getter属性;
__webpack_require__.o: o 是 hasOwnProperty 的缩写;判断对象是否有该属性;
__webpack_require__.r: r 是 makeNamespaceObject 的缩写;要来定义esModule模块的导出。

再去观察"./src/es_sync_index.js"模块,import加载的模块,依然是用__webpack_require__来替换的;说明引入模块的方式没变。_webpack_require__.r(__webpack_exports__)这一句是用来模拟esModule的定义;

正真关键的处理在于这一句:

__webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"sync\": () => (/* binding */ sync)\n/* harmony export */ });

解答问题一webpack是如何做到原始值变了,import加载的值也会跟着变的?

解答:与CommonJs风格处理方式不同的是,并不是直接给导出模块module.exprts赋值了对象,而是在导出模块module.exports定义了一个访问属性(getter)sync,利用闭包缓存了对于变量sync的引用,保证了每次访问sync属性都是拿到最新的值(变量sync);经过1000ms 变量sync 被赋值为 'sync_change' ;再经过2000ms后读取 _es_sync__WEBPACK_IMPORTED_MODULE_0__.sync ,获取访问属性(getter), 获取到的是最新的值'sync_change';正是这种巧妙的处理,让webpack做到原始值改变了,import加载的值也会跟着变

解答问题二webpack如何做到import输入的变量不允许改写的呢?

解答:并且在严格模式下,无法set赋值。也就做到了做到import输入的变量不允许改写。 webpack就是通过这种方式模拟了esModule的模块化方式。

总结:

webpack之所以能处理不同风格的模块化方式,是因为webpack通过编译源码,处理入口文件及遍历它同步依赖的各个文件,将这些文件编译成一个个模块(函数),并缓存在 __webpack_modules__ 中;立即调用入口文件对应的模块(函数),在执行的过程中,通过 __webpack_require__ 来调用各个依赖的模块(函数)。总的来说webpack就是通过编译源码,实现了自己的模块化方式,统一处理了各个模块化的风格。

同步加载的模块化处理到这就到此结束了,下一讲我们来讲解webpack模块化的异步加载模块化处理。