This is the sixth day of my participation in the August More text Challenge. For details, see: August More Text Challenge

Preface 🥝

As we all know, WebPack is a packaging tool. It is also a packaging tool that was generated after a series of code was written before we configured it. So what’s going on behind the scenes?

Today, we’re going to use native JS to write a simple packaging tool called Bundler to package your project code.

Let’s start with the explanation of this article

🍉 i. Module analysis (entry file code analysis)

1. Project structure

Let’s take a look at our project file structure. See the picture below 👇

2. Install third-party dependencies

We need to use four third-party dependencies, which are:

  • Babel/Parser — Helps analyze source code and generate abstract syntax trees (AST);
  • @babel/traverse – Helps us traverse abstract syntax trees and parse the statements in the syntax trees;
  • Babel /core – Package raw code into code that the browser can run;
  • @babel/preset-env — used for configuration when the abstract syntax tree is resolved.

The following commands are given to install the four libraries in sequence:

(1) @ Babel/parser

npm install @babel/parser --save
Copy the code

(2) @ Babel/traverse by

npm install @babel/traverse --save
Copy the code

(3) @ Babel/core

npm install @balbel/core --save
Copy the code

(4) @ Babel/preset – env

npm install @babel/preset-env --save
Copy the code

3. Business code

When we go to do a project package, we first need to analyze the modules in the project, now we first analyze the entry file. Suppose we want to implement a business and the output is Hello Monday. So let’s write our business code first.

Step 1: Write the code for the word.js file. The specific code is as follows:

export const word = 'monday';
Copy the code

Step 2: Code the message.js file. The specific code is as follows:

import { word } from './word.js';

const message = `hello ${word}`;

export default message;
Copy the code

Step 3: Code the index.js file. The specific code is as follows:

import message from "./message.js";

console.log(message);
Copy the code

4. Start packing

After writing the business code, we now need to package the entry file index.js. Note that we don’t use any tools here except Babel, no webpack, no webpack-CLI, etc.

We’ll start by creating a file in the root directory, called bundler. Js, and use this file to write our packaging logic. The specific code is as follows:

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

