揭祕 webpack5 模塊打包
在上一節中我們初步瞭解了webpack
可以利用內置靜態模塊類型 (asset module type
) 來處理資源文件,我們所知道的本地服務,資源的壓縮,代碼分割,在webpack
構建的工程中有一個比較顯著的特徵是,模塊化,要麼commonjs
要麼esModule
, 在開發環境我們都是基於這兩種,那麼通過webpack
打包後,如何讓其支持瀏覽器能正常的加載兩種不同的模式呢?
接下來我們一起來探討下webpack
中打包後代碼的原理
正文開始...
初始化基礎項目
新建一個文件夾webpack-05-module
,
npm init -y
我們安裝項目一些基礎支持的插件
npm i webpack webpack-cli webpack-dev-server html-webpack-plugin babel-loader @babel
l/core -D
在根目錄新建webpack.config.js
,配置相關參數,爲了測試 webpack 打包cjs
與esModule
我在entry
寫了兩個入口文件,並且設置mode:development
與devtool: 'source-map'
, 設置source-map
是爲了更好的查看源代碼
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
entry: {
cjs: './src/commonjs_index.js',
esjs: './src/esmodule_index.js'
},
devtool: 'source-map',
output: {
filename: 'js/[name].js',
path: path.resolve(__dirname, 'dist'),
assetModuleFilename: 'images/[name][ext]'
},
mode: 'development',
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
options: {
presets: ['@babel/env']
}
},
{
test: /\.(png|jpg)$/i,
type: 'asset/resource'
// generator: {
// // filename: 'images/[name][ext]',
// publicPath: '/assets/images/'
// }
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html'
})
]
};
在src
目錄下新建commonjs_index.js
, esmodule_index.js
文件
commonjs_index.js
// commonjs_index.js
const { twoSum } = require('./utils/common.js');
import imgSrc from './assets/images/1.png';
console.log('cm_sum=' + twoSum(1, 2));
const domApp = document.getElementById('app');
var img = new Image();
img.src = imgSrc;
domApp.appendChild(img);
引入的common.js
// utils/common.js
function twoSum(a, b) {
return a + b;
}
module.exports = {
twoSum
};
esmodule_index.js
// esmodule_index.js
import twoSumMul from './utils/esmodule.js';
console.log('es_sum=' + twoSumMul(2, 2));
引入的esmodule.js
// utils/esmodule.js
function twoSumMul(a, b) {
return a * b;
}
// esModule
export default twoSumMul;
當我們運行npm run build
命令,會在根目錄dist/js
文件夾下打包入口指定的兩個文件
webpack 打包 cjs 最終代碼
我把對應註釋去掉後就是下面這樣的
// cjs.js
(() => {
var __webpack_modules__ = {
'./src/utils/common.js': (module) => {
function twoSum(a, b) {
return a + b;
}
module.exports = {
twoSum: twoSum
};
},
'./src/assets/images/1.png': (module, __unused_webpack_exports, __webpack_require__) => {
'use strict';
module.exports = __webpack_require__.p + 'images/1.png';
}
};
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__ "moduleId");
return module.exports;
}
(() => {
__webpack_require__.g = (function () {
if (typeof globalThis === 'object') return globalThis;
try {
return this || new Function('return this')();
} catch (e) {
if (typeof window === 'object') return window;
}
})();
})();
(() => {
__webpack_require__.r = (exports) => {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
(() => {
var scriptUrl;
if (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + '';
var document = __webpack_require__.g.document;
if (!scriptUrl && document) {
if (document.currentScript) scriptUrl = document.currentScript.src;
if (!scriptUrl) {
var scripts = document.getElementsByTagName('script');
if (scripts.length) scriptUrl = scripts[scripts.length - 1].src;
}
}
if (!scriptUrl) throw new Error('Automatic publicPath is not supported in this browser');
scriptUrl = scriptUrl
.replace(/#.*$/, '')
.replace(/\?.*$/, '')
.replace(/\/[^\/]+$/, '/');
__webpack_require__.p = scriptUrl + '../';
})();
var __webpack_exports__ = {};
(() => {
'use strict';
__webpack_require__.r(__webpack_exports__);
var _assets_images_1_png__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./assets/images/1.png */ './src/assets/images/1.png');
var _require = __webpack_require__(/*! ./utils/common.js */ './src/utils/common.js'),
twoSum = _require.twoSum;
console.log('cm_sum=' + twoSum(1, 2));
var domApp = document.getElementById('app');
var img = new Image();
img.src = _assets_images_1_png__WEBPACK_IMPORTED_MODULE_0__;
domApp.appendChild(img);
})();
})();
初次看,感覺webpack
打包cjs
的代碼太長了,但是刪除掉註釋後,我們仔細分析發現,並沒有那麼複雜
首先是該模塊採用 IFEE 模式,一個匿名的自定義自行函數內包裹了幾大塊區域
1、初始化定義了 webpack 依賴的模塊
var __webpack_modules__ = {
'./src/utils/common.js': (module) => {
function twoSum(a, b) {
return a + b;
}
// 當在執行時,返回這個具體函數體內容
module.exports = {
twoSum: twoSum
};
},
'./src/assets/images/1.png': (module, __unused_webpack_exports, __webpack_require__) => {
'use strict';
// 每一個對應的模塊對應的內容
module.exports = __webpack_require__.p + 'images/1.png';
}
};
我們發現webpack
是用模塊引入的路徑當成key
, 然後value
就是一個函數,函數體內就是引入的具體代碼內容,並且內部傳入了一個形參module
, 實際上這個module
就是爲{exports: {}}
定義的對象,把內部函數twoSum
綁定了在對象上
2、調用模塊優先從緩存對象模塊取值
var __webpack_module_cache__ = {};
// moduleId 就是引入的路徑
function __webpack_require__(moduleId) {
// 根據moduleId優先從緩存中獲取__webpack_modules__中綁定的值 {twoSum: TwoSum}
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// 傳入__webpack_modules__內部value的形參 module
var module = (__webpack_module_cache__[moduleId] = {
exports: {}
});
__webpack_modules__[moduleId](module, module.exports, __webpack_require__ "moduleId");
// 根據moduleId依次返回 {twoSum: twoSum}、__webpack_require__.p + 'images/1.png‘圖片路徑
return module.exports;
}
3、綁定全局對象,引入圖片的資源路徑,主要是__webpack_require__.p
圖片地址
(() => {
__webpack_require__.g = (function () {
if (typeof globalThis === 'object') return globalThis;
try {
return this || new Function('return this')();
} catch (e) {
if (typeof window === 'object') return window;
}
})();
})();
(() => {
var scriptUrl;
if (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + '';
var document = __webpack_require__.g.document;
if (!scriptUrl && document) {
if (document.currentScript) scriptUrl = document.currentScript.src;
if (!scriptUrl) {
var scripts = document.getElementsByTagName('script');
if (scripts.length) scriptUrl = scripts[scripts.length - 1].src;
}
}
if (!scriptUrl) throw new Error('Automatic publicPath is not supported in this browser');
scriptUrl = scriptUrl
.replace(/#.*$/, '')
.replace(/\?.*$/, '')
.replace(/\/[^\/]+$/, '/');
// 獲取圖片路徑
__webpack_require__.p = scriptUrl + '../';
})();
4、將esModule
轉換,用Object.defineProperty
攔截exports
(module.exports) 對象添加__esModule
屬性
(() => {
__webpack_require__.r = (exports) => {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
5、__webpack_require__(moduleId)
執行獲取對應的內容
var __webpack_exports__ = {};
(() => {
'use strict';
// 在步驟4中做對象攔截,添加__esMoules屬性
__webpack_require__.r(__webpack_exports__);
//根據路徑獲取對應module.exports的內容也就是__webpack_require__中的module.exports對象的數據
var _assets_images_1_png__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./assets/images/1.png */ './src/assets/images/1.png');
var _require = __webpack_require__(/*! ./utils/common.js */ './src/utils/common.js'),
twoSum = _require.twoSum;
console.log('cm_sum=' + twoSum(1, 2));
var domApp = document.getElementById('app');
var img = new Image();
img.src = _assets_images_1_png__WEBPACK_IMPORTED_MODULE_0__;
domApp.appendChild(img);
})();
})();
webpack 打包 esModule 最終代碼
我們看下具體代碼
// esjs.js
(() => {
// webpackBootstrap
'use strict';
var __webpack_modules__ = {
'./src/utils/esmodule.js': (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
function twoSumMul(a, b) {
return a * b;
}
const __WEBPACK_DEFAULT_EXPORT__ = twoSumMul;
__webpack_require__.d(__webpack_exports__, {
default: () => __WEBPACK_DEFAULT_EXPORT__
});
}
};
// 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__ "moduleId");
// Return the exports of the module
return module.exports;
}
(() => {
// 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 });
};
})();
/************************************************************************/
var __webpack_exports__ = {};
(() => {
__webpack_require__.r(__webpack_exports__);
var _utils_esmodule_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils/esmodule.js */ './src/utils/esmodule.js');
console.log('es_sum=' + (0, _utils_esmodule_js__WEBPACK_IMPORTED_MODULE_0__['default'])(2, 2));
})();
})();
看着代碼似乎與cjs
大體差不多,事實上有些不一樣
當我們執行_utils_esmodule_js__WEBPACK_IMPORTED_MODULE_0__
這個方法時,實際會在__webpack_modules__
方法會根據moduleId
執行 value 值的函數體,而函數體會被__webpack_require__.d
這個方法進行攔截,會執行 Object.defineProperty
的get
方法,返回綁定在__webpack_exports__
對象的值上
主要看以下兩段代碼
var __webpack_modules__ = {
'./src/utils/esmodule.js': (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
// 這裏定義模塊時就已經先進行了攔截,這裏與cjs有很大的區別
__webpack_require__.r(__webpack_exports__);
function twoSumMul(a, b) {
return a * b;
}
const __WEBPACK_DEFAULT_EXPORT__ = twoSumMul;
__webpack_require__.d(__webpack_exports__, {
default: () => __WEBPACK_DEFAULT_EXPORT__
});
}
};
...
(() => {
// 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 轉換esModule
代碼中, 同樣會是有優先從緩存對象中獲取,通過調用 __webpack_modules__[moduleId](module, module.exports, __webpack_require__ "moduleId");
這個方法,改變module.exports
根據moduleId
獲取函數體內的值twoSumMul
函數
最後畫了一張簡易的圖,文字理解還是有點多,紙上得來終學淺,絕知此事要躬行,還是得寫個簡單的demo
自己深深體會下,具體參考文末的code example
總結
-
webpack 打包
cjs
與esModule
的區別,本質上就是爲了在瀏覽器支持 webpack 中使用export default {}
與module.exports
在瀏覽器定義了一個全局變量__webpack_modules__
根據引入的模塊路徑變成key
,value
就是在webpack
中的cjs
或者esModule
中函數體。 -
當我們在
cjs
使用require('/path')
、或者在esModule
中使用import xx from '/path'
時,實際上webpack
把require
orimport
轉變成了一個定義的函數__webpack_require__('moduleId')
的可執行函數。 -
cjs
是在執行__webpack_require__.r(__webpack_exports__)
是就已經預先將__webpack_require__
返回的函數體內容進行了綁定,只有在執行_webpack_require__(/*! ./utils/common.js */ './src/utils/common.js')
返回函數體,本質上就是在運行時執行 -
esMoule
實際上是在定義時就已經進行了綁定,在定義__webpack_exports__
時,執行了__webpack_require__.r(__webpack_exports__);
動態添加__esModule
屬性, 根據moduleId
定義模塊時,執行了__webpack_require__.d(__webpack_exports__, { default: () => __WEBPACK_DEFAULT_EXPORT__});
, 將對應模塊函數體會直接用對象攔截執行Object.defineProperty
的get
方法, 執行definition[key]
從而返回函數體。本質上就是在編譯前執行,而不是像cjs
一樣在函數體執行階段直接輸出對應內容。 -
他們相同點就是優先會從緩存
__webpack_module_cache__
對象中根據moduleId
直接獲取對應的可執行函數體 -
本文 code example[1]
參考資料
[1]
code example: https://github.com/maicFir/lessonNote/tree/master/webpack/webpack-05-module
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/4kD3xQKtskItj2c2SVUIiA