Webpack catalog

  1. The core configuration of webpack5
  2. Webpack5 modularity principle
  3. Webpackage 5 with Babel/ESlint/ browser compatibility
  4. Webpack5 performance optimization
  5. Webpack5 Loader and Plugin implementation
  6. Webpack5 core source code analysis

Basic implementation of Loader

We have mentioned a lot of loaders in the core configuration, such as style-loader, CSS-loader, vue-loader, babel-loader, etc. How to implement a custom Loader? Loader is essentially a JavaScript module exported as a function. The Loader Runner library calls this function and then passes in the results or resource files generated by the previous Loader.

Now we are developing a custom loader. Let’s create a new loaders directory and create a new yJ-loader.js

// loaders/yj-loader.js
module.exports = function(content, map, meta) {
  console.log(content)
  console.log(map)
  console.log(meta)
  return content
}
Copy the code

The function takes three arguments

  • content: The contents of the resource file
  • map: sourcemAP Related data
  • meta: Some metadata

The following aspects are discussed from the introduction path of loader, execution sequence, asynchronous Loader, obtaining parameters and realizing a Loader.

The introduction of the path

Now we configure the custom loader in Webpack

{
  test: /\.js$/,
  use: [
    './loaders/yj-loader']},Copy the code

As you can see, the custom loader path we introduced is relative and based on the context property, but if we still want to load our own Loader file directly, we can configure the resolveLoader property

{
  resolveLoader: {
    modules: [
      'node_modules'.'./loaders']}}Copy the code

The default is node_modules. If node_modules does not exist, go to our loaders directory. We can add our loaders directory to the module property

{
  test: /\.js$/,
  use: [
    'yj-loader']},Copy the code

Execution order

Create three custom Loaders to prove this result. Create yJ-loader01. js, yJ-loader02.js, yJ-loader03.js. And print in each loader

// yj-loader01.js
module.exports = function(content, map, meta) {
  console.log('loader01 execution')
  return content
}

// yj-loader02.js
module.exports = function(content, map, meta) {
  console.log('loader02 execution')
  return content
}

// yj-loader03.js
module.exports = function(content, map, meta) {
  console.log('loader03 execution')
  return content
}
Copy the code
// webpack
{
  test: /\.js$/,
  use: [
    'yj-loader01'.'yj-loader02'.'yj-loader03']},Copy the code

Now let’s pack up the NPM Run build

Loader03 is executed first, loader02 is executed second, and loader01 is executed last. In fact, we can also configure a pitch loader in loader, let’s modify the loader

// yj-loader01.js
module.exports = function(content, map, meta) {
  console.log('loader01 execution')
  return content
}
module.exports.pitch = function() {
  console.log('the pitch - loader01 perform')}// yj-loader02.js
module.exports = function(content, map, meta) {
  console.log('loader02 execution')
  return content
}
module.exports.pitch = function() {
  console.log('the pitch - loader02 perform')}// yj-loader03.js
module.exports = function(content, map, meta) {
  console.log('loader03 execution')
  return content
}
module.exports.pitch = function() {
  console.log('the pitch - loader03 perform')}Copy the code

Then repackage

Loader-runner: lib/LoaderRunner. Js: lib/LoaderRunner. Js: LoaderRunner: lib/LoaderRunner.

The iteratePitchingLoaders function is executed first, that is, pitch-loader is executed first.

And in iteratePitchingLoaders loaderContext loaderIndex++, and recursive implementation iteratePitchingLoaders, iterateNormalLoaders after execution of the implementation, That is, a normal Loader.

Looking down can see loaderContext. LoaderIndex — –, and perform iterateNormalLoaders. Therefore, loaderIndex is used to execute loaderIndex

Conclusion:

  • RunLoader executes PitchLoader first and loaderIndex++ during PitchLoader execution
  • NormalLoader is then executed by runLoader, and loaderIndex– is executed when NormalLoader is executed

Can we customize the execution order? Yes, we need to split it into multiple Rule objects and use Enforce to change their order

Enforce has four methods:

  • By default, all loaders arenormal
  • The loader set in the line isinline
  • You can set this by Using Enforcepreandpost
  1. PitchingStage: Pitch method on loader, according toAfter (POST), Inline (Inline), Normal (normal), Front (pre)Sequential call of
  2. NormalStage: General method on loader, as perFront (Pre), Normal (normal), Inline (Inline), Post (POST)Is called sequentially. Conversion of module source code occurs in this phase.

