WHAT is Webpack

Webpack is a static module packaging tool that combines each module in our project into one or more bundles.

Some core concepts related to this (configured in the webpack.config.js configuration file) :

  • Entry: The entry to the package that tells WebpackWhere to start
  • Output: Tells WebpackWhere to print itThe bundle it creates, andHow to nameThese documents. The default value for the main output file is./dist/main.js, and the other generated files are placed in the./dist folder by default
  • Loader: Webpack can only understand JavaScript and JSON files, which are available in webPack out of the box. But we don’t just have these two types of files in our project, there are other types, what do we do with them? Loader is webpack capableHandle other types of filesCan process and convert other types of files into valid modules, such as SASS to CSS, or ES6 to ES5. Specifically,Loader is a functionIs responsible for converting the input source text to a specific text output. They can be either asynchronous or synchronous. When configured, loader has two attributes: the test attribute (to identify which files will be converted) and the use attribute (to define which loader will be used for conversion).
const path = require('path');

module.exports = {
  output: {
    filename: 'my-first-webpack.bundle.js',},// Loader configuration rules are defined in module.rules
  module: {
    rules: [{ test: /\.txt$/, use: 'raw-loader'}].// Use raw-loader conversion before packaging when encountering "path parsed to '.txt' in require()/import statement"}};Copy the code
  • Plugin: Plugins are used forChange the behavior of the build processSuch as automatically uploading static resources to the cloud, removing duplicate files from the output, injecting environment variables, etc. Specifically, a plug-in is an instance of a class that can be hooked up to the lower-level API of WebPack. To use a plug-in, simply require it and add it to the Plugins array. Most plug-ins can be customized with options, or the same plug-in can be used multiple times in a configuration file for different purposes. In this case, you need to create an instance of the plug-in using the new operator.

If you need to convert Vue/React code, SASS, or some other translation language, use loader. If you need to tweak JavaScript, or work with files in a certain way, use the plugin.

const HtmlWebpackPlugin = require('html-webpack-plugin'); // Install via NPM
const webpack = require('webpack'); // Used to access built-in plug-ins

module.exports = {
  module: {
    rules: [{ test: /\.txt$/, use: 'raw-loader'}],},plugins: [new HtmlWebpackPlugin({ template: './src/index.html'})].// html-webpack-plugin can generate an HTML file and automatically inject all generated bundles into this file
};
Copy the code

WHY: Webpack

To do a good job, he must sharpen his tools.

When we write large complex projects where we decouples business logic through modularity, can there be a way that not only lets us write modules, but also supports any module format (at least prior to ESM) and can handle various resources at the same time?

That’s why we use WebPack, which packages JavaScript applications (ESM and CommonJS support) that can be extended to support many different static resources.

HOW does Webpack work

After knowing WHAT & WHY, we are most curious about HOW? How exactly does Webpack work? Let’s start with a simple example. What does WebPack compile into our code? Since the output of webpack compilation and packaging is essentially the same for complex projects as it is for simple lines of code, we can start with the simplest case and explore the secrets of packaging output.

1. A simple example

Create index.js from SRC:

const sayHello = require('./hello.js') 
console.log(sayHello('hui ho'))
Copy the code

hello.js:

module.exports = function (name) { 
    return 'hello ' + name 
}
Copy the code

Importing packaged files in index.html:

<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <script type="text/javascript" src=".. /dist/bundle.js" charset="utf-8"></script>
    </body>
</html>
Copy the code

After executing the package command, open dist/bundle.js(the output file name specified) :

/ * * * * * * / (() = > { // webpackBootstrap
/ * * * * * * /  var __webpack_modules__ = ({

/ * * * / "./src/hello.js":
/ *! * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./src/hello.js ***! \ * * * * * * * * * * * * * * * * * * * * * * /
/ * * * / ((module) = > {

    eval("module.exports = function (name) {\n return 'hello ' + name\n}\n\n//# sourceURL=webpack://webpack/./src/hello.js?");

    / * * * / }),
    
    / * * * / "./src/index.js":
    / *! * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./src/index.js ***! \ * * * * * * * * * * * * * * * * * * * * * * /
    / * * * / ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) = > {
    
    eval("const sayHello = __webpack_require__(/*! . / hello * / \ ". / SRC/hello. Js \ ") \ nconsole log (sayHello (' hui ho)) \ n \ n / / # sourceURL = webpack: / / webpack /. / SRC/index. Js?");
    
    / * * * / })
    
    / * * * * * * /  });
    / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
    / * * * * * * /  // The module cache
    / * * * * * * /  var __webpack_module_cache__ = {};
    / * * * * * * /  
    / * * * * * * /  // The require function
    / * * * * * * /  function __webpack_require__(moduleId) {
    / * * * * * * /    // Check if module is in cache
    / * * * * * * /    var cachedModule = __webpack_module_cache__[moduleId];
    / * * * * * * /    if(cachedModule ! = =undefined) {
    / * * * * * * /      return cachedModule.exports;
    / * * * * * * /    }
    / * * * * * * /    // Create a new module (and put it into the cache)
    / * * * * * * /    var module = __webpack_module_cache__[moduleId] = {
    / * * * * * * /      // no module.id needed
    / * * * * * * /      // no module.loaded needed
    / * * * * * * /      exports: {}
    / * * * * * * /    };
    / * * * * * * /  
    / * * * * * * /    // Execute the module function
    / * * * * * * /    __webpack_modules__[moduleId](module.module.exports, __webpack_require__);
    / * * * * * * /  
    / * * * * * * /    // Return the exports of the module
    / * * * * * * /    return module.exports;
    / * * * * * * /  }
    / * * * * * * /  
    / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
    / * * * * * * /  
    / * * * * * * /  // startup
    / * * * * * * /  // Load entry module and return exports
    / * * * * * * /  // This entry module can't be inlined because the eval devtool is used.
    / * * * * * * /  var __webpack_exports__ = __webpack_require__("./src/index.js");
    / * * * * * * /  
    / * * * * * * / })()
    ;
