webpack的模块化处理(一)
webpack的模块化处理(一)
前言及准备工作:
-
本系列文章致力于让读者了解 webpack 模块化相关知识;
-
我们会从打包结果来探索 webpack 对模块化的具体处理方式;但是不会涉及 webpack 的具体编译细节(如 acorn 进行 AST 转换等)和底层构建方式;
-
本篇文章采用当前最新的 webpack(5.66.0);在阅读本篇文章之前,建议先 clone 代码后参照文章学习,这样理解起来效果会更好:
-
-
我们知道,webpack 支持各种模块语法风格,包括 ES6,CommonJS 和 AMD 等;
- 那么webpack究竟有什么魔法做到兼容各种模块化风格的呢?
- 在不支持 es Module 的浏览器中,也能实现 import 函数异步加载,它又是如何处理的呢? 带着这些疑问开启我们的 webpack 探索之旅吧。
在了解webpack的模块化方式之前,我们先看我们的文件结构:
红框所标记的,分别为异步加载模块化,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__等私有属性),封装模块业务逻辑等。我们先不着急细致的去看,我们先分析一下打包结果的大致结构:
对这四个部分,也引出了我们要提出与之对应的四个问题:
- 到底什么是__webpack__modules__?我们编写的源码哪去了?所谓的__webpack__modules__与我的源码有什么关系?
- __webpack_module_cache__是什么?有什么作用?
- __webpack_require__函数是干什么用的?有什么作用?
- 我们在运行打包后的文件会立即从加载入口文件开始加载执行,这个入口是如何确认的?
接下来我们带着我们的疑问一步步的进行分析: 首先我们先解决第一个疑问,__webpack__modules__是什么,我们先分析一下__webpack__modules__。
- 我们的打包结果包含两个模块(函数):
- 一个是入口文件对应的模块(函数) ,模块id为
./src/commonJs_sync.js
(在开发模式下,id名字为该文件的相对路径); - 一个是入口同步引用文件("./commonJs_sync.js")对应的模id为
./src/commonJs_sync_index.js
的模块(函数); 一般来说模块与源文件一一对应;而__webpack_modules__对象就保存了所有的模块。这些模块(函数)是通过入口文件开始,遍历引用的文件,生成模块(函数)。
- 一个是入口文件对应的模块(函数) ,模块id为
webpack模块其实就是一个个经过webpack处理后的函数;之所以把它封装成函数,就是利用函数特点来模拟模块,隔离上下文,创建私有的变量和方法,这样不会破坏全局的命名空间。最重要的是可以利用调用函数的方式来模拟调用模块;做到只有调用该模块时,才会真正去执行。
解答问题一:到底什么是__webpack__modules__?我们编写的源码哪去了?所谓的__webpack__modules__与我的源码有什么关系?
解答:__webpack_modules__ 就是缓存当前所有模块(函数)的对象。 我们如何调用模块的呢?利用的就是__webpack_require__;我们接着分析调用__webpack_require__我们做了什么?
如图所见,我们就是运用了一个缓存代理(__webpack_module_cache__),保存了各模块的导出;调用__webpack_reqruie__经历以下过程:
-
在__webpack_module_cache__中查找,如果有缓存的模块则直接返回缓存结果;
-
没有缓存的模块则生成新的模块(对象)并缓存在__webpack_module_cache__,注意这里的模块(对象)不同于上文提到的模块(函数);这里的模块(对象)指的是该模块(函数)执行后的的导出结果;
-
在__webpack_modules__找到对应的模块函数并执行模块函数
-
返回模块的导出;
可以看到__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会产出两个最终文件,它分为以下三类:
- 入口文件:由入口文件及其同步加载的依赖构成(本章的这种情况);
- 异步加载:由异步加载进来的模块构成(比如用import()异步加载;下一章节会详细说明);
- 代码分割:依靠
optimization.splitChunks
配置生产。
以下是webpack打包的过程;
到这里webpack对于CommonJs的模块化处理的讲解就到此结束了
对esModule的模块化的处理(对应es_sync)
这时你可能会有疑问,如果是webpack是实现了类似commonJs模块化的方式,是如何支持esModule方式的模块化的:
- webpack是如何做到原始值变了,import加载的值也会跟着变的?
- 做到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模块化的异步加载模块化处理。
转载自:https://juejin.cn/post/7069738874804633630