Now we will set pre for loader02

{
  test: /\.js$/,
  use: [
    'yj-loader01'],}, {test: /\.js$/,
  use: [
    'yj-loader02',].enforce: 'pre'
},
{
  test: /\.js$/,
  use: [
    'yj-loader03']},Copy the code

Now you can see that loader02 is executed first and pitch loader02 is executed last.

Asynchronous loader

All loaders created by default are synchronous loaders. This Loader must return the result through return or this.callback and hand it to the next Loader for processing. Usually we use this.callback in case of errors

This. Callback can be used as follows:

  • The first argument must be Error or NULL
  • The second argument is a string or Buffer
// yj-loader.js
module.exports = function(content, map, meta) {
  console.log('execution loader')
  return this.callback(null, content)
}
Copy the code

Now it is ok to use this. Callback method to return the result of Loader processing. Sometimes we use Loader to perform some asynchronous operation, and we hope to return the result of Loader processing after the asynchronous operation. Loader-runner has given us the implementation of this. Async function, which we use as follows

// yj-loader03.js
module.exports = function(content, map, meta) {
  console.log('execution loader03')
  const callback = this.async()
  setTimeout(() = > {
    callback(null, content)
  }, 3000)}Copy the code

It can still be printed in sequence, and in the packaging process, it can be seen that loader03 is printed about 3S later than loader02 and loader01.

To obtain parameters

We used csS-loader or babel-loader to configure the parameters, so how can we also configure the parameters and get them? Loader-utils, a resolver library provided by WebPack, has a getOptions method to help us get the configuration, and the library automatically installs webPack when we install it. Modify our loader and add parameters to loader