Copy the code

There are a lot of comments, so let’s extract the core:

// The outermost layer is an immediate execute function IIFE
(() = > { // webpackBootstrap
    // Defines a __webpack_modules__ object with a file name attribute whose value is the corresponding file content
    var __webpack_modules__ = ({
      "./src/hello.js": ((module) = > {
        eval("module.exports = function (name) {\n return 'hello ' + name\n}\n\n//# sourceURL=webpack://webpack/./src/hello.js?");
       }),
      "./src/index.js":
        eval("const sayHello = __webpack_require__(/*! . / hello * / \ ". / SRC/hello. Js \ ") \ nconsole log (sayHello (' hui ho)) \ n \ n / / # sourceURL = webpack: / / webpack /. / SRC/index. Js?");
      })
    
    // The module cache
    // Cache module
    var __webpack_module_cache__ = {};
    // The require function
    function __webpack_require__(moduleId) {
      // Check if module is in cache
      var cachedModule = __webpack_module_cache__[moduleId];
      if(cachedModule ! = =undefined) {
        return cachedModule.exports;
      }
      // Create a new module (and put it into the cache)
      var module = __webpack_module_cache__[moduleId] = {
      // no module.id needed
      // no module.loaded needed
        exports: {}};// Execute the module function
       __webpack_modules__[moduleId](module.module.exports, __webpack_require__);
      // Return the exports of the module
      return module.exports;
    }
    var __webpack_exports__ = __webpack_require__("./src/index.js"); }) ();Copy the code

We can draw the conclusion that:

  • The webpack result is oneIIFE, commonly called webpackBootstrap;
  • In the package result, a module loading function webpack_require is defined;
  • First use the webpack_require loading function to load the entry module./ SRC /index.js.
  • The loading function webpack_require uses the closure variable webpack_module_cache to cache the loaded module results.

2. Working principle

It can be summarized as the following figure [from network] :

  • First, WebPack reads the ones defined by the developer in the projectwebpack.config.jsConfiguration file, or obtain the necessary parameters from shell statements to complete the configuration reading.
  • Next, the required WebPack plug-in is instantiated and the plug-in hooks are mounted on the WebPack event stream so that the plug-in has the ability to alter the output during the appropriate build process.
  • Also, based on the entry file defined by the configuration,Import file(There can be more than one)To start with dependency collection: Compiles all dependent files. This compilation process depends on Loader. Different types of files are parsed according to different Loaders defined by the developer. Compiled content parsing generates AST static syntax trees, analyzes file dependencies, and implements modularized implementation with WebPack’s own loader.
  • After the above process is complete, the results are produced and packaged into the appropriate directory according to the developer’s configuration.

Some core concepts:

  • AST: Our old friend, abstract syntax trees, are JS objects that help us with code analysis.
  • Compiler: Instances of the Compiler object containComplete WebPack configurationThere is only one compiler instance globally. When the plug-in is instantiated, it receives a Compiler object through which it passesYou can access the internal environment of Webpack.
  • Compilation object: When WebPack is running in development mode, a new Compilation object is created whenever a file change is detected. This object contains information about the current module resources, build resources, changed files, and so on. In other words,All build data generated during the build process is stored on this objectIt controls every step of the build process and provides many event callbacks for plug-ins to extend.

