Simple Packaging Tool Implemented by Simulating Web Pack

Posted by gabeg on Tue, 23 Jul 2019 11:02:17 +0200

Webpack is a front-end project building tool. With the development of front-end ecology, webpack has become one of the necessary skills for front-end developers. When many developers start using react and vue, they will use the default single page should create instructions to create an engineering project. In fact, these engineers The projects are all based on Web pack.
When we are familiar with the use of these engineering phone files, we will start to think about why the code we write can not run directly in the browser, after the webpack package can run on the browser, what happened to the packaging process?

In fact, web pack is based on node. The process of packaging includes reading file stream for processing and module dependency for importing, parsing and exporting. Here is a simple implementation of this process. github source address: https://github.com/wzd-front-...

Project Initialization

First, we create a new folder named bundler and initialize it with npm init in the command line tool (black window). In the process of initialization, we are asked to input some information about the project, as follows

Press ^C at any time to quit.
package name: (bundler)
version: (1.0.0)
description:
entry point: (index.js)
test command:
git repository: (https://github.com/wzd-front-end/bundler.git)
keywords:
author:
license: (ISC)

If we want to skip this link, we can use NPM init-y and add-y to automatically generate the default configuration without further queries.

Next, before creating test cases, let's build our project. Here's our directory structure. The files under the src folder are our test cases.

--bundler
 --src
    index.js
    message.js
    word.js
 --node_modules
 --bundler.js
 --package.json
 --README.md

word.js code

export const word = 'hello';

message.js code

import { word } from './word.js';
const message = `say ${word}`;
export default message;

index.js code

import message from './message.js';
console.log(message);

By looking at the code of the three simple files above, we will find that the main function modules of these codes are import and export parsing, which is also the main function of the packaging tool. How these codes are converted into browser recognizable codes? Next, we will demonstrate the process through code demonstration.

Module parsing

First, we create the bundler.js file under the bundler file as the execution file of our packaging process, then we execute node bundler.js to execute the packaging process. First, we create a function called module analyser to parse the module, which receives a filename address string and gets the corresponding location. Address the file and pass it
@ The parser method of the babel/parser module converts the corresponding file string into an abstract node tree. It is not clear that the small partner of the abstract node tree can print ast in the console of the following code to observe its structure. After we generate the node tree, we need to get the import node. Many people can think of it. Yeah, can't u just intercept the import character from the string?
When there is only one import, it can be done, but many times, we can intercept to achieve more complex, at this time, we can use the help of
@ babel/traverse to help us achieve, specific implementation can see the babel official network, after the introduction of this module, we can get parser ast as a parameter to pass in; through the node tree output above, we can find that the type of import node is ImportDeclaration, we can pass in the second parameter of traverse(). An object, named by the type type of the node, can help us get the corresponding node. Finally, we convert the treated ast back into a code string, which is implemented 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) => {
    // The file api is read asynchronously by fs module to get the file of the incoming path, and the encoding format is'utf-8'
    const content = fs.readFileSync(filename, 'utf-8');
    // The parser.parse method converts the read code into an abstract node tree, where the sourceType type specifies how to import the file
    const ast = parser.parse(content, {
        sourceType: "module"
    });
    const dependencies = {}
    // Get the node type ImportDeclaration in the node tree by traverse and save its mapping relationship to dependencies object
    traverse(ast, {
        ImportDeclaration({ node }) {
            // The Root Path to Get Entry Strength
            const dirname = path.dirname(filename)
            // Path of actually introducing files into stitching files

            const newFile = dirname + node.source.value
            // Store mapping relationships in dependencies objects
           dependencies[node.source.value] = newFile
        }
    })
    // ast is transformed into es5 code by presets. The first parameter is abstract node tree, the second parameter is source code, and the third parameter is configuration.
    const { code } = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"]
    })
    return {
        filename,
        dependencies,
        code
    }
}
console.log(moduleAnalyser('./src/index.js'))

Through the above code, we can get a module entry file analysis, including the module name, dependencies and code, but we only get an entry file analysis, the entry module has its own dependencies, dependencies and their own dependencies, so we need to go deep into each module. Degree analysis;

....
// Used to call multiple modules in a loop
const makeDependenciesGraph = (entry) => {
  // First, we get the analysis object of the entry module.
  const entryModule = moduleAnalyser(entry)
  // Save the analysis objects of all modules
  const graphArray = [entryModule]
  // Analyse every dependency in graph Array, and if it exists, we will analyze the new dependency module until all the dependencies are found.
  for (let i = 0; i < graphArray.length; i++) {
    const item = graphArray[i]
    const {dependencies} = item
    // If dependencies are not empty objects, we use for..in to enumerate each dependency module in the object, store the path of the dependency module, analyze and generate a new analysis result object, and store it in the graphArray array.
    if (JSON.stringify(dependencies) !== '{}') {
      for (let j in dependencies) {
        graphArray.push(moduleAnalyser(dependencies[j]))
      }
    }
  }
  // We store the final result in the graph object as the key value through the filename of each analysis result object in order to facilitate the subsequent value selection through the module path.
  const graph = {}
  graphArray.forEach(item => {
    graph[item.filename] = {
      dependencies: item.dependencies,
      code: item.code
    }
  })
  return graph
}
console.log(makeDependenciesGraph('./src/index.js'))

After the above operation, all the relevant modules have been parsed through the entry file. Next, we need to convert these modules into the code that browsers can execute. In the code generated after transformation, we will find that the require method and export object are included. This is us. Browsers do not have, we need to further declare the corresponding method, so that browsers can find the corresponding method to execute, and then we execute the last step of generating code operation.

....
const generateCode = (entry) => {
  // Because we need to return the corresponding executable string, we need to convert the object into a string first, otherwise'[object, object]'will appear.
  const graph = JSON.stringify(makeDependenciesGraph(entry));
  // Return strings use template strings and use closures to prevent global contamination
  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});
    `;
}
const code = generateCode('./src/index.js')
console.log(code)

Finally, we copy the code output from the console to the browser's control elevation, and print out the results according to the predetermined results. The running code is as follows:

(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 = \"say \".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 = 'hello';\nexports.word = word;"}});

The above code is our packaged code. We will find that when we need to use other modules after packaging, we will call the require method. The require method will query the object generated by us with filename as the key value by passing in the address path parameter, find the corresponding code, and use the eval() side. Law to execute, this is a basic principle of packaging tools.

Topics: Front-end Webpack github JSON npm