History of front-end modularization

A decade ago, modularity was simply implementing a namespace using “closures.” This solution makes it easy to handle global variables and dependencies.

Modularity has been around for more than a decade, with different tools and wheels popping up all over the place. Here are some of the most popular tools or frameworks:

1. The time line

CommonJS 2009 Node.js 2009 NPM 2010 requireJS(AMD) 2010 seaJS(CMD) 2011 Broswerify 2011 WebPack 2012 Grunt 2012 GULp 2013 React 2013 VUE 2014 ES6(Module) 2015 Angular 2016 Redux 2015 Vite 2020 Snowpack 2020Copy the code

Unconsciously, modular development has been more than a decade.

2. Purpose of modularity

Front-end modularity, the default is to talk about JavaScript modularity, from the beginning of the positioning as a simple web scripting language, to now can develop complex interaction front-end, modular development naturally, the purpose is nothing more than for code organizational reuse, isolation, maintainability, version management, dependency management and so on.

Front-end modules run on the browser side, asynchronously loading JavaScript scripts, making modular considerations more important than back-end implementations that load modules directly and quickly locally.

Let’s take a look at some of the biggest influences on front-end modularization:

3. CommonJS came out of nowhere

On January 29, 2009, Kevin Dangoor published an article called “What Server Side JavaScript Needs” and created a ServerJS group in Google Groups, To build a better JavaScript ecosystem, both server-side and browser-side, and renamed itself the CommonJS Group.

Let’s go back to January 29, 2009, in the middle of a normal winter when things started to grow, and the JavaScript community, Mozilla engineer Kevin Dangoor published an article called “What Server Side JavaScript Needs” and created a ServerJS group in Google Groups, To build a better JavaScript ecosystem, both server-side and browser-side, the CommonJS Group launched the CommonJS proposal.

Then came Node.js, which adopted the CommonJS modular specification and brought with it NPM (the world’s largest module repository).

“Nodejs modules are not implemented exactly in accordance with CommonJS specifications, but rather with trade-offs and a few features added.”

Node.js was released in late 2009, modularized based on the original mainstream specification of the CommonJS community, But node.js, which has since won the server-side JavaScript wars, values the voice of the actual developers more than the many rotten conventions of the CommonJS community. Since then, however, node.js modules have actually diverge from CommonJS.

The CommonJS specification performs well on the server side, making JavaScript shine on the server side, competing with or even overtaking traditional server languages such as PHP and Python. Programmers came up with the idea of porting it to the browser.

However, since CommonJS modules are loaded synchronously. As we know, modules loaded on the server side are loaded from memory or disk and take negligible time. However, on the browser side, it will cause blocking, white screen time is too long, and user experience is not friendly.

As a result, some branches gradually emerged from CommonJS, which are also known as AMD, CMD and so on.

Here’s where RequireJS and SeaJS come in:

  • RequireJS follows the AMD (Asynchronous Module Definition) specification;
  • SeaJS follows the CMD (Common Module Definition) specification.

With the release of ECMAScript for ES6 Modules in June 2015, browser vendors and Node.js have followed the lead, and the market for modular loading libraries has faded, indirectly condemning the CommonJS community to death. Instead of using ES6 Modules on the browser side, transpiler such as Babel is used to deal with issues arising from different browser versions and asynchronous features on the browser side. Node.js Modules still use CommonJS mode in large numbers. With the improvement of support for ES6 Modules and compatibility with previous CommonJS Modules, it is only a matter of time before CommonJS writing is transitioned to ES6 Modules.

4. Compatibility between AMD and CommonJS

The CommonJS specification loads modules synchronously, meaning that subsequent operations cannot be performed until the load is complete. The AMD specification loads modules asynchronously and allows you to specify callback functions. Node.js is mainly used for server programming, and module files usually already exist on the local hard disk, so it can be loaded quickly without considering the asynchronous loading mode, so the CommonJS specification is suitable. However, in the browser environment, to load modules from the server side, then must be in asynchronous mode, so the browser side generally uses the AMD specification.

The AMD specification uses the define method to define modules. Here is an example:

define(['package/lib'].function(lib){
  function foo(){
    lib.log('hello world! ');
  }

  return {
    foo: foo
  };
});
Copy the code

The AMD specification allows the output module to be compatible with the CommonJS specification. The define method should be written as follows:

define(function (require.exports.module){
  var someModule = require("someModule");
  var anotherModule = require("anotherModule");

  someModule.doTehAwesome();
  anotherModule.doMoarAwesome();

  exports.asplode = function (){
    someModule.doTehAwesome();
    anotherModule.doMoarAwesome();
  };
});
Copy the code

5. Differences between AMD and CMD

  • AMD is the canonical output of RequireJS’s module definition in its promotion process.

  • CMD is the normalized output of SeaJS module definitions during the roll-out process.

The above two specifications are modular development for JavaScript. At present, both of them can achieve the purpose of modular development on the browser side. The difference is that CMD is lazy execution and AMD is pre-execution.

The difference between:

  • For dependent modules, AMD executes early and CMD executes late. Since 2.0, however, RequireJS has also been deferred (handled differently depending on how it is written). CMD advocates as lazy as possible.
  • CMD advocates dependency nearby, AMD advocates dependency front.

