The output

First create a new project and install dependencies

npm init -y
npm install --save-dev webpack
npm install --save-dev webpack-cli
Copy the code

Create new files SRC /index.js and SRC /hello.js with index.js as the default entry file:

const sayHello = require('./hello')
console.log(sayHello('nick'))
Copy the code

hello.js

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

Run NPX webpack –mode=development from the command line to open the dist/main.js compilation file:

*/ /******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ({ /***/ "./src/hello.js": /*! * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./src/hello.js ***! \**********************/ /***/ ((module) => { eval("module.exports = function(name) {\r\n return 'hello' + name\r\n}\n\n//# sourceURL=webpack://commonJS/./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\")\r\nconsole.log(sayHello('nick'))\n\n//# sourceURL=webpack://commonJS/./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’s actually an IIFE wrapped around it (call function expression now)

(() => { var __webpack_modules__ = ({... }) var __webpack_module_cache__ = {}; function __webpack_require__(moduleId){... } var __webpack_exports__ = __webpack_require__("./src/index.js"); }) ()Copy the code

That’s the core code

  • The packaging result of webpack is an IIFE, called WebPack Bootstrap
  • Webpack_modules is a module-loading function that defines two functions, which are our packaged files:./src/hello.js:(() => {})and./src/index.js:(() => {})
  • Defines the Webpack_module_cache cache object
  • The webpack_require function takes the path of an entry file as an argument and determines whether the parameter is in the cache object. If so, it returns the value in the cache object. If not, write the parameter to the cache object first and then create a new onemoduleObject and executewebpack_modulesFunction, which actually calls webpack_modules./src/index.js:(() => {})
The working process

  • Entry - options startFirst webPack reads the configuration file in the projectwenpack.config.jsOr get the necessary parameters from shell statements, which is how Webapck receives business information from within.
  • The run is instantiatedCompiler initializes the Compiler object with the parameters obtained in the previous step, loads the configured plug-in, and executes the object’srunMethod starts compiling
  • entryDetermine entry: Locate all entry files according to the entry in the configuration
  • makeCompile module: from the entry file, call all configuredloaderTranslate the module, recursively find the module that the module depends on,
  • build moduleComplete module compilation: After using loader to translate all modules above, obtain the final content of each module after translation and the dependencies between them.
  • sealOutput resources are assembled into chunks containing multiple modules according to the dependency between the entry and modules, and then each chunk is converted into a separate file and added to the output list.
  • emitOutput complete: After determining the output content, determine the output path and file name based on the configuration, and write the file content to the system file
Abstract syntax tree

In computer science, Abtract Syntax Tree (AST) is an abstract representation of the Syntax structure of source code. It represents the syntax structure of a programming language in the form of a tree, each node in the tree represents a structure and source code

The syntax is abstract because it does not represent every detail that occurs in real grammar.

The purpose of WebPack’s conversion of files to AST is to make it easy for developers to extract key information from module files so that we know exactly what the developer has written and can analyze and extend it based on that writing.

Can code on https://esprima.org/demo/parse.html# parsed into the AST tree.

var answer = 6 * 7;
Copy the code
{
  "type": "Program"."body": [{"type": "VariableDeclaration"."declarations": [{"type": "VariableDeclarator"."id": {
            "type": "Identifier"."name": "answer"
          },
          "init": {
            "type": "BinaryExpression"."operator": "*"."left": {
              "type": "Literal"."value": 6."raw": "6"
            },
            "right": {
              "type": "Literal"."value": 7."raw": "Seven"}}}]."kind": "var"}]."sourceType": "script"
}
Copy the code
The compiler and compilation

Compiler and Compilation are two of the most important concepts in the core principles of WebPack. They are the basis for understanding how WebPack works, loaders, and plug-ins.

  • Compiler object: Its instance contains the complete WebPack configuration, and there is only one compiler instance globally, so it is like the skeleton or nerve center of WebPack. When the plug-in is instantiated, it receives a Compiler object that provides access to the internal environment of the WebPack.
  • Compilation object: When Webapck 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. That is, all build data generated during the build process is stored on this object, which controls every part of the build process. The object also provides a number of event callbacks for plug-ins to extend.

The construction process of Webpack is controlled by compiler flow and code parsing is done through compilation. When developing the plug-in, we can get all the content associated with the WebPack main environment, including the event hooks, from the Compiler object

Both the Compiler object and the compilation object inherit from the Tapable library, which exposes all event-related publish-subscribe methods. The tapable library based on event flow in Webpack not only ensures the order of plug-ins, but also makes the whole system more extensible.

Hand write a simple Webpack
Project initialization
mkdir wpk
npm init -y
Copy the code

Create the files SRC /index.js and SRC /greeting.js for our business code to be packaged.

Create the compiler file lib/compiler.js, build the module output file, lib/index.js instantiates the Compiler class, pass in configuration parameters, and lib/parser.js is responsible for parsing.

Create the configuration file wpk.config.js

const path = require("path");
global.filename = path.join(__dirname,'./src')
module.exports = {
  entry: path.join(__dirname, "./src/index.js"),
  output: {
    path: path.join(__dirname, "./dist"),
    filename: "bundle.js",}};Copy the code

Here the entry and exit are defined

At the same time our business code

src/index.js

import { greeting } from "./greeting.js"; Document. Write (greeting(" god covers earth tiger "));Copy the code

src/greeting.js

Export function greeting(name) {return "password:" + name; }Copy the code

The required dependencies of this project

package.json

"Dependencies" : {" @ Babel/preset - env ":" ^ 7.15.6 ", "Babel - core" : "^ 6.26.3", "Babel - preset - env" : "^ 1.7.0", "Babel - traverse by" : "^ 6.26.0", "Babylon" : "^ 6.18.0"}Copy the code

And babelrc.

{
    "presets": [
        "@babel/preset-env"
    ]
}
Copy the code
parsing

The project initialization is complete, and the parse.js writing is completed first.

const fs = require("fs");
const babylon = require("babylon");

module.exports = {

  getAST: (path) = > {
    const source = fs.readFileSync(path, "utf-8");

    return babylon.parse(source,{
        sourceType:'module'})}};Copy the code

Using Babylon, parse the file into an AST tree.

Babylon is the JavaScript parser used in Babel.

Babylon generates an AST based on the format of Babel AST. It is based on the ESTree specification with the following differences (which can now be removed using the ESTree plug-in) :

  • Literal symbols are replaced with strings, numbers, booleans, Null, and regular expressions
  • Property symbols are replaced with ObjectProperty and ObjectMethod
  • Method definitions are replaced with class methods
  • instructionandGrammar blockthedirectivesField contains additionalinstructionandInstruction character set
  • Properties of class methods, object properties, and object method value properties in function expressions are forced/into the main method node.

Create a new test file, SRC /test.js

const path = require("path"); const { getAST} = require('./parser'); let ast = getAST(path.join(__dirname,'.. /src/index.js')) console.log(ast)Copy the code

With the generated AST visible on the command line, use babel-traverse to parse all of the file’s dependencies

getDependencies: (ast) = > {
    const dependencies = [];
    traverse(ast, {
      ImportDeclaration: ({ node }) = >{ dependencies.push(node.source.value); }});return dependencies;
  },
Copy the code

Next, convert the ES6 code to ES5

transform: (ast) => {
    const { code } = transformFromAst(ast, null, {
      presets: ["env"],
    });
    return code;
  },
Copy the code

There are three main methods in parser.js:

  • getAST: Parses the obtained module content intoASTThe syntax tree
  • getDependencies: traversalASTTo collect the dependencies used
  • transform: Take the acquiredES6theASTConverted intoES5
compile

Next, start writing Compiler.js and create the Compiler class to do the following

  • receivewpk.config.jsConfigure the parameters and initialize thementry,output
  • Open the compilationrunMethods. Handles building blocks, collecting dependencies, output files, and so on.
  • buildModuleMethods. Mainly used to build modules (byrunMethod call)
  • emitFilesMethods. Output files (also byrunMethod call)
const path = require("path");
const fs = require("fs");

module.exports = class Compiler {
  constructor(options) {
    const { entry, output } = options;
    this.entry = entry;
    this.output = output;
    this.modules = [];
  }
  // Enable compilation
  run() {}
  // Build module related
  buildModule(filename, isEntry) {
    // filename: indicates the filename
    // isEntry: whether it is an entry file
  }
  // Output file
  emitFiles(){}};Copy the code

New Compiler(options).run(); , initialize entry, output, and modules in the constructor.

Start building modules

 run() {
    const entryModule = this.buildModule(this.entry, true);
    console.log(entryModule)
    this.modules.push(entryModule);
    this.modules.map((_module) = > {
      _module.dependencies.map((dependency) = > {
        this.modules.push(this.buildModule(dependency));
      });
    });
    console.log(this.modules);
  }

  buildModule(filename, isEntry) {
    let ast;
    if (isEntry) {
      ast = getAST(filename);
    } else {
      // const absolutePath = path.join(process.cwd(), './src',filename);
      const absolutePath = path.join(global.filename, filename);
      ast = getAST(absolutePath);
    }

    return {
      filename, // File name
      dependencies: getDependencies(ast), // Dependency list
      transformCode: transform(ast), // The converted code
    };
  }
Copy the code

The buildModule function parses the file into an AST based on the name of the file passed in and returns the built Module, which is essentially an object containing the filename, dependency list, and transformed code.

In the run function, we pass the entry file path defined in the configuration file to buildModule, then store the module built by the entry file into Modules, and start recursively iterating through all the dependencies in the entry file and building them into modules.

Once you have all the modules lists, output the lists as a file. Iterating through the modules list, turning all modules into an anonymous function named with the file name, and passing in an IIFE that mimics the webpack4 output file

emitFiles() {
    const outputPath = path.join(this.output.path, this.output.filename);
    let modules = "";
    this.modules.map((_module) = > {
      modules += ` '${_module.filename}' : function(require, module, exports) {${_module.transformCode}}, `;
    });

    const bundle = `
      (function(modules) {
        function require(fileName) {
          const fn = modules[fileName];
          const module = { exports:{}};
          fn(require, module, module.exports)
          return module.exports
        }
        require('The ${this.entry}') ({})${modules}})
    `;
    // console.log(bundle)
    fs.writeFileSync(outputPath, bundle, "utf-8");
  }
Copy the code

The IIFE output from Webpack4

(function(modules) {// 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; } __webpack_require__(0); })([ /* 0 module */ (function(module, exports, __webpack_require__) { ... }), /* 1 module */ (function(module, exports, __webpack_require__) { ... }), /* n module */ (function(module, exports, __webpack_require__) { ... })]);Copy the code
  • webpackWrap all the modules (easily understood as files) in a function, pass in the default parameters, and put all the modules into an array calledmodulesAnd is represented by the index of the arraymoduleId.
  • willmodulesPass in a self-executing function that contains ainstalledModulesAlready loaded modules and a module loading function, finally loading the entry module and returning.
  • __webpack_require__Check whether the module is loadedinstalledModulesCheck whether it is loaded. If it is loaded, return directlyexportsData, pass without loading the modulemodules[moduleId].call(module.exports, module, module.exports, __webpack_require__)Execute the module and willmodule.exportsTo return.

If you open dist/bundle.js, you can see the packaged file, create dist/index.html, and see the output content of the page.

At present simple Webpack has been written, the fly in the ointment is that the authentic Webpack running commands are Webpack, we also here to improve the next. Add it in package.json

"bin": {
    "wpk": "lib/index.js" // Import file
  },
Copy the code

Run NPM link in the root directory to link the specified file globally, WPK can do the packaging. If you want to use NPM run build, you can also add “build”: “WPK” to the script in package.json.

The complete code