preface

Modern front-end development deals with modules all the time. For example, introducing a plug-in in a project, or implementing a JS file for global use of the component. These can all be called modules.

When designing a program structure, it is impossible to put all the code together. A friendlier way of organizing code is to break it up into pieces of code for a specific function, each of which implements a function or a specific purpose, and then combine them together through an interface. That’s the idea of modules.

Modules in JavaScript

As we all know, JavaScript didn’t have modules in its early days. The only way to insert multiple JS files into HTML is through the SRCIpt tag. This approach can lead to a number of problems as projects get bigger and bigger:

  • You need to manually maintain the loading order of the JavaScript. There are often many dependencies between scripts, but these relationships are implicit unless you look at the comments one by one (if there are none, then…). Otherwise it is difficult to indicate who depends on whom.
  • Name conflict. All content defined by all script files is shared by the global scope. One person development is good, meet with many people cooperation development, that is disaster.
  • This can also affect page loading if there are too many scripts. Because script tags require resources to be requested from the server, too many requests can seriously slow down rendering.

And modularization can solve the above problems very well.

What is a module

Modules are JS files loaded in different ways. This different pattern is necessary because it has very different semantics from scripts, with ES6 modules having the following semantics:

  • The module runs automatically in strict mode.
  • Variables created in the top-level scope of a module are not automatically added to the shared global scope; they then live in their respective top-level scope of the module.
  • Module dependencies can be clearly indicated by importing and exporting statements.

These differences represent significant changes in the loading and execution of JS code.

Module development

Since 2009, the JavaScript community has been trying to modularize, and AMD, CommonJs, CMD and other solutions have emerged successively.

Nodejs is the primary practitioner of the CommonJs specification, so it is the most widely used of these solutions. But these are community-generated specifications, not features of the language itself.

By 2015, ECMAScript6.0 formally defined the JavaScript module standard.

CommonJs

In 2009, JavaScript proposed the CommonJs specification, which contains a series of standards including modules, files, IO and console. Nodejs implements a portion of CommonJS and tweaks it. What we’re talking about now is the version of CommonJs in Nodejs, not its original definition.

CommonJs was originally intended only for the server side. Because it defines two main concepts:

  • Require function, used for import;
  • Module. exports variable, used for exports;

The browser does not support either keyword. Until the community came up with libraries such as Browserify that compiled CommonJS into a syntax that browsers could support. This means that the client code can be written in accordance with the Commonjs standard.

In addition, Nodejs’ NPM package manager allows developers to tap into the community’s excellent code base, or to distribute their code to those who need it. This approach has made CommonJs increasingly popular in front-end development.

The module

The CommonJs specification states that each file is a module. Files imported using require form a module scope of their own, so that variable and function declarations do not contaminate the global scope. All variables and functions are accessible only to the module itself and are not visible to the outside world.

For example:

// foo.js
var name = 'foo';

// bar.js
var name = 'bar';
require('./foo.js');
console.log(name); // bar
Copy the code

Foo.js is loaded in bar.js via the require function. When run, the output is’ bar ‘, indicating that variable declarations in foo.js do not affect bar.js. Each file has its own scope.

export

Exporting is the only way a module exposes itself to the outside world. In CommonJs, the contents of modules can be exported through module.exports.

For example:

module.exports = {
    name: 'foo'
}
Copy the code

The CommonJs module has a module object that holds information about the current module. The following objects are defined at the beginning of each module:

var module = {};
// ...
module.exports = {};
Copy the code

CommonJs also supports another export: exports.

exports.name = 'foo'
Copy the code

Implementatively, this code is no different from the module.exports code above; the underlying mechanism is that exports points to Module. exports. CommonJs adds the following code at the beginning of each module by default:

var module = {
    exports: {}
}
var exports = module.exports;
Copy the code

Thus, assigning a value to export.name is equivalent to adding a name attribute to the module.exports object.

Exports and module.exports just point to the same object. So an assignment to exports to point to a new object will not work.

For example:

exports = {
    name: 'foo'
}
Copy the code

The name attribute is not exported.

The other two methods, they don’t work together. Using module.exports assigns the same thing as pointing to a new object. Previous exports assignments will be invalidated.

The import

CommonJs uses the require function for import operations.

For example:

// foo.js
module.exports = {
    sayname: function () {
        console.log('foo'); }}; // bar.js var sayname = require('./foo.js').sayname;
sayname(); // foo
Copy the code

Import foo.js in bar.js and call its sayname function.

There are two situations when requiring a module:

  • The module is loaded for the first time by require. The module is executed first, then the content is exported.
  • Modules have been loaded by require before. The result of the execution is directly exported.

For example:

// foo.js
console.log('running foo.js')
exports.name = 'foo';

// bar.js
var firstname = require('./foo').name;
console.log('firstname:', firstname); 

var lastname = require('./foo').name;
console.log('lastname:', lastname);
Copy the code

The output is:

running foo.js

fistname:foo

lastname:foo

The code above requires foo files in two places, but as a result, foo.js is only run once.

The Module object has a loaded property to record whether the module has been loaded. The default value is false. When the module is loaded for the first time, it is assigned true. When the module is loaded again, it checks whether module.loaded is true, and if so, it returns the result without executing the code again.

The require function can accept expressions and use this feature to dynamically specify a module’s load path.

For example:

var path = ['foo.js'.'bar.js'];
path.forEach(name => {
    require('/' + name);
})
Copy the code

ES6Module

CommonJs can be said to better solve the problem of modules, but these are only specifications proposed by the community, not the characteristics of the language itself.

By 2015, ECMAScript6.0 formally defined the JavaScript module standard. This is where modules came into the JavaScript language.

The module

Rewrite the CommonJs example with ES6Module.

// foo.js
export default {
    sayname: function () {
        console.log('foo'); }}; // bar.js import foo from'./foo.js'
foo.sayname(); // foo
Copy the code

ES6Module also treats each file as a module, and each module has its own scope. The difference is the import and export statements. Import and export are also added as reserved keywords in ES6, and ES6Module automatically adopts strict mode.

export

Use the export command in ES6Module to export modules. Export has two forms:

  • Named after the export
  • The default is derived

1, a module can have multiple named exports, there are two different ways to write:

/ / 1export const name = 'foo';

// 2
const name = 'foo';
export { name }
Copy the code

Variables can be renamed using the AS keyword.

Such as:

const name = 'foo';
export { name as nickname }
Copy the code

There can be only one default export:

export default {
    name: 'foo'
}
Copy the code

You can think of export default as output of a variable named default.

The import

ES6Module uses the import syntax to import modules.

For example:

// foo.js
export const name = 'foo';
// bar.js
import { name } from './foo'
console.log(name)
Copy the code

When loading a module with a command export, import is followed by curly braces to enclose the imported variable name, which must be exactly the same as the exported variable name. Importing variables has the effect of declaring them in the current scope and cannot be aligned for modification.

Similar to command exports, you can rename imported variables using the AS keyword.

For example:

// foo.js
export const name = 'foo';
// bar.js
import { name as nickname } from './foo'Console. log(nickname) // Import * as name from'./foo'
console.log(name.name)
Copy the code

Examples of default imports:

// foo.js
export default {
    name: 'foo'
}

// bar
import name from './foo'
console.log(name.name)
Copy the code

CommonJs vs. ES6Module

Differences in the handling of module dependencies

The fundamental difference between CommonJs and ES6Module is that the former resolves module dependencies dynamically, while the latter is static.

  • Dynamic: module dependencies are established during code runtime;
  • Static: module dependencies are established during code compilation;

In CommonJs, when module A loads module B, it executes B’s code and returns its module.exports object as the return value of the require function. The module path of requrie can be specified dynamically, a representation can be passed in, and a module can even be loaded using an if statement. So there is no way to determine explicit dependencies before CommonJs modules are executed, and module import/export takes place at runtime.

ES6Module import and export statements are declarative. The unsupported import path is an expression, and import and export statements must be in the top-level scope of the module. Module dependencies can be analyzed during compilation of ES6 code.

Import differences in module values

When importing a module, for CommonJs you get a copy of the exported value; In ES6Module it is a dynamic mapping of values, and the mapping is read-only.

For example:

// foo
var count = 0;
module.exports = {
    count: count,
    add: function (a, b) {
        count++;
        return a+b;
    }
}

// bar
var count = require('./foo').count;
var add = require('./foo').add; console.log(count); / / 0 add (2, 3); console.log(count); // 0 count += 1; console.log(count); // 1(copy value can be changed)Copy the code

Count in bar is a copy of the value of count in foo, so when you call add, you change the value of count in foo, but you don’t change the imported value in bar.

Copy values, on the other hand, can be changed.

Rewrite using ES6Module

// foo
let count = 0;
const add = function (a,b) {
    count++;
    return a+b;
}
export { count, add }

// bar
import { count, add } from './foo'; console.log(count); / / 0 add (2, 3); console.log(count); // count++; // error count isread-only
Copy the code

A mapping relationship can be understood as a mirror in which the original things can be observed in real time, but the image in the mirror cannot be manipulated.

The difference between cyclic dependencies

Examples of cyclic dependencies in CommonJs:

// foo
const bar = require('./bar')
console.log('From bar:', bar);
module.exports = 'foo';

// bar
const foo = require('./foo');
console.log('From foo:', foo);
module.exports = 'bar; // index require('./foo')
Copy the code

Output on the console:

From Foo: () from bar: barCopy the code

First of all, to sort out the implementation process:

  • Foo is introduced in the index file, and the code in foo is executed;
  • Foo’s first sentence imports bar, which means foo doesn’t execute down, but inside the bar.
  • In bar, foo is introduced, where circular dependencies are created. Foo is not executed again, but the return value, module.exports, is exported directly. But since foo is unfinished, the exported value is the default empty object, so when bar executes to console.log, it prints an empty object.
  • When bar completes execution, Foo continues down until the process ends.

Rewrite the above example using ES6Module:

// foo
import bar from './bar';
console.log('From bar:', bar);
export default 'foo'

// bar
import foo from './foo'
console.log('From foo:', foo);
export default 'bar'

// index
import foo from './foo'
Copy the code

Result: from foo: undefined from bar: bar

You can’t get the correct export value for foo in bar, but instead of CommonJS exporting an empty object by default, you get undefined.

At the end

Modules are an important concept in programming, and I hope this gives you an idea of what front-end modules are. Search the official website or books for detailed usage.

For more articles, please go to Github, if you like, please click star, which is also a kind of encouragement to the author.