This is the 101st original article without water. If you want to get more original articles, please search the official account and pay attention to us.
Step by step to get you started with the Webpack Plugin

About Webpack

Before we talk about Plugins, let’s take a look at Webpack. Essentially, Webpack is a static module packaging tool for modern JavaScript applications. It parses our code, generates the corresponding dependencies, and then assembles the different modules into one or more bundles.

The basic concept of Webpack consists of the following:

  1. Entry: An Entry file for Webpack, which indicates which module should be used as the Entry to build the internal dependency diagram.
  2. Output: Tells Webpack where to Output the bundles it creates, how to name them, where to Output them, and so on.
  3. Loader: Module code converter, which enables Webpack to handle other types of files besides JS and JSON.
  4. Plugin: Plugin provides the ability to perform a wide range of tasks, including packaging optimization, resource management, injection of environment variables, and more.
  5. Mode: The necessary parameters for executing different optimization parameters according to different operating environments.
  6. Browser Compatibility: Supports all ES5 standard browsers (IE8 and above).

With the basic Webpack concepts in mind, let’s look at why we need a Plugin.

The role of the Plugin

Let me give you an example from the inside of our politics:

In the React project, our Router file is generally written in a project. If the project contains many pages, it is inevitable that all business modules will be coupled with the Router. Therefore, we developed a Plugin. This Plugin will read the index.js files in all folders and merge them together to form a unified Router file, which can easily solve the business coupling problem. This is the application of the Plugin (the implementation is explained in the last section).

Take a look at our pre-composite project code structure:

─ package.json High School School ─ Readme.md High School School ─ Zoo.config. JS High School School ─ ESlintignore High School School ─.ESlintrc High School School ─.gitignore High School School ─.stylelintrc High School School ── │ ├─ sigma ─ webpack.dev.conf.js - SRC │ ├─ index.hbs │ ├─ main.js (Inlet File) │ ├─ sigma ─ common (Inlet File) │ ├ ─ sigma... │ ├─ Exercises - Components │ ├─ Exercises - Components │ ├─ Exercises - Exercises - ├─ sigma... │ ├─ Exercises - Exercises - └ (2) │ ├─ Exercises - Routes │ │ ├─ Exercises - Config │ │ ├─ Exercises - Routes │ ├─ Exercises - Routes │ ├─ Exercises - Routes │ │ ├─ Exercises - - Exercises - - Exercises - - Exercises - - Exercises - - Exercises │ │ │ ├─ Exercises - Services │ │ ├─ sigma, sigma, sigma... │ │ ├─ sigma, sigma, sigma, sigma, sigma... │ │ │ ├─ sigma, sigma, sigma, sigma, sigma, sigma, sigma, sigma, sigma, sigma, sigma

Take a look at the structure after the Plugin compositing Router:

─ package.json High School School ─ Readme.md High School School ─ Zoo.config. JS High School School ─ ESlintignore High School School ─.ESlintrc High School School ─.gitignore High School School ─.stylelintrc High School School ── │ ├─ sigma ─ webpack.dev.conf.js - SRC │ ├─ index.hbs │ ├─ main │ ├─ Exercises - Common, Router, Router, Router, Router, Router, Router... │ ├─ Exercises - Components │ ├─ Exercises - Components │ ├─ Exercises - Exercises - ├─ sigma... │ ├─ Exercises - Exercises - └ (2) │ ├─ Exercises - Routes │ │ ├─ Exercises - Config │ │ ├─ Exercises - Routes │ ├─ Exercises - Routes │ ├─ Exercises - Routes │ │ ├─ Exercises - - Exercises - - Exercises - - Exercises - - Exercises - - Exercises │ │ │ ├─ Exercises - Services │ │ ├─ sigma, sigma, sigma... │ │ ├─ sigma, sigma, sigma, sigma, sigma... Anti-Flag ─.eslintignore Anti-Flag ─.eslintrc Anti-Flag ─.gitignore ├─ stylelintrc

To summarize, Plugin does the following:

  1. Some other things are provided that the Loader cannot solve
  2. Provides powerful extension methods to perform a wider range of tasks

Now that you know what a Plugin does, let’s talk about how to create a Plugin.

Create a Plugin