const moduleAnalyser = (filename) = > {
    //1. Get the file name first. After getting the file name, we read the contents of the file
    const content = fs.readFileSync(filename, 'utf-8');
    //2. Convert the js string in the file into a JS object with the help of Babel-Parser -> This JS object is called abstract syntax tree
    const ast = parser.parse(content, {
        // 3. If you pass in ES6 syntax, set sourceType to Module
        sourceType: 'module'
    });

    // Collect the dependent files in the entry file
    const dependencies = {};

    traverse(ast, {
        /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Use the @babel/traverse tool, which shows that when the abstract syntax tree has a statement like ImportDeclaration, it will continue with the following function */
        ImportDeclaration({ node }) {
            // console.log(node);
            const dirname = path.dirname(filename);
            const newFile = '/' + path.join(dirname, node.source.value);
            // console.log(newFile);
            //6. Assemble the import statements into an object named Dependencies (store them in key-value pairs)dependencies[node.source.value] = newFile; }});/* 7. Compile the source code of the module. By using transformFromAst, we transform it from an ES Module into a syntax that the browser can execute and store it in code, which generates code that we can run in the browser */
    const { code } = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"]})return {
        // Return the name of the entry file
        filename,
        // Return the dependent files in the entry file
        dependencies,
        // Return code that can be run on the browser
        code
    }
    // console.log(dependencies);
}

const moduleInfo = moduleAnalyser('./src/index.js'); 
console.log(moduleInfo);
Copy the code

Through the above code, I believe you have a basic understanding of the package entry file. After that, you can run the Node bundler. Js command on the console to see the various analyses in the packaging process.

Let’s move on to the second section

🥑 Dependencies Graph

For all that, we’re just talking about analyzing an entry file. But it’s not enough. So, now we are going to analyze the entire project document.

1. Result analysis

Let’s first look at the print in the above code when we analyze only the entry file. The specific code is as follows:

{
  filename: './src/index.js',
  dependencies: { './message.js': './src\\message.js' },
  code: '"use strict"; \n' + '\n' + 'var _message = _interopRequireDefault(require("./message.js")); \n' + '\n' + 'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : {"default": obj }; }\n' +
    '\n' +
    'console.log(_message["default"]);'
}
Copy the code

As you can see, once the entry file is analyzed, there are layers and layers of dependencies and code. Now, we need to follow these dependencies to figure out the content of the project.

2. Analyze the dependencies of all modules

Now let’s upgrade Bundler. Js to map out the dependencies of all modules. The specific code is as follows:

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

const moduleAnalyser = (filename) = > {
    //1. Get the file name first. After getting the file name, we read the contents of the file
    const content = fs.readFileSync(filename, 'utf-8');
    //2. Convert the js string in the file into a JS object with the help of Babel-Parser -> This JS object is called abstract syntax tree
    const ast = parser.parse(content, {
        // 3. If you pass in ES6 syntax, set sourceType to Module
        sourceType: 'module'
    });

    // Collect the dependent files in the entry file
    const dependencies = {};

    traverse(ast, {
        /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Use the @babel/traverse tool, which shows that when the abstract syntax tree has a statement like ImportDeclaration, it will continue with the following function */
        ImportDeclaration({ node }) {
            // console.log(node);
            const dirname = path.dirname(filename);
            const newFile = '/' + path.join(dirname, node.source.value);
            // console.log(newFile);
            //6. Assemble the import statements into an object named Dependencies (store them in key-value pairs)dependencies[node.source.value] = newFile; }});/* 7. Compile the source code of the module. By using transformFromAst, we transform it from an ES Module into a syntax that the browser can execute and store it in code, which generates code that we can run in the browser */
    const { code } = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"]})return {
        // Return the name of the entry file
        filename,
        dependencies,
        code
    }
    // console.log(dependencies);
}

const makeDependenciesGraph = (entry) = > {
    //1. Perform an analysis on the entry module
    const entryModule = moduleAnalyser(entry);
    // console.log(entryModule);

    //2. Define an array to hold entry files and dependencies
    const graphArray = [ entryModule ];
    //3. Iterate over the graphArray
    for(i = 0; i < graphArray.length; i++){
        //4. Take out each term in the graphArray
        const item = graphArray[i];
        //5. Select dependencies for each item
        const { dependencies } = item;
        //6. Loop over dependencies if the entry file has dependencies
        if(dependencies) {
            /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
            for(let j in dependencies) {
                /*8. Recursion by queue (first in, first out); Why recursion? The analysis is done recursively because there may be dependencies */ below each dependency
                graphArray.push(
                    moduleAnalyser(dependencies[j])
                )
                
            }
        }
    }
    //9. The graphArray is an array. Now you need to convert it to a format
    const graph = {};
    graphArray.forEach(item= > {
        graph[item.filename] = {
            dependencies: item.dependencies,
            code: item.code
        }
    });
    
    return graph;
}

// './ SRC /index.js' is the entry file
const graphInfo = makeDependenciesGraph('./src/index.js'); 
console.log(graphInfo);
Copy the code

As you can see, we created a new function, makeDependenciesGraph, to describe the dependencies of all the modules, and finally converted it into our ideal JS object. Now, let’s look at the printout of the dependencies. The print result is as follows:

{
  './src/index.js': {
    dependencies: { './message.js': './src\\message.js' },
    code: '"use strict"; \n' + '\n' + 'var _message = _interopRequireDefault(require("./message.js")); \n' + '\n' + 'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : {"default": obj }; }\n' +
      '\n' +
      'console.log(_message["default"]); ' }, './src\\message.js': { dependencies: { './word.js': './src\\word.js' }, code: '"use strict"; \n' + '\n' + 'Object.defineProperty(exports,"__esModule", {\n' +
      '  value: true\n' + '}); \n' + 'exports["default"] = void 0; \n' + '\n' + 'var _word = require("./word.js"); \n' + '\n' + 'var message ="hello ".concat(_word.word); \n' + 'var _default = message; \n' + 'exports["default"] = _default; ' }, './src\\word.js': { dependencies: {}, code: '"use strict"; \n' + '\n' + 'Object.defineProperty(exports,"__esModule", {\n' +
      '  value: true\n' + '}); \n' + 'exports.word = void0; \n' +"var word = 'monday'; \n"+ 'exports.word = word; '}}Copy the code

As you can see, all the dependencies of the modules have been traversed. So that means that we have succeeded in this step of analysis.

🍐 3. Generate code

1. Logic writing

Now that we’ve successfully generated the dependency graph, let’s take the dependency graph and generate code that actually runs in the browser. Let’s go ahead and write a code generation logic on bundle.js. The specific code is as follows:

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

const moduleAnalyser = (filename) = > {
    //1. Get the file name first. After getting the file name, we read the contents of the file
    const content = fs.readFileSync(filename, 'utf-8');
    //2. Convert the js string in the file into a JS object with the help of Babel-Parser -> This JS object is called abstract syntax tree
    const ast = parser.parse(content, {
        // 3. If you pass in ES6 syntax, set sourceType to Module
        sourceType: 'module'
    });

    // Collect the dependent files in the entry file
    const dependencies = {};

    traverse(ast, {
        /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Use the @babel/traverse tool, which shows that when the abstract syntax tree has a statement like ImportDeclaration, it will continue with the following function */
        ImportDeclaration({ node }) {
            // console.log(node);
            const dirname = path.dirname(filename);
            const newFile = '/' + path.join(dirname, node.source.value);
            // console.log(newFile);
            //6. Assemble the import statements into an object named Dependencies (store them in key-value pairs)dependencies[node.source.value] = newFile; }});/* 7. Compile the source code of the module. By using transformFromAst, we transform it from an ES Module into a syntax that the browser can execute and store it in code, which generates code that we can run in the browser */
    const { code } = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"]})return {
        // Return the name of the entry file
        filename,
        dependencies,
        code
    }
    // console.log(dependencies);
}

const makeDependenciesGraph = (entry) = > {
    //1. Perform an analysis on the entry module
    const entryModule = moduleAnalyser(entry);
    // console.log(entryModule);

    //2. Define an array to hold entry files and dependencies
    const graphArray = [ entryModule ];
    //3. Iterate over the graphArray
    for(i = 0; i < graphArray.length; i++){
        //4. Take out each term in the graphArray
        const item = graphArray[i];
        //5. Select dependencies for each item
        const { dependencies } = item;
        //6. Loop over dependencies if the entry file has dependencies
        if(dependencies) {
            /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
            for(let j in dependencies) {
                /*8. Recursion by queue (first in, first out); Why recursion? The analysis is done recursively because there may be dependencies */ below each dependency
                graphArray.push(
                    moduleAnalyser(dependencies[j])
                )
                
            }
        }
    }
    //9. The graphArray is an array. Now you need to convert it to a format
    const graph = {};
    graphArray.forEach(item= > {
        graph[item.filename] = {
            dependencies: item.dependencies,
            code: item.code
        }
    });
    
    return graph;
}

const generateCode = (entry) = > {
    //1. Convert the generated dependency graph into a format
    const graph = JSON.stringify(makeDependenciesGraph(entry));
    Dependencies [relative]); /** * 2. Create a function called require(graph[module]. Dependencies [relative])
    return `
        (function(graph){
            function require(module){
                function localRequire(relativePath) {
                    return require(graph[module].dependencies[relativePath])
                }
                var exports = {};
                (function(require, exports, code){
                    eval(code)
                })(localRequire, exports, graph[module].code);
                return exports;
            };
            require('${entry}')
        })(${graph});
    `;
}

// './ SRC /index.js' is the entry file
const code = generateCode('./src/index.js'); 
console.log(code);
Copy the code

As you can see from the code above, we first format the generated dependency graph, then construct the require function and exports object, and finally convert it into a string that the browser knows.

2. Result analysis

Through the above business writing, we completed the process of packaging the entire project. Now, let’s look at the printout:

(function(graph){
            function require(module){
                function localRequire(relativePath) {
                    return require(graph[module].dependencies[relativePath])
                }
                var exports = {};
                (function(require, exports, code){
                    eval(code)
                })(localRequire, exports, graph[module].code);
                return exports;
            };
            require('./src/index.js')
        })({"./src/index.js": {"dependencies": {"./message.js":"./src\\message.js"},"code":"\"use strict\"; \n\nvar _message = _interopRequireDefault(require(\"./message.js\")); \n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_message[\"default\"]);"},"./src\\message.js": {"dependencies": {"./word.js":"./src\\word.js"},"code":"\"use strict\"; \n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n}); \nexports[\"default\"] = void 0; \n\nvar _word = require(\"./word.js\"); \n\nvar message = \"hello \".concat(_word.word); \nvar _default = message; \nexports[\"default\"] = _default;"},"./src\\word.js": {"dependencies": {},"code":"\"use strict\"; \n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n}); \nexports.word = void 0; \nvar word = 'monday'; \nexports.word = word;"}});
Copy the code

Next, we take this print and put it in the browser to check. The test results are as follows:

As you can see, the result of the package runs successfully in the browser, and it says hello Monday, so at this point, our project is packaged successfully.

🍓 4. Conclusion

In the previous article, we looked at the entire operation of the packaging tool, from module entry file analysis, to dependency graph parsing, to generating code that the browser knows.

This is the end of this article! Hope to help you ~

If the article is wrong or have not understood the place, welcome small partners to leave a message in the comment area ~💬

🐣 Easter Eggs One More Thing

(: Recommended in the past

Webpack entry core knowledge 👉 ten thousand words summary webPack ultra entry core knowledge

Webpack entry advanced knowledge 👉 ten thousand words summary Webpack entry advanced knowledge

Webpack actual case configuration 👉 ten thousand words summary webpack actual case configuration

Handwritten Loader and Plugin 👉 Webpack actual handwritten loader and plugin

(: External chapter

  • Follow the public account Monday research, the first time to pay attention to quality articles, more selected columns for you to unlock ~

  • If this article is useful to you, make sure you leave footprints before you go

  • That’s all for this article! See you next time! 👋 👋 👋