Why is front-end development modular?

Before we talk about modularity, let’s take a look at how the old code was organized:

<body>
  <! -- lots of HTML code -->
  <script scr='./index.js'></script>
</body>
Copy the code

At first, we might write all the JS code in an index. JS file and import it through the script tag. Later, as the business grew more complex, the index.js file became large and difficult to maintain. So we consider splitting the code into different files according to function modules, and then introducing multiple JS files:

<body>
  <script src='./a.js'></script>
  <script src='./b.js'></script>
  <script src='./c.js'></script>.</body>
Copy the code

By doing this split, we have less code for each file, which does look cleaner, but it also introduces several problems:

  • Naming conflicts and variable contamination

    // a.js
    var a = 1;
    
    // b.js
    var b = 2;
    
    // index.html
    <script src="./a.js"></script>
    <script src="./b.js"></script>
    <script>
      console.log(a, b); 
    </script>
    Copy the code

    Suppose there are two functional modules A and B, split into A. js and B. js respectively. The above code prints 1 and 2. There is no problem, but if one day, a new programmer defines a variable A in B. Js.

    // a.js
    var a = 1;
    
    // b.js
    var b = 2;
    var a = 3;
    Copy the code

    At this point, the console prints the code: 3, 2. Since both A and B are defined in the global scope, the variable names used by the two modules conflict. We can avoid it by changing variable names, but this is a palliative, because they do not have their own scope, so we can still modify the variable of module A in module B, which may cause some bugs.

  • Dependencies between resources or modules

    In the code snippet above, adding many script tags to the HTML to import resources one by one can cause many requests in the first place, causing the page to stall.

    More importantly, if there are dependencies between resources, sort them from top to bottom by dependency. If you introduce defer or async properties, the logic becomes more complex and difficult to maintain.

Implement modularity

So how do we solve these two problems?

Resolve naming conflicts and variable contamination

To avoid naming conflicts and variable contamination, we came up with the idea of creating a private scope for each module to avoid naming conflicts and only access exposed variables outside the module to avoid variable contamination.

Since a local scope can be formed inside a function, we can wrap the module’s code in a function, and since we usually only need to call the function once, we can use immediate function expressions (IIFE) :

// a.js
var moduleA = (function() {
  var a = 1;
  function foo(){
    console.log(a);
  }
  return{ foo, }; }) ();// b.js
var moduleB = (function() {
  var b = 2;
  function foo(){
    console.log(a, b);
  }
  var a = 3;
  return {
    foo,
  }
})();
Copy the code
<! -- index.html -->
<script src="./a.js"></script>
<script src="./b.js"></script>
<script>
  moduleA.foo(); / / 1
	moduleB.foo(); 2 / / 3
</script>
Copy the code

At this point, although both Modules have variable A and method foo, they are in their own scope and do not cause naming conflicts, and moduleB cannot modify moduleA’s variables without causing variable contamination.

Resolve dependencies between modules

One of our modules can have dependencies on another module, so in HTML, we need to be careful to introduce them in order. For example, we have dependency diagrams like this:By topological sorting, our introduction order might be:

<script src="./d.js"></script>
<script src="./c.js"></script>
<script src="./b.js"></script>
<script src="./a.js"></script>
Copy the code

At this point, if a new moduleE is added, the dependency needs to be reanalyzed to put the introduction of the moduleE in place. With the development of business, every time a module is added, it has to be re-analyzed, which is not only cumbersome, but also difficult to maintain.

So manually managing dependencies is not a good idea after all.

Modular specification

Although we discussed modularity implementations above, there are two problems:

  • The modular implementation methods are not uniform
  • Manual maintenance module dependency is difficult

To address these two issues, developers have come up with modularity specifications, which are ways to define modules uniformly and free up manual maintenance dependencies.

The three leading modularity specifications are:

  • CommonJS
  • AMD
  • CMD

CommonJS

This specification is widely used in Node.js, where each file is a module. There are four key environment variables:

  • module: Each module has one such variable inside, representing the current module
  • exports: a property of module that represents the exposed interface
  • global: Global environment (Node)
  • require: synchronously loads the exports property of a module
// a.js
var a = 1;
var addA = function(value) {
  return a + value;
}
module.exports = {
  a,
  addA
}

// b.js
const { a, addA } = require('./a.js');
console.log(a); / / 1
console.log(addA(2)); / / 3
Copy the code

In this specification, modules are loaded synchronously. On the server side, module files are stored on the local disk and can be read quickly. This is not a problem, but on the browser side, asynchronous loading should be used for network reasons, and pre-compiled and packed.

