先看下webpack官方文檔中對模塊的描述:css
在模塊化編程中,開發者將程序分解成離散功能塊(discrete chunks of functionality),並稱之爲模塊。
每一個模塊具備比完整程序更小的接觸面,使得校驗、調試、測試垂手可得。 精心編寫的模塊提供了可靠的抽象和封裝界限,使得應用程序中每一個模塊都具備條理清楚的設計和明確的目的。
webpack 的核心概念之一就是一切皆模塊,webpack 在項目中的做用就是,分析項目的結構,找到 JavaScript 模塊以及其餘一些瀏覽器不能直接運行的拓展語言(less,scss,typescript),並將其打包爲合適的格式以供瀏覽器使用,它從一個項目的主文件開始,根據引用路徑,找到全部其所依賴的文件,同時將這些文件進行處理(各類loader來解析,編譯處理瀏覽器不能直接使用的文件),而後打包爲一個或者多個瀏覽器可識別的JavaScript文件。html
本文不會詳細描述 webpack 的構建流程,畢竟官網已經說得比較詳細了,這裏主要是分析下 webpack 打包後的文件,將文件打包成什麼樣子,又是如何使用模塊的。webpack 最先支持的代碼模塊化方式是 CommonJS,後面慢慢支持了 ES六、AMD 等,不論使用的是哪一種方式,webpack 均可以對其進行解析和打包,本文例子中使用的是 CommonJS 規範,更多規範介紹可查看官方文檔。node
爲方便後面的說明,首先建立一個項目,也就是先創建一個文件夾 webpack-test(名字自擬),而後在裏面新建一個 package.json 文件,用來作 npm 的說明,在 webpack-test 文件夾中使用命令:webpack
npm init
執行命令後會詢問一些問題,一路回車便可。而後安裝下 webpack 的依賴包,以下命令:web
npm install --save-dev webpack
再新建幾個文件:
一、在項目根目錄下新建文件夾 app 用來存放業務代碼、文件夾 public 存放打包後的文件;
二、在app中新建入口文件 main.js;
三、在app中新建功能模塊 hello.js,bye.js,to.js;
四、在項目根目錄下,創建 index.html 文件;typescript
而後依次來給這幾個文件分別填寫如下內容:npm
// 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命令,也許會報如下的錯誤:json
WARNING in configuration The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment. You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/ ERROR in multi ./app/main.js ./public/bundle.js Module not found: Error: Can't resolve './public/bundle.js' in '/Users/zhaohaipeng/soyoung-project/webpack-test' @ multi ./app/main.js ./public/bundle.js main[1]
webpack4.0+以後,針對第一個報錯,須要指定環境 --mode development;第二個報錯,是由於咱們沒有使用配置文件的方式打包,而是直接使用的命令指定的打包輸出位置,因此須要聲明輸出文件,綜上,正確的命令以下:api
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:01 Asset 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來處理對應的模塊,加入已加載緩存,執行,標記,返回exports function __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 的執行過程:
一、自執行函數
最後的輸出的文件 bundle.js 是一個 JavaScript 文件,其自己實際上是一個自執行函數
(function(參數){})(參數)。
二、參數
自執行方法的參數爲全部模塊組成的對象,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?"); }) }
三、執行
(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模塊處理的簡單理解,主要基於官方文檔的介紹和打包後文件的分析,源碼讀起來仍是比較難懂,暫時不敢照量。對於 ES六、AMD 的模塊化方式,代碼分割的等,後續再進行補充。
以上若有問題,歡迎指正!