本篇文章给大家分享的是有关什么是webpack,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。

webpack是目前最为流行的打包工具之一,其配置简单,功能强大,拥有丰富的加载器和插件系统,为前端开发者提供了诸多便利。笔者默认各位看官在阅读本章之前已经有了一定的使用经验,所以对webpack的使用方式不做赘述。

阅读本章,你可以了解到如下内容:

Webpack打包后代码结构Webpack核心架构 —— TapableWebpack事件流Webpack插件实现机制Webpack加载器实现机制

Webpack打包后代码结构

简单打包
我们首先写一个最简单的方法,然后使用webpack进行打包:

// /webpack/bundles/simple/moduleA.jswindow.printA = function printA() { console.log(`This is module A!`);}

一个比较基本的webpack配置文件:

// /webpack/bundles/simple/webpack.config.jsconst 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' }) ]}

创建一个HTML文件用于在浏览器环境下测试:

<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Webpack - Simple Bundle</title></head><body> </body></html>

执行打包命令webpack 后我们获得了一个 dist 目录,我们打开 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内部定义了一个 webpack_require 的方法,这个方法的实质很简单:

多模块间存在简单依赖
例如 moduleB.js 依赖于 moduleA.js 文件。

// /webpack/bundles/simpleDependencies/moduleA.jsmodule.exports = window.printA = function printA() { console.log(`This is module A!`);}

// /webpack/bundles/simpleDependencies/moduleB.jsconst 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!`);}/***/ })/******/ ]);

我们可以发现这块有点变化:

/* 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 ,因而需要先执行 __webpack_require(1) 拿到模块A后,再进行下一步。

多入口
需要注意,打包的文件中moudleId是不会重复的,如果有两个入口文件的情况,则入口模块id都为0,其他依赖模块id不重复。我们创建如下几个文件,其中 index0.js 依赖于 common.js 与 dependency.js ,而 index1.js 依赖于 index0.js 和 common.js 两个文件。

// /webpack/bundles/multi/common.jsmodule.exports = function() { console.log('This is common module!');}

// /webpack/bundles/multi/dependency .jsmodule.exports = function() { console.log('This is dependency module!');}

// /webpack/bundles/multi/index0.jsconst common = require('./common');const dependency = require('./dependency');module.exports = window.print0 = function() { common(); dependency(); console.log('This is module 0!');}

// /webpack/bundles/multi/index1.jsconst 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 这个插件之前,这两个文件是存在重复代码的。也就是每个入口都会独立进行打包。
我们看如果添加了 CommonsChunkPlugin 这个插件后的情况(修改 webpack.config.js):

// /webpack/bundles/CommonsChunkPlugin/webpack.config.jsplugins: [ // ... new webpack.optimize.CommonsChunkPlugin({ name: 'common', filename: 'common.js' })]

这样一来会生成三个文件,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 已经包含了所有的公共方法,并且在浏览器 window 对象中创建了一个名为 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/******/ };// ...

这个方法与 __webpack_require__ 较为类似,同样也是将模块缓存进来。只不过 webpack 会预先抽取公共模块,先将其缓存进来,而后可以在其他的 bundle.js 中使用 webpackJsonp 方法进行模块加载。

// /webpack/bundles/CommonsChunkPlugin/dist/index0.bundle.jswebpackJsonp([1],[],[1]);

// /webpack/bundles/CommonsChunkPlugin/dist/index1.bundle.jswebpackJsonp([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核心架构 —— Tapable

github上将webpack源码克隆至本地,我们可以先了解到 webpack 的一个整体流程:

lib/webpack.js中返回一个compiler对象,并调用了compiler.run()lib/Compiler.js中,run方法触发了before-run、run两个事件,然后通过readRecords读取文件,通过compile进行打包,打包后触发before-compile、compile、make等事件;compile是主要流程,该方法中实例化了一个Compilation类,并调用了其finish及seal方法。lib/Compilation.js中定义了finish及seal方法,还有一个重要方法addEntry。这个方法通过调用其私有方法_addModuleChain完成了两件事:根据模块的类型获取对应的模块工厂并创建模块;构建模块。lib/Compiler.js中没有显式调用addEntry,而是触发make事件,lib/DllEntryPlugin.js为一个监听make事件的插件,在回调函数中调用了addEntry。