Look at the code:

// CMD
define(function (require.exports.module) {
    var a = require('./a') 
    a.doSomething()
    // Omit 100 lines here
    var b = require('./b') // Dependencies can be written nearby
    b.doSomething()
    // ... 
})
// AMD's default recommendation is
define(['./a'.'./b'].function (a, b) {
    Dependencies must be written in the beginning
    a.doSomething()
    // Omit 100 lines here
    b.doSomething()
    / /...
})
Copy the code

Although AMD also supports CMD and requires as a dependency, RequireJS authors prefer it by default and it is the default module definition in the official documentation.

● AMD API default is a when multiple use, CMD API strictly distinguished, praise single responsibility.

  • In AMD, require is divided into global require and local require, both called require.
  • In CMD, there is no global require, but according to the completeness of the module system, seajs.use is provided to realize the loading and starting of the module system. In CMD, every API is simple and pure.

6. Differences between RequirJS and SeaJS

The main differences between the two are as follows:

  1. The positioning is different. RequireJS wants to be a module loader for browsers as well as environments such as Rhino/Node. Sea.js focuses on the Web browser side, while Node extensions make it easy to run in Node environments.
  2. Different norms are followed. RequireJS follows the AMD (asynchronous module definition) specification, while Sea-.js follows the CMD (common module definition) specification. Different specifications lead to different apis. Sea-.js is closer to CommonJS Modules/1.1 and Node Modules specifications.
  3. There are differences in promotion concepts. RequireJS is trying to get third-party libraries to modify themselves to support RequireJS, and only a few communities have adopted it so far. Sea.js is not forced to adopt independent packaging to “accept all rivers into the Sea”. At present, there is a relatively mature packaging strategy.
  4. Support for development debugging varies. Sea.js pays much attention to the development and debugging of code, with nocache, DEBUG and other plugins for debugging. RequireJS has no obvious support for this.
  5. The plugin mechanism is different. RequireJS adopts the form of interface reserved in the source code, and the plug-in type is relatively single. Sea-.js adopts a common event mechanism, with richer plug-in types.

In short, if RequireJS is the Prototype library, sea-js aims to be the jQuery library.

7. ES6 Module native support

Javascript programs are small — in the early days, they were mostly used to perform stand-alone scripting tasks that provided some interaction where your Web page needed it, so you generally didn’t need much scripting. Over the years, we now have complex programs that run lots of Javascript scripts, and some that are used in other environments (such as Node.js).

In recent years, therefore, it has become necessary to start thinking about providing a mechanism for breaking uP JavaScript programs into separate modules that can be imported on demand. Node.js has provided this capability for a long time, and many Javascript libraries and frameworks have started using modules (for example, CommonJS and other AMD-based module systems such as RequireJS, as well as the latest Webpack and Babel).

The good news is that the latest browsers are starting to support module functionality natively, which is the focus of this article. This can be a good thing – browsers can optimize the loading of modules, making it more efficient than using libraries: using libraries often requires extra client-side processing.

The ES6 module is designed to be as static as possible, so that the module dependencies, as well as the input and output variables, can be determined at compile time. Both CommonJS and AMD modules can only determine these things at runtime. For example, a CommonJS module is an object, and you have to look for object properties when you enter it.

/ / CommonJS module
let { stat, exists, readfile } = require('fs');

/ / is equivalent to
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
Copy the code

The above code essentially loads the FS module as a whole (that is, all the methods that load FS), generates an object (_fs), and then reads three methods from that object. This type of loading is called “runtime loading” because the object is only available at runtime, making it completely impossible to do “static optimization” at compile time.

An ES6 module is not an object. Instead, it explicitly specifies the output code through the export command, which is then input through the import command.

/ / ES6 module
import { stat, exists, readFile } from 'fs';
Copy the code

The essence of the above code is to load three methods from the FS module and none of the others. This type of loading is called “compile-time loading” or static loading, meaning ES6 modules can be loaded at compile time, which is much more efficient than CommonJS modules.

Below we explain in detail the specific use method and details of each modular scheme:

CommonJS

CommonJS is a project created with the goal of building a JavaScript ecosystem outside of the browser environment, such as in server and desktop environments.

Node applications are composed of modules that follow the CommonJS module specification.

The CommonJS specification states that within each module, the module variable represents the current module. This variable is an object whose exports property (module.exports) is the interface to the outside world. Loading a module loads the module.exports property of that module.