Hook

Before we talk about creating a Plugin, let’s talk about what a Hook is.

Webpack triggers a series of processes during compilation, and in this series of processes, Webpack exposes some key process nodes for developers to use. These are hooks, which are analogous to React lifecycle hooks.

Plugins expose methods on these hooks for the developer to perform additional operations. When writing a Plugin, we need to know which hooks we should operate on.

How to create a Plugin

Let’s take a look at the Webpack official example:

const pluginName = 'ConsoleLogOnBuildWebpackPlugin'; Class ConsoleLogOnBuildWebpackPlugin {the apply (compiler) {/ / is for before you start reading records executive compiler. Hooks. Run. Tap (pluginName, Compilation => {console.log(" Webpack build process started!" ); }); }}

From the above code we can summarize the following:

  • A Plugin is simply a class.
  • Class requires an apply method to execute the specific plug-in method.
  • One thing the plug-in method does is register a synchronous print log method on the run Hook.
  • The argument to the apply method injects a Compiler instance, which is Webpack’s backbone engine and represents all configuration items passed by the CLI and the Node API.
  • The Hook callback method injects the Compiler instance, which can access the module and its corresponding dependencies at the time of the current build.
Compiler object contains all Webpack environment configuration information, including options, loaders, plugins, etc. This object is instantiated at Webpack startup and is globally unique. It can simply be understood as a Webpack instance. The Compilation object contains the current module resources, compiled generated resources, changed files, etc. When Webpack is running in development mode, a new Compilation will be created whenever a file change is detected. The Compilation object also provides a number of event callbacks for plug-ins to extend. Compiler objects can also be read through Compilation. -- Excerpt from "Simple Webpack"
  • A number of Hooks are defined on the Compiler instance and the Compilation instance respectively, which can be passedExampleThree methods are exposed on HOOK for use, namely TAP, TAPAsync and TApPromise. These three methods are used to define how to execute hooks. For example, TAP means to register synchronous hooks, TapAsync means to register asynchronous hooks in the callback way, and TApPromise means to register asynchronous hooks. Take a look at the Webpack source code for these three types of implementations, which I’ve annotated for ease of reading.
// The TAP method's type is sync, the TAPApMISE method's type is async, and the TApApMISE method's type is promise // The source code is taken from the HOOK factory method: lib/HookCodeFactory.js create(options) { this.init(options); let fn; Switch (this.options.type) {case "sync": Fn = new Function(this.args(), // String 'use strict'; \ n '+ this. The header () + / / public methods, generate some need to define variables of enclosing contentWithInterceptors ({generate actual execution code / / onError: err = > ` throw ${err}; \n ', // Error callback onResult: result => 'return ${result}; \n ', // ResultTurns: true, onDone: () => "", rethrowifPosable: true})); break; case "async": fn = new Function( this.args({ after: "_callback" }), '"use strict"; This. \ n '+ header () + / / public methods, generate some variables need to define this. ContentWithInterceptors ({onError: err = > `, _callback, (${err}); \n ', // OnResult: result => '_callback(null, ${result}); \n ', // OnDone: () => "_callback(); \n" // No result, finish})); break; case "promise": let errorHelperUsed = false; const content = this.contentWithInterceptors({ onError: err => { errorHelperUsed = true; return `_error(${err}); \n`; }, onResult: result => `_resolve(${result}); \n`, onDone: () => "_resolve(); \n" }); let code = ""; code += '"use strict"; \n'; code += this.header(); Code += "return new Promise((function(_resolve, _reject) {\n"; // return Promise if (errorHelperUsed) {code += "var _sync = true; \n"; code += "function _error(_err) {\n"; code += "if(_sync)\n"; code += "_resolve(Promise.resolve().then((function() { throw _err; }))); \n"; code += "else\n"; code += "_reject(_err); \n"; code += "}; \n"; } code += content; If (errorHelperUsed) {code += "_sync = false; \n"; } code += "})); \n"; fn = new Function(this.args(), code); break; } this.deinit(); // empty options and _args return fn; }

All specific Hooks in the code are one of the following ten Hooks provided by Webpack.

