“This is the first day of my participation in the First Challenge 2022. For details: First Challenge 2022”

preface

Why should we learn about the WebPack execution process?

First: Webpack is often used and necessary for development. It contains many interesting functions, such as hot update, start server, Babel parsing and so on. It is simply a contractor, making our development simple and efficient

Second: from the perspective of architecture, we can learn the implementation process, further in-depth source code, to see the framework design of Webpack, learn some advantages

Learning is not aimless, not after reading the article feel that they have mastered him, to ask more why? , more hands-on, more interactive

Github address please refer to github.com/Y-wson/Dail…

What can we learn from this passage?

    1. Loader and plugin principle, and simple loader and plugin writing
    1. Simple use of Tapable
    1. How are Babel and AST resolved
    1. Webpack executes the process
    1. How are files packaged by WebPack executed

Execute the process

Let’s talk about the execution flow of Webpack in words first

  • Initialization parameters: from the configuration file andShellStatement read and merge parameters, get the final parameter;
  • Start compiling: Initialize with the parameters obtained in the previous stepCompilerClass to load all configured plug-ins that execute objectsrunMethod starts compiling; Determine the entry: according to the configurationentryFind all the entry files
  • Compile module: from the entry file, call all configuredLoaderCompile the module, then find the module that the module depends on, and then recurse this step until all the entry dependent files have been processed by this step;
  • Complete module compilation: use after step 3LoaderAfter all modules are translated, the final content of each module is translated and the dependencies between them are obtained
  • Output resources: Assembled into modules based on the dependencies between entry and moduleChunkAnd then put eachChunkAdd it to the output list as a separate file. This step is the last chance to modify the output
  • Output complete: After determining the output content, determine the output path and file name based on the configuration, and write the file content to the file system

Code implementation

Next, we will explain it point by point according to the above implementation process

Before we write the code, let’s talk about the library that the project will use

@babel/core Babel/presets -env is used to convert ES6 code to ES5 code EJS template file, support JAVASCRIPT tapable in HTML, and preset to release and subscribe mode. Webpack implements the core of wiring plug-ins togetherCopy the code

Yarn Add install it

The directory structure is as follows

So let’s just write webpack.config.js, webpack.config.js, we all know what it does, is the webpack configuration item

// webpack.config.js
const path = require("path");

