Build the column series catalog entry

Jiao Chuankai, Platform support group, Weimi Front End Technology Department. Learning should also be breathing and breathing.

A, the Loader

1.1 What does loader do?

Webpack can only understand JavaScript and JSON files, which is a built-in capability available out of the box. ** Loader ** enables WebPack to process other types of files and convert them into valid modules for use by applications and to be added to the dependency graph.

That is, WebPack treats any file as a module, and loader can import any type of module, but WebPack does not natively support parsing CSS files, for example, so we need to use our Loader mechanism. Our Loader uses two properties to enable linkage recognition by our WebPack:

  1. The test attribute identifies which files will be converted.
  2. The use attribute defines which loader should be used for the conversion.

So what do you need to do to customize a loader?

1.2 Development Criteria

As the saying goes, no standards can be achieved without rules. When compiling our Loader, the official also gave us a set of Guidelines, which should be followed to standardize our loader:

  • Easy to use.
  • Use chain transfer. (Since Loaders can be called in chains, ensure that each loader has a single responsibility)
  • Modular output.
  • Ensure stateless. (Do not allow the previous state to be preserved in the loader’s conversion. Each run should be independent of other compiled modules and previous compiled results of the same module.)
  • Make full use of the official Loader utilities.
  • Record loader dependencies.
  • Resolve module dependencies.

Depending on the module type, there may be different patterns to specify dependencies. For example, in CSS, use @import and url(…) Statement to declare a dependency. These dependencies should be resolved by the module system.

This can be done in one of two ways:

  • By converting them to require statements.
  • Resolve the path using the this.resolve function.
  • Extract the generic code.
  • Avoid absolute paths.
  • Use peer Dependencies. If your loader simply wraps another package, you should introduce this package as a peerDependency.

1.3 get started

A Loader is a nodeJS module that exports a function that takes only one argument, a string containing the contents of the resource file, and the return value of the function is the processed content. In other words, the simplest loader looks like this:

module.exports = function (content) {
	// Content is the string of source content passed in
  return content
}
Copy the code

When a Loader is used, it can only accept one input parameter, which is a string containing the contents of the resource file. Yes, so far, a simple loader has been completed! Now let’s see how to add rich features to it.

1.4 4 kinds of loader

We can basically divide common loaders into four types:

  1. Synchronous loader
  2. Asynchronous loader
  3. “Raw” Loader
  4. Pitching loader

① Synchronous and asynchronous Loaders

We can return the result of our processing by returning the result directly as described above:

module.exports = function (content) {
	// Do something with the content
  const res = dosth(content)
  return res
}
Copy the code

You can also use the this.callback() API directly and then tell WebPack to look for the result by returning undefined at the end. The API accepts these parameters:

This callback (err: Error | null, / / a can't normal compile-time Error or directly to a null content: String | Buffer, / / we deal with the content of the return Can be a string or Buffer sourceMap ()? : SourceMap, // Can optionally be a properly parsed source map meta? : any // Can be anything, such as a common AST syntax tree);Copy the code

Here’s an example:Note here[this.getOptions()](https://webpack.docschina.org/api/loaders/#thisgetoptionsschema)Can be used to get the configuration parameters

Starting with WebPack 5, this.getoptions can be retrieved from the Loader context object. It is used instead of the getOptions method from Loader-utils.

module.exports = function (content) {
  // Get the parameter passed by the user to the current loader
  const options = this.getOptions()
  const res = someSyncOperation(content, options)
  this.callback(null, res, sourceMaps);
  }}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
  return
}
Copy the code

Then a synchronized loader is complete!

Let’s talk about asynchrony: The difference between synchronous and asynchronous is easy to understand. Generally, our transformation process is synchronous, but when we encounter situations such as network requests, we will use asynchronous build to avoid blocking the build step. This.async () is used to tell WebPack that the build operation is asynchronous.

module.exports = function (content) {
  var callback = this.async()
  someAsyncOperation(content, function (err, result) {
    if (err) return callback(err)
    callback(null, result, sourceMaps, meta)
  })
}
Copy the code

(2) the “Raw” loader

By default, resource files are converted to UTF-8 strings and passed to the Loader. By setting RAW to true, the loader can receive the original Buffer. Each loader can pass its results in the form of a String or a Buffer. Complier will convert them between Loaders. The familiar file-loader uses this. In short: you add module.exports.raw = true; It’s passed to you as a Buffer, and it doesn’t have to be a Buffer to handle the return type. Webpack has no restrictions.

module.exports = function (content) {
  console.log(content instanceof Buffer); // true
  return doSomeOperation(content)
}
// Draw key ↓
module.exports.raw = true;
Copy the code

(3) Pitching loader

