preface

Webpack has been updated to version 5.X, but for some mid-level engineers, webPack proficiency is limited to configuration. In an era of rapid front-end development, simply knowing how to use webPack without understanding how it works can hinder their career development. This article is to take you into a more in-depth understanding of webPack packaging principles. And realize its packaging function.

When I first learned how to configure Webpack, I also wanted to read and have a look at the files packaged with Webpack, but I mistakenly thought it would be difficult to understand, so I gave up directly. Now, looking back, it is not difficult. To sum it up in one sentence, use nodeJS ‘FS module to read file contents and create a’ path-code block ‘map, write it into a JS file, and execute it using eval.

What does WebPack look like when it’s packaged

We only look at the code packaged with WebPack in the development environment, you can see what the packaging of Webpack is very intuitive, because in the production environment, Webpack will default to enable code compression, Treeshaking and other optimization methods, increasing the difficulty of understanding.

Under the SRC file//index.js
import { cute } from "./cute.js";
import add from "./add.js";

const num1 = add(1.2);
const num2 = cute(100.22);
console.log(num1, num2);

//add.js
const add = (a, b) = > {
  return a + b;
};
export default add;

//cute.js
import getUrl from "./utils/index.js";
export const cute = (a, b) = > {
  return a - b;
};
getUrl();

// utils/index.js
const getUrl = () = > {
  const url = window.location.pathname;
  return url;
};
export default getUrl;
Copy the code

We started packing using the index.js file as the entry file

//webpack.config.js
const path = require("path");