4. How about implementing a mini-Webpack yourself

First we’ll install a few packages to use:

  • Babel /parser: Used to parse input code into abstract syntax trees (AST)
  • Babel /traverse: Used to traverse abstract syntax trees (AST) of input
  • @babel/core: the core module of Babel, which performs code conversion
  • @babel/ PRESET -env: ES6 + code can be automatically converted to ES5 based on the target browser or runtime environment configured
npm install @babel/parser @babel/traverse @babel/core @babel/preset-env -D
Copy the code

1. Core code

First we need a configuration file:

// mini-webpack.config.js
const path = require('path')

module.exports = {
    entry: './src/index.js'.mode: 'development'.output: {
        path: path.resolve(__dirname, './dist'),
        filename: 'bundle.js'}}Copy the code

Then create a new bundle.js that implements the core package:

const options = require("./mini-webpack.config");
const fs = require("fs");
const path = require('path');
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");

// Create a MiniWebpack class
class MiniWebpack {
  constructor(options) {
    / / configuration items
    this.options = options;
  }

  // Parse the entry file
  parse(filename) {
    // Use Node's core module fs to read files
    const fileBuffer = fs.readFileSync(filename, "utf-8");
    // Get AST from @babel/parser
    const ast = parser.parse(fileBuffer, { sourceType: "module" });
    const deps = {}; // To collect dependencies
    / / traverse the AST
    traverse(ast, {
      ImportDeclaration({ node }) {
        const dirname = path.dirname(filename);
        const absPath = ". /"+ path.join(dirname, node.source.value); deps[node.source.value] = absPath; }});// Code conversion
    const { code } = babel.transformFromAst(ast, null, {
      presets: ["@babel/preset-env"]});const moduleInfo = { filename, deps, code };
    return moduleInfo;
  }
  
  // Collect the module dependency graph
  analyse(file) {
    // Define the dependency graph
    const depsGraph = {};
    // Get the entry information first
    const entry = this.parse(file);
    const temp = [entry];
    for (let i = 0; i < temp.length; i++) {
      const item = temp[i];
      const deps = item.deps;
      if (deps) {
        // Iterate over module dependencies to get module information recursively
        for (const key in deps) {
          if (deps.hasOwnProperty(key)) {
            temp.push(this.parse(deps[key]));
          }
        }
      }
    }
    temp.forEach((moduleInfo) = > {
      depsGraph[moduleInfo.filename] = {
        deps: moduleInfo.deps,
        code: moduleInfo.code,
      };
    });
    return depsGraph;
  }
  
  // Generate the code for final execution
  generate(graph, entry) {
    // is an immediate function
    return `(function(graph){
        function require(file) {
            var exports = {};
            function absRequire(relPath){
                return require(graph[file].deps[relPath])
            }
            (function(require, exports, code){
                eval(code)
            })(absRequire, exports, graph[file].code)
            return exports
        }
        require('${entry}')
    })(${graph}) `;
  }
  
  // Specify the directory for the packaged files
  outputFile(output, code) {
      const {path: dirPath, filename} = output;
      const outputPath = path.join(dirPath, filename);
      if(! fs.existsSync(dirPath)){ fs.mkdirSync(dirPath) } fs.writeFileSync(outputPath, code,'utf-8')}// String all the packaging logic together
  bundle(){
      const {entry, output} = this.options
      const graph = this.analyse(entry)
      const graphStr = JSON.stringify(graph)
      const code = this.generate(graphStr, entry)
      this.outputFile(output, code)
  }
}

// Instantiate a MiniWebpack
const miniWebpack = new MiniWebpack(options)

// Run the packaging logic
miniWebpack.bundle()
Copy the code

2. Here’s an example

The entire directory is shown in the figure below:

Create a few new files in the SRC directory:

index.html:

<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <script type="text/javascript" src=".. /dist/bundle.js" charset="utf-8"></script>
    </body>
</html>
Copy the code

Index. Js:

import minus from './minus.js' 
import add from './add.js' 

console.log('3-1 = > > > > > >', minus(3.1)) 
console.log('3 + 1 = > > > > > >', add(3.1))
Copy the code

minus.js:

// minus.js 
export default (a, b) => { return a - b }
Copy the code

add.js:

// add.js 
export default (a, b) => { return a + b }
Copy the code

Run the bundle logic with the node bundle.js command.

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"."build": "node bundle.js"
},
Copy the code

Execute NPM run build:

OK! We have successfully packaged the file: dist/bundle.js. We have introduced this file in index. HTML.

