preface

Front-end modularization is an old topic. Before writing this article, I fumbled through a lot of articles, found that the summary is very good, I want to take advantage of this opportunity to share their own experience, but also to master the knowledge to do a review, but also hope that you are looking at the front-end modular have a new understanding and understanding.

💭 First Nugget of this article: A little bit of front-end modularity summary

This paper mainly involves the following knowledge points:

IIFE

CommonJS

AMD/CMD/UMD

ES6 differs from CommonJS

Module. exports/exports differs from export default/export

What is modularity

In a word: The process of gradually dividing a project into submodules in order to decouple business code

Here’s an example:

Suppose there is a XXX management platform, which involves a login function, we can treat it as a large module

And because the login function includes the login box, login account type distinction, login verification… Such as the widget

These widgets all serve the login function, so we can again think of these widgets as sub-modules under the larger login function

We call this process of gradually breaking up a large module into sub-modules modular

Why modularity

Historically, the front-end has become more and more complex, requiring higher coding efficiency and maintenance costs. Now with modularity, we can solve these problems in a targeted way

File splitting

To do modularity, the first thing that comes to mind is to start with files. We split large files into different smaller files, but there are problems with that:

Advantages: Modules are independent

Disadvantages: can only be introduced one by one through script tags, variables may conflict, scope problems can not be solved gracefully, low scalability, poor reuse

Solve the problem caused by file split IIFE

Invoked function expression immediately (Infrely-Invoked Function Expression, IIFE)

Calling function expressions immediately allows variables declared in their functions to bypass JavaScript’s variable-top declaration rules. It also prevents new variables from being interpreted as full-domain variables or function names from taking over global variable names

With IIFE, we can safely splice or combine all split files

But what if the files are interdependent? For example, if file B uses A function in file A, how would this dependency be expressed?

The emergence of Node.js and modularity specifications

The reason for the modular specification is partly to solve the dependency problem mentioned earlier, and partly to constrain everyone to write modules in the same way (to code according to the same specification).

Before ES6 came along, some communities developed a series of module loading schemes, typically CommonJS and AMD/CMD/UMD

CommonJS

CommonJS introduces the require mechanism, which allows you to load and use a module in the current file. The ability to import every module we need, right out of the box, helps us solve the problem of scope and module dependencies.

Node.js is the primary practitioner of the CommonJS specification

Features:

  • CommonJS loads modules synchronously
  • A single file is a module
  • Exports within a module is used for exposure, require is used for import
  • Used for server modules

Disadvantages:

  • Synchronous loading does not work in the browser environment. Synchronization means blocking, and blocking causes the browser to freeze
  • You cannot load multiple modules in parallel
// count.js

var msg = 'a + b = ';
function sum(a, b) {
  var result = a + b
  console.log(msg + result)
}

/ / exposure
module.exports = {
  sum: sum
}
Copy the code
// main.js

/ / introduction
var count = require('./count')
count.sum(1.2) // a + b = 3
Copy the code

AMD

AMD (Asynchronous Module Definition) is designed for browser environments

It is, literally, an asynchronous loading module specification that works around the CommonJS problem of only loading synchronously

All statements that depend on this module are defined in a callback function that will not be executed until all the loading is complete

RequireJS is the typical class library that implements this specification, and in fact AMD is the canonical output in its promotion process

Features:

  • Suitable for asynchronously loading modules in a browser environment
  • Favoring pre-execution of dependencies (i.e., when defining modules, declare all modules that need to be relied on)

Disadvantages:

  • All dependencies must be loaded in advance because of dependency preloading. Modules that do not use dependencies in callback functions will still be loaded

CMD

The CMD specification is similar to the AMD specification, except that AMD relies on front-loading and up-front execution, while CMD relies on nearby and delayed execution.

When you need it, you load it

Features:

  • Rely on proximity and delay execution
  • CMD is used to load dependencies on demand, and then require that module

Differences between CMD and AMD

, AMD CMD
Dependency handling Dependencies are front-loaded and executed ahead of time Rely on proximity and delay execution
Loading way asynchronous asynchronous
Module loading mode After all dependencies have been loaded and executed, the callback function is entered to execute the main logic

It’s worth notingThe order in which dependencies are written is not necessarily the order in which they are executed, but the main logic must not execute until all dependencies have been executed.
A dependency is not executed immediately after being loaded. Instead, it executes the corresponding Module when it encounters the require statement,Modules are written in the same order as the execution sequence