具体分析_addModuleChain,其完成的第二件事构建模块又可以分为三部分:

调用loader处理模块之间的依赖。将loader处理后的文件通过acorn抽象成抽象语法树AST。遍历AST,构建该模块的所有依赖。

具体看 lib/webpack.js 这个文件,此文件为 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 (编译器)对象注册并执行 NodeEnvironmentPlugin执行钩子 environment 里的方法执行钩子 afterEnvironment 里的方法注册并执行各种插件将 compiler 向外导出

显然,Compiler是我们需要深究的一个部分,因为 webpack 最终向外部返回也就是这个 Compiler 实例。大致了解下 Compiler 的实现:

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,在其构造方法中,定义了一些事件钩子(hooks)、一些变量以及一些方法。这些变量以及方法目前看来还是非常抽象的,所以我们有必要去了解下 Tapable 的实现。

Tapable的Github主页 对 Tapable 的介绍如下:

The tapable packages exposes many Hook classes, which can be used to create hooks for plugins.

实际上,webpack基于事件流机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable,webpack中最核心的负责编译的Compiler和负责创建bundles的Compilation都是Tapable的实例。Tapable 向外暴露许多的钩子类,这些类可以很方便地为插件创建事件钩子。 Tapable 中定义了如下几种钩子类:

SyncHookSyncBailHookSyncWaterfallHookSyncLoopHookAsyncParallelHookAsyncParallelBailHookAsyncSeriesHookAsyncSeriesBailHookAsyncSeriesWaterfallHook

所有钩子类的构造函数都接收一个可选的参数,这个参数是一个由字符串参数组成的数组,如下:

const hook = new SyncHook(["arg1", "arg2", "arg3"]);

钩子概览

Tapable的钩子分为两类,同步和异步,其中异步又分为并行和串行:

每种钩子都有各自的使用方式,如下表:

序号钩子名执行方式使用要点1SyncHook同步串行不关心监听函数的返回值2SyncBailHook同步串行只要监听函数中有一个函数的返回值不为 null,则跳过剩下所有的逻辑3SyncWaterfallHook同步串行上一个监听函数的返回值可以传给下一个监听函数4SyncLoopHook同步循环当监听函数被触发的时候,如果该监听函数返回true时则这个监听函数会反复执行,如果返回 undefined 则表示退出循环5AsyncParallelHook异步并发不关心监听函数的返回值6AsyncParallelBailHook异步并发只要监听函数的返回值不为 null,就会忽略后面的监听函数执行,直接跳跃到callAsync等触发函数绑定的回调函数,然后执行这个被绑定的回调函数7AsyncSeriesHook异步串行不关系callback()的参数8AsyncSeriesBailHook异步串行callback()的参数不为null,就会直接执行callAsync等触发函数绑定的回调函数9AsyncSeriesWaterfallHook异步串行上一个监听函数的中的callback(err, data)的第二个参数,可以作为下一个监听函数的参数

Sync钩子

同步串行
(1) SyncHook
不关心监听函数的返回值

使用

const { SyncHook } = require("tapable");let queue = new SyncHook(['name']); //所有的构造函数都接收一个可选的参数,这个参数是一个字符串的数组。// 订阅queue.tap('1', function (name, name2) {// tap 的第一个参数是用来标识订阅的函数的 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);});// 发布queue.call('webpack', 'webpack-cli');// 发布的时候触发订阅的函数 同时传入参数// 执行结果:/*webpack undefined 1 // 传入的参数需要和new实例的时候保持一致,否则获取不到多传的参数webpack 2webpack 3*/原理

