Project structures,

Create a new folder, my-WebPack, and use development tools to open the project directory.

  • Execute NPM init -y to generate package.json

  • Creating the bin directory

    The bin directory represents the executable file. Under bin, create the main program execution entry my-webpack.js

    #! /usr/bin/env node
    // The above code is used to declare the execution environment
    console.log("Study well, I'm sleepy.");
    Copy the code
  • Configure the bin field in backage.json

    {
      "bin": {
        // Declare the directive and the file to execute it
        "my-webpack": "./bin/my-webpack.js"}},Copy the code
  • Perform NPM link to link the current project into the global package

    To use the Webpack directive globally like webPack, we must link the package globally

  • My-webpack is executed from the command line and the program is successfully executed

Analysis of the Bundle

Create a new project to do a simple package, and analyze the packaged bundles

  • Build a Webpack project
    • newdemoDirectory, and indemoDirectory executionyarn init -y
    • The installationwebpack

      yarn add webpack webpack-cli -D
    • Creating a Service Modulesrc/index.js,src/moduleA.js,src/moduleB.js
      // src/index.js  
      const moduleA = require("./moduleA.js")
      console.log("Index.js, imported successfully" + moduleA.content);
      
      // src/moduleA.js  
      const moduleB = require("./moduleB.js")
      console.log("ModuleA module, imported successfully" + moduleB.content);
      module.exports = {
        content: "ModuleA module"
      }
      
      // src/moduleB.js  
      module.exports = {
        content: "ModuleB module"
      } 
      Copy the code
    • newwebpack.config.jsConfiguring packaging Parameters
      const path = require("path")
      module.exports = {
        entry: "./src/index.js".output: {
          filename: "bundle.js".path: path.resolve("./build")},mode: "development"
      }
      Copy the code
    • newbuild-scriptScript and executenpm run build
      // package.json
      {
        "scripts": {
          "build": "webpack"}},Copy the code
  • Yeah, packedbuild/bundle.jsanalysis
    (() = > {
      /** * all modules ** All modules are waiting to be loaded in __webpack_modules__ as key-value pairs with module object keys as module ID(path) values as module contents. * Other modules are loaded within the module via the webpack_require__ function wrapped in webpack */
      var __webpack_modules__ = ({
        "./src/index.js":
          (function (__unused_webpack_module, __unused_webpack_exports, __webpack_require__) {
            eval("const moduleA = __webpack_require__(/*! ./moduleA */ \"./src/moduleA.js\")\r\nconsole.log(\"this\", this); \r\nconsole.log(\"index.js, successfully imported \" + modulea.content); \n\n//# sourceURL=webpack://demo/./src/index.js?");
          }),
    
        "./src/moduleA.js":
          ((module, __unused_webpack_exports, __webpack_require__) = > {
            eval("Const moduleB = __webpack_require__(\"./ SRC/moduleb.js \")\r\nconsole.log(\"moduleA module, successfully imported \" + moduleb.content); \ r \ nmodule exports = {\ r \ n content: \ "moduleA module \ \ r \ n} \ n \ n / / # sourceURL = webpack: / / demo /. / SRC/moduleA. Js?");
          }),
    
        "./src/moduleB.js":
          ((module) = > {
            eval("Module. Exports = {\ r \ n content: \" moduleB module \ \ r \ n} \ n \ n / / # sourceURL = webpack: / / demo /. / SRC/moduleB js?"); })});/** * Module cache ** The cache will be added after each new module is loaded. The cache will be directly used before the same module is loaded next time to avoid repeated loading. * /
      var __webpack_module_cache__ = {};
    
      /** * Webpack_modules__ this function loads modules based on the module ID, and checks whether there is a cache in the module cache before loading, */ if there is no cache, and loads the module from all modules (__webpack_modules__) */
      function __webpack_require__(moduleId) {
        // Check if there is any available cache before loading
        var cachedModule = __webpack_module_cache__[moduleId];
        if(cachedModule ! = =undefined) {
          return cachedModule.exports;
        }
        // Create a new empty module and add it to the cache
        var module = __webpack_module_cache__[moduleId] = {
          exports: {}};// Executes the module method, which loads the code in the module and retrieves the exported content of the module
        __webpack_modules__[moduleId].call(module.exports, module.module.exports, __webpack_require__);
    
        // Returns the final exported data of the module
        return module.exports;
      }
      
        /** * this is the starting point for bundle.js execution, If there is a dependency, the import continues through __webpack_require__, and then executes the code * "./ SRC /index.js" in the import file module. Import "./ SRC/modulea.js "through __webpack_exports__ and "./ SRC/moduleb.js" */ through __webpack_exports__
        var __webpack_exports__ = __webpack_require__("./src/index.js");
      /** * the execution process * __webpack_require__ loads a module in __webpack_modules__, whose execution method recursively calls __webpack_require__ to load the next module until all modules are loaded */}) ();Copy the code

    right_webpack_require_analysis

    _webpack_require_Non-critical dog utility functions of the bundle, so to speak. import all dependencies by recursively calling this function

    var __webpack_modules__ = ({
      "./src/index.js":
        (function (__unused_webpack_module, __unused_webpack_exports, __webpack_require__) {
          eval("const moduleA = __webpack_require__(/*! ./moduleA */ \"./src/moduleA.js\")\r\nconsole.log(\"this\", this); \r\nconsole.log(\"index.js, successfully imported \" + modulea.content); \n\n//# sourceURL=webpack://demo/./src/index.js?"); }),... })function __webpack_require__(moduleId) {
      // Cache processing
      var cachedModule = __webpack_module_cache__[moduleId];
      if(cachedModule ! = =undefined) {returncachedModule.exports; }// Create a module object and add a cache
      var module = __webpack_module_cache__[moduleId] = {exports: {}};
    
      /** * Webpack implements the __webpack_modules__ module based on the Node environment, passing in module, module.exports and changing this pointer
      __webpack_modules__[moduleId].call(module.exports, module.module.exports, __webpack_require__);
    
      // Returns the final exported data of the module
      return module.exports;
    }
    Copy the code

Dependency module analysis

_webpack_modules_ holds all modules, and the most complex is the dependency of all modules, which involves js, nod basics, and the concept of abstract syntax trees. The work of Webpack is module analysis, and then process the files through various plugins and loaders to generate _webpack_modules_. The biggest difficulty in implementing Webpack is module analysis.

Here is the import file “./ SRC /index.js” as an example to analyze the module dependency generated _webpack_modules_

Start parsing

Go back to the my-webpack main program execution file bin/my-webpack.js

  • Read the package configuration file, get the package configuration parameters (inlet, outlet, etc.)

    // my-webpack/bin/my-webpack.js
    #!/usr/bin/env node
    const path = require("path")
    
    // 1. Import the packaging configuration file to obtain the packaging configuration
    // When using the my-webpack tool to package other projects, you need to obtain the absolute path of the project packaging configuration file
    const config = require(path.resolve("webpack.config.js"))
    console.log("config", config);
    Copy the code
  • Back in the DEMO project, enter the instruction my-webpack in the terminal to use your own packaging tool

    Package configuration parameters were successfully obtained

Code parser

Use a parser to parse project code against configuration parameters

  • Create lib/Compiler.js in the tool’s my-webpack directory

    Create a new Compiler class using object-oriented thinking

    // lib/Compiler.js  
    const path = require("path")
    const fs = require("fs")
    class Compiler {
      constructor(config) {
        this.config = config
        this.entry = config.entry
        // process. CWD can obtain the absolute path of the node execution file
        // Get the file path of the packaged project
        this.root = process.cwd()
      }
      // Parse the file module based on the file path passed in
      depAnalyse(modulePath) {
        const file = this.getSource(modulePath)
        console.log("file", file);
      }
      // Pass the file path, read the file, and return
      getSource(path) {
        // Read the file in utF-8 encoding format and return
        return fs.readFileSync(path, "utf-8")}// Execute the parser
      start() {
        // Pass in the absolute path of the entry file to start parsing dependencies
        // Note: __dirname cannot be used here. __dirname represents the exclusive path to the "my-webpack" root directory of the utility library, not the root path of the project to be packaged
        this.depAnalyse(path.resolve(this.root, this.entry))
      }
    }
    module.exports = Compiler
    
    // bin/my-webpack.js  
    // 2. Import the parser, create a new instance, and execute the parser
    const Compiler = require(".. /lib/Compiler")
    new Compiler(config).start()
    Copy the code
  • In the packaged project DEMO, re-executing my-webpack successfully reads the entry file

Abstract syntax tree

After successfully reading the module code, the module code can be converted into an abstract syntax tree (AST) and the require syntax replaced by its own wrapper loading function _webpack_require_

Code online to abstract syntax tree: astexplorer.net/

Two packages are needed to generate and traverse the abstract syntax tree in the packaging project: @babel/ Parser and @babel/traverse

  • Install NPM I @babel/ parser@babel /traverse -S

  • Generate an AST and transform the syntax

    // my-webpack/lib/Compiler.js  
    // Import the parser
    const parser = require("@babel/parser")
    // Import converter es6 export requires.defult
    const traverse = require("@babel/traverse").default
    
    class Compiler {
      depAnalyse(modulePath) {
        const code = this.getSource(modulePath)
        // Parse the code into an AST abstract syntax tree
        const ast = parser.parse(code)
        /** * traverse to convert syntax, which receives two arguments * -ast: abstract syntax tree node tree before conversion * -options: Traverse syntax tree nodes that are triggered when a node meets a hook condition * -callexpression: */
        traverse(ast, {
          // This hook is triggered when an abstract syntax tree node type is CallExpression (expression invocation)
          CallExpression(p) {
            console.log("Name of syntax node of this type", p.node.callee.name); }})}}Copy the code
  • Go back to the DEMO project and execute my-WebPack to repackage using your own library

  • Replace keywords in code

    // my-webpack/lib/Compiler.js  
    traverse(ast, {
      // This hook is triggered when an abstract syntax tree node type is CallExpression (expression invocation)
      CallExpression(p) {
        console.log("Name of syntax node of this type", p.node.callee.name);
          if (p.node.callee.name === 'require') {
            / / modify the require
            p.node.callee.name = "__webpack_require__"
    
            // Change the path of the current module dependent module. Using Node to access resources must be in the form of "./ SRC /XX"
            let oldValue = p.node.arguments[0].value
            // change the "./ XXX "path to "./ SRC/XXX"
            oldValue = ". /" + path.join("src", oldValue)
    
            // Avoid window path with "\"
            p.node.arguments[0].value = oldValue.replace(/\\/g."/")
            console.log("Path", p.node.arguments[0].value); }}})Copy the code
  • Back at DEMO, execute my-Webpack to repack to see the console output