var x = 5;
var addX = function (value) {
  return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
Copy the code

The code above exports the x variable and the function addX via module.exports.

The require method is used to load modules.

var example = require('./example.js');

console.log(example.x); / / 5
console.log(example.addX(1)); / / 6
Copy the code

You can also assign objects to the module.exports property.

The square module is defined in square.js:

// assigning to exports does not modify modules, must use module.exports
module.exports = class Square {
  constructor(width) {
    this.width = width;
  }

  area() {
    return this.width ** 2; }};Copy the code

Here, bar.js uses the Square module that exports the Square class:

const Square = require('./square.js');
const mySquare = new Square(2);
console.log(`The area of mySquare is ${mySquare.area()}`);
Copy the code

The CommonJS module has the following features:

  • All code runs in the module scope and does not pollute the global scope.
  • Modules can be loaded multiple times, but only run once on the first load, and then the results are cached and read directly from the cache when they are loaded later. For the module to run again, the cache must be cleared.
  • The order in which modules are loaded, in the order they appear in the code.

1. Exports

Exports can be exported through module.exports, but this is a bit cumbersome.

For convenience, Node provides an exports variable for each module, pointing to module.exports. This equates to a line of command in the header of each module.

var exports = module.exports;
Copy the code

It allows a shortcut, module.exports. F =… It can be written more succinctly as exports. F =… .

For example, here’s the contents of circle.js:

const { PI } = Math;

exports.area = (r) = > PI * r ** 2;

exports.circumference = (r) = > 2 * PI * r;
Copy the code

The module circ.js has exported functions area() and circumference(). Add functions and objects to the root of the module by specifying additional properties on special exports objects.

A file named foo.js:

const circle = require('./circle.js');
console.log(`The area of a circle of radius 4 is ${circle.area(4)}`);
Copy the code

On the first line, foo.js loads the module circ.js in the same directory as foo.js.

Note, however, that as with any variable, if you assign a new value to exports, it is no longer bound to

moduleExports:module.exports.hello = true; // Export from the module's require
exports = { hello: false };  // Not exported, only available in modules
Copy the code

Exports is also normally reassigned when the module.exports property is completely replaced by a new object:

module.exports = exports = function Constructor() {
  / /... And so on.
};
Copy the code

2. Require loading modules

Node uses the CommonJS module specification with the built-in require command to load module files.

The require command is used to import modules, JSON, and local files.

// example.js
var invisible = function () {
  console.log("invisible");
}

exports.message = "hi";

exports.say = function () {
  console.log(message);
}
Copy the code

Run the following command to export the exports object.

var example = require('./example.js');
example
/ / {
// message: "hi",
// say: [Function]
// }
Copy the code

If a module outputs a function, it cannot be defined on an exports object, it is defined on an exports objectmodule.exportsAbove the variable.

module.exports = function () {
  console.log("hello world")}require('./example2.js') ()Copy the code

In the code above, the require command calls itself require(‘./example2.js’)(), which executes module.exports and thus prints hello world.

3. Load rules

The require command is used to load a file. The default suffix is.js.

var foo = require('foo');
/ / is equivalent to
var foo = require('foo.js');
Copy the code

The require command looks for module files in different paths, depending on the format of the argument.

(1) If the parameter string starts with “/”, it means that a module file is loaded in an absolute path. For example, require(‘/home/ Marco /foo.js’) will load /home/marco/foo.js.

(2) If the argument string begins with a “./ “, it means that a module file is loaded in a relative path (compared to where the script is currently executed). For example, require(‘./circle’) will load circ.js in the same directory as the current script.

(3) If the parameter string does not start with a “./ “or”/”, it means that it is loading a core module provided by default (in Node’s system installation directory) or an installed module (globally or locally) in each node_modules directory.

Script, for example, / home/user/projects/foo js performed the require (‘ bar. Js) command, the Node will search the following files in turn.

/usr/local/lib/node/bar.js
/home/user/projects/node_modules/bar.js
/home/user/node_modules/bar.js
/home/node_modules/bar.js
/node_modules/bar.js
Copy the code

This is designed so that different modules can localize their dependencies.

(4) If the parameter string does not start with a “./ “or”/” and is a path, such as require(‘example-module/path/to/file’), then the location of example-module will be found, and then the path will be found using it as an argument.

(5) If the specified module file is not found, Node will try to add.js,.json, and.node to the file name and then search for it. .js files are parsed as text JavaScript script files,.json files are parsed as json text files, and.node files are parsed as compiled binary files.

(6) If you want to get the exact filename of the require command load, use require.resolve().

● Directory loading rules

Usually, we will put related files in a directory, easy to organize. In this case, it is best to set up an entry file for the directory that the require method can use to load the entire directory.

Place a package.json file in the directory and write the entry file to the main field. Here’s an example.

// package.json
{ "name" : "some-library"."main" : "./lib/some-library.js" }
Copy the code

When require finds that the parameter string points to a directory, it automatically looks at that directory’s package.json file and loads the entry file specified by the main field. If the package.json file does not have a main field, or if there is no package.json file at all, the index.js file or index.node file in that directory will be loaded.

● Environment variable NODE_PATH

When Node executes a script, it first looks at the environment variable NODE_PATH. It is a set of absolute paths separated by colons. When the specified module can’t be found anywhere else, Node looks in these paths.

NODE_PATH can be added to.bashrc.

export NODE_PATH="/usr/local/lib/node"
Copy the code

So, if you have a complicated relative path, like the following.

var myModule = require('.. /.. /.. /.. /lib/myModule');Copy the code

You can either add the file to node_modules or modify the NODE_PATH environment variable. The package.json file can be written as follows.

{
  "name": "node_path"."version": "1.0.0"."description": ""."main": "index.js"."scripts": {
    "start": "NODE_PATH=lib node index.js"
  },
  "author": ""."license": "ISC"
}
Copy the code

NODE_PATH is a legacy path solution that should not normally be used, but the node_modules directory mechanism.

4. Cache of modules

Modules are cached after the first load. This means (like other caches) that every call to require(‘foo’) will return exactly the same object (if resolved to the same file).

If require.cache is not modified, calling require(‘foo’) more than once will not cause the module code to be executed more than once. This is an important feature. With it, “partially complete” objects can be returned, allowing pass-through dependencies to be loaded, even if they cause loops.

To get the module to execute the code multiple times, export the function and then call it.

5. Cyclic loading of modules

If cyclic loading of modules occurs, where A loads B and B loads A, THEN B will load an incomplete version of A.

When the loop require() is called, the module may not have finished executing when it returns.

Consider this case:

a.js:

console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');
Copy the code

b.js:

console.log('b starting');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('b done');
Copy the code

main.js:

console.log('main starting');
const a = require('./a.js');
const b = require('./b.js');
console.log('in main, a.done = %j, b.done = %j', a.done, b.done);
Copy the code

When main.js loads a.js, a.js loads b.js in turn. At this point, B.js tries to load A.js. To prevent infinite loops, an incomplete copy of the A.js export object is returned to the B.js module. Then B. Js finished loading and provided its exports object to the A. js module.

Both modules are complete by the time main.js loads them. Therefore, the output of the program would be:

$ node main.js
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done = true, b.done = true
Copy the code

Careful planning is required to allow cyclic module dependencies to work properly in your application.

6. Module loading mechanism

The loading mechanism for CommonJS modules is that the input is a copy of the output value. That is, once a value is printed, changes within the module do not affect that value. Take a look at the following example.

Here is a module file lib.js.

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};
Copy the code

The code above prints the internal variable counter and overwrites the internal method incCounter.

Then, load the module above.

// main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;

console.log(counter);  / / 3
incCounter();
console.log(counter); / / 3
Copy the code

As the code above shows, once counter is printed, changes within the lib.js module do not affect counter.

AMD and RequirJS

AMD’s brief introduction:

  • AMDThe whole is calledAsynchromous Module Definition(Asynchronous module definition)
  • AMDRequireJSIt is a specification for modular development on the browser side.
  • AMDMode can be used in a browser environment and allows modules to be loaded asynchronously, in the correct order, or dynamically on demand.

Summary of RequireJS.

RequireJS is a JavaScript file and module loader. It is optimized for in-browser use and is ideal for use in browsers, but can also be used in other JavaScript environments, such as Rhino and Node. Using modular script loaders like RequireJS will improve the speed and quality of your code.

1. AMD module specification

AMD loads modules asynchronously. Module loading does not affect subsequent statements. All statements that depend on some module are placed in the callback function.

The AMD specification defines only one function, DEFINE, through which modules are defined.

This function is described as follows:

define(id? , dependencies? , factory)

  • id: Indicates the name of the module in the definition (optional). If this parameter is not provided, the name of the module should default to the name of the specified script requested by the module loader. If provided, the module name must be “top-level” and absolute (relative names are not allowed).
  • dependencies: Optional array literals that the current module depends on and are identified by the module defined by the module.
  • factory: a function or object that needs to be instantiated.

The AMD specification allows the output module to be compatible with the CommonJS specification.

define(function (require.exports.module) {
    var reqModule = require("./someModule");
    requModule.test();
      
    exports.asplode = function () {
        //someing}});Copy the code

This is in line with the “COMPATIBILITY between AMD and CommonJS” we talked about earlier.

2. RequireJS module system

The basic idea of RequireJS is to passdefineMethod to define the code as a module; throughrequireMethod to achieve code module loading.

First, embed require.js into your web page, and then you can modularize your web page.

 <script data-main="scripts/main" src="scripts/require.js"></script>
Copy the code

The data-main attribute of the above code cannot be omitted. It is used to specify the script file where the main code resides, in this example, the main.js file in the scripts subdirectory. The custom code is stored in the main.js file.

3. Define the module

The define method is used to define modules, and RequireJS requires each module to be in a separate file.

Depending on whether to rely on other modules, the discussion can be divided into two cases. The first case is to define independent modules, that is, the defined module does not depend on other modules; The second case is when you define a dependent module, where the defined module depends on other modules.

(1) Independent modules

If the defined module is a stand-alone module that does not depend on any other modules, it can be generated directly with define.

define({
    method1: function() {},
    method2: function() {}});Copy the code

The code above generates a module with method1 and method2 methods.

An equivalent way of writing an object is to write it as a function whose return value is the module of the output.

define(function () {
	return {
	    method1: function() {},
		method2: function() {}}; });Copy the code

The latter way gives you a little more freedom and allows you to write some module initialization code inside the function.

It is worth pointing out that the module defined by DEFINE can return any value, not just an object.

(2) Independent modules

If the module being defined needs to depend on other modules, the define method must have the following format.

define(['module1'.'module2'].function(m1, m2) {... });Copy the code

The first argument to the define method is an array whose members are the modules on which the current module depends. For example, [‘ module1 ‘, ‘module2’] indicates that the new module we define depends on module1 and Module2, and the new module can run properly only if the two modules are loaded first. Js file and module2.js file in the current directory, which are equivalent to [‘./module1 ‘, ‘./module2 ‘].

The second argument to the define method is a function that will be called after all the members of the front array have loaded successfully. Its arguments correspond to the members of the array, such as function(m1, m2), where the first argument m1 corresponds to module1 and the second argument m2 corresponds to Module2. This function must return an object that can be called by other modules.

define(['module1'.'module2'].function(m1, m2) {
    return {
        method: function() { m1.methodA(); m2.methodB(); }}; });Copy the code

MethodA (m1) methodB (m2); methodA (m2); methodB (m2); methodA (m2);

Note that the callback must return an object, which is the module you defined.

If you depend on a large number of modules, the one-to-one mapping of parameters to modules is cumbersome.

define(
    [       'dep1'.'dep2'.'dep3'.'dep4'.'dep5'.'dep6'.'dep7'.'dep8'].function(dep1, dep2, dep3, dep4, dep5, dep6, dep7, dep8){... });Copy the code

RequireJS provides a simpler way to avoid writing code as verbose as the code above.

define(
    function (require) {
        var dep1 = require('dep1'),
            dep2 = require('dep2'),
            dep3 = require('dep3'),
            dep4 = require('dep4'),
            dep5 = require('dep5'),
            dep6 = require('dep6'),
            dep7 = require('dep7'),
            dep8 = require('dep8'); . }});Copy the code

Here’s an example of define in action.

define(['math'.'graph'].function ( math, graph ) {
		return {
            plot: function(x, y){
                returngraph.drawPie(math.randomGrid(x,y)); }}};) ;Copy the code

The module defined in the above code relies on the Math and Graph libraries and then returns an object with the plot interface.

Another practical example is choosing to load Zepto or jQuery by determining if the browser is IE.

define(('__proto__' in{}? ['zepto'] : ['jquery']), function($) {
    return $;
});
Copy the code

This code defines an intermediate module that determines whether the __proto__ attribute is supported by the browser (all browsers except IE do), and loads the Zepto library if true, or jQuery library if not.

4. Require calls the module

The require method is used to call the module. Its parameters are similar to the define method.

require(['foo'.'bar'].function ( foo, bar ) {
        foo.doSomething();
});
Copy the code

The above method loads modules foo and bar, and when both modules are loaded successfully, a callback function is executed. This callback function is used to accomplish a specific task.

The first argument to the require method is an array of dependencies. This array can be written flexibly, as shown in the following example.

require([window.JSON ? undefined : 'util/json2'].function ( JSON ) {
  JSON = JSON || window.JSON;

  console.log( JSON.parse( '{ "JSON" : "HERE" }')); });Copy the code

When the above code loads the JSON module, it first checks whether the browser natively supports JSON objects. If so, pass undefined to the callback function, otherwise load the json2 module in the util directory.

The require method can also be used inside the define method.

define(function (require) {
   var otherModule = require('otherModule');
});
Copy the code

The following example shows how to load modules dynamically.

define(function ( require ) {
    var isReady = false, foobar;
 
    require(['foo'.'bar'].function (foo, bar) {
        isReady = true;
        foobar = foo() + bar();
    });
 
    return {
        isReady: isReady,
        foobar: foobar
    };
});
 
Copy the code

The above code defines a module that loads foo and bar internally. The isReady property is false before the module is loaded, and becomes true after the module is loaded. Therefore, the next action can be determined based on the value of the isReady attribute.

In the following example, the output of the module is a Promise object.

define(['lib/Deferred'].function( Deferred ){
    var defer = new Deferred(); 
    require(['lib/templates/? index.html'.'lib/data/? stats'].function( template, data ){
            defer.resolve({ template: template, data:data }); });return defer.promise();
});
Copy the code

The define method of the code above returns a Promise object that specifies the next action in the then method of that object.

If the server is in JSONP mode, it can be called directly in require by specifying the JSONP callback parameter define.

require(["http://someapi.com/foo?callback=define"].function (data) {
    console.log(data);
});
Copy the code

The require method allows you to add a third argument, the error-handling callback function.

require(["backbone"].function ( Backbone ) {
        return Backbone.View.extend({ / *... * / });
    }, 
    function (err) {
		// ...});Copy the code

The third argument to the require method, the error-handling callback, takes an error object as an argument.

The Require object also allows you to specify a listener for global Error events. This listener is triggered for any errors not caught by the above methods.

requirejs.onError = function (err) {
    // ...
};
Copy the code

Summary:

The methods that define and call modules, define and require, are collectively referred to as AMD patterns. The methods defined by its modules are very clear, do not pollute the global environment, and clearly show dependencies.

AMD mode can be used in a browser environment and allows modules to be loaded asynchronously or dynamically as needed.

CMD and SeaJS

SeaJS was written by “Yubo”.

CMD is the normalized output of SeaJS module definitions during the roll-out process.

In sea-.js, all JavaScript modules follow the CMD (Common Module Definition) Module Definition specification. This specification specifies the basic writing format and basic interaction rules for modules.

CMD represents the general module definition, which was developed in China and proposed by Yubo of Ali. Just as AMD has a requireJS and CMD has a browser implementation SeaJS, SeaJS and requireJS are modular solutions to javascript.

Developed with KISS (Keep It Simple, Stupid) in mind, SeaJS has a single digit API of its own, making It stress-free to learn. The essence of the KISS principle — do one thing, do one thing well — can be felt everywhere while learning SeaJS.

1. SeaJS module system

Sea-.js is a module loader for Web browsers. In Sea-js, everything is a module, and all modules work together to build a module system. Sea-js first and foremost addresses the fundamental problems of module systems:

  1. What is a module?
  2. How do modules interact?

In the field of front-end development, a module can be A JS module, a CSS module, or a Template module. In sea-js, we focus on JS modules (other types of modules can be converted to JS modules) :

  1. A module is a piece of JavaScript code with a common basic writing format.
  2. Modules can reference each other and work cooperatively through basic interaction rules.

A modular system can be constructed by clearly describing the basic writing formats and interaction rules mentioned in the above two points. A detailed description of the writing format and interaction rules is the Module Definition Specification. Modules 1.1.1 from CommonJS, Modules from NodeJS, AMD from RequireJS, etc.

2. CMD module specification

In sea-.js, all JavaScript modules follow the CMD (Common Module Definition) Module Definition specification. This specification specifies the basic writing format and basic interaction rules for modules.

In the CMD specification, a module is a file. The code is written in the following format:

define(factory);
Copy the code

3. Define the module

Define is a global function that defines a module.

lowdefine(factory)

Define takes the factory argument, which can be a function, an object or a string.

When factory is an object or string, the interface representing the module is that object or string. For example, you can define a JSON data module as follows:

define({ "foo": "bar" });
Copy the code

Template modules can also be defined as strings:

define('I am a template. My name is {{name}}.');
Copy the code

When factory is a function, it represents a module constructor. Execute this constructor to get the interface that the module provides externally. The factory method is passed three arguments by default: require, exports, and module:

define(function(require.exports.module) {

  // Module code

});
Copy the code

lowdefine(id? , deps? , factory)

Define can also accept more than two parameters. The string ID represents the module id, and the array DEPS is a module dependency. Such as:

Define ('hello', ['jquery'], function(require, exports, module) {// module code});Copy the code

The ID and DEPS parameters can be omitted. When omitted, it can be automatically generated by the build tool.

Note: The use of define with id and deps parameters does not belong to the CMD specification, but to the Modules/Transport specification.

Low define. CMDObject

An empty object that can be used to determine if the current page has a CMD module loader:

if (typeof define === "function" && define.cmd) {
  CMD module loaders such as sea-.js exist
}
Copy the code

4. Require loading modules

Require is the first argument to the factory function.

lowrequire(id)

Require is a method that takes a module identity as a unique argument to get interfaces provided by other modules.

define(function(require.exports) {

  // Get the interface of module A
  var a = require('./a');

  // Call the method of module A
  a.doSomething();

});
Copy the code

Note: At development time, the writing of require needs to follow some simple conventions.

lowrequire.async(id, callback?)

The require.async method is used to load the module asynchronously within the module and execute the specified callback when the load is complete. The callback parameter is optional.

define(function(require.exports.module) {

  // Asynchronously load a module, and when the load is complete, execute a callback
  require.async('./b'.function(b) {
    b.doSomething();
  });

  // Load multiple modules asynchronously and execute a callback when the load is complete
  require.async(['./c'.'./d'].function(c, d) {
    c.doSomething();
    d.doSomething();
  });

});
Copy the code

Note: require is executed synchronously down, require.async is executed asynchronously back. Require. async is generally used to load modules that can be deferred asynchronously.

lowrequire.resolve(id)

Use the internal path resolution mechanism of the module system to resolve and return the module path. This function does not load the module and only returns the absolute parsed path.

define(function(require.exports) {

  console.log(require.resolve('./b'));
  // ==> http://example.com/path/to/b.js

});
Copy the code

This can be used to retrieve module paths, typically in plug-in environments or scenarios where module paths need to be dynamically concatenated.

5. Exports module

Exports is an object that provides module interfaces.

Define (function(require, exports) {// exports.foo = 'bar'; // exports doSomething method. DoSomething = function() {}; });Copy the code

In addition to adding members to exports objects, you can also use return to provide interfaces directly to exports.

Define (function(require) {return {foo: 'bar', doSomething: function() {}}; });Copy the code

If the return statement is the only code in the module, it can also be simplified as:

define({
  foo: 'bar',
  doSomething: function() {}
});
Copy the code

The format above is particularly suitable for defining JSONP modules.

Special note: the following is incorrect!

Define (function(require, exports) {// error!! ! exports = { foo: 'bar', doSomething: function() {} }; });Copy the code

Exports: / / module. Exports: / / module. Exports: / / module. Exports: / / module. Exports: / / module.

Define (function(require, exports, module) {module.exports = {foo: 'bar', doSomething: function() {}}; });Copy the code

Tip: exports is just a reference to module.exports. Module. exports does not change its value when reassigning exports inside the factory. So assignments to exports are not valid and cannot be used to change module interfaces.

6. The module object

A module is an object that stores properties and methods associated with the current module.

When the module idString

Unique identification of a module.

Define ('id', [], function(require, exports, module) {// module code});Copy the code

In the code above, the first parameter to define is the module identifier.

When the module. The uriString

The absolute path of the module is obtained according to the path resolution rules of the module system.

define(function(require, exports, module) {

  console.log(module.uri); 
  // ==> http://example.com/path/to/this/file.js

});
Copy the code

In general (when the ID parameter is not written in define), the value of module.id is the same as that of module.uri.

When the module dependenciesArray

Dependencies are an array that represents the dependencies of the current module.

When the module exportsObject

Interface provided by the current module.

The exports argument passed to the Factory constructor is a reference to the module.exports object. Providing an interface only through the exports parameter sometimes does not satisfy all the needs of developers. For example, if the interface of a module is an instance of a class, it is implemented through module.exports:

Define (function(require, exports, module) {// exports is a reference to console.log(module.exports === exports); Module.exports = new SomeClass(); // module. Exports = new SomeClass(); // exports is no longer equal to module.exports console.log(module.exports === exports); // false });Copy the code

This is all there is to the CMD module definition specification. Define, require, require.async, exports, module. Exports are the only commonly used apis. Other apis have a good impression, when you need to refer to the documentation, do not deliberately to remember.

Compared to the AMD specification of RequireJS, the CMD specification is kept as simple as possible and remains largely compatible with Modules specifications of CommonJS and Node.js. Modules written by the CMD specification can be easily run in Node.js.

ES6 Module

Javascript programs are small — in the early days, they were mostly used to perform stand-alone scripting tasks that provided some interaction where your Web page needed it, so you generally didn’t need much scripting. Over the years, we now have complex programs that run lots of Javascript scripts, and some that are used in other environments (such as Node.js).

In recent years, therefore, it has become necessary to start thinking about providing a mechanism for breaking uP JavaScript programs into separate modules that can be imported on demand. Node.js has provided this capability for a long time, and many Javascript libraries and frameworks have started using modules (for example, CommonJS and other AMD-based module systems such as RequireJS, as well as the latest Webpack and Babel).

The good news is that the latest browsers are starting to support module functionality natively, which is the focus of this article. This can be a good thing – browsers can optimize the loading of modules, making it more efficient than using libraries: using libraries often requires extra client-side processing.

The ES6 module is designed to be as static as possible, so that the module dependencies, as well as the input and output variables, can be determined at compile time. Both CommonJS and AMD modules can only determine these things at runtime. For example, a CommonJS module is an object, and you have to look for object properties when you enter it.

/ / CommonJS module
let { stat, exists, readfile } = require('fs');

/ / is equivalent to
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
Copy the code

The above code essentially loads the FS module as a whole (that is, all the methods that load FS), generates an object (_fs), and then reads three methods from that object. This type of loading is called “runtime loading” because the object is only available at runtime, making it completely impossible to do “static optimization” at compile time.

An ES6 module is not an object. Instead, it explicitly specifies the output code through the export command, which is then input through the import command.

/ / ES6 module
import { stat, exists, readFile } from 'fs';
Copy the code

The essence of the above code is to load three methods from the FS module and none of the others. This type of loading is called “compile-time loading” or static loading, meaning ES6 modules can be loaded at compile time, which is much more efficient than CommonJS modules.

◾️ uses JavaScript modules that rely on import and export

1. Export export

The first thing you do to get the functionality of your modules is export them. Use the export statement to do this.

The easiest way to do this is to put it in front of the item you want to export, such as:

export const name = 'square';

export function draw(ctx, length, x, y, color) {
  ctx.fillStyle = color;
  ctx.fillRect(x, y, length, length);

  return {
    length: length,
    x: x,
    y: y,
    color: color
  };
}
Copy the code

You can export functions, var, let, const, and classes that you’ll see later. Export should be in the outermost layer; For example, you cannot use export in a function.

● A more convenient way to export all modules you want is to use an export statement at the end of the module file. The statement is a comma-separated list enclosed in curly braces. Such as:

export { name, draw, reportArea, reportPerimeter };
Copy the code

2. The import import

If you want to use some functions outside the module, you need to import them. The simplest is something like this:

import { name, draw, reportArea, reportPerimeter } from '/js-examples/modules/basic-modules/modules/square.mjs';
Copy the code

Use the import statement, then you have a comma-delimited list of features you want to import surrounded by curly braces, then the keyword from, then the path to the module file. The path to the module file is relative to the root directory of the site.

Of course, the path we write is a bit different — we use dot syntax to mean “current path”, followed by the path containing the file we want to find. This is much better than writing down the entire relative path each time, because it’s shorter and makes the URL portable — it still works if you move it under a different path in the site layer.

3. Export default export by default

Default export VS named exports

  • The functions we’ve exported so far have consisted of named exports — each item (function, constant, etc.) is referred to by its name when exported, and that name is also used to refer to it when imported.

  • There is also an export type called default Export — this makes it easy for modules to provide default functionality and also helps JavaScript modules interoperate with existing CommonJS and AMD module systems.

When using the import command, the user needs to know the name of the variable or function to be loaded; otherwise, the command cannot be loaded. However, users will want to get started quickly and may not want to read the documentation to learn what properties and methods a module has.

● To make it easier for users to load modules without reading the documentation, use the export default command to specify the default output for the module.

// export-default.js
export default function () {
  console.log('foo');
}
Copy the code

The above code is a module file, export-default.js, whose default output is a function.

When other modules load this module,importThe command can specify any name for the anonymous function.

// import-default.js
import customName from './export-default';
customName(); // 'foo'
Copy the code

The import command in the above code can point to the methods output by export-default.js with any name, so you do not need to know the function name output by the original module. Note that curly braces are not used after the import command.

● The export default command can also be used in front of non-anonymous functions.

// export-default.js
export default function foo() {
  console.log('foo');
}

// or write it as

function foo() {
  console.log('foo');
}

export default foo;
Copy the code

In the above code, the function name foo of function foo is invalid outside the module. When loaded, it is treated as if an anonymous function is loaded.

Let’s compare the default output to the normal output.

/ / the first group
export default function crc32() { / / output
  // ...
}

import crc32 from 'crc32'; / / input

/ / the second group
export function crc32() { / / output
  // ...
};

import {crc32} from 'crc32'; / / input
Copy the code

The above code is written in two groups:

  • The first group is usingexport default, corresponding toimportStatements do not need braces;
  • The second group did not use itexport default, corresponding toimportThe statement requires braces.

The export default command is used to specify the default output of the module. Obviously, a module can have only one default output, so the export default command can only be used once. Therefore, the import command is not followed by parentheses, because it can only correspond to the export default command.

Essentially, export default simply prints a variable or method called default, and the system allows you to call it whatever you want. So, the following is valid.

// modules.js
function add(x, y) {
  return x * y;
}
export {add as default};
/ / is equivalent to
// export default add;

// app.js
import { default as foo } from 'modules';
/ / is equivalent to
// import foo from 'modules';
Copy the code

Because the export default command simply prints a variable named default, it cannot be followed by a variable declaration statement.

/ / right
export var a = 1;

/ / right
var a = 1;
export default a;

/ / error
export default var a = 1;
Copy the code

In the above code, export default A means to assign the value of variable A to variable default. So, the last one will give you an error.

Similarly, because the essence of the export default command is to assign the following value to the default variable, you can write a value directly after export Default.

/ / right
export default 42;

/ / an error
export 42;
Copy the code

In the above code, the latter statement fails because the external interface is not specified, while the former statement specifies the external interface is default.

With the export default command, it is straightforward to enter modules, such as the Lodash module.

import _ from 'lodash';
Copy the code

If you wanted to enter both default methods and other interfaces in an import statement, you could write it as follows.

import _, { each, forEach } from 'lodash';
Copy the code

The export statement corresponding to the above code is as follows.

If you wanted to enter both default methods and other interfaces in an import statement, you could write it as follows.

import _, { each, forEach } from 'lodash';
Copy the code

The export statement corresponding to the above code is as follows.

export default function (obj) {
  / /...
}

export function each(obj, iterator, context) {
  / /...
}

export { each as forEach };
Copy the code

The last line of the code above exposes the forEach interface, which by default points to each, meaning that forEach and each point to the same method.

Export default can also be used to output classes.

// MyClass.js
export default class {... }// main.js
import MyClass from 'MyClass';
let o = new MyClass();
Copy the code

4) renamed

In the braces of your import and export statements, you can use the as keyword with a new name to change the identification name of the functionality you want to use in the top-level module. So, for example, both of the following will do the same job, albeit in slightly different ways:

// inside module.mjs
export {
  function1 as newFunctionName,
  function2 as anotherNewFunctionName
};

// inside main.mjs
import { newFunctionName, anotherNewFunctionName } from '/modules/module.mjs';
Copy the code
// inside module.mjs
export { function1, function2 };

// inside main.mjs
import { function1 as newFunctionName,
         function2 as anotherNewFunctionName } from '/modules/module.mjs';
Copy the code

5. Overall loading of modules

In addition to specifying that an output value is loaded, you can also use global loading, where you specify an object with an asterisk (*) on which all output values are loaded.

Below is a circle. Js file that outputs two methods area and circumference.

// circle.js

export function area(radius) {
  return Math.PI * radius * radius;
}

export function circumference(radius) {
  return 2 * Math.PI * radius;
}
Copy the code

Now load the module.

// main.js

import { area, circumference } from './circle';

console.log('Circle area:' + area(4));
console.log('Circumference:' + circumference(14));
Copy the code

The above is written to specify the methods to be loaded one by one. The overall loading is written as follows.

import * as circle from './circle';

console.log('Circle area:' + circle.area(4));
console.log('Circumference:' + circle.circumference(14));
Copy the code

Note that the object to which the whole module is loaded (circle, for example) should be parsed statically, so runtime changes are not allowed. None of the following is allowed.

import * as circle from './circle';

// The following two lines are disallowed
circle.foo = 'hello';
circle.area = function () {};
Copy the code

reference

  • JavaScript modules module
  • Module syntax – Getting started with ECMAScript 6
  • CommonJS module
  • CommonJS specification
  • RequireJS and AMD specifications
  • The difference between AMD and CMD
  • Development of front-end modularization
  • What are the differences between AMD and CMD
  • Ten years of front-end modularization
  • Similarities and differences between SeaJS and RequireJS
  • LABjs, RequireJS or SeaJS, which is best? why
  • SeaJS – A Module Loader for the Web
  • RequireJS – Guide for beginners and detailed explanation of advanced usage