We can have a pitch method for each loader. We know that loaders are called from right to left, but there is actually a procedure that executes the pitch method for each loader from left to right. The pitch method has three parameters:

  1. remainingRequest: of the loader and resource files that follow the loader in the loader chainAn absolute pathIn order to!A string formed as a hyphen.
  2. precedingRequest: of the loader that ranks ahead of its own in the loader chainAn absolute pathIn order to!A string formed as a hyphen.
  3. Data: a fixed field stored in the context of each Loader that can be used by the pitch to pass data to the Loader.

The data passed to data in the pitch can be obtained in this.data during the subsequent execution of the call:

module.exports = function (content) {
  return someSyncOperation(content, this.data.value);This.data. value === 42
};

module.exports.pitch = function (remainingRequest, precedingRequest, data) {
  data.value = 42;
};
Copy the code

Attention!If a loader returns a value in its pitch method, then it will directlyWalk back“And skip the next steps. Here’s an example:Suppose we now look like this:use: ['a-loader', 'b-loader', 'c-loader'],So the normal call order is like this:Now the PITCH of b-loader has been changed to have a return value:

// b-loader.js
module.exports = function (content) {
  return someSyncOperation(content);
};

module.exports.pitch = function (remainingRequest, precedingRequest, data) {
  return "Hey, I'm going straight back, just to play."
};
Copy the code

So now the call will look like this, going straight back, skipping the other three steps:

1.5 other apis

  • This. addDependency: Join a file to listen on, and call the loader again if the file changes
  • This.cacheable: By default, loader results will be cached. Passing false to this method will disable this effect
  • This. clearDependencies: Clears all loader dependencies
  • This.context: the directory in which the file is located (without filename)
  • This.data: An object shared between the pitch phase and the normal call phase
  • This.getoptions (schema) : Used to get the configured loader parameter options
  • This.resolve: Resolve a request like the require expression.resolve(context: string, request: string, callback: function(err, result: string))
  • This. loaders: an array of loaders. It can be written in the pitch phase.
  • This. resource: Gets the current request path, including parameters:'/abc/resource.js? rrr'
  • This. resourcePath: path that does not contain parameters:'/abc/resource.js'
  • This. sourceMap: bool indicates whether a sourceMap should be generated

The official also provides a lot of utility Api, this value enumerates some may be commonly used, more can poke the link 👇 for more details of the official link

1.6 A simple practice

Function implementation

Next, let’s simply make two loaders. We can add /** company @ year */ to the compiled code and simply remove console.log from the code, and we can call them in chain:

company-loader.js

module.exports = function (source) {
  const options = this.getOptions() // Get the option passed from the WebPack configuration
  this.callback(null, addSign(source, options.sign))
  return
}

function addSign(content, sign) {
  return ` / * *${sign} */\n${content}`
}
Copy the code

console-loader.js

module.exports = function (content) {
  return handleConsole(content)
}