To generate the source code

After processing the AST, code can be generated with @babel/ Generator

  • The installation

    npm i @babel/generator -S
  • After parsing the AST, build the code
    // my-webpack/lib/Compiler.js  
    // Import the generator
    const generator = require("@babel/generator").default
    
    class Compiler {
      depAnalyse(modulePath) {
        traverse(ast, {
          ......
        })
        // Generate code from the abstract syntax tree
        const sourceCode = generator(ast).code
        console.log("Source", sourceCode); }}Copy the code
  • Go back toDEMO, the implementation ofmy-webpackTo rebuild

Build dependencies recursively

In Compiler, we use depAnalyse to convert syntax in the./ SRC /index.js module and dependency module paths by passing in module paths. This only parses the “./ SRC /index.js” layer, and its dependencies don’t parse builds, so we recursively execute depAnalyse for all modules

class Compiler {
  depAnalyse(modulePath) {
    
    // The dependency array of the current module stores all the dependency paths of the current module
    let dependencies = []
   
    traverse(ast, {
      CallExpression(p) {
        if (p.node.callee.name === 'require') {
          p.node.callee.name = "__webpack_require__"
          let oldValue = p.node.arguments[0].value
          oldValue = ". /" + path.join("src", oldValue)
          p.node.arguments[0].value = oldValue.replace(/\\+/g."/")

          // Every time you parse require, place the path of the dependent module in dependencies
          dependencies.push(p.node.arguments[0].value)
        }
      },
    })
    
    const sourceCode = generator(ast).code
    console.log("sourceCode", sourceCode);

    // If the module has other dependencies it recursively calls depAnalyse and continues parsing the code until it has no dependencies
    dependencies.forEach(e= > {
      // The absolute path to the module passed in
      this.depAnalyse(path.resolve(this.root, depPath))
    })
  }
}
Copy the code

