An overview of the

Because busy for a long time did not update the article, happened to do technology sharing in the company, privately wrote an article. Hope to bring help to partners in need, grow up together ~💪

Here’s what you can learn:

  1. WebpackIn the configurationLoaderThe use of the
  2. WebpackIn the configurationPluginThe use of the
  3. Loader 和 PluginThe difference between
  4. How to write a customLoader
  5. How to write a customPlugin

As we all know, Webpack can only handle JavaScript and JSON files, other types of files can only be converted into JavaScript code by the corresponding loader for Webpack!

To explore the Loader

Loader is a code transcoder that converts various resources. Next, we will thoroughly understand the essence of Loader from its characteristics, classification, usage and execution sequence, so as to pave the way for us to realize custom Loader and understand the underlying principle.

  • LoaderFeatures: single principle, eachLoaderJust do the right thing
  • LoaderThe classification of:pre,normal(Default),inline,postFour kinds of
  • LoaderUsage: singleloaderMore than,loaderObject form
  • LoaderOrder: right to left, bottom to top

Conclusion: Loader is a function that takes the original resource as an argument and outputs the converted content.

Basic usage of Loader

Next, we’ll create a project to explore how loaders are used in Webpack, what to watch out for, and how to customize a Loader.

mkdir webpack-loader-plugin
cd webpack-loader-plugin
npm init -y
npm install webpack webpack-cli -D
Copy the code

After installing the dependency and creating the corresponding directory, create the webpack configuration file, webpack.config.js, to try the use of the loader, and import a non-JS file and package it. You may need an appropriate loader to handle this file type. Therefore, non-JS files loaded in Webpack need to be converted by the corresponding loader before packaging. Let’s take the.css style file as an example:

// webpack.config.js
const path = require('path')
module.exports = {
  mode: 'development'.entry: './src/index.js'.output: {
    filename: 'bundle.js'.path: path.resolve(__dirname, 'dist'),},module: {
    rules: [{test: /\.css$/,
        use: ['style-loader'.'css-loader'],},],},}Copy the code

The above are the characteristics and basic usage of Loader. The following describes the classification and sequence of Loader by customizing loader.

Custom Loader

Create a loader folder to store our custom loader1.js, loader2.js, loader3.js, loader4.js and modify webpack.config.js configuration:

