本文抄自《深刻淺出webpack》,建議想學習原理的手打一遍,操做一遍,給別人講一遍,而後就會了在閱讀前但願您已有webpack相關的實踐經驗,否則讀了也讀不懂javascript
本文閱讀須要幾分鐘,理解須要本身動手操做蠻長時間css
首先簡單看一下webpack配置文件(webpack.config.js):html
var path = require('path'); var node_modules = path.resolve(__dirname, 'node_modules'); var pathToReact = path.resolve(node_modules, 'react/dist/react.min.js'); module.exports = { // 入口文件,是模塊構建的起點,同時每個入口文件對應最後生成的一個 chunk。 entry: { bundle: [ 'webpack/hot/dev-server', 'webpack-dev-server/client?http://localhost:8080', path.resolve(__dirname, 'app/app.js') ] }, // 文件路徑指向(可加快打包過程)。 resolve: { alias: { 'react': pathToReact } }, // 生成文件,是模塊構建的終點,包括輸出文件與輸出路徑。 output: { path: path.resolve(__dirname, 'build'), filename: '[name].js' }, // 這裏配置了處理各模塊的 loader ,包括 css 預處理 loader ,es6 編譯 loader,圖片處理 loader。 module: { loaders: [ { test: /\.js$/, loader: 'babel', query: { presets: ['es2015', 'react'] } } ], noParse: [pathToReact] }, // webpack 各插件對象,在 webpack 的事件流中執行對應的方法。 plugins: [ new webpack.HotModuleReplacementPlugin() ] };
在瞭解webpack原理以前,須要掌握如下幾個核心概念java
webpack從啓動到結束依次執行如下操做:node
graph TD 初始化參數 --> 開始編譯 開始編譯 -->肯定入口 肯定入口 --> 編譯模塊 編譯模塊 --> 完成編譯模塊 完成編譯模塊 --> 輸出資源 輸出資源 --> 輸出完成
各個階段執行的操做以下:react
在以上過程當中,webpack會在特定的時間點廣播特定的事件,插件監聽事件並執行相應的邏輯,而且插件能夠調用webpack提供的api改變webpack的運行結果webpack
webpack構建流程可分爲如下三大階段。git
若是隻執行一次,流程如上,但在開啓監聽模式下,流程以下圖es6
graph TD 初始化-->編譯; 編譯-->輸出; 輸出-->文本發生變化 文本發生變化-->編譯
在初始化階段會發生的事件以下github
事件 | 描述 |
---|---|
初始化參數 | 從配置文件和shell語句中讀取與合併參數,得出最終的參數,這個過程還會執行配置文件中的插件實例化語句 new Plugin() |
實例化Compiler | 實例化Compiler,傳入上一步獲得的參數,Compiler負責文件監聽和啓動編譯。在Compiler實例中包含了完整的webpack配置,全局只有一個Compiler實例。 |
加載插件 | 依次調用插件的apply方法,讓插件能夠監聽後續的全部事件節點。同時向插件中傳入compiler實例的引用,以方便插件經過compiler調用webpack的api |
environment | 開始應用Node.js風格的文件系統到compiler對象,以方便後續的文件尋找和讀取 |
Entry-option | 讀取配置的Entrys,爲每一個Entry實例化一個對應的EntryPlugin,爲後面該Entry的遞歸解析工做作準備 |
After-plugins | 調用完全部內置的和配置的插件的apply方法 |
After-resolvers | 根據配置初始化resolver,resolver負責在文件系統中尋找指定路徑的文件 |
#### 1.3.2 編譯階段 (事件名全爲小寫)
事件 | 解釋 |
---|---|
run | 啓動一次編譯 |
Watch-run | 在監聽模式下啓動編譯,文件發生變化會從新編譯 |
compile | 告訴插件一次新的編譯將要啓動,同時會給插件帶上compiler對象 |
compilation | 當webpack以開發模式運行時,每當檢測到文件的變化,便有一次新的compilation被建立。一個Compilation對象包含了當前的模塊資源、編譯生成資源、變化的文件等。compilation對象也提供了不少事件回調給插件進行拓展 |
make | 一個新的compilation對象建立完畢,即將從entry開始讀取文件,根據文件類型和編譯的loader對文件進行==編譯==,編譯完後再找出該文件依賴的文件,遞歸地編譯和解析 |
after-compile | 一次compilation執行完成 |
invalid | 當遇到錯誤會觸發改事件,該事件不會致使webpack退出 |
在編譯階段最重要的事件是compilation,由於在compilation階段調用了Loader,完成了每一個模塊的==轉換==操做。在compilation階段又會發生不少小事件,以下表
事件 | 解釋 |
---|---|
build-module | 使用相應的Loader去轉換一個模塊 |
Normal-module-loader | 在使用loader轉換完一個模塊後,使用acorn解析轉換後的內容,輸出對應的抽象語法樹(AST),以方便webpack對代碼進行分析 |
program | 從配置的入口模塊開始,分析其AST,當遇到require等導入其餘模塊的語句時,便將其加入依賴的模塊列表中,同時對於新找出來的模塊遞歸分析,最終弄清楚全部模塊的依賴關係 |
seal | 全部模塊及依賴的模塊都經過Loader轉換完成,根據依賴關係生成Chunk |
輸出階段會發生的事件及解釋:
事件 | 解釋 |
---|---|
should-emit | 全部須要輸出的文件已經生成,詢問插件有哪些文件須要輸出,有哪些不須要輸出 |
emit | 肯定好要輸出哪些文件後,執行文件輸出,==能夠在這裏獲取和修改輸出的內容== |
after-mit | 文件輸出完畢 |
done | 成功完成一次完整的編譯和輸出流程 |
failed | 若是在編譯和輸出中出現錯誤,致使webpack退出,就會直接跳轉到本步驟,插件能夠在本事件中獲取具體的錯誤緣由 |
在輸出階段已經獲得了各個模塊通過轉化後的結果和其依賴關係,而且將相應的模塊組合在一塊兒造成一個個chunk.在輸出階段根據chunk的類型,使用對應的模板生成最終要輸出的文件內容. |
//如下代碼用來包含webpack運行過程當中的每一個階段 //file:webpack.config.js const path = require('path'); //插件監聽事件並執行相應的邏輯 class TestPlugin { constructor() { console.log('@plugin constructor'); } apply(compiler) { console.log('@plugin apply'); compiler.plugin('environment', (options) => { console.log('@environment'); }); compiler.plugin('after-environment', (options) => { console.log('@after-environment'); }); compiler.plugin('entry-option', (options) => { console.log('@entry-option'); }); compiler.plugin('after-plugins', (options) => { console.log('@after-plugins'); }); compiler.plugin('after-resolvers', (options) => { console.log('@after-resolvers'); }); compiler.plugin('before-run', (options, callback) => { console.log('@before-run'); callback(); }); compiler.plugin('run', (options, callback) => { console.log('@run'); callback(); }); compiler.plugin('watch-run', (options, callback) => { console.log('@watch-run'); callback(); }); compiler.plugin('normal-module-factory', (options) => { console.log('@normal-module-factory'); }); compiler.plugin('context-module-factory', (options) => { console.log('@context-module-factory'); }); compiler.plugin('before-compile', (options, callback) => { console.log('@before-compile'); callback(); }); compiler.plugin('compile', (options) => { console.log('@compile'); }); compiler.plugin('this-compilation', (options) => { console.log('@this-compilation'); }); compiler.plugin('compilation', (options) => { console.log('@compilation'); }); compiler.plugin('make', (options, callback) => { console.log('@make'); callback(); }); compiler.plugin('compilation', (compilation) => { compilation.plugin('build-module', (options) => { console.log('@build-module'); }); compilation.plugin('normal-module-loader', (options) => { console.log('@normal-module-loader'); }); compilation.plugin('program', (options, callback) => { console.log('@program'); callback(); }); compilation.plugin('seal', (options) => { console.log('@seal'); }); }); compiler.plugin('after-compile', (options, callback) => { console.log('@after-compile'); callback(); }); compiler.plugin('should-emit', (options) => { console.log('@should-emit'); }); compiler.plugin('emit', (options, callback) => { console.log('@emit'); callback(); }); compiler.plugin('after-emit', (options, callback) => { console.log('@after-emit'); callback(); }); compiler.plugin('done', (options) => { console.log('@done'); }); compiler.plugin('failed', (options, callback) => { console.log('@failed'); callback(); }); compiler.plugin('invalid', (options) => { console.log('@invalid'); }); } }
#在目錄下執行 webpack #輸出如下內容 @plugin constructor @plugin apply @environment @after-environment @entry-option @after-plugins @after-resolvers @before-run @run @normal-module-factory @context-module-factory @before-compile @compile @this-compilation @compilation @make @build-module @normal-module-loader @build-module @normal-module-loader @seal @after-compile @should-emit @emit @after-emit @done Hash: 19ef3b418517e78b5286 Version: webpack 3.11.0 Time: 95ms Asset Size Chunks Chunk Names bundle.js 3.03 kB 0 [emitted] main [0] ./main.js 44 bytes {0} [built] [1] ./show.js 114 bytes {0} [built]
下面經過 Webpack 構建一個採用 CommonJS 模塊化編寫的項目,該項目有個網頁會經過 JavaScript 在網頁中顯示 Hello,Webpack
。
運行構建前,先把要完成該功能的最基礎的 JavaScript 文件和 HTML 創建好,須要以下文件:
頁面入口文件 index.html
<html> <head> <meta charset="UTF-8"> </head> <body> <div id="app"></div> <!--導入 Webpack 輸出的 JavaScript 文件--> <script src="./dist/bundle.js"></script> </body> </html>
JS 工具函數文件 show.js
// 操做 DOM 元素,把 content 顯示到網頁上 function show(content) { window.document.getElementById('app').innerText = 'Hello,' + content; } // 經過 CommonJS 規範導出 show 函數 module.exports = show;
JS 執行入口文件 main.js
// 經過 CommonJS 規範導入 show 函數 const show = require('./show.js'); // 執行 show 函數 show('Webpack');
Webpack 在執行構建時默認會從項目根目錄下的 webpack.config.js
文件讀取配置,因此你還須要新建它,其內容以下:
const path = require('path'); module.exports = { // JavaScript 執行入口文件 entry: './main.js', output: { // 把全部依賴的模塊合併輸出到一個 bundle.js 文件 filename: 'bundle.js', // 輸出文件都放到 dist 目錄下 path: path.resolve(__dirname, './dist'), } };
因爲 Webpack 構建運行在 Node.js 環境下,因此該文件最後須要經過 CommonJS 規範導出一個描述如何構建的 Object
對象。
|-- index.html |-- main.js |-- show.js |-- webpack.config.js
一切文件就緒,在項目根目錄下執行 webpack
命令運行 Webpack 構建,你會發現目錄下多出一個 dist
目錄,裏面有個 bundle.js
文件, bundle.js
文件是一個可執行的 JavaScript 文件,它包含頁面所依賴的兩個模塊 main.js
和 show.js
及內置的 webpackBootstrap
啓動函數。 這時你用瀏覽器打開 index.html
網頁將會看到 Hello,Webpack
。
看以前記住:一個模塊就是一個文件,
首先看下bundle.js長什麼樣子:
注意:序號1處是個自執行函數,序號2做爲自執行函數的參數傳入
具體代碼以下:(建議把如下代碼放入編輯器中查看,最好讓index.html執行下,弄清楚執行的順序)
(function(modules) { // webpackBootstrap // 1. 緩存模塊 var installedModules = {}; // 2. 定義能夠在瀏覽器使用的require函數 function __webpack_require__(moduleId) { // 2.1檢查模塊是否在緩存裏,在的話直接返回 if(installedModules[moduleId]) { return installedModules[moduleId].exports; } // 2.2 模塊不在緩存裏,新建一個對象module=installModules[moduleId] {i:moduleId,l:模塊是否加載,exports:模塊返回值} var module = installedModules[moduleId] = { i: moduleId,//第一次執行爲0 l: false, exports: {} };//第一次執行module:{i:0,l:false,exports:{}} // 2.3 執行傳入的參數中對應id的模塊 第一次執行數組中傳入的第一個參數 //modules[0].call({},{i:0,l:false,exports:{}},{},__webpack_require__函數) modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // 2.4 將這個模塊標記爲已加載 module.l = true; // 2.5 返回這個模塊的導出值 return module.exports; } // 3. webpack暴露屬性 m c d n o p __webpack_require__.m = modules; __webpack_require__.c = installedModules; __webpack_require__.d = function(exports, name, getter) { if(!__webpack_require__.o(exports, name)) { Object.defineProperty(exports, name, { configurable: false, enumerable: true, get: getter }); } }; __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; }; __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; __webpack_require__.p = ""; // 4. 執行reruire函數引入第一個模塊(main.js對應的模塊) return __webpack_require__(__webpack_require__.s = 0); }) ([ // 0. 傳入參數,參數是個數組 /* 第0個參數 main.js對應的文件*/ (function(module, exports, __webpack_require__) { // 經過 CommonJS 規範導入 show 函數 const show = __webpack_require__(1);//__webpack_require__(1)返回show // 執行 show 函數 show('Webpack'); }), /* 第1個參數 show.js對應的文件 */ (function(module, exports) { // 操做 DOM 元素,把 content 顯示到網頁上 function show(content) { window.document.getElementById('app').innerText = 'Hello,' + content; } // 經過 CommonJS 規範導出 show 函數 module.exports = show; }) ]);
以上看上去複雜的代碼實際上是一個自執行函數(文件做爲自執行函數的參數),能夠簡寫以下:
(function(modules){ //模擬require語句 function __webpack_require__(){} //執行存放全部模塊數組中的第0個模塊(main.js) __webpack_require_[0] })([/*存放全部模塊的數組*/])
bundles.js能直接在瀏覽器中運行的緣由是,在輸出的文件中經過__webpack_require__
函數,定義了一個能夠在瀏覽器中執行的加載函數(加載文件使用ajax實現),來模擬Node.js中的require語句。
原來一個個獨立的模塊文件被合併到了一個單獨的 bundle.js 的緣由在於瀏覽器不能像 Node.js 那樣快速地去本地加載一個個模塊文件,而必須經過網絡請求去加載還未獲得的文件。 若是模塊數量不少,加載時間會很長,所以把全部模塊都存放在了數組中,執行一次網絡加載。
修改main.js,改爲import引入模塊
import show from './show'; show('Webpack');
在目錄下執行webpack
,會發現:
([//自執行函數和上面相同,參數不一樣 /* 0 */ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__show__ = __webpack_require__(1); Object(__WEBPACK_IMPORTED_MODULE_0__show__["a" /* default */])('Webpack'); }), /* 1 */ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; /* harmony export (immutable) */ __webpack_exports__["a"] = show; function show(content) { window.document.getElementById('app').innerText = 'Hello,' + content; } }) ]);
參數不一樣的緣由是es6的import和export模塊被webpack編譯處理過了,其實做用是同樣的,接下來看一下在main.js中異步加載模塊時,bundle.js是怎樣的
main.js
修改以下
import('./show').then(show=>{ show('Webpack') })
構建成功後會生成兩個文件
其中0.bundle.js文件的內容以下:
webpackJsonp(/*在其餘文件中存放的模塊的ID*/[0],[//本文件所包含的模塊 /* 0 */, /* 1 show.js對應的模塊 */ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /* harmony export (immutable) */ __webpack_exports__["default"] = show; function show(content) { window.document.getElementById('app').innerText = 'Hello,' + content; } }) ]);
bundle.js文件的內容以下:
注意:bundle.js比上面的bundle.js的區別在於:
__webpack_require__.e
,用於加載被分割出去的須要異步加載的chunk對應的文件(function(modules) { // webpackBootstrap // install a JSONP callback for chunk loading var parentJsonpFunction = window["webpackJsonp"]; // webpackJsonp用於從異步加載的文件中安裝模塊 // 將webpackJsonp掛載到全局是爲了方便在其餘文件中調用 /** * @param chunkIds 異步加載的模塊中須要安裝的模塊對應的id * @param moreModules 異步加載的模塊中須要安裝模塊列表 * @param executeModules 異步加載的模塊安裝成功後須要執行的模塊對應的index */ window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) { // add "moreModules" to the modules object, // then flag all "chunkIds" as loaded and fire callback var moduleId, chunkId, i = 0, resolves = [], result; for(;i < chunkIds.length; i++) { chunkId = chunkIds[i]; if(installedChunks[chunkId]) { resolves.push(installedChunks[chunkId][0]); } installedChunks[chunkId] = 0; } for(moduleId in moreModules) { if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { modules[moduleId] = moreModules[moduleId]; } } if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules); while(resolves.length) { resolves.shift()(); } }; // The module cache var installedModules = {}; // objects to store loaded and loading chunks var installedChunks = { 1: 0 }; // The require function function __webpack_require__(moduleId) { // Check if module is in cache if(installedModules[moduleId]) { return installedModules[moduleId].exports; } // Create a new module (and put it into the cache) var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; // Execute the module function modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // Flag the module as loaded module.l = true; // Return the exports of the module return module.exports; } // This file contains only the entry chunk. // The chunk loading function for additional chunks /** * 用於加載被分割出去的須要異步加載的chunk對應的文件 * @param chunkId 須要異步加載的chunk對應的id * @returns {Promise} */ __webpack_require__.e = function requireEnsure(chunkId) { var installedChunkData = installedChunks[chunkId]; if(installedChunkData === 0) { return new Promise(function(resolve) { resolve(); }); } // a Promise means "currently loading". if(installedChunkData) { return installedChunkData[2]; } // setup Promise in chunk cache var promise = new Promise(function(resolve, reject) { installedChunkData = installedChunks[chunkId] = [resolve, reject]; }); installedChunkData[2] = promise; // start chunk loading var head = document.getElementsByTagName('head')[0]; var script = document.createElement('script'); script.type = "text/javascript"; script.charset = 'utf-8'; script.async = true; script.timeout = 120000; if (__webpack_require__.nc) { script.setAttribute("nonce", __webpack_require__.nc); } script.src = __webpack_require__.p + "" + chunkId + ".bundle.js"; var timeout = setTimeout(onScriptComplete, 120000); script.onerror = script.onload = onScriptComplete; function onScriptComplete() { // avoid mem leaks in IE. script.onerror = script.onload = null; clearTimeout(timeout); var chunk = installedChunks[chunkId]; if(chunk !== 0) { if(chunk) { chunk[1](new Error('Loading chunk ' + chunkId + ' failed.')); } installedChunks[chunkId] = undefined; } }; head.appendChild(script); return promise; }; // expose the modules object (__webpack_modules__) __webpack_require__.m = modules; // expose the module cache __webpack_require__.c = installedModules; // define getter function for harmony exports __webpack_require__.d = function(exports, name, getter) { if(!__webpack_require__.o(exports, name)) { Object.defineProperty(exports, name, { configurable: false, enumerable: true, get: getter }); } }; // 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 = ""; // on error function for async loading __webpack_require__.oe = function(err) { console.error(err); throw err; }; // Load entry module and return exports return __webpack_require__(__webpack_require__.s = 0); }) /************************************************************************/ ([//存放沒有通過異步加載的,隨着執行入口文件加載的模塊 /* 0 */ /***/ (function(module, exports, __webpack_require__) { __webpack_require__.e/* import() */(0).then(__webpack_require__.bind(null, 1)).then(show=>{ show('Webpack') }) /***/ }) ]);