Super simple Webpack implementation, understand the core principle of Webpack

First give the project address: github.com/luch1994/mi…

In the demo folder, there is an index.js, an add.js, and a toint. js, which are respectively as add.js

import { toInt } from "./toInt.js";
export const add = (a, b) = > toInt(a) + toInt(b);
Copy the code

toInt.js

export const toInt = num= > parseInt(num);
Copy the code

index.js

import { add } from "./add.js";
const res = add("1"."5");
document.querySelector("#app").innerHTML = `<h1>hahha${res}</h1>`
Copy the code

Our goal is to package the above two interdependent JS files into a single JS file (bundle.js) that can be run in the browser.

The overall process is as follows

  1. Read the entry file for module analysis
  2. Parsing recursively retrieves all dependencies and compiles all collected dependencies into code that the browser can run
  3. Consolidate code to generate code files that the browser can run

1. Module analysis

The analysis of the module is equivalent to parsing the code string of the file read, and this step is actually the same as the compilation process of high-level languages. We do this with Babel/Parser, which parses the module into an abstract syntax tree, AST. The installation package

npm i @babel/parser @babel/traverse @babel/core @babel/preset-env 
Copy the code

Read JS, collect entry js all dependent modules, compile with AST parsing

const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");

module.exports = function (file) {
    const dirname = path.dirname(file);
    / / read js
    const body = fs.readFileSync(file, "utf-8");
    // Parse the AST syntax tree
    const ast = parser.parse(body, {
        sourceType: "module".// To parse the esModule
    });
    console.log(ast.program.body); 
}
Copy the code

The ast. Program. body is printed as follows, containing the code information, such as the ImportDeclaration for type, the import statement, and the source for the module it depends on

2. Analyze and collect dependencies

We use traverse to traverse our parsed tree. The ImportDeclaration of the second argument means that all nodes whose type is ImportDeclaration will enter the function. With traverse we can get all the modules that the entry file depends on

const deps = {};
traverse(ast, {
    ImportDeclaration({ node }) {
        // All import nodes enter here
        // Get the absolute path
        constabspath = path.join(dirname, node.source.value); deps[node.source.value] = abspath; }});Copy the code

The dePS obtained are as follows:

Babel/PRESET -env (presets, presets, presets, presets, presets) There are also @babel/preset-typescript for typescript and @babel/preset-react for react, not to be stressed

const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]});Copy the code

The code you got is as follows

At this point, we have the dependency on dePS for the entry file and the code transformed by Babel

The final parseModule looks like this

const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");

module.exports = function (file) {
    const dirname = path.dirname(file);
    const body = fs.readFileSync(file, "utf-8");
    // Parse the AST syntax tree
    const ast = parser.parse(body, {
        sourceType: "module".// To parse the esModule
    });

    // Traverse ast syntax tree with traverse to collect all dependencies of the current file (import)
    const deps = {};
    traverse(ast, {
        ImportDeclaration({ node }) {
            // All import nodes enter here
            // Get the absolute path
            constabspath = path.join(dirname, node.source.value); deps[node.source.value] = abspath; }});const { code } = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"]});// Import becomes require

    return {
        file,
        deps,
        code,
    }
}
Copy the code

According to the above code and deps, we can see that the transformed code, import becomes require, the import file depends on DEps, key is the value of require, value is the path of the file, we can according to deps, Further collect demo/index.js dependencies and codes

const getModuleInfo = require("./getModuleInfo.js");
module.exports = function (file) {
    const info = getModuleInfo(file);
    const temp = [info];
    getDeps(temp, info.deps);
    console.log(temp);
}

function getDeps(temp, deps) {
    Object.keys(deps).forEach(key= > {
        constdepInfo = getModuleInfo(deps[key]); temp.push(depInfo); getDeps(temp, depInfo.deps); })}Copy the code

The temp obtained is as follows

We convert the data to an object with file path key

const depsGraph = {};
for (const moduleInfo of temp) {
  depsGraph[moduleInfo.file] = {
    deps: moduleInfo.deps,
    code: moduleInfo.code,
  };
}
console.log(depsGraph);
Copy the code

At this point, we have all files corresponding to the browser code, but we can see that the above code package includes require and exports, the browser environment, there are no such variables, can we customize these variables? The answer is yes

3. Integrate code to generate browser-executable files

3.1 Implement the require function