const path = require('path')
module.exports = {
    ...
    module: {
        rules: [{test: /\.js$/,
                use: [path.resolve(__dirname, 'loader/loader1.js'), path.resolve(__dirname, 'loader/loader2.js'), path.resolve(__dirname, 'loader/loader3.js'), path.resolve(__dirname, 'loader/loader4.js']}]}}Copy the code

Import mode of loader:

    1. throughnpmPackages installedloader, just use the name
        {
            test: /\.css$/,
            use: 'css-loader'
        }
    Copy the code
    1. The customloader, the absolute path is used
        {
            test: /\.css$/,
            use: path.resolve(__dirname, 'loader/loader1.js')}Copy the code
    1. Configuring alias Mode
    resolveLoader: {
        alias: {
            'loader1': path.resolve(__dirname, 'loader/loader1.js')}},module: {
        rules: [{test: /\.css$/,
                use: 'loader1'}}]Copy the code
    1. Configuring the Lookup Mode
    resolveLoader: {
        modules: ['node_modules', path.resolve(__dirname, 'loader')]},module: {
        rules: [{test: /\.css$/,
                use: 'loader1'}}]Copy the code

Loader execution sequence:

As shown in the preceding example, the normal Loader is executed from right to left and from bottom to top.

By changing the execution order of the four loader types: pre, normal, inline, and POST. This allows us to change the execution order of the Loader at will.

{
    test: /.js$/,
    use: 'loader1'.enforce: 'pre'
},
{
    test: /.js$/,
    use: 'loader2'
},
{
    test: /.js$/,
    use: 'loader3'.enforce: 'post'
}
Copy the code

If the enforce attribute is not added, the loading sequence is loader3 -> Loader2 -> loader1 from bottom to top. However, after configuring the invincibility attribute, the loading sequence is changed to loader1 -> loader2 -> loader3

Inline loader loading rules

  • !: ignorenormal loader
  • -!: ignorenormal loader,pre loader
  • !!!!!: ignorenormal loader,pre loader,post loader

Summary: Use prefix to disable different types of Loaders

Picth method

Loader execution is divided into two stages: Pitch stage and Noraml stage. When defining a Loader function, you can export a pitch method that is executed before the Loader function is executed.

The loader executes pitch, obtains resources, and then executes normal Loader. If the pitch has a return value, it will not go to the later loader and return the return value to the previous loader. That’s why a pitch is a circuit breaker!

// loader/loader1.js
function loader1(source) {
  console.log('loader1~~~~~~', source)
  return source
}

module.exports = loader1

module.exports.pitch = function (res) {
  console.log('pitch1')}/ / loader/loader2. Js in the same way
/ / loader/loader3. Js in the same way
/ / loader/loader4. Js in the same way
Copy the code

To obtain parameters

To get a parameter passed in by the user, you need to download a dependency:

NPM install loader-utils -d // Note the [email protected] versionCopy the code

Loader-utils dependencies have a getOptions method that gets the configuration of loader options

// webpack.config.js
{
    test: /.js$/,
    use: [
        {
            loader: 'loader1'.options: {
                name: 'tmc'}}}]// loader1.js
const loaderUtil = require('loader-utils')
function loader1 (source) {
    const options = loaderUtil.getOptions(this) | | {}console.log(options)
    return source
}

module.exports = loader1

{name: 'TMC'}
Copy the code

Validate parameter

To verify that the parameter passed by the user is valid, you need to download a dependency:

npm install schema-utils -D
Copy the code

The schema-utils dependency has a validate method that verifies that the options configuration in the loader is valid

// loader/loader1.js
const { getOptions } = require('loader-utils')
const { validate } = require('schema-utils')
const schemaJson = require('./schema.json')
function loader1 (source) {
    const options = getOptions(this) || {}
    validate(schemaJson, options, { name: 'loader1' })
    return source
}

module.exports = loader1

// schema.json
{
    "type": "object"."properties": {
        "name": {
            "type": "string"."description": "Name"}},"additionalProperties": true
}
Copy the code

The key name in “properties” is the name of the field in “options” that we need to check. AdditionalProperties indicates whether additionalProperties are allowed for options.

Note: The additionalProperties property defaults to false. When loader allows multiple options properties, change the value to true.

Synchronous & asynchronous

Loaders are classified into synchronous and asynchronous loaders

When converting content synchronously, results can be returned using either return or this.callback()

The detailed method for callback is as follows:

callback({
    error: Error | Null, // Returns an Error to Webpack when the original content cannot be converted
    content: String | Buffer, // The converted contentsourceMap? : SourceMap,// The converted content generates the Source Map of the original content (optional)abstrctSyntaxTree? : AST// The number of AST syntax generated from the original content (optional)
})
Copy the code
function loader3 (source, map, meta) {
    console.log('loader3~~~~~~', source)
    return source
}
/ / or
function loader3 (source, map, meta) {
    console.log('loader3~~~~~~', source)
    this.callback(null, source, map, meta)
    return;
}

module.exports = loader3
// Map and meta are optional parameters
Copy the code

Note: When callback() is called, undefined is always returned

When transforming content asynchronously, use the this.async() form to get the callback function for the asynchronous operation and return the result in the callback function

function loader4 (source) {
    console.log('loader4~~~~~~', source)
    const callback = this.async()
    setTimeout(() = > {
        callback(null, source) // The first argument is an error object that can be set to null
    }, 1000)}module.exports = loader4
Copy the code

skills

  • webpackAll are cached by defaultloaderIf you don’t want to use cachingthis.cacheable(false)
  • Working with binary data
module.exports = function(source) {
    source instanceof Buffer === true;
    return source;
};
// Tells Webpack whether the Loader needs binary data via the exports.raw property
module.exports.raw = true;
Copy the code

Note: The most critical code is the last line of module.exports. Raw = true; Loader can only get the string without the line

In actual combat

Next, let’s write a Loader to see how it works:

Requirements: Simulate the functions of babel-loader

// loader/babelLoader.js
const {
    getOptions
} = require('loader-utils')
const {
    validate
} = require('schema-utils')
const babel = require('@babel/core')
const schema = require('./babelSchema.json')

function babelLoader(source) {
    // Get the parameters passed in by the user
    const options = getOptions(this)
    // Validate parameters
    validate(schema, options, {
        name: 'babelLoader'
    })
    const callback = this.async()
    // Convert the code
    babel.transform(source, {
        presets: options.presets,
        sourceMap: true
    }, (err, result) = > {
        callback(err, result.code, result.map)
    })
}

module.exports = babelLoader

// loader/babelSchema.json
{
    "type": "object"."properties": {
        "presets": {
            "type": "array"}},"additionalProperties": true
}

/ / use
module.exports = {
    module: {
        rules: [{test: /.js$/,
                use: {
                    loader: 'babelLoader'.options: {
                        presets: [
                            "@babel/preset-env"]}}}]},// Parse loader rules
    resolveLoader: {
        modules: ['node_modules', path.resolve(__dirname, 'loader')]}}Copy the code

To explore the Plugin

Plugin is an extender that is more flexible than Loader because it has access to the Webpack compiler. A number of events are broadcast during the life cycle of a Webpack run, and the Plugin can listen for these events and change the output when appropriate through the API provided by Webpack. This allows Plugin to intercept Webpack execution through hook functions that do things other than Webpack packaging. Like: package optimization, resource management, injection of environment variables, and so on.

Basic usage of Plugin

There are three steps to using a plugin:

    1. npmInstall the corresponding plug-in
    1. Introduce installed plug-ins
    1. inpluginsThe use of
// 1. NPM installs the corresponding plug-in
npm install clean-webpack-plugin -D

// 2. Import the installed plug-in
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

// 3. Use in plugins
export default {
    plugins: [
        new CleanWebpackPlugin()
    ]
}
Copy the code

A custom Plugin

Before writing the plug-in, we first need to familiarize ourselves with the overall running flow of WebPack. The running of Webpack is essentially a mechanism of event flow. During the running of Webpack, a series of processes including initialization parameters, compiling files, confirming entry, compiling modules, compiling completion, generating resources and finally outputting resources are carried out. Along the way, WebPack broadcasts events from the corresponding nodes, and we can listen for those events and do something with the API provided by WebPack at the right time.

The Webpack plug-in consists of:

  1. aJavaScriptNaming functions
  2. In the plug-in functionpropotypeDefine aapplymethods
  3. Make a binding towebpackIts own event hook
  4. To deal withwebpackSpecific data for the internal instance
  5. Called when the function completeswebpackThe callback provided

The core of the Plugin is that when the Apply method executes, it operates on hooks that are packed by WebPack at different points in time. Its working process is as follows:

  1. webpackAfter startup, executenew myPlugin(options)Initialize the plug-in and get the instance
  2. Initialize thecompilerObject, callmyPlugin.apply(compiler)Passing in a plug-incompilerobject
  3. Plug-in instance acquisitioncompilerIt listenswebpackBroadcast the event throughcompileroperationwebpack

In other words, a plug-in is a class, using the plug-in is a new instance of the plug-in, and passing the configuration parameters required by the plug-in to the constructor of the class. Gets the user-passed argument in the constructor.

The first step in writing a plug-in is as follows:

class myPlugin {
    constructor(options) {
        this.options = options // The parameter passed by the user}}module.exports = myPlugin
Copy the code

According to the Webpack source code, the plug-in instance will have an apply method with compiler as its parameter.

/ / webpack. Js source code
if (Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
        if (typeof plugin === "function") {
            plugin.call(compiler, compiler);
        } else{ plugin.apply(compiler); }}}Copy the code

Extensions: Webpack plug-ins have an Apply method, just like Vue plug-ins have an Install method

The second step of writing a plug-in is as follows:

class myPlugin {
    constructor(options) {
        this.options = options // The parameter passed by the user
    }
    apply(compiler){}}module.exports = myPlugin
Copy the code

The two most commonly used object Compiler and Compilation in Plugin development, both inherited from Tapable, are the bridge between Plugin and Webpack. React-redux is a bridge between React and Redux.

Core object

  • Responsible for the overall compilation processCompilerobject
  • Responsible for compiling the ModuleCompilationobject

Compiler

The Compiler object represents the complete configuration of the Webpack environment (think of it as an instance of Webpack). This object is created once when webPack is started, and all the actionable Settings are configured, including Options, Loader, and plugin. When a plug-in is applied in a WebPack environment, the plug-in receives a reference to this Compiler object. You can use it to access the main webPack environment.

Compilation

The Compilation object represents a resource version build. When running the WebPack development environment middleware, each time a file change is detected, a new Compilation is created, resulting in a new set of Compilation resources. A Compilation object represents the current module resources, the compile-generated resources, the changing files, and the state information of the dependencies being tracked. The Compilation object also provides a number of critical timing callbacks that plug-ins can choose to use when doing custom processing.

Note: The difference between the two is that one represents the entire build process and the other represents a module in the build process

  • compilerRepresents the wholewebpackLifecycle from startup to shutdown, whilecompilationIt just represents a one-time compilation process.
  • compiler å’Œ compilationBoth expose a number of hooks that we can customize to the scenario we need.

Tapable

Tapable provides a publish-subscribe API for a series of events that can be registered with Tapable to trigger registered event callbacks at different times for execution. The Plugin mechanism in Webpack is based on this mechanism to call different plug-ins at different compilation stages and thus affect compilation results.

const {
 SyncHook,
 SyncBailHook,
 SyncWaterfallHook,
 SyncLoopHook,
 AsyncParallelHook,
 AsyncParallelBailHook,
 AsyncSeriesHook,
 AsyncSeriesBailHook,
 AsyncSeriesWaterfallHook
} = require("tapable");
Copy the code

There is only one way to register synchronous hooks

  1. Can only be registered through TAP, bycallTo perform the

Asynchronous hooks provide three ways to register:

  1. tap: Registers hooks synchronously with thecallTo perform the
  2. tapAsync: Registers hooks asynchronously withcallAsyncTo perform the
  3. tapPromise: Registers hooks asynchronously and returns onePromise

Note: Asynchronous hooks can be divided into:

  • Asynchronous serial hook (AsyncSeries) : Asynchronous hook functions that can be executed concatenated (called sequentially)
  • Asynchronous parallel hooks (AsyncParallel) : an asynchronous hook function that can be executed in parallel (called concurrently)

What are the differences between different types of hooks?

  • Basic type: it does not care about the return value of each called event and only executes the registered callback function
  • Waterfall type: passes the return value of the previous callback function to the next callback function as an argument
  • Insurance type: if not in the callback functionundefinedWhen a value is returned, subsequent callbacks are not executed
  • Loop type: If there is no in the callback functionundefinedAll callbacks are restarted until all callbacks have returnedundefined

Once we know what events WebPack broadcasts, we can listen for events in Apply and write the corresponding logic, as follows:

class myPlugin {
    constructor(options) {
        this.options = options // The parameter passed by the user
    }
    apply(compiler) {
        // Listen for an event
        compiler.hooks.'Compiler event name'.tap('myPlugin'.(compilation) = > {
            // Write the corresponding logic}}})module.exports = myPlugin
Copy the code

Note: While listening for compilation events in the Compiler object, you can also continue listening for compilation events in the compiler object in the callback function, as follows:

class myPlugin {
    constructor(options) {
        this.options = options // The parameter passed by the user
    }
    apply(compiler) {
        // Listen for an event
        compiler.hooks.'Compiler event name'.tap('myPlugin'.(compilation) = > {
            // Write the corresponding logic
            compilation.hooks.'Compilation Event Name'.tap('myPlugin'.() = > {
                // Write the corresponding logic}}})})module.exports = myPlugin
Copy the code

The events listened for above are synchronized hooks registered with tap. When listening for asynchronous hooks, we need to register with tapAsync and tapPromise. We also need to pass in a cb callback function. After the plugin is finished, we must call this cb callback function as follows:

class myPlugin {
    constructor(options) {
        this.options = options // The parameter passed by the user
    }
    apply(compiler) {
        // Listen for an event
        compiler.hooks.emit.tapAsync('myPlugin'.(compilation, cb) = > {
            setTimeout(() = > {
                // Write the corresponding logic
                cb()
            }, 1000)}}}module.exports = myPlugin
Copy the code

Get & validate parameters

We know that we can get arguments passed to the plug-in from the hook function of the plug-in. When writing a plug-in, we need to verify that the parameters passed in are valid. Similar to Loader, to verify the validity of parameters, download:

npm install schema-utils -D
Copy the code

Debugging tips:

"scripts": {
    "start": "node --inspect-brk ./node_modules/webpack/bin/webpack.js"
},
Copy the code

The WebPack plug-in is essentially a publish-subscribe model, listening for events on compiler. It then packages the events that trigger listening during compilation to add some logic to affect the packaging results.

In actual combat

Next, let’s put the Plugin into practice by writing it by hand

Requirement: Remove comments from js files before packaging

class MyPlugin {
      constructor(options) {
        console.log('Plug-in options:', options)
        this.userOptions = options || {}
      }

      // Must have the apply method
      apply(compiler) {
        compiler.hooks.emit.tap('Plug-in name'.(compilation) = > {
          // compilation Specifies the compilation context
          console.log('Webpack build process begins! ', compilation)
          for (const name in compilation.assets) {
            // if(name.endsWith('.js'))
            if (name.endsWith(this.userOptions.target)) {
              // Get the content before processing
              const content = compilation.assets[name]
              // Remove comments from the original content via regular expressions
              const noComments = content.replace(/\/\*[\s\S*?] \*\//g.' ')
              // Replace the processed result
              compilation.assets[name] = {
                source: () = > noComments,
                size: () = > noComments.length,
              }
            }
          }
        })
      }
    }

    module.exports = MyPlugin

    / / use
    const MyPlugin = require('./plugin/my-plugin')

    module.exports = {
      // Plug-in configuration
      plugins: [
        new MyPlugin({
          // Plug-in options
          target: '.js',})],}Copy the code

conclusion

From the above we have a general understanding of two important concepts in Webpack: Loader and Plugin. Front-end engineering is becoming more and more important in the field of front-end engineering, whether in daily work or interview to improve their technical skills. Mastering the use of Webpack and understanding how it works is of great benefit!