// webpack
{
  test: /\.js$/,
  use: [
    {
      loader: 'yj-loader03'.options: {
        name: 'lyj'.age: 18}}},Copy the code
// yj-loader-03.js
const { getOptions } = require('loader-utils')

module.exports = function(content, map, meta) {
  console.log('loader03 execution')
  
  // Get parameters
  const options = getOptions(this)
  console.log(options)
  
  // Get the asynchronous Loader
  const callback = this.async()
  
  setTimeout(() = > {
    callback(null, content)
  }, 3000)}Copy the code

As you can see, we got the parameters by calling getOptions(this), so how do we verify the parameters passed in? We can use an official webPack validation library schema-utils, which has the validate method to validate parameters, and this library installs WebPack for us when we install it.

Now we need a validation rule file and create a loader-schema.json

// loader-schema.json
{
  "type": "object".// Pass in the type
  "properties": {    / / property
    "Name": {
      "type": "string"."description": "Please enter your name"
    },
    "age": {
      "type": "number"."description": "Please enter your age"}},"additionalProperties": true  // indicates that additional attributes can be added in addition to the above attributes
}
Copy the code
// yj-loader-03.js
const { getOptions } = require('loader-utils')
const { validate } = require('schema-utils') // Used to verify loader parameters
const loaderSchema = require('./loader-schema.json')

module.exports = function(content, map, meta) {
  console.log('loader03 execution')
  
  // Get parameters
  const options = getOptions(this)
  console.log(options)
  
  // Check parameters
  validate(loaderSchema, options)
  
  // Get the asynchronous Loader
  const callback = this.async()
  
  setTimeout(() = > {
    callback(null, content)
  }, 3000)}Copy the code

Now we pass in the age string and repackage it

Schema-utils helped us validate the parameters and prompt the description, and blocked the build, indicating that the validation was successful.

Implementing a Loader

Now let’s implement a simple Markdown loader that installs marked, highlight.js. Go straight to code

// mkdown-loader.js
const marked = require('marked')
const hljs = require('highlight.js')

module.exports = function(content) {
  // Set code highlighting
  marked.setOptions({
    highlight: function(code, lang) {
      return hljs.highlight(lang, code).value
    }
  }) 
  
  / / to HTML
  const htmlContent = marked(content)
  
  // Switch to modular export
  const innerContent = '` + htmlContent +'`
  const moduleCode = `var code = ${innerContent}; export default code; `
  console.log(moduleCode)
  return moduleCode
}
Copy the code
// WebPack Loader configuration
{
  test: /\.md$/,
  use: 'mkdown-loader'
}
Copy the code
// test.md
# loader realize

## Import path

## Execution order

Asynchronous loader # #
``` module.exports = function(content, map, Meta) {console.log(' loader03') const callback = this.async() setTimeout(() => {callback(null, content)}, 3000)} ' '## parameter fetchCopy the code
// main.js
import mdContent from './test.md'
import 'highlight.js/styles/default.css'
document.body.innerHTML = mdContent
Copy the code

After repackaging, we can see our mkDown compilation on the page

The underlying implementation of Plugins

Webpack has two very important classes, Compiler and Compilation, which listen to the entire Webpack process by injecting plugins that need hooks managed by an officially maintained Tapable library. So we need to figure out how to use this library first.

Tapable

The Tapable export contains the following hooks

  • SyncHook
  • SyncBailHook
  • SyncWaterfallHook
  • SyncLoopHook
  • AsyncParallelHook
  • AsyncParallelBailHook
  • AsyncSeriesHook
  • AsyncSeriesBailHook
  • AsyncSeriesLoopHook
  • AsyncSeriesWaterfallHook

We can classify Tapable hooks as synchronous and asynchronous,

  • A Hook that begins with sync is a synchronized Hook
  • Two events that begin with async handle callbacks and do not wait for the last processing callback to finish before executing the next one

We can also classify them in other categories

  • bail: When there is a return value, no subsequent event firing is performed
  • Loop: The event is executed repeatedly when the return value is true, and exits when the return value is undefined or nothing is returned
  • Waterfall: If the return value is not undefined, the returned result will be used as the first parameter of the next event
  • Parallel: parallel: the second event processing callback is executed at the same time, and then the next event processing callback is executed
  • Series: serial, will wait for the last is asynchronous Hook

Let’s use Tapable briefly

1. Write a Tapable test file

// tapable-test.js
const { SyncWaterfallHook } = require('tapable')

class MyTapable {
  constructor() {
    this.hooks = {
      syncWaterfallHook: new SyncWaterfallHook(['myName'.'myAge'])}this.on()
  }
  / / register
  on() {
    this.hooks.syncWaterfallHook.tap('myTap1'.(name, age) = > {
      console.log('myTap1', name, age)
      return '123'
    })
    this.hooks.syncWaterfallHook.tap('myTap2'.(name, age) = > {
      console.log('myTap2', name, age)
    })
  }
  / / initialization
  emit() {
    this.hooks.syncWaterfallHook.call('lyj'.18)}}const tapable = new MyTapable()
tapable.emit()
Copy the code

2. Perform tapable – test. Js

node tapable-test.js
Copy the code

3. Print the results

You can see that the first registered hook returns 123 to the first argument of the second hook

Plugin Registration Principle

How to register plug-ins in Webpack, we can view the source code

  1. In the createCompiler method that calls the WebPack function, register all plug-ins
  2. When a plug-in is registered, the plug-in function or the Apply method of the plug-in object is called
  3. The plug-in methods receive compiler objects, which we can use to register Hook events
  4. Some plugins also pass in a compilation object, and we can listen for Hook events from a compilation

Implement a Plugin

We implement a plug-in AutoUploadPlugin that packages the build directory and automatically uploads it to the server

const { NodeSSH } = require('node-ssh');

class AutoUploadPlugin {
  constructor(options) {
    this.ssh = new NodeSSH();
    this.options = options;
  }

  apply(compiler) {
    compiler.hooks.afterEmit.tapAsync("AutoUploadPlugin".async (compilation, callback) => {

      // 1. Get the output folder
      const outputPath = compilation.outputOptions.path;

      // 2. Connect to the server (SSH connection)
      await this.connectServer();

      // 3. Delete the contents of the original directory
      const serverDir = this.options.remotePath;
      await this.ssh.execCommand(`rm -rf ${serverDir}/ * `);

      // 4. Upload files to the server (SSH connection)
      await this.uploadFiles(outputPath, serverDir);

      // 5. Disable SSH
      this.ssh.dispose();
      callback();
    });
  }

  async connectServer() {
    await this.ssh.connect({
      host: this.options.host,
      username: this.options.username,
      password: this.options.password
    });

    console.log("Connection successful ~");
  }

  async uploadFiles(localPath, remotePath) {
    const status = await this.ssh.putDirectory(localPath, remotePath, {
      recursive: true.concurrency: 10
    });
    console.log('Send to server:', status ? "Success": "Failure"); }}module.exports = AutoUploadPlugin;
Copy the code

Using the plug-in

// webpack
{
  plugins: [
    / /...
    new AutoUploadPlugin({
      host: 'xxx.xxx.xxx.xxx'.username: 'xxx'.password: 'xxx'}})]Copy the code