preface

This article focuses on laoder and plugins in Webpack. It does not cover how to use and configure Webpack, because these foundations are already clear in official documents. The focus is on how to implement custom Laoder and plugins. So before we get started, let’s take a quick look at what a build tool is.

Build tools

In web applications, in addition to HTML files, often need to use a lot of other static resources to decorate, such as images, CSS styles, JS files used in HTML, but the browser does not recognize all file resources, and correctly load.

Therefore, developers need to process different file resources in order to correctly load and use the corresponding file resources. Such as:

  • Images in addition to some common formats can be loaded and displayed normally, some special formats can not be directly used;
  • CSS styles may be used by less/SCSS/CSS in JS etc.
  • Js files may use relatively new syntax, such as ES6, ES7 or newer features, need corresponding compilation tools to do the conversion, etc.

Build tools are born out of the need to do different processing for different file resources and the maintenance problems of tools that deal with file resources.

The build tool contains solutions to most of the problems mentioned above, meaning that we originally needed different widgets to handle different file contents, but now we just need to focus on how the build tool itself is used.

webpack

What is Webpack?

Webpack, one of many build tools, is also a static module packaging tool for modern JavaScript applications.

When WebPack processes an application, it internally builds a dependency graph from one or more entry points, and then combines each module needed in the project into one or more bundles, which are static resources that are used to present your content.

The concepts of chunk and bundles can be understood as follows:

  • According to the imported file resources, a dependency map is formed, which contains the chunk of code to be processed
  • The corresponding processing of the code chunk is also called packaging. After the output, the required bundles are obtained

The five core

mode

  • Optional values: Development, production, None
  • Setting the mode parameter enables default optimizations built into WebPack for the appropriate environment
  • The mode parameter defaults to production

entry

The entry point indicates which file webPack should use as the entry module to build its internal dependency diagram, and can have multiple entry files.

output

Output is responsible for telling WebPack where to export the bundles it creates and how to name those files.

  • Default output directory:./dist
  • The default main output file name is./dist/main.js
  • Other generated files are placed by default./dist

loader

Webpack can only understand JavaScript and JSON files, and unboxed Webapck can’t recognize other file types. Loader can then convert these file types to resources that weback can recognize and convert them to valid modules that can be used in the application and added to the dependency graph.

plugin

Loader is used to transform certain types of modules, while plugin can be used to perform a wider range of tasks, including Loader. For example: package optimization, resource management, injection of environment variables, etc.

  • You can import the corresponding plugin via require and instantiate the new PluginName(opt) call in an array of options to configure plugins.
  • You can customize the WebPack plug-in to fulfill the requirements of a specific scenario

loader

What is a loader in Webpack?

A loader is essentially a function that takes three arguments:

  • content: Indicates the content of the corresponding module
  • map: Sourcemap of the corresponding module
  • meta: Metadata of the corresponding module

Loader execution sequence

In general, the loader’s writing structure determines the order of execution:

  • Left-right structure — > Execution order is from right to left
  • Top up structure — > Execute from bottom up

For clarity and intuition, here is a list of common styles related configurations in webPack configurations:

module: {
    rules: [{test: /\.css$/.// Left/right structure
        use: ['style-loader'.'css-loader']./ / or
        
        // upper and lower structure
        use: [
          'style-loader'.'css-loader']]}}Copy the code

Whether the left and right structure or the upper and lower structure, can be uniformly understood as the order from back to front to execute.

Custom loader

  • Create loader1.js and loader2.js as custom loaders. Note that in addition to the exposed function methods, a pitch method is added to this function object.

The pitch method is executed in reverse order to loader, that is, the pitch method is executed from front to back.

// loader1.js
module.exports = function(content, map, meta) {
  console.log('loader1 ... ');
  return content;
}
module.exports.pitch = function (){
  console.log('loader1 pitch... ');
}

// loader2.js
module.exports = function(content, map, meta) {
  console.log('loader2 ... ');
  return content;
}
module.exports.pitch = function (){
  console.log('loader2 pitch... ');
}
Copy the code
  • And configure it in webpack.config.js as follows:
