Webpack principle

Webpack principle

To view all document pages:Front-end development documentFor more information.
Original link:Webpack principle, the original advertising mode box block, reading experience is not good, so organize the text, easy to find.

Summary of working principle

Basic concept

Before understanding the principle of Webpack, we need to master the following core concepts to facilitate the following understanding:

  • Entry: Entry, the first step of Webpack execution will start with Entry and can be abstracted into input.
  • Module: module, everything is module in Webpack, one module corresponds to one file. Webpack recursively finds all dependent modules starting from the configured Entry.
  • Chunk: code block, a Chunk is formed by combining multiple modules and is used for code merging and splitting.
  • Loader: module converter, which is used to convert the original contents of the module into new contents according to requirements.
  • Plugin: extension plug-in, broadcasting corresponding events at a specific time in the Webpack construction process. plug-in can monitor the occurrence of these events and do corresponding things at a specific time.

Process summary

The running process of the Webpack is a serial process, and the following processes will be executed from start to end:

  1. Initialization parameters: read and merge parameters from configuration files and Shell statements to obtain final parameters;
  2. Start compilation: initialize the Compiler object with the parameters obtained in the previous step, load all configured plug-ins, and execute the run method of the object to start compilation;
  3. Confirm entry: find all entry files according to entry in the configuration;
  4. Compiling module: starting from the entry file, calling all configured Loader to translate the module, finding out the modules that the module depends on, and recursively repeating this step until all the entry dependent files have been processed in this step;
  5. Completing module compilation: after all modules are translated by using Loader in step 4, the final translated content of each module and the dependency relationship between them are obtained;
  6. Output resources: according to the dependency relationship between the portal and the modules, each Chunk containing multiple modules is assembled, and then each Chunk is converted into a separate file and added to the output list. This step is the last opportunity to modify the output content.
  7. Output Complete: After determining the output content, determine the output path and file name according to the configuration, and write the file content to the file system.

In the above process, the Webpack will broadcast specific events at specific points in time, the plug-in will execute specific logic after listening to the events of interest, and the plug-in can call the API provided by the Webpack to change the running result of the Webpack.

Process details

The construction process of Webpack can be divided into the following three phases:

  1. Initialization: start building, read and merge configuration parameters, load Plugin, instantiate Compiler.
  2. Compile: issued from Entry, the corresponding Loader is called in series for each Module to translate the file contents, and then the Module that the Module depends on is found and compiled recursively.
  3. Output: Assemble the compiled Module into Chunk, convert Chunk into file, and output to file system.

If the build is performed only once, the above phases will be performed once in sequence. However, when listening mode is turned on, the process will change as follows:

There will be many events in each big stage. Webpack will broadcast these events for Plugin to use, which will be described one by one below.

Initialization phase

Event name Explanation
Initialization parameters Read and merge parameters from configuration files and Shell statements to obtain the final parameters. This process also executes plug-in instantiation statements in the configuration filenew Plugin().
InstantiationCompiler Class with the parameters obtained in the previous stepCompilerExamples,CompilerResponsible for file monitoring and starting compilation.CompilerThe example contains the completeWebpackConfiguration, there is only one globalCompilerExamples.
Load plug-in Invoking plug-ins in turnapplyMethod to allow the plug-in to listen to all subsequent event nodes. At the same time to plug-in incomingcompilerInstance to facilitate the plug-in to pass throughcompilerCall the apis provided by Webpack.
environment Start applying Node.js style file system to compiler object to facilitate subsequent file searching and reading.
entry-option Read configuredEntrysFor eachEntryInstantiate a correspondingEntryPlugin, for the latterEntryTo prepare for recursive parsing of.
after-plugins After calling all built-in and configured plug-insapplyMethods.
after-resolvers According to the configurationresolver,resolverResponsible for finding the file with the specified path in the file system.
Spaces Spaces
Spaces Spaces
Spaces Spaces

Compile phase