// Source from: lib/index.js "use strict"; exports.__esModule = true; // exports. Synchook = require("./ Synchook "); // Exports.SyncBailHook = require("./SyncBailHook"); // Exports.SyncBailHook = require("./SyncBailHook"); / / simultaneous execution of hook, support will return values through to the next hook the exports. SyncWaterfallHook = the require (". / SyncWaterfallHook "); // Exports. Syncloophook = require("./SyncLoopHook"); // Exports. Syncloophook = require(". / / asynchronous parallel hooks exports. AsyncParallelHook = the require (". / AsyncParallelHook "); / / asynchronous parallel hooks, return is not empty, stop execution down, direct execution callback exports. AsyncParallelBailHook = the require (". / AsyncParallelBailHook "); // Exports asynchronous serial hook.AsyncSeriesHook = require("./AsyncSeriesHook"); / / asynchronous serial port of hook, return is not empty, stop execution down, direct execution callback exports. AsyncSeriesBailHook = the require (". / AsyncSeriesBailHook "); / / support asynchronous serial port && parallel hooks, return is not empty, repeat exports. AsyncSeriesLoopHook = the require (". / AsyncSeriesLoopHook "); / / asynchronous serial port of hook, dependence on the next step returns the value of exports. AsyncSeriesWaterfallHook = the require (". / AsyncSeriesWaterfallHook "); // export. HookMap = require("./HookMap"); export. HookMap = require("./HookMap"); exports.MultiHook = require("./MultiHook");

A few simple examples:

  • AsyncSeriesHook (AsyncSeriesHook) is the type of run Hook that will be executed before the records are read. It is also possible to implement the asynchronous TAPAsync and TApPromise methods, so the following will work as well:
const pluginName = 'ConsoleLogOnBuildWebpackPlugin'; class ConsoleLogOnBuildWebpackPlugin { apply(compiler) { compiler.hooks.run.tapAsync(pluginName, (compilation, Callback) => {setTimeout(() => {console.log(" Webpack build starts! "); ); callback(); }, 1000); // callback must be called in order for the build to continue; }); }}
  • Another example is Failed Hook, which is executed after a compilation failure, and its type is Synchook. If we look at the source code, we can find that when we call the TapAsync and TApPromise methods, an error will be thrown directly.

For synchronous methods, it is recommended to use TAP directly to register the method. For asynchronous schemes, TAPAPasync implements the callback method by executing the callback method. If the executed method returns a Promise, it is recommended to use TApPromise to register the method

The type of Hook can be looked up through the official API, the address portal

Js const TAP_ASYNC = () => {throw new Error(" Tapasync is not supported on a SyncHook"); }; const TAP_PROMISE = () => { throw new Error("tapPromise is not supported on a SyncHook"); }; function SyncHook(args = [], name = undefined) { const hook = new Hook(args, name); hook.constructor = SyncHook; hook.tapAsync = TAP_ASYNC; hook.tapPromise = TAP_PROMISE; hook.compile = COMPILE; return hook; }

After explaining the implementation, let’s talk about the Webpack process and what Tapable is.

Webpack && Tapable

Webpack running mechanism

To understand the Plugin, let’s take a rough look at the Webpack packaging process

  1. When we package, we will first merge the Webpack config file and command-line parameters, and merge them into options.
  2. Options is passed to the Compiler constructor, generating a Compiler instance and instantiating the Hooks on the Compiler.
  3. Compiler objects execute the run method and automatically trigger key Hooks such as beforeRun, run, beforeCompile, and compile.
  4. Call the Compilation constructor method to create the Compilation object, which is responsible for managing all modules and corresponding dependencies, and trigger a make Hook after the creation is completed.
  5. Execute the Compilation. addEntry() method, which analyzes all the entry files, recursively parses them step by step, calls the NormalModuleFactory method, and generates a Module instance for each dependency. And trigger key Hooks such as beforeResolve, resolver, afterResolve and module during execution.
  6. Using the Module instance generated in step 5 as an input argument, execute the Compilation.AddModule () and Compilation.BuildModule () methods to recursively create Module objects and dependent Module objects.
  7. The seal method is called to generate the code, collate the output master file and chunk, and finally output.

Tapable

Tapable is the core Webpack tool library, which provides abstract class definitions for all hooks. Many Webpack objects inherit from Tapable classes. For example, TAP, TAPASYNC and TApApMISE mentioned above are exposed by TAPABLE. The source code is as follows (a snippet of the code) :

// create a Plugin (); // create a Plugin (); tapable.d.ts declare class Hook<T, R, AdditionalOptions = UnsetAdditionalOptions> { tap(options: string | Tap & IfSet<AdditionalOptions>, fn: (... args: AsArray<T>) => R): void; } declare class AsyncHook<T, R, AdditionalOptions = UnsetAdditionalOptions> extends Hook<T, R, AdditionalOptions> { tapAsync( options: string | Tap & IfSet<AdditionalOptions>, fn: (... args: Append<AsArray<T>, InnerCallback<Error, R>>) => void ): void; tapPromise( options: string | Tap & IfSet<AdditionalOptions>, fn: (... args: AsArray<T>) => Promise<R> ): void; }

Common Hooks API

See Webpack

This article lists some common Hooks and their corresponding types:

Compiler Hooks

Hook type call
run AsyncSeriesHook Before I start reading records
compile SyncHook After a new compilation is created
emit AsyncSeriesHook Before the resource is generated to the output directory
done SyncHook The compilation was completed

Compilation Hooks

Hook type call
buildModule SyncHook Triggered before module build starts
finishModules SyncHook All modules are built
optimize SyncHook Triggered at the beginning of the optimization phase

The application of Plugin in a project

With all this theoretical knowledge out of the way, let’s take a look at how the Plugin works in a project: how to merge the router files from each sub-module into router-config.js.

Background:

In the React project, our Router file is generally written in a project. If the project contains many pages, it is inevitable that all business modules will be coupled with the Router. Therefore, we developed a Plugin. The Plugin will read the Router files in all folders and merge them together to form a unified Router Config file, which can easily solve the problem of business coupling. This is where the Plugin comes in.

Implementation:

const fs = require('fs'); const path = require('path'); const _ = require('lodash'); function resolve(dir) { return path.join(__dirname, '.. ', dir); } function MegerrouterPlugin (options) {// Options is the configuration file, Here you can make some options related work} MegerRouterPlugin. Prototype. Apply = function (compiler) {/ / registered before - the compile hook, Plugin ('before-compile', (Compilation, callback) => {// Const data = {}; const routesPath = resolve('src/routes'); const targetFile = resolve('src/router-config.js'); // Get all files and folders in the path const dirs = fs.readdirSync(routesPath); try { dirs.forEach((dir) => { const routePath = resolve(`src/routes/${dir}`); // If (!! fs.statSync(routePath).isDirectory()) { return true; } delete require.cache[`${routePath}/index.js`]; const routeInfo = require(routePath); // create a router if (! _.isArray(routeInfo)) { generate(routeInfo, dir, data); } else {routeInfo.map((config) => {generate(config, dir, data);} else {routeInfo.map((config) => {generate(config, dir, data); }); }}); } catch (e) { console.log(e); } if (fs.existsSync(targetFile)) {delete require.cache[targetFile];} if (fs.existsSync(targetFile)) {delete require.cache[targetFile]; const targetData = require(targetFile); if (! _.isEqual(targetData, data)) { writeFile(targetFile, data); } else {writeFile(targetFile, data); } // The last call to the callback continues executing the webpack callback(); }); }; Function generate(config, dir, data) {// MergeConfig (config, dir, data) {// MergeConfig (config, dir, data); // Merge sub-Router getChildroutes (config.Childroutes, dir, data, config.url); Function mergeConfig(config, dir, targetData) {const {view, models, extraModels, URL, childRoutes, ... rest } = config; // Routes /${dir}/models', models); // Routes /${dir}/models', models); const data = { ... rest, }; Path = '${dir}/ Views ${view? `/${view}` : ''}`; // If extraModels are available, Joining together into the object models the if (dirModels length | | (extraModels && extraModels. Length)) {data. The models = mergerExtraModels (config, dirModels); } Object.assign(targetData, { [url]: data, }); } // DvA models function getModels(modelsDir, models) {if (! fs.existsSync(modelsDir)) { return []; } let files = fs.readdirSync(modelsDir); Files.filter (item) => {return /\. JSX? $/.test(item); }); // If the models are not defined, the default is index.js if (! models || ! Models.length) {if (files.indexOf('index.js') >-1) {// remove SRC return [' ${modelsdir.replace (' SRC /', ')}/index.js']; } return []; } return models.map(item) => {if (files.indexof (' ${item}.js') >-1) {// remove SRC return '${modelsdir.replace (' SRC /', '')}/${item}.js`; }}); } // Extra models function mergerExtraModels(config, models) {return models.concat(config.extramodels? config.extraModels : []); } // Merge sub-router function getChildroutes (Childroutes, dir, targetData, oUrl) {if (! childRoutes) { return; } childRoutes.map((option) => { option.url = oUrl + option.url; If (option.childroutes) {// Router getChildRoutes(option.childroutes, dir, targetData, option.url); } mergeConfig(option, dir, targetData); }); } function writeFile(targetFile, data) {fs.writeFileSync(targetFile, 'module.exports = ${json.stringify (data, data); null, 2)}`, 'utf-8'); } module.exports = MegerRouterPlugin;

Results:

Documents prior to merger:

module.exports = [
  {
    url: '/category/protocol',
    view: 'protocol',
  },
  {
    url: '/category/sync',
    models: ['sync'],
    view: 'sync',
  },
  {
    url: '/category/list',
    models: ['category', 'config', 'attributes', 'group', 'otherSet', 'collaboration'],
    view: 'categoryRefactor',
  },
  {
    url: '/category/conversion',
    models: ['conversion'],
    view: 'conversion',
  },
];

Merged file:

module.exports = {
  "/category/protocol": {
    "path": "Category/views/protocol"
  },
  "/category/sync": {
    "path": "Category/views/sync",
    "models": [
      "routes/Category/models/sync.js"
    ]
  },
  "/category/list": {
    "path": "Category/views/categoryRefactor",
    "models": [
      "routes/Category/models/category.js",
      "routes/Category/models/config.js",
      "routes/Category/models/attributes.js",
      "routes/Category/models/group.js",
      "routes/Category/models/otherSet.js",
      "routes/Category/models/collaboration.js"
    ]
  },
  "/category/conversion": {
    "path": "Category/views/conversion",
    "models": [
      "routes/Category/models/conversion.js"
    ]
  },
}

The final project will generate the router-config.js file

At the end

I hope that after reading this chapter, you will have a preliminary understanding of the Webpack Plugin and be able to write your own Plugin to apply to your own projects.

If there is anything wrong in the article, you are welcome to correct it.

Recommended reading

Through the custom VUE instruction to realize the front-end exposure buried point

H5 page list caching scheme

Open source works

  • Politics gathers cloud front tabloids

Open source address www.zoo.team/openweekly/ (there is WeChat communication group on the homepage of the tabloid official website)

, recruiting

ZooTeam, a young, passionate and creative front-end team, is affiliated to the R&D Department of ZooTeam, based in the picturesque city of Hangzhou. There are more than 40 front-end partners in the team, with an average age of 27. Nearly 30% of them are full stack engineers, no problem young storm group. The members are not only “old” soldiers from Ali and netease, but also fresh graduates from Zhejiang University, University of Science and Technology of China and Hangzhou Electric University. In addition to daily business connection, the team also carries out technical exploration and practical practice in the directions of material system, engineering platform, construction platform, performance experience, cloud application, data analysis and visualization, promotes and lands a series of internal technical products, and continues to explore the new boundary of the front-end technical system.

If you want to change what you’ve been doing, you want to start doing it. If you want to change the way you’ve been told you need to think a little bit, you can’t break it. If you want to change, you have the power to do it, but you don’t need to; If you want to change what you want to do, you need a team to support it, but there is no place for you to bring people. If you want to change the established pace, it will be “5 years and 3 years experience”; If you want to change the original savvy is good, but there is always a layer of window paper fuzzy… If you believe in the power of belief, the ability of ordinary people to do extraordinary things, to meet better people. If you want to be a part of the take-off process and personally drive the growth of a front end team that has a deep business understanding, a sound technology system, technology that creates value, and influence that spills over, I think we should talk. Anytime, waiting for you to write something down and send it to [email protected]