webpack是目前最為流行的打包工具之一,其配置簡(jiǎn)單,功能強(qiáng)大,擁有豐富的加載器和插件系統(tǒng),為前端開(kāi)發(fā)者提供了諸多便利。筆者默認(rèn)各位看官在閱讀本章之前已經(jīng)有了一定的使用經(jīng)驗(yàn),所以對(duì)webpack的使用方式不做贅述。
創(chuàng)新互聯(lián)專業(yè)為企業(yè)提供貢山網(wǎng)站建設(shè)、貢山做網(wǎng)站、貢山網(wǎng)站設(shè)計(jì)、貢山網(wǎng)站制作等企業(yè)網(wǎng)站建設(shè)、網(wǎng)頁(yè)設(shè)計(jì)與制作、貢山企業(yè)網(wǎng)站模板建站服務(wù),十載貢山做網(wǎng)站經(jīng)驗(yàn),不只是建網(wǎng)站,更提供有價(jià)值的思路和整體網(wǎng)絡(luò)服務(wù)。閱讀本章,你可以了解到如下內(nèi)容:
Webpack打包后代碼結(jié)構(gòu)Webpack核心架構(gòu) —— TapableWebpack事件流Webpack插件實(shí)現(xiàn)機(jī)制Webpack加載器實(shí)現(xiàn)機(jī)制Webpack打包后代碼結(jié)構(gòu)
簡(jiǎn)單打包
我們首先寫(xiě)一個(gè)最簡(jiǎn)單的方法,然后使用webpack進(jìn)行打包:
// /webpack/bundles/simple/moduleA.js window.printA = function printA() { console.log(`This is module A!`); }
一個(gè)比較基本的webpack配置文件:
// /webpack/bundles/simple/webpack.config.js const path = require('path'); const webpack = require('webpack'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: { main: './moduleA.js' }, output: { path: path.resolve(__dirname, 'dist'), filename: 'simple.bundle.js' }, plugins: [ new HtmlWebpackPlugin({ template: './index.html' }) ] }
創(chuàng)建一個(gè)HTML文件用于在瀏覽器環(huán)境下測(cè)試:
Webpack - Simple Bundle
執(zhí)行打包命令webpack
后我們獲得了一個(gè)dist
目錄,我們打開(kāi)simple.bundle.js
文件:
/******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // 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; /******/ } /******/ /******/ /******/ // 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 = ""; /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = 0); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ (function(module, exports) { window.printA = function printA() { console.log(`This is module A!`); } /***/ }) /******/ ]);
主要看這段:
// ...... var installedModules = {}; /******/ /******/ // 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; /******/ } /******/ /******/ /******/ // 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 = ""; /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = 0); // ......
webpack內(nèi)部定義了一個(gè) webpack_require 的方法,這個(gè)方法的實(shí)質(zhì)很簡(jiǎn)單:
多模塊間存在簡(jiǎn)單依賴
例如 moduleB.js 依賴于 moduleA.js 文件。
// /webpack/bundles/simpleDependencies/moduleA.js module.exports = window.printA = function printA() { console.log(`This is module A!`); }
// /webpack/bundles/simpleDependencies/moduleB.js const printA = require('./moduleA'); module.exports = window.printB = function printB() { printA(); console.log('This is module B!'); }
將配置文件中的入口更改為
// /webpack/bundles/simpleDependencies/webpack.config.js // ... main: './moduleB.js' // ...
再次打包,我們獲得如下代碼:
// /webpack/bundles/simpleDependencies/dist/bundle.js /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // 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; /******/ } /******/ /******/ /******/ // 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 = ""; /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = 0); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ (function(module, exports, __webpack_require__) { const printA = __webpack_require__(1); module.exports = window.printB = function printB() { printA(); console.log('This is module B!'); } /***/ }), /* 1 */ /***/ (function(module, exports) { module.exports = window.printA = function printA() { console.log(`This is module A!`); } /***/ }) /******/ ]);
我們可以發(fā)現(xiàn)這塊有點(diǎn)變化:
/* 0 */ /***/ (function(module, exports, __webpack_require__) { const printA = __webpack_require__(1); module.exports = window.printB = function printB() { printA(); console.log('This is module B!'); }
在 moduleB.js 中,需要依賴 moduleA ,因而需要先執(zhí)行 __webpack_require(1) 拿到模塊A后,再進(jìn)行下一步。
多入口
需要注意,打包的文件中moudleId是不會(huì)重復(fù)的,如果有兩個(gè)入口文件的情況,則入口模塊id都為0,其他依賴模塊id不重復(fù)。我們創(chuàng)建如下幾個(gè)文件,其中 index0.js 依賴于 common.js 與 dependency.js ,而 index1.js 依賴于 index0.js 和 common.js 兩個(gè)文件。
// /webpack/bundles/multi/common.js module.exports = function() { console.log('This is common module!'); }
// /webpack/bundles/multi/dependency .js module.exports = function() { console.log('This is dependency module!'); }
// /webpack/bundles/multi/index0.js const common = require('./common'); const dependency = require('./dependency'); module.exports = window.print0 = function() { common(); dependency(); console.log('This is module 0!'); }
// /webpack/bundles/multi/index1.js const common = require('./common'); const index0 = require('./index0'); module.exports = window.print1 = function() { common(); console.log('This is module 1!'); }
修改 webpack.config.js 中的文件入口:
// /webpack/bundles/multi/webpack.config.js // ... entry: { index0: './index0.js', index1: './index1.js' }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].bundle.js' }, // ...
打包后的文件:
// /webpack/bundles/multi/dist/index0.bundle.js /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // 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; /******/ } /******/ /******/ /******/ // 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 = ""; /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = 1); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ (function(module, exports) { module.exports = function() { console.log('This is common module!'); } /***/ }), /* 1 */ /***/ (function(module, exports, __webpack_require__) { const common = __webpack_require__(0); const dependency = __webpack_require__(2); module.exports = window.print0 = function() { common(); dependency(); console.log('This is module 0!'); } /***/ }), /* 2 */ /***/ (function(module, exports) { module.exports = function() { console.log('This is dependency module!'); } /***/ }) /******/ ]);
// /webpack/bundles/multi/dist/index1.bundle.js /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // 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; /******/ } /******/ /******/ /******/ // 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 = ""; /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = 3); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ (function(module, exports) { module.exports = function() { console.log('This is common module!'); } /***/ }), /* 1 */ /***/ (function(module, exports, __webpack_require__) { const common = __webpack_require__(0); const dependency = __webpack_require__(2); module.exports = window.print0 = function() { common(); dependency(); console.log('This is module 0!'); } /***/ }), /* 2 */ /***/ (function(module, exports) { module.exports = function() { console.log('This is dependency module!'); } /***/ }), /* 3 */ /***/ (function(module, exports, __webpack_require__) { const common = __webpack_require__(0); const index0 = __webpack_require__(1); module.exports = window.print1 = function() { common(); console.log('This is module 1!'); } /***/ }) /******/ ]);
顯然,在未使用 CommonsChunkPlugin 這個(gè)插件之前,這兩個(gè)文件是存在重復(fù)代碼的。也就是每個(gè)入口都會(huì)獨(dú)立進(jìn)行打包。
我們看如果添加了 CommonsChunkPlugin 這個(gè)插件后的情況(修改 webpack.config.js):
// /webpack/bundles/CommonsChunkPlugin/webpack.config.js plugins: [ // ... new webpack.optimize.CommonsChunkPlugin({ name: 'common', filename: 'common.js' }) ]
這樣一來(lái)會(huì)生成三個(gè)文件,index0.bundle.js ,index1.bundel.js 以及 common.js:
// /webpack/bundles/CommonsChunkPlugin/dist/common.js /******/ (function(modules) { // webpackBootstrap /******/ // install a JSONP callback for chunk loading /******/ var parentJsonpFunction = window["webpackJsonp"]; /******/ 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()(); /******/ } /******/ if(executeModules) { /******/ for(i=0; i < executeModules.length; i++) { /******/ result = __webpack_require__(__webpack_require__.s = executeModules[i]); /******/ } /******/ } /******/ return result; /******/ }; /******/ /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // objects to store loaded and loading chunks /******/ var installedChunks = { /******/ 2: 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; /******/ } /******/ /******/ /******/ // 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; }; /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ (function(module, exports) { module.exports = function() { console.log('This is common module!'); } /***/ }), /* 1 */ /***/ (function(module, exports, __webpack_require__) { const common = __webpack_require__(0); const dependency = __webpack_require__(2); module.exports = window.print0 = function() { common(); dependency(); console.log('This is module 0!'); } /***/ }), /* 2 */ /***/ (function(module, exports) { module.exports = function() { console.log('This is dependency module!'); } /***/ }) /******/ ]);
common.js 已經(jīng)包含了所有的公共方法,并且在瀏覽器 window 對(duì)象中創(chuàng)建了一個(gè)名為 webpackJsonp 的方法。
// /webpack/bundles/CommonsChunkPlugin/dist/common.js // ... /******/ var parentJsonpFunction = window["webpackJsonp"]; /******/ 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()(); /******/ } /******/ if(executeModules) { /******/ for(i=0; i < executeModules.length; i++) { /******/ result = __webpack_require__(__webpack_require__.s = executeModules[i]); /******/ } /******/ } /******/ return result; /******/ }; // ... /******/ // objects to store loaded and loading chunks /******/ var installedChunks = { /******/ 2: 0 /******/ }; // ...
這個(gè)方法與 __webpack_require__ 較為類似,同樣也是將模塊緩存進(jìn)來(lái)。只不過(guò) webpack 會(huì)預(yù)先抽取公共模塊,先將其緩存進(jìn)來(lái),而后可以在其他的 bundle.js 中使用 webpackJsonp 方法進(jìn)行模塊加載。
// /webpack/bundles/CommonsChunkPlugin/dist/index0.bundle.js webpackJsonp([1],[],[1]);
// /webpack/bundles/CommonsChunkPlugin/dist/index1.bundle.js webpackJsonp([0],{ /***/ 3: /***/ (function(module, exports, __webpack_require__) { const common = __webpack_require__(0); const index0 = __webpack_require__(1); module.exports = window.print1 = function() { common(); console.log('This is module 1!'); } /***/ }) },[3]);
Webpack核心架構(gòu) —— Tapable
從github
上將webpack源碼克隆至本地,我們可以先了解到 webpack 的一個(gè)整體流程:
具體分析_addModuleChain,其完成的第二件事構(gòu)建模塊又可以分為三部分:
調(diào)用loader處理模塊之間的依賴。將loader處理后的文件通過(guò)acorn抽象成抽象語(yǔ)法樹(shù)AST。遍歷AST,構(gòu)建該模塊的所有依賴。具體看lib/webpack.js
這個(gè)文件,此文件為webpack
的入口文件。
const webpack = (options, callback) => { const webpackOptionsValidationErrors = validateSchema( webpackOptionsSchema, options ); if (webpackOptionsValidationErrors.length) { throw new WebpackOptionsValidationError(webpackOptionsValidationErrors); } let compiler; if (Array.isArray(options)) { compiler = new MultiCompiler(options.map(options => webpack(options))); } else if (typeof options === "object") { options = new WebpackOptionsDefaulter().process(options); compiler = new Compiler(options.context); compiler.options = options; new NodeEnvironmentPlugin().apply(compiler); if (options.plugins && Array.isArray(options.plugins)) { for (const plugin of options.plugins) { plugin.apply(compiler); } } compiler.hooks.environment.call(); compiler.hooks.afterEnvironment.call(); compiler.options = new WebpackOptionsApply().process(options, compiler); } else { throw new Error("Invalid argument: options"); } if (callback) { if (typeof callback !== "function") throw new Error("Invalid argument: callback"); if ( options.watch === true || (Array.isArray(options) && options.some(o => o.watch)) ) { const watchOptions = Array.isArray(options) ? options.map(o => o.watchOptions || {}) : options.watchOptions || {}; return compiler.watch(watchOptions, callback); } compiler.run(callback); } return compiler; };
lib/webpack.js
中流程大致如下:
Compiler
(編譯器)對(duì)象注冊(cè)并執(zhí)行NodeEnvironmentPlugin
執(zhí)行鉤子environment
里的方法執(zhí)行鉤子afterEnvironment
里的方法注冊(cè)并執(zhí)行各種插件將compiler
向外導(dǎo)出顯然,Compiler是我們需要深究的一個(gè)部分,因?yàn)?webpack 最終向外部返回也就是這個(gè) Compiler 實(shí)例。大致了解下Compiler
的實(shí)現(xiàn):
class Compiler extends Tapable { constructor(context) { super(); this.hooks = { // ... }; this._pluginCompat.tap("Compiler", options => { // ... }); // ... this.resolvers = { normal: { // ... }, loader: { // ... }, context: { // ... } }; // ... } watch(watchOptions, handler) { // ... } run(callback) { // ... } runAsChild(callback) { // ... } purgeInputFileSystem() { // ... } emitAssets(compilation, callback) { // ... } emitRecords(callback) { // ... } readRecords(callback) { // ... } createChildCompiler( compilation, compilerName, compilerIndex, outputOptions, plugins ) { // ... } isChild() { // ... } createCompilation() { // ... } newCompilation(params) { // ... } createNormalModuleFactory() { // ... } createContextModuleFactory() { // ... } newCompilationParams() { // ... } compile(callback) { // ... } }
Compiler
繼承自Tapable
,在其構(gòu)造方法中,定義了一些事件鉤子(hooks
)、一些變量以及一些方法。這些變量以及方法目前看來(lái)還是非常抽象的,所以我們有必要去了解下Tapable
的實(shí)現(xiàn)。
Tapable的Github主頁(yè) 對(duì)Tapable
的介紹如下:
實(shí)際上,webpack基于事件流機(jī)制,它的工作流程就是將各個(gè)插件串聯(lián)起來(lái),而實(shí)現(xiàn)這一切的核心就是Tapable,webpack中最核心的負(fù)責(zé)編譯的Compiler和負(fù)責(zé)創(chuàng)建bundles的Compilation都是Tapable的實(shí)例。Tapable
向外暴露許多的鉤子類,這些類可以很方便地為插件創(chuàng)建事件鉤子。Tapable
中定義了如下幾種鉤子類:
所有鉤子類的構(gòu)造函數(shù)都接收一個(gè)可選的參數(shù),這個(gè)參數(shù)是一個(gè)由字符串參數(shù)組成的數(shù)組,如下:
const hook = new SyncHook(["arg1", "arg2", "arg3"]);
鉤子概覽
Tapable的鉤子分為兩類,同步和異步,其中異步又分為并行和串行:
每種鉤子都有各自的使用方式,如下表:
Sync鉤子
同步串行
(1) SyncHook
不關(guān)心監(jiān)聽(tīng)函數(shù)的返回值
const { SyncHook } = require("tapable"); let queue = new SyncHook(['name']); //所有的構(gòu)造函數(shù)都接收一個(gè)可選的參數(shù),這個(gè)參數(shù)是一個(gè)字符串的數(shù)組。 // 訂閱 queue.tap('1', function (name, name2) {// tap 的第一個(gè)參數(shù)是用來(lái)標(biāo)識(shí)訂閱的函數(shù)的 console.log(name, name2, 1); return '1' }); queue.tap('2', function (name) { console.log(name, 2); }); queue.tap('3', function (name) { console.log(name, 3); }); // 發(fā)布 queue.call('webpack', 'webpack-cli');// 發(fā)布的時(shí)候觸發(fā)訂閱的函數(shù) 同時(shí)傳入?yún)?shù) // 執(zhí)行結(jié)果: /* webpack undefined 1 // 傳入的參數(shù)需要和new實(shí)例的時(shí)候保持一致,否則獲取不到多傳的參數(shù) webpack 2 webpack 3 */原理
class SyncHook_MY{ constructor(){ this.hooks = []; } // 訂閱 tap(name, fn){ this.hooks.push(fn); } // 發(fā)布 call(){ this.hooks.forEach(hook => hook(...arguments)); } }
(2) SyncBailHook
只要監(jiān)聽(tīng)函數(shù)中有一個(gè)函數(shù)的返回值不為 null,則跳過(guò)剩下所有的邏輯
const { SyncBailHook } = require("tapable"); let queue = new SyncBailHook(['name']); queue.tap('1', function (name) { console.log(name, 1); }); queue.tap('2', function (name) { console.log(name, 2); return 'wrong' }); queue.tap('3', function (name) { console.log(name, 3); }); queue.call('webpack'); // 執(zhí)行結(jié)果: /* webpack 1 webpack 2 */原理
class SyncBailHook_MY { constructor() { this.hooks = []; } // 訂閱 tap(name, fn) { this.hooks.push(fn); } // 發(fā)布 call() { for (let i = 0, l = this.hooks.length; i < l; i++) { let hook = this.hooks[i]; let result = hook(...arguments); if (result) { break; } } } }
(3) SyncWaterfallHook
上一個(gè)監(jiān)聽(tīng)函數(shù)的返回值可以傳給下一個(gè)監(jiān)聽(tīng)函數(shù)
const { SyncWaterfallHook } = require("tapable"); let queue = new SyncWaterfallHook(['name']); // 上一個(gè)函數(shù)的返回值可以傳給下一個(gè)函數(shù) queue.tap('1', function (name) { console.log(name, 1); return 1; }); queue.tap('2', function (data) { console.log(data, 2); return 2; }); queue.tap('3', function (data) { console.log(data, 3); }); queue.call('webpack'); // 執(zhí)行結(jié)果: /* webpack 1 1 2 2 3 */原理
class SyncWaterfallHook_MY{ constructor(){ this.hooks = []; } // 訂閱 tap(name, fn){ this.hooks.push(fn); } // 發(fā)布 call(){ let result = null; for(let i = 0, l = this.hooks.length; i < l; i++) { let hook = this.hooks[i]; result = i == 0 ? hook(...arguments): hook(result); } } }
(4) SyncLoopHook
當(dāng)監(jiān)聽(tīng)函數(shù)被觸發(fā)的時(shí)候,如果該監(jiān)聽(tīng)函數(shù)返回true時(shí)則這個(gè)監(jiān)聽(tīng)函數(shù)會(huì)反復(fù)執(zhí)行,如果返回 undefined 則表示退出循環(huán)。
const { SyncLoopHook } = require("tapable"); let queue = new SyncLoopHook(['name']); let count = 3; queue.tap('1', function (name) { console.log('count: ', count--); if (count > 0) { return true; } return; }); queue.call('webpack'); // 執(zhí)行結(jié)果: /* count: 3 count: 2 count: 1 */原理
class SyncLoopHook_MY { constructor() { this.hook = null; } // 訂閱 tap(name, fn) { this.hook = fn; } // 發(fā)布 call() { let result; do { result = this.hook(...arguments); } while (result) } }
Async鉤子
異步并行
(1) AsyncParallelHook
不關(guān)心監(jiān)聽(tīng)函數(shù)的返回值。有三種注冊(cè)/發(fā)布的模式,如下:
const { AsyncParallelHook } = require("tapable"); let queue1 = new AsyncParallelHook(['name']); console.time('cost'); queue1.tap('1', function (name) { console.log(name, 1); }); queue1.tap('2', function (name) { console.log(name, 2); }); queue1.tap('3', function (name) { console.log(name, 3); }); queue1.callAsync('webpack', err => { console.timeEnd('cost'); }); // 執(zhí)行結(jié)果 /* webpack 1 webpack 2 webpack 3 cost: 4.520ms */usage - tapAsync
let queue2 = new AsyncParallelHook(['name']); console.time('cost1'); queue2.tapAsync('1', function (name, cb) { setTimeout(() => { console.log(name, 1); cb(); }, 1000); }); queue2.tapAsync('2', function (name, cb) { setTimeout(() => { console.log(name, 2); cb(); }, 2000); }); queue2.tapAsync('3', function (name, cb) { setTimeout(() => { console.log(name, 3); cb(); }, 3000); }); queue2.callAsync('webpack', () => { console.log('over'); console.timeEnd('cost1'); }); // 執(zhí)行結(jié)果 /* webpack 1 webpack 2 webpack 3 over time: 3004.411ms */usage - promise
let queue3 = new AsyncParallelHook(['name']); console.time('cost3'); queue3.tapPromise('1', function (name, cb) { return new Promise(function (resolve, reject) { setTimeout(() => { console.log(name, 1); resolve(); }, 1000); }); }); queue3.tapPromise('1', function (name, cb) { return new Promise(function (resolve, reject) { setTimeout(() => { console.log(name, 2); resolve(); }, 2000); }); }); queue3.tapPromise('1', function (name, cb) { return new Promise(function (resolve, reject) { setTimeout(() => { console.log(name, 3); resolve(); }, 3000); }); }); queue3.promise('webpack') .then(() => { console.log('over'); console.timeEnd('cost3'); }, () => { console.log('error'); console.timeEnd('cost3'); }); /* webpack 1 webpack 2 webpack 3 over cost3: 3007.925ms */
異步串行
(1) AsyncSeriesHook
不關(guān)心callback()的參數(shù)。
const { AsyncSeriesHook } = require("tapable"); // tap let queue1 = new AsyncSeriesHook(['name']); console.time('cost1'); queue1.tap('1', function (name) { console.log(1); return "Wrong"; }); queue1.tap('2', function (name) { console.log(2); }); queue1.tap('3', function (name) { console.log(3); }); queue1.callAsync('zfpx', err => { console.log(err); console.timeEnd('cost1'); }); // 執(zhí)行結(jié)果 /* 1 2 3 undefined cost1: 3.933ms */usage - tapAsync
let queue2 = new AsyncSeriesHook(['name']); console.time('cost2'); queue2.tapAsync('1', function (name, cb) { setTimeout(() => { console.log(name, 1); cb(); }, 1000); }); queue2.tapAsync('2', function (name, cb) { setTimeout(() => { console.log(name, 2); cb(); }, 2000); }); queue2.tapAsync('3', function (name, cb) { setTimeout(() => { console.log(name, 3); cb(); }, 3000); }); queue2.callAsync('webpack', (err) => { console.log(err); console.log('over'); console.timeEnd('cost2'); }); // 執(zhí)行結(jié)果 /* webpack 1 webpack 2 webpack 3 undefined over cost2: 6019.621ms */usage - promise
let queue3 = new AsyncSeriesHook(['name']); console.time('cost3'); queue3.tapPromise('1',function(name){ return new Promise(function(resolve){ setTimeout(function(){ console.log(name, 1); resolve(); },1000) }); }); queue3.tapPromise('2',function(name,callback){ return new Promise(function(resolve){ setTimeout(function(){ console.log(name, 2); resolve(); },2000) }); }); queue3.tapPromise('3',function(name,callback){ return new Promise(function(resolve){ setTimeout(function(){ console.log(name, 3); resolve(); },3000) }); }); queue3.promise('webapck').then(err=>{ console.log(err); console.timeEnd('cost3'); }); // 執(zhí)行結(jié)果 /* webapck 1 webapck 2 webapck 3 undefined cost3: 6021.817ms */原理
class AsyncSeriesHook_MY { constructor() { this.hooks = []; } tapAsync(name, fn) { this.hooks.push(fn); } callAsync() { var slef = this; var args = Array.from(arguments); let done = args.pop(); let idx = 0; function next(err) { // 如果next的參數(shù)有值,就直接跳躍到 執(zhí)行callAsync的回調(diào)函數(shù) if (err) return done(err); let fn = slef.hooks[idx++]; fn ? fn(...args, next) : done(); } next(); } }
(2) AsyncSeriesBailHook
callback()的參數(shù)不為null,就會(huì)直接執(zhí)行callAsync等觸發(fā)函數(shù)綁定的回調(diào)函數(shù)。
const { AsyncSeriesBailHook } = require("tapable"); // tap let queue1 = new AsyncSeriesBailHook(['name']); console.time('cost1'); queue1.tap('1', function (name) { console.log(1); return "Wrong"; }); queue1.tap('2', function (name) { console.log(2); }); queue1.tap('3', function (name) { console.log(3); }); queue1.callAsync('webpack', err => { console.log(err); console.timeEnd('cost1'); }); // 執(zhí)行結(jié)果: /* 1 null cost1: 3.979ms */usage - tapAsync
let queue2 = new AsyncSeriesBailHook(['name']); console.time('cost2'); queue2.tapAsync('1', function (name, callback) { setTimeout(function () { console.log(name, 1); callback(); }, 1000) }); queue2.tapAsync('2', function (name, callback) { setTimeout(function () { console.log(name, 2); callback('wrong'); }, 2000) }); queue2.tapAsync('3', function (name, callback) { setTimeout(function () { console.log(name, 3); callback(); }, 3000) }); queue2.callAsync('webpack', err => { console.log(err); console.log('over'); console.timeEnd('cost2'); }); // 執(zhí)行結(jié)果 /* webpack 1 webpack 2 wrong over cost2: 3014.616ms */usage - promise
let queue3 = new AsyncSeriesBailHook(['name']); console.time('cost3'); queue3.tapPromise('1', function (name) { return new Promise(function (resolve, reject) { setTimeout(function () { console.log(name, 1); resolve(); }, 1000) }); }); queue3.tapPromise('2', function (name, callback) { return new Promise(function (resolve, reject) { setTimeout(function () { console.log(name, 2); reject(); }, 2000) }); }); queue3.tapPromise('3', function (name, callback) { return new Promise(function (resolve) { setTimeout(function () { console.log(name, 3); resolve(); }, 3000) }); }); queue3.promise('webpack').then(err => { console.log(err); console.log('over'); console.timeEnd('cost3'); }, err => { console.log(err); console.log('error'); console.timeEnd('cost3'); }); // 執(zhí)行結(jié)果: /* webpack 1 webpack 2 undefined error cost3: 3017.608ms */
(3) AsyncSeriesWaterfallHook
上一個(gè)監(jiān)聽(tīng)函數(shù)的中的callback(err, data)的第二個(gè)參數(shù),可以作為下一個(gè)監(jiān)聽(tīng)函數(shù)的參數(shù)
const { AsyncSeriesWaterfallHook } = require("tapable"); // tap let queue1 = new AsyncSeriesWaterfallHook(['name']); console.time('cost1'); queue1.tap('1', function (name) { console.log(name, 1); return 'lily' }); queue1.tap('2', function (data) { console.log(2, data); return 'Tom'; }); queue1.tap('3', function (data) { console.log(3, data); }); queue1.callAsync('webpack', err => { console.log(err); console.log('over'); console.timeEnd('cost1'); }); // 執(zhí)行結(jié)果: /* webpack 1 2 'lily' 3 'Tom' null over cost1: 5.525ms */usage - tapAsync
let queue2 = new AsyncSeriesWaterfallHook(['name']); console.time('cost2'); queue2.tapAsync('1', function (name, callback) { setTimeout(function () { console.log('1: ', name); callback(null, 2); }, 1000) }); queue2.tapAsync('2', function (data, callback) { setTimeout(function () { console.log('2: ', data); callback(null, 3); }, 2000) }); queue2.tapAsync('3', function (data, callback) { setTimeout(function () { console.log('3: ', data); callback(null, 3); }, 3000) }); queue2.callAsync('webpack', err => { console.log(err); console.log('over'); console.timeEnd('cost2'); }); // 執(zhí)行結(jié)果: /* 1: webpack 2: 2 3: 3 null over cost2: 6016.889ms */usage - promise
let queue3 = new AsyncSeriesWaterfallHook(['name']); console.time('cost3'); queue3.tapPromise('1', function (name) { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('1:', name); resolve('1'); }, 1000) }); }); queue3.tapPromise('2', function (data, callback) { return new Promise(function (resolve) { setTimeout(function () { console.log('2:', data); resolve('2'); }, 2000) }); }); queue3.tapPromise('3', function (data, callback) { return new Promise(function (resolve) { setTimeout(function () { console.log('3:', data); resolve('over'); }, 3000) }); }); queue3.promise('webpack').then(err => { console.log(err); console.timeEnd('cost3'); }, err => { console.log(err); console.timeEnd('cost3'); }); // 執(zhí)行結(jié)果: /* 1: webpack 2: 1 3: 2 over cost3: 6016.703ms */原理
class AsyncSeriesWaterfallHook_MY { constructor() { this.hooks = []; } tapAsync(name, fn) { this.hooks.push(fn); } callAsync() { let self = this; var args = Array.from(arguments); let done = args.pop(); console.log(args); let idx = 0; let result = null; function next(err, data) { if (idx >= self.hooks.length) return done(); if (err) { return done(err); } let fn = self.hooks[idx++]; if (idx == 1) { fn(...args, next); } else { fn(data, next); } } next(); } }
Tapable事件流
webpack中的事件歸納如下,這些事件出現(xiàn)的順序固定,但不一定每次打包所有事件都觸發(fā):
幾個(gè)關(guān)鍵的事件對(duì)應(yīng)打包的階段:
entry-option:初始化optionsrun:開(kāi)始編譯make:從entry開(kāi)始遞歸分析依賴并對(duì)依賴進(jìn)行buildbuild-moodule:使用loader加載文件并build模塊normal-module-loader:對(duì)loader加載的文件用acorn編譯,生成抽象語(yǔ)法樹(shù)ASTprogram:開(kāi)始對(duì)AST進(jìn)行遍歷,當(dāng)遇到require時(shí)觸發(fā)call require事件seal:所有依賴build完成,開(kāi)始對(duì)chunk進(jìn)行優(yōu)化(抽取公共模塊、加hash等)optimize-chunk-assets:壓縮代碼emit:把各個(gè)chunk輸出到結(jié)果文件了解以上事件,你可以很容易地寫(xiě)出一個(gè)插件。
...未完待續(xù)
引用
Webpack-源碼二,整體調(diào)用流程與Tapable事件流webpack4.0源碼分析之Tapable
相關(guān)教程推薦:《Web pack入門視頻教程》
文章題目:深入理解webpack
路徑分享:http://weahome.cn/article/cpjjps.html