As front-end engineering continues to evolve, so do the build tools. As the front end of the era of to be bestowed favor on newly, new era webpack gradually become indispensable to the front-end engineers build tools, along with the iteration webpack4, we enjoy building efficiency is improved to bring of pleasant sensation, configuration is declining and comfortable, and always take great pains to rewrite build events hook mechanism, for the plugin is not compatible dispirited and discouraged, Although the process is painful, the result is always beautiful. After some tedious configuration, I often wonder what such a neat tool is doing in the build process. With such curiosity, I read relevant books and official documents with great effort, and finally got to know the principle. Now, let’s gradually uncover the mystery of webpack and explore its operating mechanism.

This article will cover three parts: Webpack running mechanism, writing a custom Webpack Loader, writing a custom Webpack Plugin directly to the pain points of Webpack principle, and open your way to senior front-end engineer ~

For this webpack series of articles, see: github.com/jerryOnlyZR… .

This series of articles uses webpack version 4. If you have problems with other versions, please issue or leave a comment in the comments section below.

1. Running mechanism of Webpack

1.1. Overview of the running mechanism of Webpack

Before reading this article, I assumed that you have mastered the basic configuration of WebPack and can independently build a front-end automation construction system based on WebPack. Therefore, this article will not teach you how to configure or use WebPack. Naturally, I will not introduce specific concepts and face the topic directly. Start explaining webpack principles.

The running process of Webpack can be summarized as follows:

Initialize configuration parameters -> Bind event hook callback -> Confirm Entry one by one -> Compile file with Loader -> Output file

Next, we will introduce the specific process one by one.

1.2. Webpack runs the process

1.2.1. Preliminary study on webpack event flow

One concept we can use when analyzing the WebPack run process is webPack’s event flow mechanism.

What is webPack event flow?

Webpack is like a production line that goes through a series of processes to convert source files into output. Each process on this production line has a single responsibility. There are dependencies between multiple processes. Only after the current process is completed can the next process be handled. A plug-in is like a function that plugs into a production line and works with resources on the production line at a specific time. Webpack organizes this complex production line through Tapable. Webpack broadcasts events as it runs, and the plug-in only needs to listen for the events it cares about to join the production line and change how the production line works. Webpack’s event flow mechanism ensures the orderliness of plug-ins and makes the whole system very extensible. — Wu Haolin, “Simple Webpack”

We think of the WebPack event stream as a series of events in the WebPack build process that represent different build cycles and states, and we can listen for events on the stream just as we listen for click events on a browser and mount event callbacks for them. We can also customize events and broadcast them when appropriate, all managed using the Tapable module that comes with WebPack. We don’t need to install Tapable ourselves, it will be installed at the same time webPack is installed, if needed, we just need to require it directly in the file.

Tapable is basically the EventEmit that we all experience in the front-end advancement process, implemented through the publiser-subscriber pattern, and some of its core code can be summarized as follows:

class SyncHook{
    constructor(){
        this.hooks = [];
    }

    // Subscribe to events
    tap(name, fn){
        this.hooks.push(fn);
    }

    / / release
    call(){
        this.hooks.forEach(hook= > hook(...arguments));
    }
}
Copy the code

Tapable specific content can refer to the article: webpackage 4.0 source analysis of Tapable. Its usage will be covered in more detail later in the “3. Write a Custom WebPack Plugin” module.

Because Webpack 4 rewrites the event flow mechanism, the official Documentation for WebPack Hook is extremely informative, but in practice we only need to remember a few important events.

1.2.2. Detailed explanation of webpack operation process