// AMD
define(['a'.'b'].function(a, b) {
    a.todo()

    // Module B is loaded ahead of time, although b.todo() is not executed
    if (false) {
        b.todo()
    } 
})

// CMD
define(function(require, exports, module) {
    var a = require('./a') // Execute as needed
    a.todo()

    if (true) {
        var b = require('./b')
        b.todo()
    }
})

Copy the code

UMD

UMD (Universal Module Definition), it is to make the Module compatible with AMD and CommonJS specifications and appeared, mostly used in the cross-platform solution of Module Definition, and some need to support the browser side and the server side reference of the third party library

First, UMD will determine whether AMD (define) is supported or not. If it is supported, AMD specifications will be used

Otherwise, check whether CommonJS (exports) is supported. If yes, use the CommonJS specification

If none is supported, expose it to Window.

If you look at the code below, it’s actually an IIFE

Factory is a dependency callback that executes after loading and returns a module each time it executes. {a, b, c}

; (function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    
    // AMD
    define(['jquery'.'lodash'], factory)
    
  } else if (typeof exports === 'object') {

    // CommonJS
    module.exports = factory(require('jquery'), require('lodash'))}else {

    // Browser global variables (root = window)
    root.returnExports = factory(root.jQuery, root._)

  }
}(this.function ($, _) {
  function a(){}
  function b(){}
  function c(){}

  return {
    a: a,
    b: b,
    c: c
  }
}));
Copy the code

ES6

ECMAScript6 adds a module architecture definition at the language standard level, which can replace the CommonJS, AMD, CMD and UMD specifications mentioned earlier.

It has become a common module solution for browser and server, and it is the most popular one at present

The modularity of the ES6 module makes it possible to handle module dependencies, as well as imported and exported variables, statically at compile time

For example, CommonJS can only determine these things at runtime.

/ / export
module.exports = {
  a, b, c, d, e, f
}
Copy the code
/ / import
const { a, b, c } = require('xxx')
Copy the code

In essence, the whole XXX module was first loaded, and then an object XXX was assigned.

It then reads three properties a, B, and C from the XXX object.

So this loading is essentially"Runtime load"Because this object is available only at run time, there is no way to do “static optimization on demand” at compile time

If not, the following code should make sense

// const {a, b, c} = require(' XXX ')
const xxx = require('xxx')
const a = xxx.a
const b = xxx.b
const c = xxx.c
Copy the code

With the ES6, it’s a whole different story:

/ / export
export const a = 1
export const b = 2
export const c = 3
export const d = 4
export const e = 4
Copy the code
/ / import
import { a, b, c } from 'xxx'
Copy the code

In essence, a, B and C attributes are loaded from the XXX module, while D and F will not be loaded at all

This loading is"Load at compile time"That is, ES6 can load modules on demand at compile time, more efficiently than ES6CommonJSModule loading mode is high

What’s the difference between ES6 and CommonJS?

Differences between ES6 and CommonJS

The CommonJS module exports a copy of the value, the ES6 module exports a reference to the value

The CommonJS module exports a copy of the value, meaning that if the exported file changes the value, the changes in the original module will not affect the value

ES6 modules operate differently from CommonJS.

When a JavaScript engine statically analyzes a script and encounters an import, it generates a read-only reference.

When the script is actually executed, it is evaluated in the loaded module based on this read-only reference.

In other words, if the original value of an IMPORT in ES6 changes, the import load value changes as well.

CommonJS does not. Therefore, ES6 modules are referred to dynamically and do not cache values. Variables in modules are bound to their modules

CommonJS modules are loaded at run time, ES6 modules are loaded at compile time

Runtime loading: CommonJS modules are objects;

That is, the whole module is loaded, an object is generated, and then properties/methods are read from the object. This type of loading is called “runtime loading”.

Load at compile time: ES6 modules are not objects, but code that is explicitly specified to be output through the export command

Import takes the form of static commands, that is, import can specify to load the value of an export rather than load the entire module, which is called “compile time loading”.

module.exports , exports 和 export.export defaultThe difference between

Module. exports, exports and export default

First we know:

module.exports , exportsBelongs to the CommonJS specification

export.export defaultBelongs to ES6 specification

The module exports and exports

Test in node environment:

// Output: {}
console.log(exports)