// webpack.config.js
const { resolve } = require('path');
module.exports = {
  mode: 'production'.module: {
    rules: [{test: /\.js$/,
        use: [
          resolve(__dirname, 'loaders/loader1.js'),
          resolve(__dirname, 'loaders/loader2.js'),]},]}}Copy the code
  • To simplify every introduction of customizationloader, write the full path, such as:resolve(__dirname, 'loaders/xxx.js), so it can be configuredresolveLoaderUniform option designationloaderThe path to be searched is as follows:
// webpack.config.js
const { resolve } = require('path');
module.exports = {
  mode: 'production'.module: {
    rules: [{test: /\.js$/,
        use: [
          'loader1'.'loader2',]},]},resolveLoader: {
    modules: [
      resolve(__dirname, 'loaders'),
      'node_modules'].}}Copy the code
  • When the webpack directive is entered in the editor terminal for packaging, the console output is as follows:

Loader synchronization and asynchrony

Synchronous loader

Callback (); callback(); callback(); callback();

This.callback (error, content, map, meta), where error indicates the error content, if there is no error, it can be executed as null. In this way, there is no need to explicitly return.

// loader1.js
module.exports = function(content, map, meta) {
  console.log('loader1 ... ');
  this.callback(null, content, map, meta);
}
module.exports.pitch = function (){
  console.log('loader1 pitch... ');
}

// loader2.js
module.exports = function(content, map, meta) {
  console.log('loader1 ... ');
  this.callback(null, content, map, meta);
}
module.exports.pitch = function (){
  console.log('loader1 pitch... ');
}
Copy the code

Asynchronous loader

CallBack = this.async(); Method, and then the asynchronous execution is complete by calling the callBack() method.

Loader2.js can be changed into an asynchronous loader. The modification content and running result are as follows:

// loader2.js
module.exports = function(content, map, meta) {
  console.log('loader2 ... ');
  const callback = this.async();
  setTimeout(() = >{
    callback(null,content, map, meta);
  },1000);
}
module.exports.pitch = function (){
  console.log('loader2 pitch... ');
}
Copy the code

PS: After the command is executed to loader2, the system waits about 1s and then executes loader1. At the same time, there is significantly more successfully compiled time than before.

Verify the validity of options in the Loader

Why is it necessary to verify validity?

The Options configuration is provided to make the custom loader more flexible and configurable, but if such flexibility is not constrained, the Options configuration may become meaningless. Imagine that external use passes a bunch of configurations that loader doesn’t need, making the configuration look more complex and rendering the loader’s internal judgment logic useless. For all the above reasons, it is important to verify the validity of Options. Only after the validation is passed, execute other processors in loader.

Obtain the Options configuration in the Loader

To validate options, obtain options first. There are two ways to obtain options:

  • throughconst options = this.getOptions()The method of obtaining
  • By calling theloader-utilsIn the librarygetOptions(this)Methods to obtain

Verification validity

This can be verified using the validate() method in the schema-utils library.

Intuitive understanding, through an example. First of all in webpack config. Modify configuration in js, namely to loader1 incoming configuration options, and then to loader1. Js rewrite the content, as follows:

// webpack.config.js
  module: {
    rules: [{test: /\.js$/,
        use: [
          {
            loader: 'loader1'.options: {
              name: 'this is a name! '}},'loader2',]},]}// loader1.js
const { validate } = require('schema-utils');

// schema indicates the schema that defines the verification rules
const loader1_schema = {
  type: "object".properties: {
    name: {
      type: 'string',}},// additionalProperties indicates whether attributes can be appended
  additionalProperties: true
};

module.exports = function (content, map, meta) {
  console.log('loader1 ... ');

  / / get the options
  const options = this.getOptions();
  console.log('loader1 options = ',options);

  // Verify that options are valid
  validate(loader1_schema, options,{
    name: 'loader1'.baseDataPath: 'options'});this.callback(null, content, map, meta);
}

module.exports.pitch = function () {
  console.log('loader1 pitch... ');
}
Copy the code

Legal configuration in webpack.config.js:

         {
            loader: 'loader1'.options: {
              name: 'this is a name! '}}Copy the code

Illegal configuration in webpack.config.js:

         {
            loader: 'loader1'.options: {
              name: false}}Copy the code

Implement custom loader — vueLoader

Functional description

For.vue file

webapck.config.js

const { resolve } = require('path');

module.exports = {
  mode: 'production'.module: {
    rules: [{test: /\.vue$/,
        use: {
          loader: 'vueLoader'.options: {
            template: {
              path: resolve(__dirname, 'src/index.html'),
              fileName: 'app',},name: 'app'.title: 'Home Page'.reset: true}}},]},resolveLoader: {
    modules: [
      resolve(__dirname, 'loaders'),
      'node_modules'].}}Copy the code

vueLoader.js

const { validate } = require('schema-utils');
const fs = require('fs');
const { resolve } = require('path');

const vueLoader_schema = {
  type: "object".properties: {
    template: {
      type: 'object'.properties: {
        path: { type: 'string' },
        fileName: { type: 'string'}},additionalProperties: false
    },
    name: {
      type: 'string',},title: {
      type: 'string',},reset: {
      type: 'boolean',}},additionalProperties: false
};

module.exports = function (content, map, meta) {
  const options = this.getOptions();

  const regExp = {
    template: /<template>([\s\S]+)<\/template>/,
    script: /<script>([\s\S]+)<\/script>/,
    style: /<style.+>([\s\S]+)<\/style>/}; validate(vueLoader_schema, options, {name: 'vueLoader'.baseDataPath: 'options'});let template = ' ';
  let script = ' ';
  let style = ' ';

  if (content.match(regExp.template)) {
    template = RegExp. $1; }if (content.match(regExp.script)) {
    let match = RegExp. $1;let name = match.match(/name:(.+),? /) [1].replace(/("|')+/g.' ');
    script = match.replace(/export default/.`const ${name} = `);
  }
  if (content.match(regExp.style)) {
    style = RegExp. $1; }let { path, fileName } = options.template;
  fileName = fileName || path.substring(path.lastIndexOf('\ \') + 1, path.lastIndexOf('.html'));
  
  fs.readFile(path, 'utf8'.function (error, data) {
    if (error) {
      console.log(error);
      return false;
    }

    const innerRegExp = {
      headEnd: /<\/head>/,
      bodyEnd: /<\/body>/}; content = data .replace(innerRegExp.headEnd,(match, p1, index, origin) = > {
        let resetCss = "";
        if (options.reset) {
          resetCss = fs.readFileSync(resolve(__dirname, 'css/reset.css'), 'utf-8')}let rs = `<style>${resetCss} ${style}</style></head>`;
        return rs;
      })
      .replace(innerRegExp.bodyEnd, (match, p1, index, origin) = > {
        let rs = `${template}<script>${script}</script></body>`;
        return rs;
      });

    if (options.title) {
      content = content.replace(/<title>([\s\S]+)<\/title>/.() = > {
        return `<title>${options.title}</title>`
      });
    }

    fs.writeFile(`dist/${fileName}.html`, content, 'utf8'.function (error) {
      if (error) {
        console.log(error);
        return false;
      }

      console.log('Write successfully!!! ');
    });
  });

  return "";
}
Copy the code

plugins

What is a plugin in Webpack?

The plugin in Webpack consists of the following:

  • A JavaScript named function or JavaScript class
  • In the plug-in functionprototypeDefine aapply()methods
  • Specifies an event hook bound to the named function itself
  • Handles specific data for webPack internal instances
  • The callback provided by WebPack is invoked when the functionality is complete

Here is the basic structure of a plugin:

The tap() method in Apply is used to bind synchronous operations, but some plugins need to be asynchronous, and two asynchronous methods, tapAsync() or tapPromise(), can be used to bind. When using tapAsync, the callback argument has an additional callback to indicate whether asynchronous processing has ended. When the tapPromise approach was used, a Promise object was returned within it, indicating the outcome of the asynchronous processing by changing the Promise state.

class TestWebpackPlugin {
  apply(compiler) {
    compiler.hooks.emit.tap('TestWebpackPlugin'.(compilation) = > {
      console.log('tap callBack ... ');
      
      // Return true to output output, false otherwise
      return true;
    });

    compiler.hooks.emit.tapAsync('TestWebpackPlugin'.(compilation, callback) = > {
      setTimeout(() = > {
        console.log('tapAsync callBack ... ');
        callback();
      }, 2000);
    });

    compiler.hooks.emit.tapPromise('TestWebpackPlugin'.(compilation) = > {
      return new Promise((resolve, reject) = > {
        setTimeout(() = > {
          console.log('tapPromise callBack ... ');
          resolve();
        }, 1000); }); }); }}module.exports = TestWebpackPlugin;
// Output sequence:
// 1. tap callBack ...
// 2. tapAsync callBack ... (Wait for the previous tap to complete and output 2s later)
// 3. tapPromise callBack ... (Wait for the previous tapAsync execution to finish, output 1s later)
Copy the code

Order of execution in plugin

From the above example, you can see that the order of execution is:

  • Refer to the lifecycle hook functions for the timing of execution of the different hooks, which determine the order of execution
  • Callbacks registered in the same hooks in the same plugin are executed in serial order, even if asynchronous operations are involved

Verify the validity of options in plugin

The validate() method in schema-utils is used to validate this, as is the case with the loader. Unlike loader, plugin options do not need to be retrieved by this.getoptions () because plugin is a class or constructor. It can therefore be obtained directly from constructor.

Implement a custom Plugin — CopyWebpackPlugin

Functional description

Copies all files in the specified directory to the destination directory. Some files can be ignored.

webpack.config.js

const CopyWebpackPlugin = require('./plugins/CopyWebpackPlugin');
module.exports = {
  mode:'none'.plugins: [
    new CopyWebpackPlugin({
      from: './public'.to: 'dist'.ignores: ['notCopy.txt']]}});Copy the code

CopyWebpackPlugin.js

const { validate } = require('schema-utils');
const { join, resolve, isAbsolute, basename } = require('path');
const { promisify } = require('util');
const fs = require('fs');
const webapck = require('webpack');

const { RawSource } = webapck.sources;
const readdir = promisify(fs.readdir);
const readFile = promisify(fs.readFile);

const schema = {
  type: 'object'.properties: {
    from: {
      type: 'string',},to: {
      type: 'string',},ignores: {
      type: 'array',}},additionalProperties: false,}class CopyWebpackPlugin {
  constructor(options = {}) {
    this.options = options;
    // Verify that options are valid
    validate(schema, options);
  }

  apply(compiler) {
    compiler.hooks.emit.tapAsync('CopyWebpackPlugin'.async (compilation, callback) => {
      let { from, to = '. ', ignores = [] } = this.options;
      // The directory to run the instruction
      let dir = process.cwd() || compilation.options.context;
      // Check whether the path passed is an absolute path
      from = isAbsolute(from)?from : resolve(dir, from);
      to = isAbsolute(to) ? to : resolve(dir, to);

      // 1. Obtain all file or folder names in the form directory
      let dirFiles = await readdir(from.'utf-8');

      // 2. Filter file or folder names by ignores
      dirFiles = dirFiles.filter(name= >! ignores.includes(name));// 3. Read all files in the form directory
      const files = await Promise.all(dirFiles.map(async (name) => {
        const fullPath = join(from, name);
        const data = await readFile(fullPath);
        const filename = basename(fullPath);

        return {
          data,// File content data
          filename,/ / file name
        };
      }));

      // 4. Generate resources in Webpack format
      const assets = files.map(file= > {
        const source = new RawSource(file.data);
        
        return {
          source,
          filename: file.filename,
        };
      });

      // 5. Add it to compilation to output
      assets.forEach((asset) = > {
        compilation.emitAsset(asset.filename, asset.source);
      });

      // 6. Use callback to indicate that the current processing is completecallback(); }); }}module.exports = CopyWebpackPlugin;
Copy the code