Get all modules

The webpack result is that all modules are grouped together in the form of module IDS + module execution functions

class Compiler {
  constructor(config){...// Store all the packaged modules
    this.modules = {}
  }
  depAnalyse(modulePath){ traverse(ast, {..... })const sourceCode = generator(ast).code

    // The ID of the module is treated as a relative path
    let modulePathRelative = ". /" + path.relative(this.root, modulePath)
    // Replace "\" with "/"
    modulePathRelative = modulePathRelative.replace(/\\+/g."/")
    // Add the current module to modules when it is parsed
    this.modules[modulePathRelative] = sourceCode
    
    dependencies.forEach(depPath= >{... })}start() {
    this.depAnalyse(path.resolve(this.root, this.entry))
    // Get the final parsing result
    console.log("module".this.modules); }}Copy the code

Execute my-Webpack in DEMO to review the packing results

So far, we use Compiler class, recursively call depAnalyse method to parse all modules of the project, obtain modules and successfully build the whole project.

Generate _webpack_modules_

The webpack is used to generate _webPack_modules_, so the module is where the module ID+ module execution functions exist. We can use a template engine (this is an EJS example) to process module code into module execution functions

  • Install NPM I EJS-S

  • New template rendering my – webpack/template/output. The ejs

    (() = > {var __webpack_modules__ = ({/ / traverse use template syntax k is the module ID < % for (modules) in the let k {% > "< % % > - k" : (function (module, exports, __webpack_require__) { eval(`<%- modules[k]%>`); }}), < % % >}); var __webpack_module_cache__ = {}; function __webpack_require__(moduleId) { var cachedModule = __webpack_module_cache__[moduleId]; if (cachedModule ! == undefined) { return cachedModule.exports; } var module = __webpack_module_cache__[moduleId] = { exports: {} }; __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__); return module.exports; } var __webpack_exports__ = __webpack_require__("<%-entry%>"); }) ();Copy the code
  • Render code using the template and write it to the export file

    // my-webpack/lib/Compiler.js  
    
    / / import ejs
    const ejs = require("ejs")
    class Compiler {
      constructor(config){... }depAnalyse(modulePath) {....}
      getSource(path){... }start() {
       this.depAnalyse(path.resolve(this.root, this.entry))
       this.emitFile()
      }
      
      // Generate the file
      emitFile() {
        // Read the template to which the code renders
        const template = this.getSource(path.resolve(__dirname, ".. /template/output.ejs"))
        // Pass in the render template and the variables used in the template
        let result = ejs.render(template, {
          entry: this.entry,
          modules: this.modules
        })
        // Get the output path
        let outputPath = path.join(this.config.output.path,this.config.output.filename)
        // Generate the bundle file
        fs.writeFileSync(outputPath,result)
      }
    }
    Copy the code
  • In the DEMO project, re-execute my-Webpack

    Note that the output path of webpack.config.js in the DEMO project is DEMO/build/bundle.js. You must make sure the DEMO has a build folder, otherwise you may get an error when writing files using my-Webpack.