Event name Explanation
run Start a new compilation.
watch-run AndrunSimilarly, the difference is that it is a compilation started in listening mode. In this event, it can be obtained which files have changed to restart a new compilation.
compile This event is to tell the plug-in that a new compilation will be started and will be brought to the plug-in.compilerObject.
compilation WhenWebpackWhen running in development mode, every time a file change is detected, a newCompilationWill be created. OneCompilationObjects contain current module resources, compiled and generated resources, changed files, etc.CompilationObject also provides many event callbacks for plug-ins to extend.
make A new oneCompilationCreated, about to start fromEntryStart reading files, depending on file type and configurationLoaderCompile the file, find out the files that the file depends on after compilation, and compile and analyze recursively.
after-compile OnceCompilationExecution is complete.
invalid This event will be triggered when encountering exceptions such as file non-existence, file compilation error, etc. This event will not cause the Webpack to exit.
Spaces Spaces
Spaces Spaces

In the compilation phase, the most important number iscompilationThe incident, because incompilationPhase called Loader to complete the conversion of each modulecompilationThe phase also includes many small events, which are:

Event name Explanation
build-module Use the corresponding Loader to convert a module.
normal-module-loader After using Loader to convert a module, useacornAnalyzing the converted content and outputting the corresponding abstract syntax tree (ASTTo facilitate the analysis of the code behind the Webpack.
program Starting from the configured entry module, analyze its AST, and add it to the list of dependent modules when importing other module statements such as require, etc. At the same time, recursively analyze the newly found dependent modules, and finally find out the dependency relation of all modules.
seal After all modules and their dependent modules are converted by Loader, Chunk is generated according to the dependency relationship.

Output phase

Event name Explanation
should-emit All files that need to be output have been generated. Ask the plug-in which files need to be output and which do not.
emit After determining which files to output, perform file output, and you can obtain and modify the output content here.
after-emit File output is complete.
done Successfully completed the compilation and output process once completed.
failed If an exception is encountered in the compilation and output process and causes the Webpack to exit, it will jump directly to this step, and the plug-in can obtain the specific error reason in this event.

In the output stage, the converted results and their dependencies of each module have been obtained, and the relevant modules are combined together to form a Chunk. In the output stage, according to the type of Chunk, the corresponding template will be used to generate the file content to be output finally.

Output file analysis

Although you learned how to use the Webpack in the previous chapters and generally know how it works, you have thought about the output of the Webpackbundle.jsWhat does it look like? Why were the original module files merged into a single file? Why?bundle.jsCan run directly in the browser. This section will explain the above issues clearly.

Let’s take a look at what was built by the simplest project in installation and use.bundle.jsThe contents of the file, code is as follows:

<p data-height=”565″ data-theme-id=”0″ data-slug-hash=”NMQzxz” data-default-tab=”js” data-user=”whjin” data-embed-version=”2″ data-pen-title=”bundle.js” class=”codepen”>See the Penbundle.jsby whjin (@whjin) onCodePen.</p>
<script async src=”https://static.codepen.io/ass …; ></script>

The above seemingly complicated code is actually an immediate execution function, which can be abbreviated as follows:

(function(modules) {

  // 模拟 require 语句
  function __webpack_require__() {
  }

  // 执行存放所有模块数组中的第0个模块
  __webpack_require__(0);

})([/*存放所有模块的数组*/])

bundle.jsThe reason why it can run directly in the browser is that the output file passes through the__webpack_require__The function defines a load function that can be executed in the browser to simulate therequireStatement.

The original separate module files were merged into separate ones.bundle.jsThe reason is that the browser cannot load the module files locally as quickly as Node.js, but must request to load the files that have not been obtained through the network. If the number of modules is large, the loading time will be very long, so all modules are stored in an array to perform a network load.

If carefully analyzed__webpack_require__In the implementation of the function, you have also found that the Webpack has done cache optimization: modules that have been loaded will not be executed for the second time, the execution results will be cached in memory, and when a module is accessed for the second time, it will directly read the cached return value in memory.

Output when dividing code

For example, the source code of themain.jsAmend to read as follows:

// 异步加载 show.js
import('./show').then((show) => {
  // 执行 show 函数
  show('Webpack');
});

After rebuilding, two files will be output, one is the execution entry file.bundle.jsAnd asynchronously load files0.bundle.js.

among them0.bundle.jsThe content is as follows:

// 加载在本文件(0.bundle.js)中包含的模块
webpackJsonp(
  // 在其它文件中存放着的模块的 ID
  [0],
  // 本文件所包含的模块
  [
    // show.js 所对应的模块
    (function (module, exports) {
      function show(content) {
        window.document.getElementById('app').innerText = 'Hello,' + content;
      }

      module.exports = show;
    })
  ]
);

bundle.jsThe content is as follows:

<p data-height=”565″ data-theme-id=”0″ data-slug-hash=”yjmRyG” data-default-tab=”js” data-user=”whjin” data-embed-version=”2″ data-pen-title=”bundle.js” class=”codepen”>See the Penbundle.jsby whjin (@whjin) onCodePen.</p>
<script async src=”https://static.codepen.io/ass …; ></script>

Herebundle.jsAnd what was said abovebundle.jsVery similar, the difference is:

  • More than one.__webpack_require__.eIt is used to load the file corresponding to Chunk that is divided and needs to be loaded asynchronously.
  • More than one.webpackJsonpFunction is used to install modules from asynchronously loaded files.

In useCommonsChunkPluginThe output file when extracting common code is the same as the output file when using asynchronous loading, and there will be__webpack_require__.eAndwebpackJsonp. The reason is that extracting common code and asynchronous loading are essentially code segmentation.

Write Loader

Loader is like a translator. It can convert the source files to output new results, and a file can be translated by multiple translators in a chain.

Take the handling of SCSS files as an example:

  • SCSS source code will be handed over first.sass-loaderConvert SCSS to CSS;;
  • Thesass-loaderCSS output tocss-loaderProcessing, find out the resources that CSS depends on, compress CSS, etc.
  • Thecss-loaderCSS output tostyle-loaderProcessing, converting into JavaScript code loaded by scripts;

It can be seen that the above processes need to be executed in a sequential chain. Firstsass-loaderAgaincss-loaderAgainstyle-loader. The Webpack related configurations processed above are as follows:

< pdata-height = “365” data-theme-id = “0” data-slug-hash = “ylmbeq” data-default-tab = “js” data-user = “whjin” data-embedded-version = “2” data-pen-title = “write loader” class = “codepen” > see the penWrite Loaderby whjin (@whjin) onCodePen.</p>
<script async src=”https://static.codepen.io/ass …; ></script>

Loader’s responsibilities

From the above example, it can be seen that a Loader has a single function and only needs to complete a transformation. If a source file needs to undergo multi-step conversion before it can be used normally, it will be converted by multiple Loader. When multiple Loaders are called to convert a file, each Loader will execute in chain order, the first Loader will get the original content to be processed, the result processed by the previous Loader will be transferred to the next subsequent processing, and the final loader will return the processed final result to the Webpack.

Therefore, when you develop a Loader, please keep its responsibility single, you only need to care about input and output.

Loader foundation

Since the Webpack runs on Node.js, a Loader is actually a Node.js module, which needs to export a function. The job of this exported function is to obtain the original content before processing, and return the processed content after processing the original content.

The source code of one of the simplest Loader is as follows:

module.exports = function(source) {
  // source 为 compiler 传递给 Loader 的一个文件的原内容
  // 该函数需要返回处理后的内容,这里简单起见,直接把原内容返回了,相当于该 Loader 没有做任何转换
  return source;
};

Since Loader is running in Node.js, you can call any API built in Node.js or install a third-party module to make the call:

const sass = require('node-sass');
module.exports = function(source) {
  return sass(source);
};

Loader advanced

The above is just the simplest Loader, and the Webpack also provides some API for Loader to call, which will be described one by one below.

Gets the of the Loaderoptions

In the Webpack configuration where SCSS files are processed at the top, givecss-loaderPass it onoptionsParameters to controlcss-loader. How to get the information passed in by the user in the Loader written by yourselfoptions? This is required:

const loaderUtils = require('loader-utils');
module.exports = function(source) {
  // 获取到用户给当前 Loader 传入的 options
  const options = loaderUtils.getOptions(this);
  return source;
};

Return other results

The above Loader only returns the converted content of the original content, but in some scenarios it is necessary to return something other than the content.

For example, to usebabel-loaderTo convert ES6 code, for example, it also needs to output the Source Map corresponding to the converted ES5 code to facilitate debugging the source code. In order to return the Source Map to the Webpack with the ES5 code, you can write as follows:

module.exports = function(source) {
  // 通过 this.callback 告诉 Webpack 返回的结果
  this.callback(null, source, sourceMaps);
  // 当你使用 this.callback 返回内容时,该 Loader 必须返回 undefined,
  // 以让 Webpack 知道该 Loader 返回的结果在 this.callback 中,而不是 return 中 
  return;
};

thereinthis.callbackIt is an API injected by Webpack to Loader to facilitate communication between Loader and Webpack.this.callbackThe detailed usage of is as follows:

this.callback(
    // 当无法转换原内容时,给 Webpack 返回一个 Error
    err: Error | null,
    // 原内容转换后的内容
    content: string | Buffer,
    // 用于把转换后的内容得出原内容的 Source Map,方便调试
    sourceMap?: SourceMap,
    // 如果本次转换为原内容生成了 AST 语法树,可以把这个 AST 返回,
    // 以方便之后需要 AST 的 Loader 复用该 AST,以避免重复生成 AST,提升性能
    abstractSyntaxTree?: AST
);

The generation of Source Map is very time consuming. Source Map is usually generated in the development environment, but not in other environments to speed up the construction. This Webpack provides Loader withthis.sourceMapThe API tells Loader whether the user needs Source Map in the current build environment. If the Loader you write generates Source Map, please consider this.

Synchronous and asynchronous

Loader is divided into synchronous and asynchronous. Loaders described above are synchronous loaders, because their conversion processes are synchronous, and the results will be returned after conversion. However, in some scenarios, the conversion steps can only be completed asynchronously. For example, you need to obtain the results through network requests. If the network requests are synchronized, the entire construction will be blocked, resulting in very slow construction.

When the conversion step is asynchronous, you can do this:

module.exports = function(source) {
    // 告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果
    var callback = this.async();
    someAsyncOperation(source, function(err, result, sourceMaps, ast) {
        // 通过 callback 返回异步执行后的结果
        callback(err, result, sourceMaps, ast);
    });
};

Processing binary data

By default, the original content passed by Webpack to Loader is UTF-8 encoded strings. However, in some scenarios Loader does not process text files but binary files, for examplefile-loaderYou need a Webpack to pass in data in binary format to Loader. To do this, you need to write Loader like this:

module.exports = function(source) {
    // 在 exports.raw === true 时,Webpack 传给 Loader 的 source 是 Buffer 类型的
    source instanceof Buffer === true;
    // Loader 返回的类型也可以是 Buffer 类型的
    // 在 exports.raw !== true 时,Loader 也可以返回 Buffer 类型的结果
    return source;
};
// 通过 exports.raw 属性告诉 Webpack 该 Loader 是否需要二进制数据 
module.exports.raw = true;

The most critical code in the above code is the last linemodule.exports.raw = true;Without this line Loader can only get the string.

Cache acceleration

In some cases, some conversion operations require a lot of computation and are very time consuming. If the repeated conversion operation is repeated every time the construction is performed, the construction will become very slow. For this reason, the Webpack will cache the processing results of all loaders by default, that is, when the files to be processed or the files it depends on have not changed, the corresponding loaders will not be called again to perform the conversion operation.

If you want the Webpack not to cache the Loader’s processing results, you can do this:

module.exports = function(source) {
  // 关闭该 Loader 的缓存功能
  this.cacheable(false);
  return source;
};

Other loadapis

In addition to the above-mentioned Webpack API that can be called in Loader, the following common apis exist:

  • this.context: the directory where the currently processed file is located, if the file currently processed by Loader is/src/main.js, thenthis.contextIs equal to/src.
  • this.resource: the full request path of the currently processed file, includingquerystring, for example/src/main.js? name=1.
  • this.resourcePath: The path of the currently processed file, for example/src/main.js.
  • this.resourceQuery: of the file currently being processedquerystring.
  • this.target: equals Target in Webpack configuration.
  • this.loadModuleHowever, when Loader processes a file, if it depends on the processing results of other files to obtain the results of the current file, it can passthis.loadModule(request: string, callback: function(err, source, sourceMap, module))To getrequestThe processing result of the corresponding file.
  • this.resolve: likerequireStatement to obtain the full path of the specified file, using the method ofresolve(context: string, request: string, callback: function(err, result: string)).
  • this.addDependency: Add the file it depends on to the currently processed file so that when the file it depends on changes, Loader will be called again to process the file. The usage method is as followsaddDependency(file: string).
  • this.addContextDependency: andaddDependencySimilar, butaddContextDependencyIs to add the entire directory to the dependency of the file currently being processed. The usage method is as followsaddContextDependency(directory: string).
  • this.clearDependencies: Clear all dependencies on files currently being processed byclearDependencies().
  • this.emitFile: Output a file usingemitFile(name: string, content: Buffer|string, sourceMap: {...}).

Load local Loader

In the process of developing Loader, in order to test whether the prepared Loader can work normally, it needs to be configured into a Webpack before the Loader can be called. In the previous chapters, the Loader used was installed through Npm. When using Loader, the name of Loader will be directly used, and the code is as follows:

module.exports = {
  module: {
    rules: [
      {
        test: /\.css/,
        use: ['style-loader'],
      },
    ]
  },
};

If you still adopt the above methods to use the locally developed Loader, it will be very troublesome, because you need to ensure that the source code of the Loader is written innode_modulesUnder the directory. For this reason, you need to publish the Loader to Npm warehouse before installing it to the local project.

There are two convenient ways to solve the above problems, as follows:

Npm link

Npm link is specially used to develop and debug local Npm modules. It can link the source code of a module under development to the project without releasing the module.node_modulesDirectory, so that projects can directly use the local Npm module. Because it is realized through soft link, the local Npm module code is edited, and the edited code can also be used in the project.

The steps to complete Npm link are as follows:

  • Ensure that the local Npm module being developed (that is, the Loader being developed) ispackage.jsonHas been correctly configured;
  • Executed under the root directory of the local Npm modulenpm link, register the local module to the global;
  • Execute under project root directorynpm link loader-nameTo link the local Npm module registered in step 2 to the project’snode_moduelsUnder, of whichloader-nameRefers to in step 1package.json

After linking the Loader to the project, you can use the local Loader just like using a real Npm module.

ResolveLoader

ResolveLoader is used to configure how Webpack find Loader. Only by defaultnode_modulesDirectory, in order for the Webpack to load the Loader placed in the local project, it needs to be modified.resolveLoader.modules.

If the local Loader is in the./loaders/loader-name, the following configuration is required:


module.exports = {
  resolveLoader:{
    // 去哪些目录下寻找 Loader,有先后顺序之分
    modules: ['node_modules','./loaders/'],
  }
}

After adding the above configuration, the Webpack will go first.node_modulesLook for Loader under the project. If you cannot find it, you will go again../loaders/Look under the catalog.

Actual combat

The above mentioned many theories, then from the reality, to write a Loader to solve practical problems.

The Loader’s name iscomment-require-loader, the role is to JavaScript code annotation syntax:

// @require '../style/index.css'

Convert to:

require('../style/index.css');

The use scenario of the Loader is to correctly load the targetFis3Written JavaScript in which dependent CSS files are loaded through annotations.

The Loader is used as follows:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['comment-require-loader'],
        // 针对采用了 fis3 CSS 导入语法的 JavaScript 文件通过 comment-require-loader 去转换 
        include: [path.resolve(__dirname, 'node_modules/imui')]
      }
    ]
  }
};

