I’m sure you’ve all learned how to use Webpack, but how does it work? When asked how webpack packaging works, what should the interviewer say? Here I draw lessons from The Dell teacher’s tutorial, simple to write a packaging tool, take you to understand the principle of Webpack packaging, no more nonsense, directly start:

——— ——— cut ——— line ———

Part ONE: Analysis of entry files

Create a SRC folder in the root directory to save the files to be packed. Create three files word. Js, message.js, and index.js in the SRC file

// word.js

export const word = 'hello';
Copy the code
// message.js

import { word } from './word';

const message = `say ${word}`;

export default message;
Copy the code
// index.js

import message from './message';

console.log(message);
Copy the code

Bundler.js: bundler.js: bundler.js: bundler.js: bundler.js: bundler.js: bundler.js: bundler.js

The first step in packaging is to analyze the entry module, so we create a function called moduleAnalyser, where our entry file is index.js, so the function takes an entry file as an argument, and we pass in index.js

const moduleAnalyser = fileName= > {
    // Analyze the code here
};

moduleAnalyser('./src/index.js');
Copy the code

Filesync readFileSync readFileSync readFileSync readFileSync readFileSync readFileSync readFileSync

const fs = require('fs');

const moduleAnalyser = fileName= > {
    const content = fs.readFileSync(fileName, 'utf8');
    console.log(content);
};

moduleAnalyser('./src/index.js');
Copy the code

After running Bundler in the console, we can see that the output is the contents of the index file:

Ok, the first step is to get the contents of the entry file. The second step is to get the dependency of the module. In the index.js module, the dependency file is message.js, so we need to extract message.js from the code. This tool provides a parse() method that takes two parameters. The first parameter is the code to parse and the second parameter is the configuration item. Since our code uses ES Module syntax, So the second parameter needs to be configured, and then we print the result for viewing:

const fs = require('fs');
const paser = require('@babel/parser');

const moduleAnalyser = fileName= > {
    const content = fs.readFileSync(fileName, 'utf8');
    console.log(paser.parse(content, { sourceType: "module" }));
};

moduleAnalyser('./src/index.js');
Copy the code

Then execute this code in the console, and you can see that it outputs a very long object:

That’s right! This object is called the AST abstract syntax tree!

In the AST object, there is a program field, and in program, there is a body field, which we need to use. Let’s print the body and optimize our code a little bit:

const fs = require('fs');
const paser = require('@babel/parser');

const moduleAnalyser = fileName= > {
    const content = fs.readFileSync(fileName, 'utf8');
    const ast = paser.parse(content, { sourceType: "module" });
    console.log(ast.program.body);
};

moduleAnalyser('./src/index.js');
Copy the code

Then execute again and see the result:

The first node tells us, “This is an import”, which corresponds to the import message from ‘./message’ line in our index file, which is indeed an import. The second node tells us that “this is an expression”, which corresponds to the console.log(message) line in our index file, and is indeed an expression

So we use Babel parse method, can help us analyze AST abstract syntax tree, through the AST can get the declaration statement, declaration statement placed in the entry file corresponding dependencies, so we use the abstract syntax tree, our JS code into JS objects

So what we’re going to do now is we’re going to get all the dependencies in our code, and we’re going to do that by going through the body object, but instead of going through it by hand, Babel gives us a way to quickly find the import nodes, with @babel/traverse, Then pass through (.default) with traverse method, which receives two arguments: AST abstract syntax tree and configuration item

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

const moduleAnalyser = fileName= > {
    const content = fs.readFileSync(fileName, 'utf8');
    const ast = paser.parse(content, { sourceType: "module" });
    traverse(ast, {
        ImportDeclaration({ node }) {
            console.log(node); }}); }; moduleAnalyser('./src/index.js');
Copy the code

After executing the code in the console, we can see that a new object is output, which is the result of our traversal:

At this point, the code can be optimized again: Declare an empty data, every time when traversing each time met a rely on, we will put the dependence in stored in the array, it is important to note that we don’t have to keep all rely on the information, you can see, the object has a source field, this field has a value field, the value of the value is dependent on the file name, We just need to save the value and finally print out our dependency array (note that there is a little pit here: The path.join() method outputs a “/” on Mac and a “\\” on Windows, so we need to use the re here.)

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