The loading mechanism of the CommonJS module is to load a copy of the output value, which means that once the output value is output, changes made to the value within the module do not affect the output value.

// a.js
var a = 1;
var addA = function() {
  a++;
}
var getA = function() {
  return a;
}
module.exports = {
  a,
  addA,
  getA
}

// b.js
var { a, addA, getA } = require('./a');
console.log(a); / / 1
addA();
console.log(a); / / 1
console.log(getA()); / / 2
Copy the code

The addA method has changed the value of a.js to 2, but this does not affect the a value introduced by the b.js module, because a is a primitive value, which is cached when loaded.

AMD

In the browser environment, modules are loaded asynchronously, and all statements that depend on this module are defined in a callback function.

Main command: define(id? , dependency? , factory), require(modules, callback)

Usage:

// Define module A without dependencies
define('moduleA'.function(require.exports.module) {
  / /...
});
// Define module D that depends on modules A, B, and C
define('moduleD'['moduleA'.'moduleB'.'moduleC'].function(a, b, c) {
  // All dependent modules are declared and initialized first
  if(false) {// b is initialized and executed even though b is not used
    b.dosomething();
  }
  // Expose the interface with the return method
  return{... }});// Load the module
require(['moduleA'].function(a) {
// ...  
});
Copy the code

To implement this specification, we need the module loader require.js, so before importing the module, we need to import the require.js file and create main.js as the entry file. We can also use require.config() to define the configuration of module dependencies.

<script src="./require.js"></script>
<script src="./main.js"></script>
Copy the code
// main.js
require(['a'].function(a) {
  a.say(); // b a
});

// a.js
define('a'['b'].function(b) {
  function say() {
    b.say();
    console.log('a');
  }
  return {
    say
  }
});

// b.js
define('b'.function(require.exports.module){
  function say() {
    console.log('b');
  }
  exports.say = say;
})
Copy the code

CMD

Another JS modular solution, similar to AMD. AMD believes in relying on front-loading, front-loading execution. CMD, on the other hand, relies on close, delayed execution. When you use a module, you need to declare it explicitly.

define(['a'.'b'.'c'].function(a, b, c) {
  var a = require('./a'); // It must be declared when used
  a.doSomething();
})
Copy the code

To use this specification, we need to introduce the module loader SeaJS, which is similar to requireJS, but requires the explicit use of require in the callback function to introduce the module, which we won’t go over here.

As web-related standards evolve and ES6 introduces new module specifications, requireJS and SeaJS are functional but obsolete, and people no longer rely on them as they once did.

ES6 module

Both CommonJS and AMD can only determine module dependencies and input and output at runtime, while ES6 module design is to pursue as static as possible. You use the import and export keywords in a module to import or export things in the module.

The ES6 Module has several features:

  • The strict mode is automatically enabled
  • A JS file represents a JS module
  • Each module is a singleton that is loaded only once

Compared to CommonJS:

  • CommonJS prints a copy of the value, ES6 prints a reference to the value

  • CommonJS is run time loading, ES6 is compile time output interface. A commonJS module is an object that loads the entire module on input and reads the method from that object. ES6 allows import to specify that an output value is loaded.

    Because CommonJS loads an object (that is, the module.exports property), that object is only generated after the script runs. An ES6 module is not an object, and its external interface is a static definition that is generated during the code static parsing phase.

// a.js
export var a = 1;
a = 2;

// main.js
import { a } from './a';
console.log(a); / / 2
Copy the code

Note that there are two ways to export by default, which are subtly different:

// The first way
var a = 1;
export default a;

// The second way
var a = 1;
export { a as default };

Copy the code
  • In the first version, export default binds the expression A instead of the identifier. Therefore, when a value is updated after export default, it will not be reflected in the exported value.
  • In the second way, the export is bound to the identifier A, so changes to a affect the value on the export side.

Which way to actually write it depends on whether the export needs to be updated later. Anyway, it’s best to use code comments to explain what we’re trying to do.

conclusion

To address naming conflicts, variable contamination, and module dependency management, modular specifications are introduced in the front end.

Among them, the CommonJS specification is synchronous loading, suitable for server-side use.

On the browser side, we want to use asynchronous modular specifications, with AMD and CMD specifications proposed early on.

With the development of The Times, ES6 Module refers to CommonJS and AMD and standardizes the loading and parsing methods of modules. It is also easier to implement and provides more concise syntax, making it a common specification for browsers and servers.

References:

Front-end modular details

NEXT Degree Programme

JavaScript You Don’t Know (Volume 2)