How to achieve modularity with Closures (Webpack Principle)

closure

What is a closure?

The main question here is about the use of closures in modularity, which can be illustrated with webpack packaging.

Hand write a Webpack

To understand the application of closures in modularization, the best way to illustrate webpack as an example is to use the principle of Webpack to achieve a small mini Webpack.

1. Create two modules

To start modularization, first create two modules, add.js and index.js

Add.js: Provides a very simple function that takes arguments A and b. The purpose of the function is to add a and b and return the final result.

add.js

exports.default = function (a, b) { return a + b }
Copy the code

Index. js: Calls the add.js function and prints the result.

index.js

const add = require('./add.js').default
add(2, 3)
Copy the code

The above preparations have been completed.

2. Try calling index.js in the environment

Node environment

node ./index.js
Copy the code

Running result: 5

The index.js and add.js modules can be called in the Node environment.

Execute in the Web environment

Write an HTML file to import index.js and see the result.

index.html

<script src="./index.js">

</script>
Copy the code

ReferenceError: Can’t find variable: require

Indicates that our code cannot be fully executed in the Web environment.

3. Solve the modularity problem in the Web environment

There was an error importing the written module into index.html. To facilitate debugging, we can put the code of the two modules together and write it directly in index.html.

index.html

<script >
 exports.default = function (a, b) { console.log(a + b) }
 const add = require('./add.js').default
 add(2, 3)
</script>
Copy the code

Copy these three lines of code verbatim, this is the first step we need to do, run directly will definitely report an error.

Uncaught ReferenceError: exports is not defined

The exports function is not available in a pure Web environment and this is the first issue we will address.

1. Analog implementation of exports functions

(1) declare exports object

An exports object was declared on top of an exports object or method because it was not declared or defined.

The next step is actually to call the default method of the exports object.

<script > const exports = {} // Create an exports.default = function (a, B) {console.log(a + b)} exports. Default (2,3) // const add = require('./add.js'). Default // add(2, 3) </script>Copy the code

However, it is not elegant to write exports like this, which is equivalent to completely exposing exports and may affect other or even global variables. True modularity means that each module is independent and has its own scope, so we need to solve the problem of global pollution.

(2). Encapsulation of exports using closures and immediate execution functions
<script > const exports = {}; // create an exports object (function (exports, code) {eval(code)})(exports, 'exports.default = function (a, B) {console.log(a + b)}') exports. Default (2,3) // const add = require('./add.js'). Default // add(2, 3) </script>Copy the code

Running result: 5

2. Simulate the implementation of require function

Now that we’ve modeled the exports function, let’s open the require comment and see that the Web doesn’t provide require either, so we need to emulate the require function.

(1) define a require

Imagine how we would use the require function in everyday development. First require is a method, then it has at least one parameter that you want to introduce into the module’s path, and finally returns an object or method. I think that’s the basic structure of the require function.

So we need to solve three problems:

  • What should be inside the function

  • What should the return of the function be

  • How should arguments to a function be used

Define the require function framework for the moment:

const require = function (path) {
    return {}
}
Copy the code
(2).require’s internal implementation

Before thinking about how to use path as an entry, we should first consider what should be done inside the require function. The require function simply exports the contents of exports. So we’ve actually solved the first two problems:

  • What should be inside the function: the object content of exports

  • What should the return of the function be: an exports object

So we can write roughly:

const require = function (path) {
    const exports = {};
    (function (exports, code) {
      eval(code)
    })(exports, 'exports.default = function (a, b) { console.log(a + b) }')

    return exports
}
Copy the code

A brief analysis:

Let’s tackle the third problem: the use of path parameters

The require parameter refers to the path of the module, that is, we pass the path parameter variable to tell us which module we are looking for. In this way, the problem is easier to solve. We can define a route to find the corresponding module through path, so that we can achieve the effect of matching different modules according to the input parameter.

(3). Define the require route
const router = {
    'add.js': `exports.default = function (a, b) { console.log(a + b) }`,
    'index.js': `const add = require('add.js').default
              add(2, 3)`
    }
Copy the code

Routing executes code for path matching, then the complete code looks like this:

Result: 5

(4) complete realization of modularization

This has achieved what we want, but not completely. Routing is a new variable, so in order to cause variable contamination or interference between variables, we should also wrap the outermost layer with closure + execute function immediately.

So the final implementation code is:

<script > ! (function () { const router = { 'add.js': `exports.default = function (a, b) { console.log(a + b) }`, 'index.js': `const add = require('add.js').default add(2, 3)` } const require = function (path) { const exports = {}; (function (exports, code) { eval(code) })(exports, router[path]) return exports } require('index.js') })() </script>Copy the code

4. Re-implement the import file bundle.js

In the above code, we have a total of two modules, namely Add.js and index.js. When solving the modularization problem, we used the method of immediate execution of functions and closures to export them, but we directly wrote them in the script tag of index.html for the convenience of testing, without exposing the interface to the outside. So we need to write an entry file called bundle.js and just copy the code from the script.

bundle.js

! (function () { const router = { 'add.js': `exports.default = function (a, b) { console.log(a + b) }`, 'index.js': `const add = require('add.js').default add(2, 3)` } const require = function (path) { const exports = {}; (function (exports, code) { eval(code) })(exports, router[path]) return exports } require('index.js') })()Copy the code

5. Test results

Test in node environment and Web environment respectively

(1). Node environment test

The new test. Js:

require('./bundle.js')

Result: 5

(2). Web environment test

index.html

<script src="./bundle.js">

</script>
Copy the code

Result: 5

At this point, we have a modular output. This is also the basic architectural rationale for WebPack packaging.

Webpack packaging

After the above handwritten Webpack, we can also try to analyze the use of Webpack after the source code.

Delete the written test files, leaving only the index.js and add.js modules to be packed and placed in the SRC folder

Perform:

npm init -y
npx webpack
Copy the code

The final output looks like this:

Main.js is our packaged file, and we’ll try to analyze how webPack is modularized.

main.js

(() = > { var r = { 241: r = > { r.exports = function (r, o) { console.log(r + o) } } }, o = {}; (0, function t(e) { var n = o[e]; if (void 0 ! == n) return n.exports; var s = o[e] = { exports: {} }; return r[e](s, s.exports, t), s.exports }(241). default)(2, 3) }) ();Copy the code

This is compressed and obfuscated code, but does the whole thing look familiar? Yeah, that’s exactly what we did before.

So you’ve actually written a mini Webpack by hand!