Custom loader

Earlier we implemented a packaging tool, my-Webpack, which now only works with JS files. If you want to process other files or operate js codes, you need to use Loader. Loader’s main function is to process a section of code matching rules to generate the final code for output.

How to use loader

  • Install loader package
  • In the WebPack configuration file, add the configuration to the Rules of the Module
  • Some loaders also need to configure additional parameters (optional)

What is the loader

The Loader module is a function. Webpack passes resources to the Loader function, and the Loader processes the resources and returns them.

We implement a loader in the WebPack project

  • Process the exported content of SRC/moduleb.js

    Replace exported content containing “moduleB” with “module-b”

  • Create loader/index.js in the DEMO project

    // Loader is essentially a function
    module.exports = function (resource) {
      // The matched resource is processed and returned
      return resource.replace(/moduleB/g."MODULE-B")}Copy the code
  • In webpack.config.js of DEMO, configure a custom loader

    // DEMO/webpack.config.js
    module.exports = {
      module: {
        rules: [{test: /.js$/g,
            use: "./loader/index.js"}}}]Copy the code
  • Execute NPM Run build, use WebPack to package the project and execute the packaged code

Loader execution sequence

There are two scenarios for using Loaders to process a resource: Single matching rule Multiple Loaders and multiple matching rules single loader

Multiple loaders for a single matching rule:

If a single matching rule has multiple Loaders, the loader execution sequence is from the right to the left

Multiple matching rules Single loader:

When multiple matching rules are applied to a single Loader, the loader is executed from bottom to top

Loader type

Loader types include front, inline, normal, and rear