Before explaining the Webpack process, I have attached an implementation flow chart drawn by myself:

  • First, WebPack will read the configuration you pass in from the command line and the projectwebpack.config.jsFile, initialize the configuration parameters of this build, and execute the plug-in instantiation statement in the configuration file, generate the Apply method of Compiler passed in plugin, and hang custom hooks for the WebPack event flow.
  • Next comes the entryOption phase, where WebPack starts reading configured Entries, recursively traversing all entry files
  • Webpack then begins the compilation process. Each entry file will be entered in turn, and the contents of the file will be compiled (buildModule) using the loader configured by the user. We can retrieve module resources, loaders, and so on from the compilation of the incoming event callback. After that, the compiled file content is parsed by Acorn to generate AST static grammar tree (AST Static Grammar tree). The dependency relationship of the file is analyzed and the dependent modules are pulled one by one. The above process is repeatedrequireSyntax is replaced by__webpack_require__To simulate modular operations.
  • During the EMIT phase, all files have been compiled and converted, including the final output resources that we can call back to in the incoming eventcompilation.assetsGet the required data, including the resources to be exported, chunks of code, and so on.

1.2.3. What is AST?

In 1.2.2, we see an unfamiliar word — AST. Search on the Internet:

In computer science, an Abstract Syntax Tree (AST), or Syntax Tree for short, is an Abstract representation of the syntactic structure of source code. It represents the syntactic structure of a programming language as a tree, with each node in the tree representing a structure in the source code. The syntax is “abstract” because it does not represent every detail that occurs in real grammar. For example, nested brackets are implicit in the structure of the tree and are not represented as nodes; Conditional jump statements such as if-conditional-then can be represented by nodes with two branches. — Wikipedia

In fact, you just have to remember that AST is a tree, like this:

The purpose of transforming into AST is to convert our written string files into data structures that are more easily recognized by the computer, so that key information can be extracted more easily. The representation of this tree on the computer is actually a simple Object.

The example is a simple declarative assignment statement that is transformed by the AST to make the meaning of each part clearer.

1.2.4. Webpack output result parsing

Next, let’s look at the output of webPack. If we hadn’t set splitChunk, we’d just see a main.js output file in the dist directory, filtering out useless comments and some Funciton that we don’t need to worry about yet, and the resulting code would look something like this:

(function (modules) {
  // Cache module exports that are already loaded
  Module exports had js to execute before that, and caching was designed to optimize that process
  // The module cache
  var installedModules = {};

  // The require function
  /** * CommonJS require() *@param {String} ModuleId Module path */
  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 a single module JS Function and populate installedModules and modules
    // function mudule(module, __webpack_exports__[, __webpack_require__])
    // 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; .// __webpack_public_path__
  __webpack_require__.p = "";

  // Loads Entry and returns Entry exports
  // Load entry module and return exports
  return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
  // modules is an object, the key is the module's path, the value is the module's JS Function
  ({
    "./src/index.js": function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      eval("__webpack_require__.r(__webpack_exports__); \n/* harmony import */ var _module_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./module.js */ \"./src/module.js\"); \n/* harmony import */ var _module_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_module_js__WEBPACK_IMPORTED_MODULE_0__); \n{}; \nconsole.log(_module_js__WEBPACK_IMPORTED_MODULE_0___default.a.s); \n\n//# sourceURL=webpack:///./src/index.js?");
    },
    "./src/module.js": function (module.exports) {
      eval("{}; var s = 123; \nconsole.log(s); \nmodule.exports = {\n s: s\n}; \n\n//# sourceURL=webpack:///./src/module.js?"); }});Copy the code

We all know that the essence of webPack modularization in the browser is to inject all the code into the same JS file. Now we can clearly see that WebPack generates nothing more than an IIFE. All the modules we introduce are wrapped in a single function and assembled into an object. This object is passed in as an IIFE argument.

But if we configure splitChunk, the output file will be linked to your Chunk, and the code will look different:

 //@file: dist/common/runtime.js
 // When splitChunk is configured, the IIFE modules parameter is used.
 // The real Module and chunk are stored in a global array 'webpackJsonp' mounted on the window
 (function(modules) { // webpackBootstrap
	 // install a JSONP callback for chunk loading
	 /** * webpackJsonpCallback handles chunk data *@param {Array} Data [[chunkId(chunk name)], modules(Object), [...other chunks(all chunks required)]] */
 	function webpackJsonpCallback(data) {
        // The name of chunk. If it is an entry chunk, it is the key of our entry
 		var chunkIds = data[0];
        // Dependency module
 		var moreModules = data[1];
 		var executeModules = data[2];

 		// add "moreModules" to the modules object,
 		// then flag all "chunkIds" as loaded and fire callback
 		var moduleId, chunkId, i = 0, resolves = [];
 		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(data);

 		while(resolves.length) {
 			resolves.shift()();
 		}

 		// add entry modules from loaded chunk to deferred list
 		deferredModules.push.apply(deferredModules, executeModules || []);

 		// run deferred modules when all chunks ready
 		return checkDeferredModules();
 	};
 	function checkDeferredModules() {
 		var result;
 		for(var i = 0; i < deferredModules.length; i++) {
 			var deferredModule = deferredModules[i];
 			var fulfilled = true;
 			for(var j = 1; j < deferredModule.length; j++) {
 				var depId = deferredModule[j];
 				if(installedChunks[depId] ! = =0) fulfilled = false;
 			}
 			if(fulfilled) {
 				deferredModules.splice(i--, 1);
 				result = __webpack_require__(__webpack_require__.s = deferredModule[0]); }}return result;
 	}

 	// The module cache
 	var installedModules = {};

	// Cache chunk, same as module
 	// object to store loaded and loading chunks
 	// undefined = chunk not loaded, null = chunk preloaded/prefetched
 	// Promise = chunk loading, 0 = chunk loaded
 	var installedChunks = {
 		"common/runtime": 0
 	};

 	var deferredModules = [];

 	// 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; .// __webpack_public_path__
 	__webpack_require__.p = "";

 	var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] | | [];var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
 	jsonpArray.push = webpackJsonpCallback;
 	jsonpArray = jsonpArray.slice();
 	for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
 	var parentJsonpFunction = oldJsonpFunction;


 	// run deferred modules from other chunkscheckDeferredModules(); }) ([]);Copy the code
//@file: dist/common/utils.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["common/utils"] and {"./src/index.js": function (module, __webpack_exports__, __webpack_require__) {
    "use strict";
    eval("__webpack_require__.r(__webpack_exports__); \n/* harmony import */ var _module_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./module.js */ \"./src/module.js\"); \n/* harmony import */ var _module_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_module_js__WEBPACK_IMPORTED_MODULE_0__); \n{}; \nconsole.log(_module_js__WEBPACK_IMPORTED_MODULE_0___default.a.s); \n\n//# sourceURL=webpack:///./src/index.js?");
  },
  "./src/module.js": function (module.exports) {
    eval("{}; var s = 123; \nconsole.log(s); \nmodule.exports = {\n s: s\n}; \n\n//# sourceURL=webpack:///./src/module.js?"); }}]);Copy the code

All of our modules are placed in a global array called webpackJsonp, and the data is processed through the webpackJsonpCallback in IIFE.

1.3. Summarize

If we look at the WebPack construction process, we can see that the most time-consuming part of the whole construction process is the process of recursively going through each entry and looking for dependencies to compile one by one. Each recursion needs to go through the String->AST->String process. Loader also needs to process some strings or execute some JS scripts. Due to the single-threaded barrier of Node. JS, webPack has been criticized for its slow construction. This is why Happypack is so popular. Here is an example of happypack code:

// @file: webpack.config.js
const HappyPack = require('happypack');
const os = require('os');
// create a thread pool
// Get the maximum number of CPU cores in the system and happypack will fill all CPU cores with compile work
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

module.exports = {
  // ...
  plugins: [
    new HappyPack({
      id: 'js'.threadPool: happyThreadPool,
      loaders: [ 'babel-loader']}),new HappyPack({
      id: 'styles'.threadPool: happyThreadPool,
      loaders: [ 'style-loader'.'css-loader'.'less-loader']]}});Copy the code

In addition to the incremental compilation, webPack is now integrated with the multi-process build, which can be used to build a cluster. This is one of the reasons why 4 can greatly improve build efficiency.

2. Write a custom WebPack Loader

2.1. Make the WebPack Loader display the prototype