module.exports = {
  context: process.cwd(), // The current root directory
  mode: "development".// Work mode
  entry: path.join(__dirname, "src/index.js"), // Import file
  output: { // Export file
    filename: "bundle.js".path: path.join(__dirname, "./dist"),},module: {// To load the module conversion loader
    rules: [{test: /\.js$/,
        use: [
          {
            loader: path.join(__dirname, "./loaders/babel-loader.js"),
            options: {
              presets: ["@babel/preset-env"],},},],},plugins: [new RunPlugin(), new DonePlugin()], / / the plugin
};

Copy the code

Step 1: Initialize parameters: from the configuration file andShellStatement read and merge parameters, get the final parameter;

This step is simple to implement

// lib/Compiler.js
class Compiler {}let options = require(".. /webpack.config");

Copy the code

Step 2: Start compiling: initialize with the parameters obtained in the previous stepCompilerClass,

Here we write a Compiler class that stands for Compiler and initializes options as configured in webpack.config.js

//lib/Compiler
class Compiler {
    constructor(options){
        this.options = options; }}let options = require(".. /webpack.config");

Copy the code

Load all configured plugins, and then execute the corresponding parameters at the corresponding stage. For example, at the start of compilation, we execute the runPlugin, and output the start of compilation textrunMethod starts compiling;

Plugins are classes. That’s why plugins are classes in webpack.config.js, and they need to be new because plugins are classes

plugins: [new RunPlugin(), new DonePlugin()], / / the plugin
Copy the code

We have loaded two classes, new RunPlugin, to start compiling and new DonePlugin to finish executing

Each plug-in must have an apply method that registers the plug-in, and then pass an instance of the Compiler to the plug-in. The plug-in can then listen for the RUN hook

Does this sound familiar? Yes, this is the publishing subscriber model. How does the publishing subscriber model work in Webpack?

// plugins/RunPlugin
module.exports = class RunPlugin {
// Register the plug-in
    apply(compiler) {
        compiler.hooks.run.tap("RunPlugin".() = > {
            console.log("RunPlugin"); }); }};Copy the code

We’re going to talk about a library, Tapable, which is very important, because tapable is the reason webPack is able to string plug-ins together,

Here is a simple way to use the following, small partners if very interested, recommended to see juejin.cn/post/693979… Miss Jiang’s article, more usage methods of tapable

let { SyncHook } = require("tapable");
let hook = new SyncHook();
/ / to monitor
hook.tap("some name".() = > {
    console.log("some name");
});
/ / triggers
hook.call();
Copy the code

Determine the entry: according to the configurationentryFind all the entry files

Remember that in React, hooks are defined from the hooks that start with run and end with done. Remember that in React, plugins are defined from the hooks that start with run and end with done. As long as WebPack executes the corresponding lifecycle function, the code block in the lifecycle function in the plug-in executes

// Use synchronous hooks
let { SyncHook } = require("tapable");

class Compiler {

  constructor(options) {
    
    this.options = options;
    // Define two hooks
    this.hooks = {
         run: new SyncHook(),
         done: new SyncHook(),
    };
  }
    run() {
        this.hooks.run.call(); // Triggers the run hook to execute
        let entry = path.join(this.options.context, this.options.entry); }}let options = require(".. /webpack.config");
let compiler = new Compiler(options);
if (options.plugins && Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
        // Invoke the plug-in's registration methodplugin.apply(compiler); }}Copy the code

We can put the new Compiler section in a separate folder, webpack.js

// bin/webpack.js
const Compiler = require(".. /lib/Compiler");

// 1. Obtain the package configuration
const config = require(".. /webpack.config");

// 2. Create a Compiler instance

const createCompiler = function () {
  // Create the Compiler instance
  const compiler = new Compiler(config);
  // Load the plug-in
  if (Array.isArray(config.plugins)) {
    for (const plugin ofconfig.plugins) { plugin.apply(compiler); }}return compiler;
};

const compiler = createCompiler();
// 3. Enable compilation
compiler.run();
Copy the code

Package. json configures build to point to webpack.js

"scripts": {
    "build": "node bin/webpack.js"
},
Copy the code

From the entry file, invoke all configuredLoaderTo compile the module,


run(){
        this.hooks.run.call(); // Triggers the run hook to execute
        let entry = path.join(this.options.context, this.options.entry);
        // Building blocks
        this.buildModule(entry, true);
}

Copy the code

We know that Webpack has module, chunk and file concepts

file>chunk>module

For time reasons, we will only demonstrate this through Module

In the constructor we initialize modules to hold the module

 constructor(options) {
    this.options = options;
    this.modules = [];
    this.hooks = {
      run: new SyncHook(),
      done: new SyncHook(),
    };
  }
Copy the code

Build the module and perform breadth-first traversal of all dependent submodules

Read the source code


buildModule(modulePath, isEntry) {
    // Module source code
    const source = this.getSource(modulePath);
}
Copy the code

Module source code we need to compile by loader

So write a loader and configure it in webpack.config.js

module: {
    rules: [{test: /\.js$/,
        use: [
          {
            loader: path.join(__dirname, "./loaders/babel-loader.js"),
            options: {
              presets: ["@babel/preset-env"],},},],},Copy the code

Babel-loader. js is a very representative loader, so we’ll write this loader

Select presets from babel-loader to load presets from babel-loader

loader/babel-loader.js
const babel = require("@babel/core");

const loader = function (source, options) {
  let result = babel.transform(source, {
    presets: options.presets,
  });
  return result.code;
};

module.exports = loader;
Copy the code

Then our getSource will be written, returning the compiled source code

getSource(modulePath) {
    // Read the contents of the file
    let content = fs.readFileSync(modulePath, "utf-8");
    const rules = this.options.module.rules;
    for (let rule of rules) {
      const { test, use } = rule;
      if (test.test(modulePath)) {
        // Recurse all loaders
        // use is treated as an array, executed from right to left
        let length = use.length - 1;
        function loopLoader() {
          // Execute from right to left
          const { loader, options } = use[length--];
          let loaderFunc = require(loader);
          // loader is a function
          content = loaderFunc(content, options);
          if (length >= 0) { loopLoader(); }}if (length >= 0) { loopLoader(); }}}return content;
  }
Copy the code

Find the module that the module depends on, and then recurse this step until all the entry dependent files have been processed by this step;

How do we find out the module dependent module, we will use the ast (abstract syntax tree), can depend upon with convenient access to the module, this is the first point we have to do, and one more thing to do is we load module inside the path is a relative path, so we also want to hand in the path, how to get the relative path parsing module

 // Build the module and do breadth-first traversal of all dependent submodules
  buildModule(modulePath, isEntry) {
    // Module source code
    const source = this.getSource(modulePath);
    // Replace is compatible with Windows
    modulePath =
      ". /" + path.relative(this.root, modulePath).replace(/\\/g."/");
    const { sourceCode, dependencies } = this.parse(source, modulePath);
    // Here is a complete module, save to modules
    this.modules[modulePath] = JSON.stringify(sourceCode);
    // Get all module dependencies recursively and save all paths and dependent modules
    dependencies.forEach((d) = > {
      this.buildModule(path.join(this.root, d));
    }, false);
  }

Copy the code

The corresponding parsing code is

 // Parse according to module source code
  parse(source, moduleName) {
    let dependencies = [];
    const dirname = path.dirname(moduleName);
    const requirePlugin = {
      visitor: {
        // replace require with __webpack_require__
        CallExpression(p) {
          const node = p.node;
          if (node.callee.name === "require") {
            node.callee.name = "__webpack_require__";
            // Path replacement
            let modulePath = node.arguments[0].value;
            modulePath =
              ". /" + path.join(dirname, modulePath).replace(/\\/g."/"); node.arguments = [t.stringLiteral(modulePath)]; dependencies.push(modulePath); ,}}}};let result = babel.transform(source, {
      plugins: [requirePlugin],
    });
    return {
      sourceCode: result.code,
      dependencies,
    };
  }
Copy the code

Output resources: Assembled into modules based on the dependencies between entry and moduleChunkAnd then put eachChunkAdd it to the output list as a separate file. This step is the last chance to modify the output

Because of time constraints, we are not going to put together chunks files.

Now that we’ve done 90% of our work, we’re ready to package up the files, refine our run method,

Output complete: After determining the output content, determine the output path and file name based on the configuration, and write the file content to the file system

run() {
    this.hooks.run.call();
    const entry = this.options.entry;
    this.buildModule(entry, true);
    const outputPath = path.resolve(this.root, this.options.output.path);
    const filePath = path.resolve(outputPath, this.options.output.filename);
    // Output file
    this.mkdirp(outputPath, filePath);
  }
Copy the code

Let me write mkdirp

 mkdirp(outputPath, filePath) {
    console.log("Simple-webpack ------------------> File output");
    const { modules, entryPath } = this;
    // Create a folder
    if(! fs.existsSync(outputPath)) { fs.mkdirSync(outputPath); } ejs .renderFile(path.join(__dirname,"Template.ejs"), { modules, entryPath })
      .then((code) = > {
        fs.writeFileSync(filePath, code);
        console.log("Simple-webpack ------------------> Package complete");
      });
  }
Copy the code

Ejs templates for

(function (modules) {
  var installedModules = {};

  function __webpack_require__(moduleId) {
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }

    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false.exports: {}}; modules[moduleId].call(module.exports, module.module.exports, __webpack_require__);
    module.l = true;
    return module.exports;
  }

  return __webpack_require__("<%-entryPath%>"); < % ({})for (const key in modules) {%>
      "<%-key%>":
      (function (module.exports, __webpack_require__) {
          eval(<%-modules[key]%>); }}), < % % >});Copy the code

Ok, finally done, execute the package command, NPM run build

This is not similar to the require source code we wrote before, about the require source code can refer to the article I wrote before the require source code (small white level tutorial), from here we can see that Webpack implements its own modular loading mode, To solve the problem of communication between modules

(function (modules) {
  var installedModules = {};

  function __webpack_require__(moduleId) {
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }

    var module = (installedModules[moduleId] = {
      i: moduleId,
      l: false.exports: {},}); modules[moduleId].call(module.exports,
      module.module.exports,
      __webpack_require__
    );
    module.l = true;
    return module.exports;
  }

  return __webpack_require__("./src/index.js"); ({})"./src/index.js": function (module.exports, __webpack_require__) {
    eval(
      '"use strict"; \n\nvar _app = __webpack_require__("./src/app.js"); \n\nconsole.log(_app.a); '
    );
  },

  "./src/app.js": function (module.exports, __webpack_require__) {
    eval(
      '"use strict"; \n\nObject.defineProperty(exports, "__esModule", {\n value: true\n}); \nexports.a = void 0; \nvar a = "app"; \nexports.a = a; '); }});Copy the code

Then we reference the bundle.js file we packaged with index.html to see if the index.html file fails

<! DOCTYPEhtml>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
    <title>Document</title>
  </head>
  <body></body>
  <script src="./dist/bundle.js"></script>
</html>

Copy the code

When we open the browser, we see exactly what we want, and we’re done

If you have any questions, please leave a message in the comments section.

conclusion

According to the beginning of the Webpack implementation process, we handwritten a Webpack source code, through handwritten source code, we deepened the understanding of the implementation process of Webpack, you friends must be more hands-on writing

Reference:

Write a webpack by hand and see how the AST works

simple-webpack