The four Loaders are executed in the following sequence: Front > Inline > Normal > Rear

Pre-loader and post-Loader

The pre-loader and post-loader are controlled by The Enforce field of the Loader. The pre-loader is executed before all loaders, and the post-loader is executed after all Loaders are executed

Inline loader

Use inline loader to parse files, must be in the require, import imported resources before the loader, multiple loader to use! Separate cases:

import Styles from 'style-loader! css-loader? modules! ./styles.css'
Copy the code

Get the Options configuration

Most loaders can be used only after registration in rules. Some loaders with complex functions need to be configured with options. For example, the URL-loader that generates image resources sets a threshold for generating base64 file sizes.

In the loader,this can get the context and webpack configuration. Options can be obtained from this.getOptions

  • Incoming parameters

     module: {
      rules: [{test: /.js$/g,
          use: {
            loader: "./loader/index.js".options: {
              target: /moduleB/g,
              replaceContent: "MD_B"}},},]},Copy the code
  • Custom loader to use parameters

    // demo/loader/index.js  
    const loaderUtils = require("loader-utils")
    // Loader is essentially a function
    module.exports = function (resource) {
      const { target, replaceContent } = this.getOptions()
      // The matched resource is processed and returned
      return resource.replace(target, replaceContent)
    }
    Copy the code
  • Execute NPM Run build, use WebPack to package the project and execute the packaged code

My-webpack adds loader functionality

Through the previous configuration of loader and handwritten Loader, it can be found that my-Webpack adds loader functions mainly through the following steps:

  • Read the module.rules configuration item of the Webpack configuration file and iterate backwards (rules matches each matching rule in reverse order)
  • Match the file type based on the res and import the Loader function in batches
  • Call all Loader functions iteratively in reverse order
  • Finally, the processing code is returned

Code section (my-webpack/lib/ compiler.js)

  • Gets all configured in the configuration fileloader
    class Compiler {
      constructor(config){...// Get all loaders
        this.rules = config.module && config.module.rules
      }
      ...
    }
    Copy the code
  • The statementuseLoaderMethod, called when a dependency is resolved, passing in the parsed source code and module path
    class Compiler {...depAnalyse(modulePath) {
        let code = this.getSource(modulePath)
        // Use the loader to process the source code
        code = this.useLoader(code, modulePath)
        const ast = parser.parse(code)
        ...
      }
      / / use the loader
      useLoader(code, modulePath) {
        // Return source code without rules configuration
        if (!this.rules) return code
        // Obtain the source code and output it to loader, traversing loader in reverse order
        for (let index = this.rules.length - 1; index >= 0; index--) {
          const { test, use } = this.rules[index]
          // If the current module meets the Loader re match
          if (test.test(modulePath)) {
            /** * If there is a single match rule and multiple loaders, the use field is an array. For a single loader, the use field is a string or object */
            if (use instanceof Array) {
              // The loader passes the source code in reverse order for processing
              for (let i = use.length - 1; i >= 0; i--) {
                let loader = use[i];
                /** * Loader is a string or object. * String format: * use:["loader1","loader2"]  * use:[ * { * loader:"loader1", * options:.... * } * ] */
                let loaderPath = typeof loader === 'string' ? loader : loader.loader
                // Obtain the absolute path of the Loader
                loaderPath = path.resolve(this.root, loaderPath)
                // Loader context
                const options = loader.options
                const loaderContext = {
                  getOptions() {
                    return options
                  }
                }
                / / import loader
                loader = require(loaderPath)
                // The incoming context executes the loader processing source code
                code = loader.call(loaderContext, code)
              }
            } else {
              let loaderPath = typeof loader === 'string' ? use : use.loader
              // Obtain the absolute path of the Loader
              loaderPath = path.resolve(this.root, loaderPath)
              // Loader context
              const loaderContext = {
                getOptions() {
                  return use.options
                }
              }
              / / import loader
              let loader = require(loaderPath)
              // The incoming context executes the loader processing source code
              code = loader.call(loaderContext, code)
            }
          }
        }
        return code
      }
    }
    Copy the code
  • performmy-webpack, the use ofmy-webpackPackage the project and execute the packaged code

A custom plugin

Webpack’s plug-in interface gives the user access to the compile process by registering handler functions with different event node lifecycle hooks during the compile process, and by executing each hook, the plug-in has access to the current state of the compile.