In Webpack, the real compiler is our loader, that is to say, we usually Babel ES6 compilation, SCSS, LESS compilation are completed in the Loader, before you do not know the nature of loader you will feel that this is a very noble thing, Just like the principles of compilation in computer science, there must be a lot of complicated operations. In fact, a loader is just a funciton that passes in the content of a file (String) that matches it, and you just need to do something with those strings. The simplest loader might look like this:

/**
 * loader Function
 * @param {String} Content File content */
module.exports = function(content){
    return "{};" + content
}
Copy the code

Use it in the same way as babel-loader, just add an object to the module.rules array of webpack.config.js:

{
    test: /\.js$/,
    exclude: /node_modules/,
       use: {
           // This is where my custom loader is stored
           loader: path.resolve('./loaders/index.js'),
           options: {
              test: 1}}}Copy the code

The loader will then match all files ending with the.js suffix and append {}; This code can be seen in the output file:

So, once you’ve got the contents of the file, what you want to do with the strings is up to you. You can either add Babel (Content) to the Babel library, and you can compile it, or you can use UglifyJS to compress the contents of the file, and you can do it yourself.

2.2. Common Skills of Loader

2.2.1. Get the loader user – defined configuration

When we write loader configuration in webpack.config.js, we often see such a configuration item as Options, which is the customized configuration provided by Webpack for users. In our Loader, if we want to get such configuration information, Just use the wrapped library loader-utils:

const loaderUtils = require("loader-utils");

module.exports = function(content){
    // Get user-configured options
    const options = loaderUtils.getOptions(this);
    console.log('***options***', options)
    return "{};" + content
}
Copy the code

2.2.2. Format of loader to export data

In the previous example, since loader is always a Funtion, we used return to export the data after loader processing, but this is not the most recommended method. In most cases, we prefer this. Callback method to export the data. If written this way, the sample code can be rewritten as:

module.exports = function(content){
    //return "{};" + content
    this.callback(null."{};" + content)
}
Copy the code

This. Callback can be passed four arguments (the last two can be omitted), which are:

  • Error: error | null when the loader error to run out of an error
  • Content: String | Buffer, after loader compiled the content of the need to export
  • SourceMap: Source map of compiled content generated for easy debugging
  • Ast: Indicates the AST static syntax tree generated by this compilation. Subsequent loaders can use this AST directly, saving the process of repeatedly generating ast

2.2.3. Asynchronous loader

If there are asynchronous operations in the loader, such as pull requests, etc., what should we do?

Those familiar with ES6 know that the simplest solution is to wrap a Promise and ignore the async problem completely with async-await, as shown in the following code:

module.exports = async function(content){
    function timeout(delay) {
        return new Promise((resolve, reject) = > {
            setTimeout(() = > {
                resolve("{};" + content)
            }, delay)
        })
    }
    const data = await timeout(1000)
    return data
}
Copy the code

If the node version is not enough, however, we have the primitive this.async method, which returns a callback Function that can be executed when appropriate. The above example code can be rewritten as:

module.exports = function(content){
    function timeout(delay) {
        return new Promise((resolve, reject) = > {
            setTimeout(() = > {
                resolve("{};" + content)
            }, delay)
        })
    }
    const callback = this.async()
    timeout(1000).then(data= > {
        callback(null, data)
    })
}
Copy the code

Older versions of Node do the same.

2.2.4. Execution sequence of loaders

Loaders are used to compile CSS. They look like this:

In many cases, there is not only one loader in use, but the loaders are executed from back to front. You can also think of it as the process of unloading the loaders array.

2.2.5. Loader cache

By default, WebPack caches the loader’s execution results, which can greatly speed up builds, but you can also turn it off manually (I don’t know why, but I’ll give you an API for that.) Welcome to add), the sample code is as follows:

module.exports = function(content){
    // Disable the loader cache
    this.cacheable(false);
    return "{};" + content
}
Copy the code

2.2.6. Pitch Hook passes through the whole process

In the loader file you can export a function named pitch, which will be executed before all loaders, like this:

module.exports.pitch = (remaining, preceding, data) = > {
    console.log('***remaining***', remaining)
    console.log('***preceding***', preceding)
    // Data will be attached to the context this of the current loader and passed between loaders
    data.value = "test"
}
Copy the code