For the depsGraph we got above, we’re going to eventually turn it into browser-executable file code

  1. We’ll start by defining a require function that takes a file argument and starts by executing the require entry file

    <! DOCTYPEhtml>
    <html>
    
    <head>
        <title>test</title>
    </head>
    
    <body>
        <div id="app"></div>
        <script>
             var depsGraph = {
              './demo/index.js': {
                deps: { './add.js': 'demo/add.js' },
                code: '"use strict"; \n' +
                  '\n' +
                  'var _add = require("./add.js"); \n' +
                  '\n' +
                  'var res = (0, _add.add)("1", "5"); \n' +
                  'document.querySelector("#app").innerHTML = "

    hahha".concat(res, "

    "); '
    }, 'demo/add.js': { deps: { './toInt.js': 'demo/toInt.js' }, code: '"use strict"; \n' + '\n' + 'Object.defineProperty(exports, "__esModule", {\n' + ' value: true\n' + '}); \n' + 'exports.add = void 0; \n' + '\n' + 'var _toInt = require("./toInt.js"); \n' + '\n' + 'var add = function add(a, b) {\n' + ' return (0, _toInt.toInt)(a) + (0, _toInt.toInt)(b);\n' + '}; \n' + '\n' + 'exports.add = add; ' }, 'demo/toInt.js': { deps: {}, code: '"use strict"; \n' + '\n' + 'Object.defineProperty(exports, "__esModule", {\n' + ' value: true\n' + '}); \n' + 'exports.toInt = void 0; \n' + '\n' + 'var toInt = function toInt(num) {\n' + ' return parseInt(num);\n' + '}; \n' + '\n' + 'exports.toInt = toInt; '}};function require(file) { console.log(file); console.log(depsGraph[file]) eval(depsGraph[file].code); } require("./demo/index.js"); i.
    </script> </body> </html> Copy the code

DepsGraph (‘./add.j ‘) does not have a./add.j key. We also need to make some changes to the require function and do a path conversion

function outerRequire(entry) {
  function require(file) {
    var relKey = curData.deps[file];
    outerRequire(relKey);
  }
  var curData = depsGraph[entry];
  eval(depsGraph[entry].code);
}
outerRequire("./demo/index.js");
Copy the code

Add.js: exports is not defined, so I am going to define an exports variable

function outerRequire(entry) {
  console.log(entry);
  var curData = depsGraph[entry];
  function require(file) {
    var relKey = curData.deps[file];
    return outerRequire(relKey);
  }
  var exports = {};
  console.log(`${entry}Before performing `);
  eval(depsGraph[entry].code);
  console.log(`${entry}After performing `);
  return exports;
}
outerRequire("./demo/index.js");
Copy the code

The result is as follows

The exports object did not return

function outerRequire(entry) {
  var curData = depsGraph[entry];
  function require(file) {
    var relKey = curData.deps[file];
    return outerRequire(relKey);
  }
  var exports = {};
  console.log(`${entry}Before performing `);
  eval(depsGraph[entry].code);
  console.log(`${entry}After performing `);
  return exports;
}
outerRequire("./demo/index.js");
Copy the code

After the modification, the browser works properly

So our require function is done, and we’re going to generate the file

3.2 Generating Files

Let’s clean up the code, splicing the code together to generate js files, and now we’ll permanently write the dead file to the bundle.js file in the dist directory

The index.js file is implemented as follows

const parseModules = require("./parseModules.js");
const fs = require("fs");
const path = require("path");
module.exports = function (file) {
    const res = bundle(file);
    const dirExist = fs.existsSync("./dist");
    if(! dirExist) { fs.mkdirSync("./dist");
    }
    fs.writeFileSync("./dist/bundle.js", res, {
        encoding: "utf-8"})}function bundle(file) {
    const depsGraph = parseModules(file);
    return `
    var depsGraph = The ${JSON.stringify(depsGraph)};
    function outerRequire(entry) {
        var curData = depsGraph[entry];
        function require(file) {
          var relKey = curData.deps[file];
          return outerRequire(relKey);
        }
        var exports = {};
        eval(depsGraph[entry].code);
        return exports;
    }
    outerRequire("${file}");
    `;
}
Copy the code

Then we write pack.js

const miniPack = require("./src/index.js");
miniPack("./demo/index.js");
Copy the code

Running pack. Js

node pack.js
Copy the code

You can normally generate bundle.js from the dist directory

The browser also opens HTML as expected

<! DOCTYPEhtml>
<html>

<head>
    <title>test</title>
</head>

<body>
    <div id="app"></div>
    <script src=".. /dist/bundle.js"></script>
</body>

</html>
Copy the code

At this point, the core implementation of WebPack is complete

thinking

Webpack supports import of different file types, and different file types have corresponding loader to handle, its essence is also to import modules into a runnable JS file, such as a style file, which is a SECTION of JS to create style labels, how to handle the specific module depends on the specific Loader code. But the end result is a runnable PIECE of JS code

Github address: github.com/luch1994/mi…