Module {id: '.', exports: {},... }
console.log(module)
Copy the code

Module. exports and exports are {}

In fact, both objects point to the same address and exports is a global reference to module.exports

As you can see, every JS file has the following process after it is created:

var module = new Module(...)
var exports = module.exports
Copy the code

Exports and module.exports both point to the same object by default

But the use ofrequireWhen you evaluate it, what actually works ismodule.exports

That is, there is nothing wrong with writing it like this:

// Write it correctly
module.exports = {
  num: 100
}
Copy the code

If you write it this way, require will not get the value.

// Error
exports = {
  num: 100
}
Copy the code

Why require can not get, can see the official explanation code:

function require(/ *... * /) {
  const module = { exports: {}};((module, exports) = > {
    // Module code here. In this example, define a function.
    function someFunc() {}
    exports = someFunc;
    // At this point, exports is no longer a shortcut to module.exports, and
    // this module will still export an empty default object.
    module.exports = someFunc;
    // At this point, the module will now export someFunc, instead of the
    // default object.}) (module.module.exports);
  return module.exports;
}
Copy the code

Exports was implemented before Module. exports, and module.exports was returned

If you modify the exports value itself, the module.exports value still points to the original someFunc

So this is the real reason why a new object is assigned to exports that require can’t get

For details, see the official documentation description: Node.js Exports Shortcut

So, the recommended way to do this is to simply assign an object to module.exports and put all the properties that need to be exposed into that object

module.exports = {
  num1: 100.num2: 200
}
Copy the code

Or you can just mount the properties one by one to exports instead of assigning a new object to exports

exports.num1 = 100
exports.num2 = 200
Copy the code

The export and export default

When export Default is used

// animal.js
export default {
  dog: 'wang wang'.cat: 'meow meow'
}
Copy the code
// main.js
import animal from './animal'
console.log(animal) // {dog: 'woof ', cat:' meow meow '}

// main.js
import { dog, cat } from './animal'
console.log(dog, cat) // undefined undefined
Copy the code

When using the export

// animal.js
export const dog = 'wang wang'
export const cat = 'meow meow'
Copy the code
// main.js
import animal from './animal'
console.log(animal) // undefined

// main.js
import { dog, cat } from './animal'
console.log(dog, cat) // Bark meow meow meow

// main.js
import * as animal from './foo'
console.log(animal) // Object [Module] { dog: [Getter], cat: [Getter] }
console.log(animal.dog) / / wang wang
Copy the code

As can be seen from the above examples:

Export Default can only obtain attributes by importing XXX form ‘XXX’, which cannot be obtained by deconstructing assignments

Export can get attributes by deconstructing assignments and specifying new objects, whereas import XXX form ‘XXX’ cannot

Export, export default and Module. exports

Module. exports and export Default are two different specifications, one CommonJS and one ES6

However, we usually use ES6 for development purposes, and if you use Babel, you may find that ES6 is converted to CommonJS syntax in the result of compiling with Babel

Babel online conversion

We can measure how they convert to each other by taking a look at the export:

Module export

// animal.js
export default {
  dog: 'wang wang'.cat: 'meow meow'
}
Copy the code

Packaged results:

// __esModule indicates that this is an ESM module
Object.defineProperty(exports, "__esModule", {
  value: true
});

// void 0 is a javascript function that returns undefined
exports["default"] = void 0;
var _default = {
  dog: 'wang wang'.cat: 'meow meow'
};
exports["default"] = _default; // equivalent to module.exports. Default = _default
Copy the code

Export default adds the object to module.exports, and the key of the object is default

Module import

Let’s look at imports again:

// main.js
import animal from './animal'
console.log(animal)
console.log(animal.dog)
Copy the code

Packaged results:

var _animal = _interopRequireDefault(require("./animal"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }

console.log(_animal["default"]);
console.log(_animal["default"].dog);
Copy the code

As you can see, import determines whether the exported module is from ES6, and if so, obtains the object directly

If it is already from CommonJS, then the object’s key is simply a layer of default

In the end, any code from CommonJS or ES6 will do a.[“default”] syntactic sugar conversion when you use it

conclusion

Const animal = require(‘./animal’).default

With import, the export can be written as module.exports, export default, or export

You see this, guest officer. Give me a thumbs up?

Refer to the article

Differences between ES6 modules and CommonJS modules

IIFE’s – Immediately invoked function expressions