It can take three arguments, the most important of which is the third argument, data, for which you can attach some desired value. All loaders in a rule can get this value when executing.

module.exports = function(content){
    //***this data*** test
    console.log('***this data***'.this.data.value)
    return "{};" + content
}

module.exports.pitch = (remaining, preceding, data) = > {
    data.value = "test"
}
Copy the code

2.3. Summarize

Loader is actually a “plain” Funtion, which can pass in the matched file content for us to customize.

3. Write a custom WebPack Plugin

Review the Webpack event flow

Remember the WebPack event flow we talked about earlier, do you remember what webPack events are used to? The role of the WebPack plug-in is to mount callbacks or execute specified scripts for these events.

As mentioned in the article, the WebPack event stream is implemented through Tapable, which, like our EventEmit, is a tool for generating and managing this series of events. Some of its core code looks like this:

class SyncHook{
    constructor(){
        this.hooks = [];
    }

    // Subscribe to events
    tap(name, fn){
        this.hooks.push(fn);
    }

    / / release
    call(){
        this.hooks.forEach(hook= > hook(...arguments));
    }
}
Copy the code

All hooks on WebPack Hook are Tapable examples, so we can listen for events using the tap method and broadcast events using the Call method, as described in the official documentation:

compiler.hooks.someHook.tap(/ *... * /);
Copy the code

We have also introduced several commonly used hooks in the previous article. If you don’t remember them, you can go back and look at them again

3.2. What is the WebPack Plugin

If you parse the webPack Plugin for what it is, it is actually as simple as a WebPack Loader; it is really just a class with apply methods.

//@file: plugins/myplugin.js
class myPlugin {
    constructor(options){
        // User-defined configuration
        this.options = options
        console.log(this.options)
    }
    apply(compiler) {
        console.log("This is my first plugin.")}}module.exports = myPlugin
Copy the code

This implements a simple Webpack plugin. If we want to use it, we simply require and instantiate it in webpack.config.js:

const MyPlugin = require('./plugins/myplugin-4.js')

module.exports = { ...... .plugins: [
        new MyPlugin("Plugin is instancing.")]}Copy the code

Every time we need to use a plugin, we need to update the instantiation. Naturally, the parameters passed during the instance become the options in the constructor.

The time to instantiate all plugins is when the WebPack initializes all the parameters, which is when the event flow starts. So, with a tool library such as shell.js, you can execute files and related scripts at this point, which is what the WebPack Plugin does.

If you want to execute certain scripts at specified times, you can naturally use the method of mounting a callback on the WebPack event stream to perform the desired actions in the callback.

3.3. Tapable new use

If we want to give webpack event flow can our custom events be implemented?

The answer, of course, is yes!

How many steps does it take to customize a WebPack event flow event? Step four:

  • Introduce Tapable and find the hook you want to use, synchronous hook or asynchronous hook here everything -> Webpackage 4.0 source analysis of Tapable

    const { SyncHook } = require("tapable");
    Copy the code
  • Instantiate the hooks you need in Tapable and mount them in compiler or compilation

    compiler.hooks.myHook = new SyncHook(['data'])
    Copy the code
  • Tap listening at the location where you need to listen for events

    compiler.hooks.myHook.tap('Listen4Myplugin'.(data) = > {
        console.log('@Listen4Myplugin', data)
    })
    Copy the code
  • Execute the call method and pass in data whenever you need to broadcast the event

    compiler.hooks.environment.tap(pluginName, () = > {
           // Broadcast custom events
           compiler.hooks.myHook.call("It's my plugin.")});Copy the code

The full code implementation can be found in the project I posted at the front of the article, which looks something like this:

Now I instantiate a hook in my custom plug-in and mount it on the WebPack event stream

// @file: plugins/myplugin.js
const pluginName = 'MyPlugin'
// Tapable is a package that comes with WebPack and is the core implementation of WebPack
// Instead of installing standalone install, you can require webPack directly in a project that has webpack installed
// Get a synchronized hook class
const { SyncHook } = require("tapable");
class MyPlugin {
    // Pass the plugin configuration parameter in webpack Config
    constructor(options) {
        // { test: 1 }
        console.log('@plugin constructor', options);
    }