const moduleAnalyser = fileName= > {
    const content = fs.readFileSync(fileName, 'utf8');
    const ast = paser.parse(content, { sourceType: "module" });
    const dependencies = [];
    traverse(ast, {
        ImportDeclaration({ node }) {
            const dirname = path.dirname(fileName);
            const newFile = '/' + path.join(dirname, node.source.value);
            dependencies.push(newFile.replace(/\\/g.'/')); }});console.log(dependencies);
};

moduleAnalyser('./src/index.js');
Copy the code

Then execute the code on the console and see the output:

As you can see from the output, we have successfully parsed out the dependent files in the import file!

But! We can see that the printed here depend on the path is a relative path, is relatively the SRC file, but we are really doing code package, these files can’t be a relative path, must be an absolute path (even relative paths must be relative to the root path can be, so packaging wouldn’t have a problem). So how can this problem be solved? Use path.dirname() to get the relative path of the entry file. Then use path.join() to join the two paths together to get the absolute path that we need to package. If you add a relative path to your dependencies, you need to add both a relative path and an absolute path to your dependencies. < span style = “box-sizing: border-box; color: RGB (74, 74, 74); line-height: 22px; font-size: 14px! Important; word-break: inherit! Important;”

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

const moduleAnalyser = fileName= > {
    const content = fs.readFileSync(fileName, 'utf8');
    const ast = paser.parse(content, { sourceType: "module" });
    const dependencies = {};
    traverse(ast, {
        ImportDeclaration({ node }) {
            const dirname = path.dirname(fileName);
            const newFile = '/' + path.join(dirname, node.source.value);
            dependencies[node.source.value] = newFile.replace(/\\/g.'/'); }});console.log(dependencies);
};

moduleAnalyser('./src/index.js');
Copy the code

Execute the code on the console and view the output:

As you can see, the result changes from the array above to an object without any problems

The next thing you need to do is use Babel to convert, compile, and convert the code into es5 that the browser understands, install @babel/core, and introduce a tool that gives us a method transformFromAst() to convert, which takes three arguments, the first one being ast, The second argument can be null, and the third argument is a configuration item (note that “@babel/preset-env” in the configuration item also needs to be installed manually). This method converts the AST abstract syntax tree into an object and returns an object containing a code that is generated for compilation, The code for the current module can be run directly in the browser.

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

const moduleAnalyser = fileName= > {
    const content = fs.readFileSync(fileName, 'utf8');
    const ast = paser.parse(content, { sourceType: "module" });
    const dependencies = {};
    traverse(ast, {
        ImportDeclaration({ node }) {
            const dirname = path.dirname(fileName);
            const newFile = '/' + path.join(dirname, node.source.value);
            dependencies[node.source.value] = newFile.replace(/\\/g.'/'); }});const { code } = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"]});console.log(code);
};

moduleAnalyser('./src/index.js');
Copy the code

Execute the code on the console and view the output:

As you can see, print out the code is not our code in the SRC directory, before and after the translation of the code, and then put the entrance, relying on a file, and the code above three fields to return, then write here, congratulations to you, our analysis of the entry file code’re done!

Then, we can print out the final result and refine the code a little:

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

const moduleAnalyser = fileName= > {
    const content = fs.readFileSync(fileName, 'utf8');
    const ast = paser.parse(content, { sourceType: "module" });
    const dependencies = {};
    traverse(ast, {
        ImportDeclaration({ node }) {
            const dirname = path.dirname(fileName);
            const newFile = '/' + path.join(dirname, node.source.value);
            dependencies[node.source.value] = newFile.replace(/\\/g.'/'); }});const { code } = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"]});return { fileName, dependencies, code };
};

const moduleInfo = moduleAnalyser('./src/index.js');

console.log(moduleInfo);
Copy the code

Execute the code on the console and view the output:

Ok, no problem!

Up to now, we have done a complete analysis of the code of the entry file and obtained the results of the analysis. At this point, we have completed a third of the progress, isn’t it interesting?

Ok, if you understand all of part 1, let’s move on to part 2. If you don’t understand, please read it again

——— ——— cut ——— line ———

Part two: Generating dependency graphs