class SyncHook_MY{ constructor(){ this.hooks = []; } // 订阅 tap(name, fn){ this.hooks.push(fn); } // 发布 call(){ this.hooks.forEach(hook => hook(...arguments)); }}

(2) SyncBailHook
只要监听函数中有一个函数的返回值不为 null,则跳过剩下所有的逻辑

使用

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');// 执行结果:/* webpack 1webpack 2*/原理

class SyncBailHook_MY { constructor() { this.hooks = []; } // 订阅 tap(name, fn) { this.hooks.push(fn); } // 发布 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
上一个监听函数的返回值可以传给下一个监听函数

使用

const { SyncWaterfallHook} = require("tapable");let queue = new SyncWaterfallHook(['name']);// 上一个函数的返回值可以传给下一个函数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');// 执行结果:/* webpack 11 22 3*/原理

class SyncWaterfallHook_MY{ constructor(){ this.hooks = []; } // 订阅 tap(name, fn){ this.hooks.push(fn); } // 发布 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
当监听函数被触发的时候,如果该监听函数返回true时则这个监听函数会反复执行,如果返回 undefined 则表示退出循环。

使用

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');// 执行结果:/* count: 3count: 2count: 1*/原理

class SyncLoopHook_MY { constructor() { this.hook = null; } // 订阅 tap(name, fn) { this.hook = fn; } // 发布 call() { let result; do { result = this.hook(...arguments); } while (result) }}

Async钩子

异步并行
(1) AsyncParallelHook
不关心监听函数的返回值。有三种注册/发布的模式,如下:

异步订阅调用方法tapcallAsynctapAsynccallAsynctapPromisepromiseusage - tap

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');});// 执行结果/* webpack 1webpack 2webpack 3cost: 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');});// 执行结果/* webpack 1webpack 2webpack 3overtime: 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 1webpack 2webpack 3overcost3: 3007.925ms*/

异步串行
(1) AsyncSeriesHook
不关心callback()的参数。

usage - tap

const { AsyncSeriesHook} = require("tapable");// taplet 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');});// 执行结果/* 123undefinedcost1: 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');}); // 执行结果/* webpack 1webpack 2webpack 3undefinedovercost2: 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');});// 执行结果/* webapck 1webapck 2webapck 3undefinedcost3: 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的参数有值,就直接跳跃到 执行callAsync的回调函数 if (err) return done(err); let fn = slef.hooks[idx++]; fn ? fn(...args, next) : done(); } next(); }}

(2) AsyncSeriesBailHook
callback()的参数不为null,就会直接执行callAsync等触发函数绑定的回调函数。

usage - tap

const { AsyncSeriesBailHook} = require("tapable");// taplet 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');});// 执行结果:/* 1nullcost1: 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');});// 执行结果/* webpack 1webpack 2wrongovercost2: 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');});// 执行结果:/* webpack 1webpack 2undefinederrorcost3: 3017.608ms*/

(3) AsyncSeriesWaterfallHook
上一个监听函数的中的callback(err, data)的第二个参数,可以作为下一个监听函数的参数

usage - tap

const { AsyncSeriesWaterfallHook} = require("tapable");// taplet 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');});// 执行结果:/* webpack 12 'lily'3 'Tom'nullovercost1: 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');});// 执行结果:/* 1: webpack2: 23: 3nullovercost2: 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');});// 执行结果:/* 1: webpack2: 13: 2overcost3: 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中的事件归纳如下,这些事件出现的顺序固定,但不一定每次打包所有事件都触发:

类型名字事件名[C]applyPluginsBailResultentry-option[A]applyPluginsafter-plugins[A]applyPluginsafter-resolvers[A]applyPluginsenvironment[A]applyPluginsafter-environment[D]applyPluginsAsyncSeriesrun[A]applyPluginsnormal-module-factory[A]applyPluginscontext-module-factory[A]applyPluginscompile[A]applyPluginsthis-compilation[A]applyPluginscompilation[F]applyPluginsParallelmake[E]applyPluginsAsyncWaterfallbefore-resolve[B]applyPluginsWaterfallfactory[B]applyPluginsWaterfallresolver[A]applyPluginsresolve[A]applyPluginsresolve-step[G]applyPluginsParallelBailResultfile[G]applyPluginsParallelBailResultdirectory[A]applyPluginsresolve-step[G]applyPluginsParallelBailResultresult[E]applyPluginsAsyncWaterfallafter-resolve[C]applyPluginsBailResultcreate-module[B]applyPluginsWaterfallmodule[A]applyPluginsbuild-module[A]applyPluginsnormal-module-loader[C]applyPluginsBailResultprogram[C]applyPluginsBailResultstatement[C]applyPluginsBailResultevaluate CallExpression[C]applyPluginsBailResultvar data[C]applyPluginsBailResultevaluate Identifier[C]applyPluginsBailResultevaluate Identifier require[C]applyPluginsBailResultcall require[C]applyPluginsBailResultevaluate Literal[C]applyPluginsBailResultcall require:amd:array[C]applyPluginsBailResultevaluate Literal[C]applyPluginsBailResultcall require:commonjs:item[C]applyPluginsBailResultstatement[C]applyPluginsBailResultevaluate MemberExpression[C]applyPluginsBailResultevaluate Identifier console.log[C]applyPluginsBailResultcall console.log[C]applyPluginsBailResultexpression console.log[C]applyPluginsBailResultexpression console[A]applyPluginssucceed-module[E]applyPluginsAsyncWaterfallbefore-resolve[B]applyPluginsWaterfallfactory[A]applyPluginsbuild-module[A]applyPluginssucceed-module[A]applyPluginsseal[A]applyPluginsoptimize[A]applyPluginsoptimize-modules[A]applyPluginsafter-optimize-modules[A]applyPluginsoptimize-chunks[A]applyPluginsafter-optimize-chunks[D]applyPluginsAsyncSeriesoptimize-tree[A]applyPluginsafter-optimize-tree[C]applyPluginsBailResultshould-record[A]applyPluginsrevive-modules[A]applyPluginsoptimize-module-order[A]applyPluginsbefore-module-ids[A]applyPluginsoptimize-module-ids[A]applyPluginsafter-optimize-module-ids[A]applyPluginsrecord-modules[A]applyPluginsrevive-chunks[A]applyPluginsoptimize-chunk-order[A]applyPluginsbefore-chunk-ids[A]applyPluginsoptimize-chunk-ids[A]applyPluginsafter-optimize-chunk-ids[A]applyPluginsrecord-chunks[A]applyPluginsbefore-hash[A]applyPluginshash[A]applyPluginshash-for-chunk[A]applyPluginschunk-hash[A]applyPluginsafter-hash[A]applyPluginsbefore-chunk-assets[B]applyPluginsWaterfallglobal-hash-paths[C]applyPluginsBailResultglobal-hash[B]applyPluginsWaterfallbootstrap[B]applyPluginsWaterfalllocal-vars[B]applyPluginsWaterfallrequire[B]applyPluginsWaterfallmodule-obj[B]applyPluginsWaterfallmodule-require[B]applyPluginsWaterfallrequire-extensions[B]applyPluginsWaterfallasset-path[B]applyPluginsWaterfallstartup[B]applyPluginsWaterfallmodule-require[B]applyPluginsWaterfallrender[B]applyPluginsWaterfallmodule[B]applyPluginsWaterfallrender[B]applyPluginsWaterfallpackage[B]applyPluginsWaterfallmodule[B]applyPluginsWaterfallrender[B]applyPluginsWaterfallpackage[B]applyPluginsWaterfallmodules[B]applyPluginsWaterfallrender-with-entry[B]applyPluginsWaterfallasset-path[B]applyPluginsWaterfallasset-path[A]applyPluginschunk-asset[A]applyPluginsadditional-chunk-assets[A]applyPluginsrecord[D]applyPluginsAsyncSeriesadditional-assets[D]applyPluginsAsyncSeriesoptimize-chunk-assets[A]applyPluginsafter-optimize-chunk-assets[D]applyPluginsAsyncSeriesoptimize-assets[A]applyPluginsafter-optimize-assets[D]applyPluginsAsyncSeriesafter-compile[C]applyPluginsBailResultshould-emit[D]applyPluginsAsyncSeriesemit[B]applyPluginsWaterfallasset-path[D]applyPluginsAsyncSeriesafter-emit[A]applyPluginsdone

几个关键的事件对应打包的阶段:

entry-option:初始化optionsrun:开始编译make:从entry开始递归分析依赖并对依赖进行buildbuild-moodule:使用loader加载文件并build模块normal-module-loader:对loader加载的文件用acorn编译,生成抽象语法树ASTprogram:开始对AST进行遍历,当遇到require时触发call require事件seal:所有依赖build完成,开始对chunk进行优化(抽取公共模块、加hash等)optimize-chunk-assets:压缩代码emit:把各个chunk输出到结果文件

了解以上事件,你可以很容易地写出一个插件。

...未完待续

引用

Webpack-源码二,整体调用流程与Tapable事件流

webpack4.0源码分析之Tapable

以上就是什么是webpack,小编相信有部分知识点可能是我们日常工作会见到或用到的。希望你能通过这篇文章学到更多知识。更多详情敬请关注亿速云行业资讯频道。