    apply(compiler) {
        console.log('@plugin apply');
        // instantiate custom events
        compiler.hooks.myPlugin = new SyncHook(['data'])

        compiler.hooks.environment.tap(pluginName, () = > {
            // Broadcast custom events
            compiler.hooks.myPlugin.call("It's my plugin.")
            console.log('@environment');
        });

        // compiler.hooks.compilation.tap(pluginName, (compilation) => {
            // You can also mount hooks on compilation
            // compilation.hooks.myPlugin = new SyncHook(['data'])
            // compilation.hooks.myPlugin.call("It's my plugin.")
        // });}}module.exports = MyPlugin
Copy the code

Listen for my custom events in the Listener plugin

// @file: plugins/listen4myplugin.js
class Listen4Myplugin {
    apply(compiler) {
        // is broadcast in the myPlugin Environment phase
        compiler.hooks.myPlugin.tap('Listen4Myplugin'.(data) = > {
            console.log('@Listen4Myplugin', data)
        })
    }
}

module.exports = Listen4Myplugin
Copy the code

Introduce and instantiate two plug-ins in the WebPack configuration

// @file: webpack.config.js
const MyPlugin = require('./plugins/myplugin-4.js')
const Listen4Myplugin = require('./plugins/listen4myplugin.js')

module.exports = { ...... .plugins: [
        new MyPlugin("Plugin is instancing."),
        new Listen4Myplugin()
    ]
}
Copy the code

The output looks like this:

We take the data from the call method and successfully output it in the environment.

3.4. Actual combat analysis

Taking a look at the much-maltreated HTML-webpack-plugin, we find a demo at the bottom of the readme:

function MyPlugin(options) {
  // Configure your plugin with options...
}

MyPlugin.prototype.apply = function (compiler) {
  compiler.hooks.compilation.tap('MyPlugin'.(compilation) = > {
    console.log('The compiler is starting a new compilation... ');

    compilation.hooks.htmlWebpackPluginAfterHtmlProcessing.tapAsync(
      'MyPlugin'.(data, cb) = > {
        data.html += 'The Magic Footer'

        cb(null, data)
      }
    )
  })
}

module.exports = MyPlugin
Copy the code

If you read carefully on the content of the plate, you will find that the htmlWebpackPluginAfterHtmlProcessing is not the plug-in mount in webpack flow on the custom events, it will be in the generated output file to add HTML calls when you custom callback, And the information about the resource file generated after the compilation and the content of the HTML file to be injected (string form) are passed into the callback for our custom operation. Search for this hook in the project:

We will instantiate the hooks we need first. We can see from the name that only the first hook is synchronous and the others are asynchronous. Then look for the broadcast of the event:

This is exactly the same as what we just introduced, except that the asynchronous hook uses the promise method to broadcast, and the rest is completely our custom event flow. We lay the console to see if they are interested in can go htmlWebpackPluginAfterHtmlProcessing this hook to callback the incoming data, perhaps you can discover a new continent.

Hard wide

Our team is hiring!! Welcome to join bytedance’s commercial realization front end team. The technical construction we are doing includes: Front-end engineering system upgrade, team Node infrastructure construction, the front-end one-click CI publishing tools, support of service components, the internationalization of the front-end general micro front-end solutions, heavy reliance on business system reform, visual page structures, systems, business intelligence (BI, front end test automation and so on, has the nearly hundred people of the north grand front team, There will be areas of interest for you, if you want to join us, please click on my inpush channel:

✨ ✨ ✨ ✨ ✨

Push portal (for prime recruiting season, click here to get Bytedance push opportunities!)

Exclusive entrance for Bytedance enrollment (bytedance enrollment promotion code: HTZYCHN, post link: Join Bytedance – Recruitment)

✨ ✨ ✨ ✨ ✨

If you want to know about our department’s daily life (dou) and work environment (Li), you can also click here