function handleConsole(content) {
  return content.replace(/console.log\(['|"](.*?) \ [' | '] /.' ')}Copy the code

Call test mode

Here we mainly talk about how to test call our local Loader, there are two ways, one is through the Npm link way to test, the specific use of this way will not be detailed, you can simply look up. The other way is to configure the path directly in the project. There are two cases:

  1. To match (test) a single loader, you can simply set path.resolve to point to this local file in the rule object

webpack.config.js

{
  test: /\.js$/
  use: [
    {
      loader: path.resolve('path/to/loader.js'),
      options: {/ *... * /}}}]Copy the code
  1. To match multiple loaders, you can use the resolveloader.modules configuration, and WebPack will search for those loaders from those directories. For example, if your project has a /loaders local directory:

webpack.config.js

resolveLoader: {
  // If not, go to node_modules and loaders
  modules: [
    'node_modules',
    path.resolve(__dirname, 'loaders')]}Copy the code

Configured to use

Our WebPack configuration looks like this:

module: {
    rules: [{test: /\.js$/,
        use: [
          'console-loader',
          {
            loader: 'company-loader'.options: {
              sign: 'we-doctor@2021',},},],},Copy the code

Index.js in the project:

function fn() {
  console.log("this is a message")
  return "1234"
}
Copy the code

Execute the compiled bundle.js: As you can see, the functionality of both loaders is reflected in the compiled file.

/ * * * * * * / (() = > { // webpackBootstrap
var __webpack_exports__ = {};
/ *! * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./src/index.js ***! \ * * * * * * * * * * * * * * * * * * * * * * /
/** we-doctor@2021 */
function fn() {
  
  return "1234"
}
/ * * * * * * / })()
;
Copy the code

Second, the Plugin

Why have a plugin

Plugins provide a lot more functionality than loaders do. They use staged build callbacks. Webpack gives us a lot of hooks to use during the build phase to give developers the freedom to introduce their own behavior.

The basic structure

A basic plugin should contain these parts:

  • A JavaScript class
  • aapplyMethod,applyMethod is called when WebPack loads the plug-in and is passed incompilerObject.
  • Use different hooks to specify the processing actions you want to take place
  • In the asynchronous call, we need to call what WebPack provides uscallbackOr byPromiseThe way (follow-upAsynchronous compilation sectionMore on that.)
class HelloPlugin{
  apply(compiler){
    compiler.hooks.<hookName>.tap(PluginName,(params) = >{
      /** do some thing */}}})module.exports = HelloPlugin
Copy the code

Compiler and Compilation

Compiler and Compilation are the most important part of the plug-in writing process. In the! The! Heavy! ** Because almost all of our operations will revolve around them.

The Compiler object is understood to be an object bound to the webPack environment as a whole. It contains all environment configurations, including options, Loaders, and plugins. When WebPack starts, the object is instantiated and is globally unique. This is what we passed in the apply method.

Compilation is created each time a resource is built. A Compilation object represents the current module resource, the compiled generated resource, the changed files, and the status information that is being tracked. It also provides a lot of hooks.

Compiler and Compilation provide a wide variety of hooks for us to use. The combination of these methods allows us to obtain different content at different times during the build process. For details, see the official website.

There are different hook types, such as SyncHook, SyncBailHook, AsyncParallelHook, and AsyncSeriesHook, which are provided by tapable. For detailed usage and analysis of tapable, see the tapable section in our front end build Tools series column.

The basic ways to use it are:

compiler/compilation.hooks.<hookName>.tap/tapAsync/tapPromise(pluginName,(xxx) = >{/**dosth*/})
Copy the code

Tip: Previously written as compiler.plugin, this may cause problems in the latest webpack@5, see webpack-4-migration-Notes

Synchronous and Asynchronous


.tap

.tap

.tap

.tap

.tap > Please use the tapAsync or tapPromise method to tell the Webpack that the content here is asynchronous, of course, you can use tap normally if there are no asynchronous operations inside.

tapAsync

When using tapAsync, you need to pass in an extra callback, and be sure to call the callback to inform WebPack that the asynchronous operation is over. 👇 such as:

class HelloPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync(HelloPlugin, (compilation, callback) = > {
      setTimeout(() = > {
        console.log('async')
        callback()
      }, 1000)}}}module.exports = HelloPlugin
Copy the code

tapPromise

When using tapPromise to deal with async, we need to return a Promise object and let it resolve 👇 at the end

class HelloPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapPromise(HelloPlugin, (compilation) = > {
      return new Promise((resolve) = > {
        setTimeout(() = > {
          console.log('async')
          resolve()
        }, 1000)})})module.exports = HelloPlugin
Copy the code

Do a practice

Next, we will make a plug-in to comb through the overall process and scattered function points. The function of this plug-in is to add an additional Markdown file to the output folder after packaging, which records the output of the packaging time point, file and file size.

First, we determine the hook we need according to the requirements. Since the output file is needed, we need to use the emitAsset method of Compilation. Secondly because of the need to deal with assets, so we use compilation. The hooks. ProcessAssets, because processAssets is responsible for the asset disposal of the hook.

So we have our plug-in structure 👇 outlogplugin.js

class OutLogPlugin {
  constructor(options) {
    this.outFileName = options.outFileName
  }
  apply(compiler) {
    // The webPack module instance can be accessed from the compiler object
    // And ensure that the webPack version is correct
    const { webpack } = compiler
    // Obtain the Stage provided by the Compilation
    const { Compilation } = webpack
    const { RawSource } = webpack.sources
    /** compiler.hooks.<hoonkName>.tap/tapAsync/tapPromise */
    compiler.hooks.compilation.tap('OutLogPlugin'.(compilation) = > {
      compilation.hooks.processAssets.tap(
        {
          name: 'OutLogPlugin'.// Select the appropriate stage. See:
          // https://webpack.js.org/api/compilation-hooks/#list-of-asset-processing-stages
          stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
        },
        (assets) = > {
          let resOutput = `buildTime: The ${new Date().toLocaleString()}\n\n`
          resOutput += `| fileName | fileSize |\n| --------- | --------- |\n`
          Object.entries(assets).forEach(([pathname, source]) = > {
            resOutput += ` |${pathname} | ${source.size()} bytes |\n`
          })
          compilation.emitAsset(
            `The ${this.outFileName}.md`.new RawSource(resOutput),
          )
        },
      )
    })
  }
}
module.exports = OutLogPlugin
Copy the code

Configure the plug-in: webpack.config.js

const OutLogPlugin = require('./plugins/OutLogPlugin')

module.exports = {
  plugins: [
    new OutLogPlugin({outFileName:"buildInfo"}})],Copy the code

The packaged directory structure:

Dist ├─ Buildinfo.md ├─ bundle.js ├─ bundle.jsCopy the code

buildInfo.md You can see that the content is printed in exactly the format we want, and a simple plug-in is complete!

Refer to the article

Writing a Loader | webpack Writing a Plugin | webpack management.but webpack webpack/webpack | dead simple

Complete code: Github