module.exports = {
  mode: "development".entry: "./src/index.js".output: {
    filename: "bundle.js".path: path.resolve(__dirname, "build"),},module: {
    rules: [{test: /\.js$/,
        exclude: /node_modules/,
        include: path.resolve(__dirname, "./src"),
        use: [{loader: "babel-loader".options: {
              presets: ["@babel/preset-env"],},},],},},};Copy the code

Packaged results (core:

(() = > {
  // webpackBootstrap
  "use strict";
  var __webpack_modules__ = {
    "./src/add.js": (__unused_webpack_module, __webpack_exports__, __webpack_require__) = > {
      eval(
        '__webpack_require__.r(__webpack_exports__); \n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ "default": () => __WEBPACK_DEFAULT_EXPORT__\n/* harmony export */ }); \nvar add = function add(a, b) {\n return a + b; \n}; \n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (add); \n\n//# sourceURL=webpack:///./src/add.js? '
      );
    },

    "./src/cute.js": (__unused_webpack_module, __webpack_exports__, __webpack_require__) = > {
      eval(
        '__webpack_require__.r(__webpack_exports__); \n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ "cute": () => /* binding */ cute\n/* harmony export */ }); \n/* harmony import */ var _utils_index_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils/index.js */ "./src/utils/index.js"); \n\nvar cute = function cute(a, b) {\n return a - b; \n}; \n(0,_utils_index_js__WEBPACK_IMPORTED_MODULE_0__.default)(); \n\n//# sourceURL=webpack:///./src/cute.js? '
      );
    },

    "./src/index.js": (__unused_webpack_module, __webpack_exports__, __webpack_require__) = > {
      eval(
        '__webpack_require__.r(__webpack_exports__); \n/* harmony import */ var _cute_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./cute.js */ "./src/cute.js"); \n/* harmony import */ var _add_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./add.js */ "./src/add.js"); \n\n\nvar num1 = (0,_add_js__WEBPACK_IMPORTED_MODULE_1__.default)(1, 2); \nvar num2 = (0,_cute_js__WEBPACK_IMPORTED_MODULE_0__.cute)(100, 22); \nconsole.log(num1, num2); \n\n//# sourceURL=webpack:///./src/index.js? '
      );
    },

    "./src/utils/index.js": (__unused_webpack_module, __webpack_exports__, __webpack_require__) = > {
      eval(
        '__webpack_require__.r(__webpack_exports__); \n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ "default": () => __WEBPACK_DEFAULT_EXPORT__\n/* harmony export */ }); \nvar getUrl = function getUrl() {\n var url = window.location.pathname; \n return url; \n}; \n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (getUrl); \n\n//# sourceURL=webpack:///./src/utils/index.js? '); }};var __webpack_module_cache__ = {};

  function __webpack_require__(moduleId) {
    if (__webpack_module_cache__[moduleId]) {
      return __webpack_module_cache__[moduleId].exports;
    }
    var module = (__webpack_module_cache__[moduleId] = {
      exports: {},}); __webpack_modules__[moduleId](module.module.exports, __webpack_require__);

    return module.exports;
  }

  (() = > {
    __webpack_require__.d = (exports, definition) = > {
      for (var key in definition) {
        if( __webpack_require__.o(definition, key) && ! __webpack_require__.o(exports, key)
        ) {
          Object.defineProperty(exports, key, {
            enumerable: true.get: definition[key], }); }}}; }) (); (() = > {
    __webpack_require__.o = (obj, prop) = >
      Object.prototype.hasOwnProperty.call(obj, prop); }) (); (() = > {
    __webpack_require__.r = (exports) = > {
      if (typeof Symbol! = ="undefined" && Symbol.toStringTag) {
        Object.defineProperty(exports.Symbol.toStringTag, { value: "Module" });
      }
      Object.defineProperty(exports."__esModule", { value: true}); }; }) (); __webpack_require__("./src/index.js"); }) ();Copy the code

Just a quick interpretation

You can see that each file takes the current relative pathkey.A function for valueI put in this one__webpack_modules__Object, each of whichvalueThe eval function executes code in the current file.webpack.xA series of methods are used to achieveimportandexportFunction for exporting and importing variables, which we won’t go into too much here, but we’ll have our own methods later.

When the packaged JS file is executed, it will be executed from"./src/index.js"thiskeyThe corresponding value starts executing the code. Let’s look at the process:

Begin to implement

Read the code from the entry

// mypack.js
const fs = require("fs");

const getCode = (entry) = > {
  const code = fs.readFileSync(entry, "utf8");
  console.log(code)
}
getCode('./src/index.js')
Copy the code

node mypackafterWe’ve got the code for the import file, and the next step is to get the dependency files from the import file and get all the imported file paths.

Access to rely on

Fetching dependencies means gathering the file paths to which each file import is imported by traversing the AST’s @babel/traverse library to find the import nodes.

const fs = require("fs");
const parser = require("@babel/parser"); / / convert the ast
const traverse = require("@babel/traverse").default; / / traverse the ast

const getCode = (entry) = > {
  const code = fs.readFileSync(entry, "utf8");
  const ast = parser.parse(code, {
    sourceType: "module"}); traverse(ast, {ImportDeclaration(p) {
      const importPath = p.get("source").node.value;
      console.log(importPath) 
    },
  });
}
getCode('./src/index.js')
Copy the code

So we get the file path that the entry file depends on, and then we recursively get the code for all the files. Because what we can get here is the relative path between the referenced file and the referenced file, but in our method fs needs to use relative to us when reading the filemypack.jsWhich is the path ofsrcDirectory path, so we can useRelative paths:SRC pathI’m going to do a mapping, and I’m going to get the converted code in the current path, and I’m going to get one{relative path :{dependency :{relative path: SRC path}, code :{... }}}Format object.

const fs = require("fs");
const path = require('path');
const parser = require("@babel/parser"); / / convert the ast
const traverse = require("@babel/traverse").default; / / traverse the ast
const getCode = (entry) = > {
  const code = fs.readFileSync(entry, "utf8");
  const dirname = path.dirname(entry);  // Get the directory where the current file resides
  const ast = parser.parse(code, {
    sourceType: "module"});const deps = {};
  traverse(ast, {
    ImportDeclaration(p) {
      const importPath = p.get("source").node.value;
      const asbPath = ". /" + path.join(dirname, importPath); // Get the path relative to the SRC directorydeps[importPath] = asbPath; }});// Get the converted code in the current entry file
  const { code:transCode } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]});console.log(entry,deps,transCode)
};
getCode("./src/index.js");
Copy the code

And so we haveEntry file './ SRC /index.js'theThe entry path,Depend on the fileandcode. We can then recursively retrieve information about all files through the dependencies of the entry file.

Recursively retrieves information about all dependencies

const fs = require("fs");
const path = require('path');
const parser = require("@babel/parser"); / / convert the ast
const traverse = require("@babel/traverse").default; / / traverse the ast

const getCode = (entry) = > {
  const code = fs.readFileSync(entry, "utf8");
  const dirname = path.dirname(entry);  // Get the directory where the current file resides
  const ast = parser.parse(code, {
    sourceType: "module"});const deps = {};
  traverse(ast, {
    ImportDeclaration(p) {
      const importPath = p.get("source").node.value;
      const asbPath = ". /" + path.join(dirname, importPath); // Get the path relative to the SRC directorydeps[importPath] = asbPath; }});// Get the converted code in the current entry file
  const { code:transCode } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]});return { entry, code, deps }; 
};

const recurrenceGetCode = (entry) = > {
  const entryInfo = getCode(entry);  // Get all information about the entry file
  const allInfo = [entryInfo];
  / * allInfo now only the entry file information, for the [{'. / SRC/index: {deps: {'. / express. Js' : '. / SRC/express. Js', '. / add. Js' : './src/add.js' }, code:"use strict...." } }] */We also need to take the information for cute. Js, add.js, and utils/index.js, and put it in allInfoconst recurrenceDeps = (deps,modules) = > {
 	Object.keys(deps).forEach(key= >{
      const info = getCode(deps[key])
      modules.push(info);
      recurrenceDeps(info.deps,modules)
    })
 }
 recurrenceDeps(entryInfo.deps,allInfo)
 console.log(allInfo) // What do I have now
}
recurrenceGetCode("./src/index.js");
Copy the code

Take it and turn it into oneStructure of the map:

const fs = require("fs");
const path = require('path');
const parser = require("@babel/parser"); / / convert the ast
const traverse = require("@babel/traverse").default; / / traverse the ast

const getCode = (entry) = > {
  const code = fs.readFileSync(entry, "utf8");
  const dirname = path.dirname(entry);  // Get the directory where the current file resides
  const ast = parser.parse(code, {
    sourceType: "module"});const deps = {};
  traverse(ast, {
    ImportDeclaration(p) {
      const importPath = p.get("source").node.value;
      const asbPath = ". /" + path.join(dirname, importPath); // Get the path relative to the SRC directorydeps[importPath] = asbPath; }});// Get the converted code in the current entry file
  const { code:transCode } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]});return { entry, code, deps }; 
};

const recurrenceGetCode = (entry) = > {
  const entryInfo = getCode(entry);  // Get all information about the entry file
  const allInfo = [entryInfo];
  / * allInfo now only the entry file information, for the [{'. / SRC/index: {deps: {'. / express. Js' : '. / SRC/express. Js', '. / add. Js' : './src/add.js' }, code:"use strict...." } }] */We also need to take the information for cute. Js, add.js, and utils/index.js, and put it in allInfoconst recurrenceDeps = (deps,modules) = > {
 	Object.keys(deps).forEach(key= >{
      const info = getCode(deps[key])
      modules.push(info);
      recurrenceDeps(info.deps,modules)
    })
 }
 recurrenceDeps(entryInfo.deps,allInfo)
 
 const webpack_modules = {};
 allInfo.forEach(item= >{
   webpack_modules[item.entry] = {
     deps:item.deps,
     code:item.transCode,
 }
 })
 return webpack_modules;
}
const webpack_modules = recurrenceGetCode("./src/index.js");
// webpack_modules is what we want in the end
Copy the code

Printing webpack_modules looks like this

{
 './src/index.js': {deps: {},code:"..."
 },
 './src/cute.js': {deps: {},code:"..."}... }Copy the code

Write all dependency information to a JS file

Now we need to write the resulting object into a file, but we can’t write it directly, because the object structure cannot be written into a JS file, we need to convert it to a string, and we can only use json.stringify to get a JSON string, JSON string is not recognized in the JS file. And how? Webpack is a self-executing function (()=>{})(), so can we pass it as a parameter to a self-executing function, and then write it into the JS file? The answer is yes.

// Omit the above code, just look down
const webpack_modules = recurrenceGetCode("./src/index.js");
const writeFunction = `((content)=>{
  console.log(content)
})(The ${JSON.stringify(webpack_modules)}) `;
fs.writeFileSync("./exs.js", writeFunction);
Copy the code

Let’s look at the code for the generated exs.js file:

((content) = > {
  console.log(content); ({})"./src/index.js": {
    deps: { "./cute.js": "./src/cute.js"."./add.js": "./src/add.js" },
    code:
      '"use strict"; \n\nvar _cute = require("./cute.js"); \n\nvar _add = _interopRequireDefault(require("./add.js")); \n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nvar num1 = (0, _add["default"])(1, 2); \nvar num2 = (0, _cute.cute)(100, 22); \nconsole.log(num1, num2); ',},"./src/cute.js": {
    deps: { "./utils/index.js": "./src/utils/index.js" },
    code:
      '"use strict"; \n\nObject.defineProperty(exports, "__esModule", {\n value: true\n}); \nexports.cute = void 0; \n\nvar _index = _interopRequireDefault(require("./utils/index.js")); \n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nvar cute = function cute(a, b) {\n return a - b; \n}; \n\nexports.cute = cute; \n(0, _index["default"])(); ',},"./src/utils/index.js": {
    deps: {},
    code:
      '"use strict"; \n\nObject.defineProperty(exports, "__esModule", {\n value: true\n}); \nexports["default"] = void 0; \n\nvar getUrl = function getUrl() {\n var url = window.location.pathname; \n return url; \n}; \n\nvar _default = getUrl; \nexports["default"] = _default; ',},"./src/add.js": {
    deps: {},
    code:
      '"use strict"; \n\nObject.defineProperty(exports, "__esModule", {\n value: true\n}); \nexports["default"] = void 0; \n\nvar add = function add(a, b) {\n return a + b; \n}; \n\nvar _default = add; \nexports["default"] = _default; ',}});Copy the code

[“./ SRC /index.js”].code. We’ll modify the code a little bit:

// Omit the above code, just look down
const webpack_modules = recurrenceGetCode("./src/index.js");
const writeFunction = `((content)=>{
  const require = (path) => {
  	const code = content[path].code;
    eval(code)
  }
})(The ${JSON.stringify(webpack_modules)}) `;
fs.writeFileSync("./exs.js", writeFunction);
Copy the code

Add require function

Repackaged and running in a browser looks like this:Why did you report this error?Look at the 3 steps marked in the picture:

Start with the entry file./src/index.jsCode, the code runs torequire('./cute.js')Re-execute whenrequireFunction,./cute.jsPassed as a parameter, content does not./cute.jsIf the value of the key exists, the code in the key cannot be retrieved. At this point our dePS for each file comes in handy again, because there is a path map in DEPS, so we are executingrequireFunction to retrieve the corresponding key-value pair from the one currently executing codeSRC path, the corresponding key in the content.Code continues to be modified:

// Omit the above code, just look down
const webpack_modules = recurrenceGetCode("./src/index.js");
const writeFunction = `((content)=>{
  const require = (path) => {
    const getSrcPath = (p) => {
      const srcPath = content[path].deps[p];
      return require(srcPath)
    }
    ((require)=>{
      eval(content[path].code)
    })(getSrcPath)
  }
  require('./src/index.js')
})(The ${JSON.stringify(webpack_modules)}) `;
fs.writeFileSync("./exs.js", writeFunction);
Copy the code

After packaging forThis step may be a little tricky, but I’ll explain it step by step:

The require to./cute.jsBecause the require function is passed to the implementation as an argumentevalIn a self-executing function, so naturally calledgetSrcPathThis is a function of thetagetSrcPathIs executed from ContentpathTo find the correspondingSRC path, path is./src/index.js, so naturally from{ "./cute.js": "./src/cute.js", "./add.js": "./src/add.js" }It’s taken out"./cute.js"The corresponding"./src/cute.js"After you get the path, pass it in as pathrequireFunction, and then call ((require) => { eval(content[path].code); })(getSrcPath);Function, so what happens when you execute the code?

Add exports

exportsUndefined? That’s right, because js passesexportThe exported module is an object that is not present in the packaged code, so we need to use theWhen each file is executedDefine one manuallyexportsAnd to return it

// Omit the above code, just look down
const webpack_modules = recurrenceGetCode("./src/index.js");
const writeFunction = `((content)=>{
  const require = (path) => {
    const getSrcPath = (p) => {
      const srcPath = content[path].deps[p];
      return require(srcPath)
    }
    const exports = {};
    ((require)=>{
      eval(content[path].code)
    })(getSrcPath)
    return exports;
  }
  require('./src/index.js')
})(The ${JSON.stringify(webpack_modules)}) `;
fs.writeFileSync("./exs.js", writeFunction);
Copy the code

Then there are no problems in execution!

Complete code snippet

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

const getCode = (entry) = > {
  const code = fs.readFileSync(entry, "utf8");
  const dirname = path.dirname(entry);
  const ast = parser.parse(code, {
    sourceType: "module"});const deps = {};
  traverse(ast, {
    ImportDeclaration(p) {
      const importPath = p.get("source").node.value;
      const asbPath = ". /" + path.join(dirname, importPath); // Get the path relative to the SRC directorydeps[importPath] = asbPath; }});const { code: transCode } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]});return { entry, deps, transCode };
};
const recurrenceGetCode = (entry) = > {
  const entryInfo = getCode(entry); // Get all information about the entry file
  const allInfo = [entryInfo];

  const recurrenceDeps = (deps, modules) = > {
    Object.keys(deps).forEach((key) = > {
      const info = getCode(deps[key]);
      modules.push(info);
      recurrenceDeps(info.deps, modules);
    });
  };
  recurrenceDeps(entryInfo.deps, allInfo);
  const webpack_modules = {};
  allInfo.forEach((item) = > {
    webpack_modules[item.entry] = {
      deps: item.deps,
      code: item.transCode,
    };
  });
  return webpack_modules;
};

const webpack = (entry) = > {
  const webpack_modules = recurrenceGetCode(entry);
  const writeFunction = `((content)=>{
    const require = (path) => {
      const getSrcPath = (p) => {
        const srcPath = content[path].deps[p];
        return require(srcPath)
      }
      const exports = {};
      ((require,exports,code)=>{
        eval(code)
      })(getSrcPath,exports,content[path].code)
      return exports;
    }
    require('./src/index.js')
  
  })(The ${JSON.stringify(webpack_modules)}) `;
  fs.writeFileSync("./exs.js", writeFunction);
};
webpack("./src/index.js");
Copy the code