In short, custom plug-ins can perform some functions by processing the source code in the declaration cycle hooks of the WebPack compilation process.

Plug-in lifecycle hooks

hook role parameter
entryOption Called after processing entry configuration for the WebPack option context,entry
afterPlugins Called after initializing the list of internal plug-ins compiler
beforeRun Called before running Compiler compiler
run Called when Compiler starts working compiler
emit Called when asSTES is emitted to the ASSTES directory compilation
done Called after compilation is complete stats
. . .

The composition of the WebPack plug-in

  • A js named function
  • Define an apply method on the prototype of the plug-in
  • Specify a binding towebpackIts own event hook
    • Many of the event hooks within WebPack are implemented through the Tabable library, which focuses on custom event firing and handling
  • Handles specific data for webPack internal instances
  • The callback provided by WebPack is invoked when the functionality is complete

Implement a simple plugin

  • Demo project new demo/plugins/helloWorldPlugin. Js
1. Declare a named function
module.exports = class HelloWorldPlugin {
  // 2. Function prototype must have the apply method
  apply(compiler) {
    // 3. Register hook callbacks with hooks
    // 4. Trigger the subsequent callback when the done event occurs
    compiler.hooks.done.tap("HelloWorldPlugin".(stats) = > {
      console.log("The whole Webpack pack is finished.");
    })

    // 5. Trigger the subsequent callback when the done event occurs
    compiler.hooks.emit.tap("HelloWorldPlugin".(stats) = > {
      console.log("File launch is over."); }}})Copy the code
  • webpack.config.jsIntroduce custom plug-ins
    const path = require("path")
    / / import HelloWorldPlugin
    const HelloWorldPlugin = require("./plugin/helloWorldPlugin")
    module.exports = {
      entry: "./src/index.js".output: {
        filename: "bundle.js".path: path.resolve("./build")},module: {
        rules: [{test: /.js$/g,
            use: [{
              loader: "./loader/index.js".options: {
                target: /moduleB/g,
                replaceContent: "MD_B"}}]},]},// Configure the custom plug-in
      plugins: [
        new HelloWorldPlugin()
      ],
      mode: "development"
    }
    Copy the code
    • NPM run Build repackage demo

The HTML – webpack – the plugin

The html-webpack-plugin simply copies the specified HTML template and automatically imports bundle.js

How to do that? 1. Write a custom plug-in and register the afterEmit hook. 2. Read the HTML template from the template property passed in when creating the plugin instance. 4. Walk through the list of resources generated by webPack and, if there are multiple bundle.js, import them into THE HTML one by one. 5. Output the generated HTML string to the dist directory

  • demoProject constructionsrc/index.html, newplugin/HTMLPlugin.js
    // demo/plugin/HTMLPlugin.js   
    const fs = require("fs")
    const cheerio = require("cheerio")
    module.exports = class HTMLPlugin {
       constructor(options) {
         Filename: indicates the output filename of the template. * options.template: indicates the input path of the target template
         this.options = options
       }
       // plugins must have the apply method
       apply(compiler) {
         // 3, afterEmit at the end of the resource emit, fetch the bundle and other resources
         compiler.hooks.afterEmit.tap("HTMLPlugin".(compilation) = > {
         // 4, read the HTML target template passed in, get the DOM structure string
           const template = fs.readFileSync(this.options.template, "utf-8")
           /** * 5, 'yarn add cheerio' install cheerio and import it */
           let $ = cheerio.load(template)
           console.log(Object.keys(compilation.assets));
           // 6. Loop through all resources once into HTML
           Object.keys(compilation.assets).forEach(e= > $('body').append(`<script src="./${e}"></script>`))
          // output the new HTML string to the dist directory
          fs.writeFileSync("./build/" + this.options.filename, $.html())
        })
      }
    }
    Copy the code
  • Configure the plug-in
    // demo/webpack.config.js  
    / / import HTMLPlugin
    const HTMLPlugin = require("./plugin/HTMLPlugin")
    module.exports = {
      // Configure the custom plug-in
      plugins: [
        new HTMLPlugin({
          filename: "index.html".template: "./src/index.html"
        }),
        new HelloWorldPlugin(),
      ],
    }
    Copy the code
  • npm run buildPackage the project to view the generatedbuild/index.html

