javascript
webpack4.x 模块化浅析-CommonJS
先看下webpack官方文檔中對模塊的描述:
在模塊化編程中,開發者將程序分解成離散功能塊(discrete chunks of functionality),并稱之為模塊。每個模塊具有比完整程序更小的接觸面,使得校驗、調試、測試輕而易舉。 精心編寫的模塊提供了可靠的抽象和封裝界限,使得應用程序中每個模塊都具有條理清楚的設計和明確的目的。
webpack 的核心概念之一就是一切皆模塊,webpack 在項目中的作用就是,分析項目的結構,找到 JavaScript 模塊以及其他一些瀏覽器不能直接運行的拓展語言(less,scss,typescript),并將其打包為合適的格式以供瀏覽器使用,它從一個項目的主文件開始,根據引用路徑,找到所有其所依賴的文件,同時將這些文件進行處理(各種loader來解析,編譯處理瀏覽器不能直接使用的文件),然后打包為一個或者多個瀏覽器可識別的JavaScript文件。
本文不會詳細描述 webpack 的構建流程,畢竟官網已經說得比較詳細了,這里主要是分析下 webpack 打包后的文件,將文件打包成什么樣子,又是如何使用模塊的。webpack 最早支持的代碼模塊化方式是 CommonJS,后面慢慢支持了 ES6、AMD 等,不論使用的是哪種方式,webpack 都可以對其進行解析和打包,本文例子中使用的是 CommonJS 規范,更多規范介紹可查看官方文檔。
例子
為方便后面的說明,首先創建一個項目,也就是先建立一個文件夾 webpack-test(名字自擬),然后在里面新建一個 package.json 文件,用來做 npm 的說明,在 webpack-test 文件夾中使用命令:
npm init執行命令后會詢問一些問題,一路回車即可。然后安裝下 webpack 的依賴包,如下命令:
npm install --save-dev webpack再新建幾個文件:
1、在項目根目錄下新建文件夾 app 用來存放業務代碼、文件夾 public 存放打包后的文件;
2、在app中新建入口文件 main.js;
3、在app中新建功能模塊 hello.js,bye.js,to.js;
4、在項目根目錄下,建立 index.html 文件;
然后依次來給這幾個文件分別填寫以下內容:
// webpack-test/app/hello.js const to = require('./to.js'); module.exports = function() {var hello = document.createElement('div');hello.textContent = "Say Hello to " + to.name;return hello; }; // webpack-test/app/bye.js const to = require('./to.js'); module.exports = function() {var bye = document.createElement('div');bye.textContent = "Say Bye to " + to.name;return bye; }; // webpack-test/app/to.js module.exports = {name: "小明"}; // webpack-test/app/main.js const hello = require('./hello.js'); const bye = require('./bye.js');document.querySelector("#root").appendChild(hello()).appendChild(bye());; // webpack-test/index.html <!DOCTYPE html> <html lang="en"><head><meta charset="utf-8"><title>Webpack Test Project</title></head><body><div id='root'></div>// bundle.js 文件就是一會兒我們要打包app中的文件后生成的結果文件<script src="public/bundle.js"></script></body> </html>業務模塊 hello.js 和 bye.js 做了各自的操作,同時引用了共同的文件 to.js;主文件 main.js 中引用并執行了模塊 hello.js 和 bye.js;index.html 文件引入了 main.js 打包后的最終文件 bundle.js。
打包
接下來進行打包操作,先確保 webpack 是全局安裝的,否則執行時需要指定 webpack 的路徑,比如在 4.0 以下版本中使用 node_modules/.bin/webpack ./app/main.js ./public/bundle.js;
如果你使用的是 webpack4.0+ 的話,使用 webpack ./app/main.js ./public/bundle.js命令,也許會報以下的錯誤:
webpack4.0+之后,針對第一個報錯,需要指定環境 --mode development;第二個報錯,是因為我們沒有使用配置文件的方式打包,而是直接使用的命令指定的打包輸出位置,所以需要聲明輸出文件,綜上,正確的命令如下:
webpack app/main.js --output public/bundle.js --mode development執行結果:
? webpack-test webpack app/main.js --output public/bundle.js --mode development Hash: a4e2f9ecc51b64891624 Version: webpack 4.25.1 Time: 90ms Built at: 2018-11-08 17:11:01Asset Size Chunks Chunk Names bundle.js 5.16 KiB main [emitted] main Entrypoint main = bundle.js [./app/bye.js] 165 bytes {main} [built] [./app/hello.js] 173 bytes {main} [built] [./app/main.js] 144 bytes {main} [built] [./app/to.js] 30 bytes {main} [built] ? webpack-test瀏覽器打開 index.html 文件,即可看到結果
Say Hello to 小明Say Bye to 小明
但是 webpack 作為一個能簡化我們開發難度和使用便捷的工具,顯然像上面那樣通過敲很多命令來打包,并不方便,所以下面采用配置文件的方式再來一次:
根目錄創建 webpack.config.js 文件,并配置下打包入口和出口:
// webpack-test/webpack.config.js module.exports = {mode: "development",//webpack.0之后需要聲明環境entry: __dirname + "/app/main.js",//唯一入口文件output: {path: __dirname + "/public",//打包后的文件存放目錄filename: "bundle.js"//打包后輸出文件名} }再次打包的時候,只需要使用命令 webpack 就可以了,webpack 默認讀取當前路徑下的 webpack.config.js 文件。
最終打包好的 bundle.js 文件,去除了多余注釋,調整了代碼格式,內容如下:
// 自執行函數,參數為所有模塊組成的,形勢為key:value,key是模塊名 (function(modules) { // webpackBootstrap// 已加載模塊的緩存,記錄模塊的加載情況,也是為了避免重復打包,節省資源var installedModules = {};// webpack 使用 require 方式加載模塊的方法(模擬ConmmonJS reqiure()),作用為根據傳進來的模塊id來處理對應的模塊,加入已加載緩存,執行,標記,返回exportsfunction __webpack_require__(moduleId) {// moduleId 為模塊路徑// 檢測模塊是否已加載,已加載的話直接返回該模塊if(installedModules[moduleId]) {return installedModules[moduleId].exports;}// 當前模塊未加載的話,新建,并存于緩存var module = installedModules[moduleId] = {i: moduleId,l: false,exports: {}};// 在當前模塊的 exports 下,也就是模塊的內部執行模塊代碼,突出作用域modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);// 標記模塊已經加載module.l = true;// 返回模塊的導出內容return module.exports;}// 掛載屬性,該模塊 (__webpack_modules__)__webpack_require__.m = modules;// 掛載屬性,模塊加載緩存__webpack_require__.c = installedModules;// 本代碼中未執行,暫時不分析// 在 exports 中定義 getter 方法__webpack_require__.d = function(exports, name, getter) {// 當 name 屬性不是定義在對象本身,而是繼承自原型鏈,則在在 exports 中定義 getter 方法if(!__webpack_require__.o(exports, name)) {Object.defineProperty(exports, name, { enumerable: true, get: getter });}};// 本代碼中未執行,暫時不分析// 在 exports 中定義 __esModule,定義key為Symbol的屬性(在__webpack_require__.t中被調用)// 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 = "";// 加載入口模塊 main.js ,返回 exports,從而從入口文件開始執行,以遞歸的方式,將所有依賴執行并返回return __webpack_require__(__webpack_require__.s = "./app/main.js"); })({"./app/bye.js": (function(module, exports, __webpack_require__) {eval("const to = __webpack_require__(/*! ./to.js */ \"./app/to.js\");\nmodule.exports = function() {\n var bye = document.createElement('div');\n bye.textContent = \"Say Bye to \" + to.name;\n return bye;\n};\n\n//# sourceURL=webpack:///./app/bye.js?");}),"./app/hello.js": (function(module, exports) {eval("module.exports = function() {\n var hello = document.createElement('div');\n hello.textContent = \"Say Hello!\";\n return hello;\n};\n\n//# sourceURL=webpack:///./app/hello.js?");}),"./app/main.js": (function(module, exports, __webpack_require__) {eval("const hello = __webpack_require__(/*! ./hello.js */ \"./app/hello.js\");\nconst bye = __webpack_require__(/*! ./bye.js */ \"./app/bye.js\");\n\ndocument.querySelector(\"#root\").appendChild(hello()).appendChild(bye());;\n\n//# sourceURL=webpack:///./app/main.js?");}),"./app/to.js": (function(module, exports) {eval("module.exports = {name: \"小明\"};\n\n//# sourceURL=webpack:///./app/to.js?");})});分析
webpack 的運行過程可分為:讀取配置參數,實例化插件,模塊解析處理(loader),輸出打包文件;在上面例子中,僅為 JavaScript 的引用,沒有使用插件和像CSS、less、圖片之類需要loader處理的模塊,所以上面的例子,過程只有讀取配置,識別入口及其引用模塊,打包幾步,生成最終的 bundle.js 文件。
簡單描述下 webpack 在這個過程中的執行流程:在配置文件中讀取入口,如果有配置 plugins 參數,那么也是在此時進行插件的實例化和鉤子函數的綁定;模塊解析,也就是loader加入的時刻,從入口文件開始,根據入口文件對其他模塊的依賴,結合配置文件中對不同種類型文件所使用的 loader(加載器) 說明,一個一個逐級對這些模塊進行解析處理,或壓縮,或轉義,生成瀏覽器可以直接識別的內容;最后將所有模塊進行打包,輸出打包后的文件。在上面的代碼中,已經對 bundle.js 內容進行了內容注釋,下面我們來分析下 bundle.js 的執行過程:
1、自執行函數
最后的輸出的文件 bundle.js 是一個 JavaScript 文件,其本身其實是一個自執行函數
(function(參數){})(參數)。2、參數
自執行方法的參數為所有模塊組成的對象,key 為各個模塊的路徑,值為各模塊內部的執行代碼,觀察參數內部的代碼,對比打包前的源碼,可以發現凡是 require 都變成了__webpack_require__這個webpack自定義的模塊調用方法,而且源碼中的相對路徑也變成了最終執行位置的文件的相對路徑。
{"./app/bye.js": (function(module, exports, __webpack_require__) {eval("const to = __webpack_require__(/*! ./to.js */ \"./app/to.js\");\nmodule.exports = function() {\n var bye = document.createElement('div');\n bye.textContent = \"Say Bye to \" + to.name;\n return bye;\n};\n\n//# sourceURL=webpack:///./app/bye.js?");}),"./app/hello.js": (function(module, exports) {eval("module.exports = function() {\n var hello = document.createElement('div');\n hello.textContent = \"Say Hello!\";\n return hello;\n};\n\n//# sourceURL=webpack:///./app/hello.js?");}),"./app/main.js": (function(module, exports, __webpack_require__) {eval("const hello = __webpack_require__(/*! ./hello.js */ \"./app/hello.js\");\nconst bye = __webpack_require__(/*! ./bye.js */ \"./app/bye.js\");\n\ndocument.querySelector(\"#root\").appendChild(hello()).appendChild(bye());;\n\n//# sourceURL=webpack:///./app/main.js?");}),"./app/to.js": (function(module, exports) {eval("module.exports = {name: \"小明\"};\n\n//# sourceURL=webpack:///./app/to.js?");})}3、執行
(1)自執行文件開始執行后,到自執行函數最底部,首先從入口文件開始加載
return __webpack_require__(__webpack_require__.s = "./app/main.js");(2)__webpack_require__函數被調用,傳入參數 ./app/main.js,
function __webpack_require__(moduleId) {// moduleId 為 ./app/main.js// 首次進來,未加載,模塊還沒有緩存if(installedModules[moduleId]) {return installedModules[moduleId].exports;}// 新建 ./app/main.js 模塊,并存于緩存var module = installedModules[moduleId] = {i: moduleId,l: false,exports: {}};modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);// 標記模塊已經加載module.l = true;// 輸出模塊的內容return module.exports;}此時方法中執行 modules[moduleId].call(module.exports, module, module.exports,__webpack_require__); 相當于在名為 ./app/main.js 的模塊中執行如下代碼:
(function(module, exports, __webpack_require__) {eval("const hello = __webpack_require__(/*! ./hello.js */ \"./app/hello.js\");\nconst bye = __webpack_require__(/*! ./bye.js */ \"./app/bye.js\");\n\ndocument.querySelector(\"#root\").appendChild(hello()).appendChild(bye());;\n\n//# sourceURL=webpack:///./app/main.js?");})()由于引用關系,接下來會再次執行兩次__webpack_require__方法,分別傳參模塊路徑 ./app/hello.js 和 ./app/bye.js;
(3)執行第一個__webpack_require__過程,除了傳參不同、執行的模塊不同,與第二步基本一致,再次找到了依賴模塊 to.js,再次調用__webpack_require__。
"./app/hello.js": (function(module, exports, __webpack_require__) {eval("const to = __webpack_require__(/*! ./to.js */ \"./app/to.js\");\nmodule.exports = function() {\n var hello = document.createElement('div');\n hello.textContent = \"Say Hello to \" + to.name;\n return hello;\n};\n\n//# sourceURL=webpack:///./app/hello.js?");}),(4)執行第二個__webpack_require__時,在 bye.js 中找到了對于 to.js 的依賴,所以將繼續調用__webpack_require__方法,只是傳參變成了./app/to.js,達到終點。
"./app/bye.js": (function(module, exports, __webpack_require__) {eval("const to = __webpack_require__(/*! ./to.js */ \"./app/to.js\");\nmodule.exports = function() {\n var bye = document.createElement('div');\n bye.textContent = \"Say Bye to \" + to.name;\n return bye;\n};\n\n//# sourceURL=webpack:///./app/bye.js?");})(5)到此時,整個從入口文件的開始的針對所依賴模塊的解析已經完成,所有的 js 代碼也已經引用完畢且放到了 bundle.js 中。
總結
到這里可以看到,webpack對js的打包,就是封裝為一個個單獨的方法,通過對這些方法的引用,達到模塊化的效果;而打包的過程,就是查找、解析、封裝這些方法的過程,整個執行路徑類似于一棵樹,從主干出發,沿著樹枝遞歸式的執行“require”方法,而且是直到這一根樹枝走到盡頭的時候才回頭尋找其他的方法,由于node的單線程,當項目龐大或者模塊間依賴錯綜復雜時,webpack打包會更加的耗費時間。
以上為對webpack4.x中針對js模塊處理的簡單理解,主要基于官方文檔的介紹和打包后文件的分析,源碼讀起來還是比較難懂,暫時不敢照量。對于 ES6、AMD 的模塊化方式,代碼分割的等,后續再進行補充。
以上如有問題,歡迎指正!
總結
以上是生活随笔為你收集整理的webpack4.x 模块化浅析-CommonJS的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mongodb分片概念和原理-实战分片集
- 下一篇: 面试官问我:什么是JavaScript闭