Success! 🎉

Implement a simple loader

A loader has a single responsibility to complete the smallest unit of file conversion. A source file may need to go through multi-step conversion before it can be used normally. For example, the Sass file is output to CSS through Sas-Loader, and then the content is sent to CSS-Loader for processing. Even the output content of CSS-Loader is also sent to style-loader for processing. Convert to JavaScript code loaded through a script. Use as follows:

module.exports = {
  // ...
  module: {
    rules: [{test: /\.less$/,
        use: [
          {
            loader: "style-loader".// Create a style node from the JS string
          },
          {
            loader: "css-loader".// Compile CSS to comply with the CommonJS specification
          },
          {
            loader: "less-loader".// Compile less to CSS},],},],},};Copy the code

When we call multiple Loaders in series to convert a file, each loader is executed in chained order. In Webpack, when there are multiple matching Loaders for the same file, follow the following principles:

  • The loader execution sequence is the same as the configuration sequenceOn the contraryThat is, the last loader configured is executed first and the first loader is executed last.
  • The first loader that executes receives the contents of the source file as a parameter, and the other Loaders receive the return value of the previous loader that executes as a parameter. The last loader executed returns the final result.

When configuring loader, you can add some configurations, such as:

module: {
  rules: [{test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel-loader".options: {
        plugins: ["dynamic-import-webpack",}},]; }Copy the code

How to get the options passed in here? Using the loader-utils module:

const loaderUtils = require("loader-utils");
module.exports = function (source) {
  // Get developer configuration options
  const options = loaderUtils.getOptions(this);
  // ...
  return content;
};
Copy the code

Loader is essentially a function, we only care about its input and output.

For example, if we want to implement a replace-loader, this loader can be customized to replace the string we configured to replace, using and configuring as follows:

const path = require("path");
module.exports = {
  // ...
  module: {
    rules: [{test: /\.js$/,
        use: {
          loader: "replaceLoader".options: {
            str: "let".replaceStr: "const".// Replace let with const},},},],},};Copy the code

In replaceLoader. In js:

const loaderUtils = require("loader-utils");
module.exports = function (source) {
  / / get the options
  const options = loaderUtils.getOptions(this);
  return source.replace(options.str, options.replaceStr);
};
Copy the code

Back to the previous example:

let sayHello = require('./hello.js') 
console.log(sayHello('hui ho'))
Copy the code

After packing:

The loader takes effect

Implement a simple plugin

Webpack has a stream of events, and many events are triggered during the life cycle of a WebPack build. At this point, various plug-ins registered under development can listen for events related to themselves as needed. After the event is captured, the compiled output can be changed through the API provided by WebPack when appropriate.

So the difference between loader and plugin is obvious:

  • Loader is oneconverterTo perform a simple file conversion operation.
  • The plugin is aextender, it enriches webpack itself. After the loader process ends, during the whole process of Webpack packaging, Weback Plugin does not operate files directly, but works based on the event mechanism, listens to some events in the process of Webpack packaging, and modifies the packaging results.

The Webpack plug-in is a JavaScript object with the Apply method. The Apply method is called by the Webpack Compiler and the Compiler object is accessible throughout the compile life cycle.

Since plug-ins can carry parameters/options, you must pass a new instance to the plugins property in the WebPack configuration.

So the plugin we implement should be a class or constructor.

Let’s simply implement an HtmlWebpackPlugin, generate an HTML file after packaging, and introduce the JS file generated by packaging in this file.

// my-html-webpack-plugin.js
const pluginName = "MyHtmlWebpackPlugin";

class MyHtmlWebpackPlugin {
  apply(compiler) {
    const filename = compiler.options.output.filename;
    compiler.hooks.emit.tapAsync(pluginName, (compilation, callback) = > {
      const content = ` <! DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Webpack</title> <script defer src="./${filename}"></script>
        </head>
        <body>
        </body>
      </html>
      `;
      // Insert this file as a new file resource into the WebPack build:
      compilation.assets["index.html"] = {
        source: function () {
          return content;
        },
        size: function () {
          returncontent.length; }}; callback(); }); }}module.exports = MyHtmlWebpackPlugin;
Copy the code

Configure in webpack.config.js:

const path = require("path");
const MyHtmlWebpackPlugin = require('./my-html-webpack-plugin');

module.exports = {
  mode: "development".entry: {
    main: "./src/index.js",},output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bundle.js",},// ...
  plugins: [new MyHtmlWebpackPlugin()],
};
Copy the code

Execute the package command:

Success!

👉 : 🐶 compiler – hooks