Compiler differs from Compilation

  • Compiler objects are the tools that value WebPack uses to package project files
  • Compilation objects are the products that are packaged at each stage of a Webpack each time it is packaged

My-webpack adds plugin functionality

tapable

Project files are packaged and processed within WebPack through various event flow concatenation plug-ins. The core of the event flow mechanism is implemented through Tapable, which is similar to the Events library of Node. The core principle of Tapable is publish and subscribe.

Add plugin functionality

We’ll do a simple demonstration here, setting up a few hook functions at key nodes. The internal implementation of Webpack is far more complex than ours, after all, there are only dozens of hook functions.

  • Install YARN Add Tapable

  • Load plugin, declare hook, execute hook

    // my-webpack/lib/Compiler.js
    / / import tabable
    const { SyncHook } = require("tapable")
    
    class Compiler {
      constructor(config){...// 1
        this.hooks = {
          compile: new SyncHook(),
          afterCompile: new SyncHook(),
          emit: new SyncHook(),
          afterEmit: new SyncHook(['modules']),
          done: new SyncHook()
        }
    
        // get all plugin objects and execute the apply method
        if (Array.isArray(this.config.plugins)) {
          this.config.plugins.forEach(e= > {
            // Pass in the Compiler instance, and the plug-in can register the hooks in the Apply method
            e.apply(this)}}}start() {
        // Execute compile hook before parsing
        this.hooks.compile.call()
        this.depAnalyse(path.resolve(this.root, this.entry))
        // After analysis, execute the afterCompile hook
        this.hooks.afterCompile.call()
        // Execute emit hook before resource launches
        this.hooks.emit.call()
        this.emitFile()
        // After the resource is fired, execute the afterEmit hook
        this.hooks.afterEmit.call()
        // When parsing is complete, execute the done hook
        this.hooks.done.call()
      }
    }
    Copy the code
  • Register the hook in helloWorldPlugin

    // demo/plugin/helloWorldPlugin.js
    module.exports = class HelloWorldPlugin {
      apply(compiler) {
        compiler.hooks.done.tap("HelloWorldPlugin".(stats) = > {
          console.log("The whole Webpack pack is finished.");
        })
    
        compiler.hooks.emit.tap("HelloWorldPlugin".(stats) = > {
          console.log("File launched."); }}})Copy the code
  • Execute my-webpack and repack

The last

The purpose of this article is to understand what WebPack “is” and how it works by implementing the basic features of webPack, each part of which and each feature can be far more complex than ours. Finally, paste the compiler code

const path = require("path")
const fs = require("fs")
// Import the parser
const parser = require("@babel/parser")
// Import converter es6 export requires.defult
const traverse = require("@babel/traverse").default

// Import the generator
const generator = require("@babel/generator").default

/ / import ejs
const ejs = require("ejs")

/ / import tabable
const { SyncHook } = require("tapable")
class Compiler {
  constructor(config) {
    this.config = config
    this.entry = config.entry
    // process. CWD can obtain the absolute path of the node execution file
    // Get the file path of the packaged project
    this.root = process.cwd()

    // Store all the packaged modules
    this.modules = {}

    // Get all loaders
    this.rules = config.module && config.module.rules

    // 1
    this.hooks = {
      compile: new SyncHook(),
      afterCompile: new SyncHook(),
      emit: new SyncHook(),
      afterEmit: new SyncHook(['modules']),
      done: new SyncHook()
    }

    // get all plugin objects and execute the apply method
    if (Array.isArray(this.config.plugins)) {
      this.config.plugins.forEach(e= > {
        // Pass in the Compiler instance, and the plug-in can register the hooks in the Apply method
        e.apply(this)}}}// Parse the file module based on the file path passed in
  depAnalyse(modulePath) {
    let code = this.getSource(modulePath)
    // Use the loader to process the source code
    code = this.useLoader(code, modulePath)

    // Parse the code into an AST abstract syntax tree
    const ast = parser.parse(code)

    // The dependency array of the current module stores all the dependency paths of the current module
    let dependencies = []

    /** * traverse to convert syntax, which receives two arguments * -ast: abstract syntax tree node tree before conversion * -options: Traverse syntax tree nodes that are triggered when a node meets a hook condition * -callexpression: */
    traverse(ast, {
      // This hook is triggered when an abstract syntax tree node type is CallExpression (expression invocation)
      CallExpression(p) {
        if (p.node.callee.name === 'require') {
          / / modify the require
          p.node.callee.name = "__webpack_require__"

          // Change the path of the current module dependent module. Using Node to access resources must be in the form of "./ SRC /XX"
          let oldValue = p.node.arguments[0].value
          // change the "./ XXX "path to "./ SRC/XXX"
          oldValue = ". /" + path.join("src", oldValue)

          // Avoid window path with "\"
          p.node.arguments[0].value = oldValue.replace(/\\+/g."/")

          // Every time you parse require, place the path of the dependent module in dependencies
          dependencies.push(p.node.arguments[0].value)
        }
      },
    })

    const sourceCode = generator(ast).code

    // The ID of the module is treated as a relative path
    let modulePathRelative = ". /" + path.relative(this.root, modulePath)
    // Replace "\" with "/"
    modulePathRelative = modulePathRelative.replace(/\\+/g."/")
    // Add the current module to modules when it is parsed
    this.modules[modulePathRelative] = sourceCode


    // If the module has other dependencies it recursively calls depAnalyse and continues parsing the code until it has no dependencies
    dependencies.forEach(depPath= > {
      // The absolute path to the module passed in
      this.depAnalyse(path.resolve(this.root, depPath))
    })
  }
  // Pass the file path to read the file
  getSource(path) {
    // Read the file in utF-8 encoding format and return
    return fs.readFileSync(path, "utf-8")}// Execute the parser
  start() {
    // Execute compile hook before parsing
    this.hooks.compile.call()
    // Pass in the absolute path of the entry file to start parsing dependencies
    // Note: the path cannot be __dirname. __dirname represents the absolute path of the "my-webpack" root directory of the utility library, not the root path of the project to be packaged
    this.depAnalyse(path.resolve(this.root, this.entry))
    // After analysis, execute the afterCompile hook
    this.hooks.afterCompile.call()
    // Execute emit hook before resource launches
    this.hooks.emit.call()
    this.emitFile()
    // After the resource is fired, execute the afterEmit hook
    this.hooks.afterEmit.call()
    // When parsing is complete, execute the done hook
    this.hooks.done.call()
  }
  // Generate the file
  emitFile() {
    // Read the template to which the code renders
    const template = this.getSource(path.resolve(__dirname, ".. /template/output.ejs"))
    // Pass in the render template and the variables used in the template
    let result = ejs.render(template, {
      entry: this.entry,
      modules: this.modules
    })
    // Get the output path
    let outputPath = path.join(this.config.output.path, this.config.output.filename)
    // Generate the bundle file
    fs.writeFileSync(outputPath, result)
  }
  / / use the loader
  useLoader(code, modulePath) {
    // Return source code without rules configuration
    if (!this.rules) return code
    // Obtain the source code and output it to loader, traversing loader in reverse order
    for (let index = this.rules.length - 1; index >= 0; index--) {
      const { test, use } = this.rules[index]
      // If the current module meets the Loader re match
      if (test.test(modulePath)) {
        /** * If there is a single match rule and multiple loaders, the use field is an array. For a single loader, the use field is a string or object */
        if (use instanceof Array) {
          // The loader passes the source code in reverse order for processing
          for (let i = use.length - 1; i >= 0; i--) {
            let loader = use[i];
            /** * Loader is a string or object. * String format: * use:["loader1","loader2"]  * use:[ * { * loader:"loader1", * options:.... * } * ] */
            let loaderPath = typeof loader === 'string' ? loader : loader.loader
            // Obtain the absolute path of the Loader
            loaderPath = path.resolve(this.root, loaderPath)
            // Loader context
            const options = loader.options
            const loaderContext = {
              getOptions() {
                return options
              }
            }
            / / import loader
            loader = require(loaderPath)
            // The incoming context executes the loader processing source code
            code = loader.call(loaderContext, code)
          }
        } else {
          let loaderPath = typeof loader === 'string' ? use : use.loader
          // Obtain the absolute path of the Loader
          loaderPath = path.resolve(this.root, loaderPath)
          // Loader context
          const loaderContext = {
            getOptions() {
              return use.options
            }
          }
          / / import loader
          let loader = require(loaderPath)
          // The incoming context executes the loader processing source code
          code = loader.call(loaderContext, code)
        }
      }
    }
    return code
  }
}
module.exports = Compiler
Copy the code