As a refresher, in Part 1 we completed a moduleAnalyser function that, when we pass in a file, analyzes the file’s dependencies and source code and returns the result

Above us only on a module are analyzed, in this part, it is our task to analyze all the modules in the whole project, so the next step, we have to analyze other file corresponding module information, one layer analysis, finally put all the modules of information analysis, in order to achieve this effect, we want to write a function, Here you can define a new function that, like the moduleAnalyser above, receives an entry file:

const makeDependenciesGraph = entry= > {
    const entryModule = moduleAnalyser(entry);
    console.log(entryModule);
};

makeDependenciesGraph('./src/index.js');
Copy the code

The result is still the same as before

Obviously, this is not enough, because in this section, we need to analyze not only the entry file, but also the dependent file. Here, we can implement recursion:

  1. Create a new array graphArray and save the entryModule parsed above into the array
  2. The array graphArray is iterated over and the dependency information dependencies are extracted from each item iterated
  3. Check for dependencies dependencies (” for “, “for”, “for”, “for”, “for”, “for”, “for”, “for”, “for”) In traverses, not for
  4. After walking through, the moduleAnalyser method is called again to analyze each item in the dependency information
  5. After analyzing the results, we push the results into the graphArray, and then we achieve the recursive effect

At the end of the first loop, not only the entry file, but also the dependency module has been analyzed. At this point, the graphArray array length has changed, and it will continue to iterate, and then analyze the next dependency. Over and over again, we end up separating the import file, the dependent file, and the dependent file dependencies… Layer by layer all push inside graphArray, the code is as follows:

const makeDependenciesGraph = entry= > {
    const entryModule = moduleAnalyser(entry);
    const graphArray = [entryModule];
    for (let i = 0; i < graphArray.length; i++) {
        const item = graphArray[i];
        const { dependencies } = item;
        if (dependencies) {
            for (let j in dependencies) {
                constresule = moduleAnalyser(dependencies[j]); graphArray.push(resule); }}}console.log(graphArray);
};

makeDependenciesGraph('./src/index.js');
Copy the code

Execute the code on the console, print out the graphArray array, and view the print result:

As you can see, the information in each file is perfectly analyzed!

Here array graphArray, can be used as a dependency graph, it can accurately reflect the dependency information of our project!

Next, we’ll do a format conversion to the array, convert the array to an object, and return it to make wrapping code easier:

const makeDependenciesGraph = entry= > {
    const entryModule = moduleAnalyser(entry);
    const graphArray = [entryModule];
    for (let i = 0; i < graphArray.length; i++) {
        const item = graphArray[i];
        const { dependencies } = item;
        if (dependencies) {
            for (let j in dependencies) {
                constresule = moduleAnalyser(dependencies[j]); graphArray.push(resule); }}}const graph = {};
    graphArray.forEach(item= > {
        const { fileName, dependencies, code } = item;
        graph[fileName] = { dependencies, code };
    });
    return graph;
};

const graphInfo = makeDependenciesGraph('./src/index.js');

console.log(graphInfo);
Copy the code

Execute the code in the console, print graphInfo, and view the print result:

OK, so the structure is very clear!

So far, we have generated the dependency map for the entire project, and we are two thirds of the way through. Victory is in sight!

Ok, if you understand all of part 2, let’s move on to Part 3. If you don’t understand, please read it again

——— ——— cut ——— line ———

Part three: Generating the final code

In the first two parts, we did code analysis for one module, code analysis for all modules, and a dependency graph for the project. In this part, we used the dependency graph to generate code that actually runs in the browser

Here we can create a new function generateCode, which also takes an entry file as an argument. Of course, the function eventually generates code, so the return should be a string, so this returns a string:

const generateCode = entry= > {
    const graph = makeDependenciesGraph(entry);
    return '// Generated code';
};

const code = generateCode('./src/index.js');

console.log(code);
Copy the code

First of all, we know that all the code in a web page should be placed in a large closure to avoid disturbing the global environment, so the first step is to write a closure:

const generateCode = entry= > {
    const graph = makeDependenciesGraph(entry);
    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}'); }) (The ${JSON.stringify(graph)});
    `;
};

const code = generateCode('./src/index.js');

console.log(code);
Copy the code