The implementation of the Loader is very simple. The complete code is as follows:

function replace(source) {
    // 使用正则把 // @require '../style/index.css' 转换成 require('../style/index.css');  
    return source.replace(/(\/\/ *@require) +(('|").+('|")).*/, 'require($2);');
}

module.exports = function (content) {
    return replace(content);
};

Writing Plugin

Webpack makes it more flexible through Plugin mechanism to adapt to various application scenarios. Many events will be broadcast in the life cycle of the Webpack operation. Plugin can monitor these events and change the output results through the API provided by the Webpack at an appropriate time.

One of the most basic Plugin code is like this:

class BasicPlugin{
  // 在构造函数中获取用户给该插件传入的配置
  constructor(options){
  }

  // Webpack 会调用 BasicPlugin 实例的 apply 方法给插件实例传入 compiler 对象
  apply(compiler){
    compiler.plugin('compilation',function(compilation) {
    })
  }
}

// 导出 Plugin
module.exports = BasicPlugin;

When using this Plugin, the relevant configuration code is as follows:

const BasicPlugin = require('./BasicPlugin.js');
module.export = {
  plugins:[
    new BasicPlugin(options),
  ]
}

After the Webpack is started, it will be executed first during the process of reading the configuration.new BasicPlugin(options)Initialize aBasicPluginGet an example of it. At initializationcompilerObject, then call thebasicPlugin.apply(compiler)Incoming to plug-in instancecompilerObject. Plug-in instance getscompilerObject, you can pass theCompiler.plugin (event name, callback function)Listen to events broadcast by Webpack. And can pass throughcompilerObject to manipulate the Webpack.

Through the simplest Plugin above, I believe you probably understand the working principle of Plugin, but there are still many details that need to be paid attention to in actual development, which will be described in detail below.

CompilerAndCompilation

The two most commonly used objects in the development of Plugin are Compiler and Compilation, which are bridges between Plugin and Webpack. Compiler and Compilation have the following meanings:

  • The Compiler object contains all the configuration information of the Webpack environment, includingoptions,loaders,pluginsThis information, this object is instantiated when the Webpack is started, it is globally unique and can be simply understood as a Webpack instance.
  • The Compilation object contains the current module resources, compiled and generated resources, changed files, etc. When the Webpack is running in development mode, a new Compilation will be created whenever a file change is detected. The Compilation object also provides many event callbacks for the plug-in to ex tend. Compiler objects can also be read through Compilation.

The difference between Compiler and Compilation is that Compiler represents the life cycle of the entire Webpack from startup to shutdown, while Compilation only represents a new compilation.

Event flow

Webpack is just like a production line. It takes a series of processing procedures to convert source files into output results. The responsibilities of each processing flow on this production line are single, and there are dependencies among multiple flows. Only after the current processing is completed can the next flow be processed. Plug-ins are like a function inserted into a production line, processing resources on the production line at a specific time.

Webpack organizes this complex production line through Tapable. The Webpack broadcasts events during its operation. The plug-in can be added to this production line to change the operation of the production line only by monitoring the events it cares about. The event flow mechanism of Webpack ensures the order of plug-ins and makes the whole system extensible.

The event flow mechanism of Webpack applies observer mode, which is very similar to EventEmitter in Node.js Compiler and Compilation both inherit from Tapable and can broadcast and monitor events directly on Compiler and Compilation objects as follows:

/**
* 广播出事件
* event-name 为事件名称,注意不要和现有的事件重名
* params 为附带的参数
*/
compiler.apply('event-name',params);

/**
* 监听名称为 event-name 的事件,当 event-name 事件发生时,函数就会被执行。
* 同时函数中的 params 参数为广播事件时附带的参数。
*/
compiler.plugin('event-name',function(params) {

});

Similarly,compilation.applyAndcompilation.pluginThe use method is the same as above.

When developing plug-ins, you may not know how to start, because you do not know which event to monitor to complete the task.

When developing plug-ins, you should also pay attention to the following two points:

  • As long as you can get the Compiler or Compilation object, you can broadcast new events, so you can also broadcast events in newly developed plug-ins for other plug-ins to monitor and use.
  • The Compiler and Compilation objects passed to each plug-in are the same reference. In other words, modifying the properties on the Compiler or Compilation object in one plug-in will affect the subsequent plug-ins.
  • Some events are asynchronous. These asynchronous events will be accompanied by two parameters. The second parameter is a callback function. When the pl ug-in finishes processing the task, it needs to call the callback function to notify the Webpack before entering the next processing flow. For example:
 compiler.plugin('emit',function(compilation, callback) {
    // 支持处理逻辑

    // 处理完毕后执行 callback 以通知 Webpack 
    // 如果不执行 callback,运行流程将会一直卡在这不往下执行 
    callback();
  });

Common apis

Plug-ins can be used to modify output files, add output files, and even improve the performance of the Webpack. In short, plug-ins can accomplish many things by calling the API provided by the Webpack. Due to the large number of apis provided by Webpack, many apis are rarely used and the limited space, here are some commonly used apis.

Read output resources, code blocks, modules and their dependencies

Some plug-ins may need to read the processing results of the Webpack, such as output resources, code blocks, modules and their dependencies, in order to do the next processing.

InemitWhen the event occurs, the conversion and assembly of the representative source file has been completed. Here, the resources, code blocks, modules and their dependencies that will eventually be output can be read, and the contents of the output resources can be modified. The plug-in code is as follows:

<p data-height=”585″ data-theme-id=”0″ data-slug-hash=”RJwjPj” data-default-tab=”js” data-user=”whjin” data-embed-version=”2″ data-pen-title=”emit” class=”codepen”>See the Penemitby whjin (@whjin) onCodePen.</p>
<script async src=”https://static.codepen.io/ass …; ></script>

Monitor file changes

The Webpack will start from the configured portal module and find out all the dependent modules in turn. When the portal module or its dependent modules change, a new Compilation will be triggered.

When developing plug-ins, it is often necessary to know which file changes led to the new Compilation, so the following code can be used:

<p data-height=”255″ data-theme-id=”0″ data-slug-hash=”jKOabJ” data-default-tab=”js” data-user=”whjin” data-embed-version=”2″ data-pen-title=”Compilation” class=”codepen”>See the PenCompilationby whjin (@whjin) onCodePen.</p>
<script async src=”https://static.codepen.io/ass …; ></script>

By default, the Webpack only monitors whether the portal and the modules it depends on have changed. In some cases, the project may need to introduce a new file, such as an HTML file. Since JavaScript files do not import HTML files, the Webpack will not listen for changes in HTML files and will not trigger a new Compilation when editing HTML files. In order to monitor the changes of HTML files, we need to add HTML files to the dependency list, so we can use the following code:

compiler.plugin('after-compile', (compilation, callback) => {
  // 把 HTML 文件添加到文件依赖列表,好让 Webpack 去监听 HTML 模块文件,在 HTML 模版文件发生变化时重新启动一次编译
    compilation.fileDependencies.push(filePath);
    callback();
});

Modify output resources

In some scenarios, plug-ins need to modify, add, and delete output resources, which requires monitoring.emitThe incident occurred becauseemitAt the time of the event, the conversion of all modules and the files corresponding to the code blocks have been generated, and the resources to be output will be output soon. ThereforeemitThe event is the last opportunity to modify the Webpack output resource.

All resources that need to be exported are stored incompilation.assetsChina,compilation.assetsIs a key-value pair, the key is the name of the file to be output, and the value is the corresponding content of the file.

Set upcompilation.assetsThe code for is as follows:

compiler.plugin('emit', (compilation, callback) => {
  // 设置名称为 fileName 的输出资源
  compilation.assets[fileName] = {
    // 返回文件内容
    source: () => {
      // fileContent 既可以是代表文本文件的字符串,也可以是代表二进制文件的 Buffer
      return fileContent;
      },
    // 返回文件大小
      size: () => {
      return Buffer.byteLength(fileContent, 'utf8');
    }
  };
  callback();
});

readcompilation.assetsThe code for is as follows:


compiler.plugin('emit', (compilation, callback) => {
  // 读取名称为 fileName 的输出资源
  const asset = compilation.assets[fileName];
  // 获取输出资源的内容
  asset.source();
  // 获取输出资源的文件大小
  asset.size();
  callback();
});

Determine which plug-ins are used by the Webpack

When developing a plug-in, you may need to make the next decision according to whether the current configuration uses another plug-in, so you need to read the current plug-in configuration of the Webpack. To determine whether the current use of ExtractTextPlugin, for example, can use the following code:

// 判断当前配置使用使用了 ExtractTextPlugin,
// compiler 参数即为 Webpack 在 apply(compiler) 中传入的参数
function hasExtractTextPlugin(compiler) {
  // 当前配置所有使用的插件列表
  const plugins = compiler.options.plugins;
  // 去 plugins 中寻找有没有 ExtractTextPlugin 的实例
  return plugins.find(plugin=>plugin.__proto__.constructor === ExtractTextPlugin) != null;
}

Actual combat

Let’s take a practical example and take you step by step to implement a plug-in.

The name of the plug-in is EndWebpackPlugin. Its function is to attach some additional operations when the Webpack is about to exit, for example, to upload the output file to the server after the Webpack has successfully compiled and output the file. At the same time, the plug-in can also distinguish whether the Webpack is successfully built or not. When using the plug-in, the method is as follows:

module.exports = {
  plugins:[
    // 在初始化 EndWebpackPlugin 时传入了两个参数,分别是在成功时的回调函数和失败时的回调函数;
    new EndWebpackPlugin(() => {
      // Webpack 构建成功,并且文件输出了后会执行到这里,在这里可以做发布文件操作
    }, (err) => {
      // Webpack 构建失败,err 是导致错误的原因
      console.error(err);        
    })
  ]
}

To implement the plug-in, two events are required:

  • done: Occurs when the Webpack is about to exit after the file is successfully built and exported;
  • failed: Occurs when the build fails due to an exception in the build and the Webpack is about to exit;

The implementation of the plug-in is very simple. The complete code is as follows:

class EndWebpackPlugin {

  constructor(doneCallback, failCallback) {
    // 存下在构造函数中传入的回调函数
    this.doneCallback = doneCallback;
    this.failCallback = failCallback;
  }

  apply(compiler) {
    compiler.plugin('done', (stats) => {
        // 在 done 事件中回调 doneCallback
        this.doneCallback(stats);
    });
    compiler.plugin('failed', (err) => {
        // 在 failed 事件中回调 failCallback
        this.failCallback(err);
    });
  }
}
// 导出插件 
module.exports = EndWebpackPlugin;

From the development of this plug-in, it can be seen that finding the appropriate event point to complete the function is particularly important when developing the plug-in. InSummary of working principleThe following is a detailed introduction to the common events widely broadcast during the operation of the Webpack. You can find the events you need from it.

Debugging Webpack

When writing Plugin and Loader for Webpack, the execution result may be different from what you expected, just like you encountered strange bugs when writing code. For bugs that cannot see the problem at a glance, it is usually necessary to debug the program source code to find out the problem.

Although can passconsole.logThis section will teach you how to debug breakpointsSummary of working principleThe plug-in code in. Because Webpack runs on Node.js, debugging Webpack is relative to debugging Node.js programs.

Debugging in Webstorm

Webstorm integrates the debugging tools of Node.js, so it is very simple to debug Webpack with Webstorm.

1. Set Breakpoints

Set a breakpoint where you think there may be problems. Click on the red dot on the left side of the edit area code to indicate that a breakpoint has been set.

2. Configure the execution portal

Tell Webstorm how to start the Webpack. Since the Webpack is actually a Node.js application, it is necessary to create a new Node.js type execution portal.

There are three points to note in the above configuration:

  • NameSet todebug webpack, like set up an alias, convenient memory and distinction;
  • Working directorySet as the root directory of the project where the plug-in to be debugged is located;
  • JavaScript fileThat is, the execution portal file of Node.js is set as the execution portal file of Webpack.node_modules/webpack/bin/webpack.js.

3. Start debugging

After the above two steps, the preparatory work has been completed. Start debugging below and select the one set before when starting.debug webpack.

4. Execution to Breakpoint

After startup, the program will stop at the breakpoint, where you can easily check the current state of variables and find out the problem.

Principle summary

Webpack is a huge Node.js application. if you read its source code, you will find that to implement a complete Webpack requires writing a lot of code. But you don’t need to know all the details, just the whole structure and some details.

For users of Webpack, it is a simple and powerful tool. For developers of Webpack, it is a highly scalable system.

The success of Webpack lies in that it hides the complicated implementation and exposes only a simple tool for users to achieve their goals quickly. At the same time, the overall architecture design is reasonable, the expansibility is high, and the development and expansion difficulty is not high. The community has made up a large number of missing functions, making the Webpack competent for almost any scene.

Through the study in this chapter, I hope you can not only learn how to write Webpack extensions, but also learn how to design a good system architecture.