I’ve been working on the webpack recently, so here’s a quick note.

The module

As a newbie, I probably knew webPack was a Bundler that solved the problem of how to bundle multiple modules together. The module definition here is very extensive, can be a JS file, can be a picture, can be a style file… Of course, because we are front-end developers, the most understanding of the concept of modules, should be JS files.

In the normal development process, we usually treat a JS file as a Module, for Module specification connected to ES Module, probably can give such an example:

/ / the import
import message from './message.js';
import {word} from './word.js'
import a from './a.js'
/ / export export
export const a = 1;
export default.export function.Copy the code

I think the important thing to understand here is:

  1. We can get throughimportorexportTo introduce or export something.
  2. We will beimportWhat comes in is assigned to a variable inside the current module.
  3. weexportThe thing that goes out can be used as an import for other module files, and we can call the thing that goes outexports.

I can probably draw a picture:

The import statement is used to get exports from other module files. We also know that the browser will not recognize the import syntax without the module attribute attached to the

Why does Webpack pack our code by the entry file, packaged JS file hanging on the HTML file can run it?

Based on the above behavior, we can basically think of:

  • forimportWe can abstract out a functionrequire(path)To get the JS file exported under pathexportsObject.
  • forexportThe behavior of, we simply understand that it is calledexportsObject assignment, as for thisexportsWhere did the object come from? Let’s leave it for a moment.

That’s where THE concept came from. I found a handwritten Bundler code I’d copied from somewhere.

Hand write a Bundler

The code is here. Please do ignore my grassy doghouse. Please refer to the code for the following information.

First let’s look at the overall process.

First of all, we should convert the syntax of all files, at least not import and export. Instead, we should convert the syntax of the require function to fetch the exports of another module, so that we can generate transformed code for each file.

Then, the question arises, how do we start from the entry file and find all the relevant files for this packaging?

Module analysis

Let’s start with the analysis of the entry file.

We use fs module to read the contents of the entry file according to the path, and use @babel/ Parser to convert it into abstract syntax tree AST.

All we need to know is that the AST is a tree structure, and each import statement corresponds to a node of type ImportDeclaration in the tree, which contains some meta information about the corresponding import statement, such as the path of the dependent module following from.

We traversed the AST with @babel/traverse. For each ImportDeclaration node, we did some path mapping with respect to the entry file and placed the mapping in a Dependencies object.

Finally, we used @babel/core in combination with @babel/preset-env to convert the AST to the desired syntax we said earlier, that is, the format without import and export syntax.

We can look at the current entry file index.js:

import message from './message.js';
console.log(message);
export const a = 1;
Copy the code

We can get something like this:

const result = {
  filename: './src/index.js'.dependencies: { './message.js': 'src/message.js' },
  code:
    '"use strict"; \n\nObject.defineProperty(exports, "__esModule", {\n value: true\n}); \n exports.a = void 0; \n\nvar _message = _interopRequireDefault(require("./message.js")); \n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log(_message["default"]); \nvar a = 1; \nexports.a = a; '};Copy the code
  • The import syntax has become a require function, and the export syntax has become an assignment to an exports variable.

  • The end result is a Denpendencies object, in which the key is a path to the current file (in this case, the entry file) and the value is a path to our Bundler.

Dependency graph generation

We get the dependencies map of the entry file, so we can analyze the dependencies again. In fact, it is breadth-first traversal, and we can easily obtain the analysis results of all the required modules.

const graph = {
  './src/index.js': {
    dependencies: { './message.js': 'src/message.js' },
    code:'... '
  },
  'src/message.js': {
    dependencies: { './word.js': 'src/word.js' },
    code:'... '
  },
  'src/word.js': {
    dependencies: {},
    code:'... '}};Copy the code

The generated code

We need to start generating the final runnable code.

In order not to pollute the global scope, we wrap our code with the execute now function, passing in the dependency graph as an argument:

(function(graph){
  // todo
})(graph)
Copy the code

We need to start running the code for the entry file, so we have to find the corresponding code for the entry file in graph and run it:

(function(graph){
  function require(module){
    eval(graph[module].code)
  }
  require('./src/index.js')
})(graph)
Copy the code

However, in code, we also need to call require to get the exports of other modules, so the require function must have exports. It also supports internal calls to the require function, but beware!! The require function is not declared as the require function, because we can see from the compiled code that in code the require function passes the path relative to the current Module. At this point, the dependencies map we stored for each module comes in handy again.

(function(graph){
  function require(module){
    // define code internal use of require function -> localRequire
    function localRequire(relativePath){
      return require(graph[module].dependencies[relativePath])
    }
    
    var exports = {};
    eval(graph[module].code)
    return exports;
  }
  require('./src/index.js')
})(graph)
Copy the code

To override the require variable in the current scope chain, we immediately execute functions around eval, passing localRequire, exports, and code as arguments. This also ensures that the names of the code-related functions in Eval correspond.

(function(graph){
  function require(module){
    // define code internal use of require function -> localRequire
    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')
})(graph);
Copy the code

Bundler is written, and the resulting code runs directly in the browser.

Still, I was curious to see what the webPack would look like when packaged.

Webpack results analysis

We’ll use the same file, do a package with WebPack, and do some analysis of the results (since the code is quite long, I recommend doing it yourself).

Start with the outermost layer:

(function (modules) {
  // ...({})'./src/index.js': function (module, __webpack_exports__, __webpack_require__) {
    'use strict';
    eval(
      // ...
    );
  },

  './src/message.js': function (module, __webpack_exports__, __webpack_require__) {
    'use strict';
    eval(
      '__webpack_require__.r(__webpack_exports__); \n/* harmony import */ var _word_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./word.js */ "./src/word.js"); \n\n\nconst message = `say ${_word_js__WEBPACK_IMPORTED_MODULE_0__["word"]}`; \n\n/* harmony default export */ __webpack_exports__["default"] = (message); \n\n\n//# sourceURL=webpack:///./src/message.js? '
    );
  },

  './src/word.js': function (module, __webpack_exports__, __webpack_require__) {
    'use strict';
    eval(
      // ...); }});Copy the code

According to our close inspection, we found that Webpack also converted import and export. The above mentioned require function and exports object inside code became __webpack_require__ and __webpack_exports__.

More smartly, the __webpack_require__ parameter in each module’s code is now converted to the bundler path instead of the previous path relative to the module.

In addition, the value for each key in the graph becomes a function, much like the immediate function we wrote outside the eval code.

Step two, go inside and analyze the core logic:

// The module cache
var installedModules = {}
function __webpack_require__(moduleId) {
		// Check if module is in cache
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports
    }
  	// Create a new module (and put it into the cache)
    var module = installedModules[moduleId] = {
			i: moduleId,
			l: false.exports: {}}// Execute the module function
    modules[moduleId].call(module.exports, module.module.exports, __webpack_require__)
		// Flag the module as loaded
    module.l = true
		// Return the exports of the module
    return module.exports
}

// ...

return __webpack_require__(__webpack_require__.s = './src/index.js')
/ /...
Copy the code

The result is to call the declared __webpack_require__ function with the entry in webpack.config.js as the moduleId.

The __webpack_require__ function internally preferentially returns the exports object found in the cache. Module. exports = module.exports = module.exports = module.exports = module.exports = module.exports = module.exports = module.

There is a detail here, although we bound module.exports to call, we actually used this in the outermost layer of our module, which was converted to undefined when we coded graph.

I feel I have a rough understanding of Webpack.