With the advent of the Internet era, front-end technologies are being updated at a faster and faster pace. Initially, a few lines of code embedded in script
tags were enough to implement some basic user interaction, but now with the development of Ajax, jQuery, MVC and MVVM, the amount of Javascript code has become increasingly large and complex.
Web pages are becoming more and more like desktop programs, requiring a team to divide and collaborate, progress management, unit testing, etc. Developers have to use software engineering methods to manage the business logic of web pages, and Javascript modular development has become an urgent need. Ideally, developers would only need to implement the core business logic, and the rest could be loaded with modules already written by others.
However, Javascript is not a modular programming language, and it does not support classes, let alone modules. It was not until ES6 was finalized that Javascript began to officially support classes and modules, but it will be a long time before they are fully operational.
What is modularity
Modules are an integral part of any large application architecture. A module is a block of code or file that implements a specific function. Modules allow us to clearly separate and organize the units of code in a project. In project development, by removing dependencies and loosely coupling the application can be made more maintainable. With modules, developers can more easily use other people’s code and load whatever functionality they want. Module development needs to follow certain specifications, otherwise it will be chaotic.
The Javascript community has made a lot of efforts to implement “modules” into existing runtime environments. This article summarizes current best practices for modular programming in Javascript and shows how to put them to use.
Javascript Modularization Basic Writing Method
In the first part, modular writing based on traditional Javascript syntax will be discussed.
Primitive Writing
A module is a set of methods that implement a specific function. Simply putting different functions (and variables that record state) together is considered a module.
The above functions func1()
and func2()
make up a module. When you use them, you can just call them directly. The disadvantages of this approach are obvious: it “pollutes” the global variables, it does not guarantee that there are no variable name conflicts with other modules, and there is no direct relationship between module members.
Object writing method
To solve the above drawback, you can write the module as an object, and all the module members are put inside this object.
The above functions func1()
and func2()
are encapsulated in the moduleA
object, and when used, they call the properties of this object.
|
|
However, this way of writing exposes that all module members, internal state can be rewritten externally. For example, external code can directly change the value of the internal counter.
|
|
Immediately-Invoked Function Expression
Using Immediately-Invoked Function Expression (IIFE), you can achieve the goal of not exposing private members.
With the above writeup, external code cannot read the internal _count
variable.
|
|
moduleA
is the basic way to write a Javascript module. Next, this writing style is reworked.
Augmentation mode
If a module is large and must be divided into several parts, or if a module needs to inherit from another module, it is necessary to use the “augmentation mode” (augmentation).
The above code adds a new method func3()
to the moduleA
module, and then returns the new moduleA` module.
Loose augmentation mode
In a browser environment, the various parts of a module are usually fetched from the web, and sometimes it is impossible to know which part will be loaded first. If the previous section is written, the first part to be executed may load a non-existent empty object, and then “Loose augmentation mode” is used.
In contrast to the “augmentation mode”, the “Loose augmentation mode” means that the parameters of the “Immediately-Invoked Function” can be empty objects.
Enter global variables
Independence is an important feature of modules, and it is desirable that modules do not interact directly with other parts of the program internally. In order to call global variables inside a module, other variables must be explicitly entered into the module.
The moduleA
module above requires the use of the jQuery library and the YUI library, so the two libraries (actually two modules) are entered as parameters to moduleA
. In addition to ensuring module independence, this also makes the dependencies between the modules obvious. For more discussion on this, see Ben Cherry’s famous article “JavaScript Module Pattern: In-Depth”.
Specification-Based Javascript Modularization
In order for developers to all write modules in the same way, a specification for modules must be developed. While there is no official specification for Javascript modules to date, there are two common specifications for Javascript modules: CommonJS and AMD.
CommonJS
In 2009, American programmer Ryan Dahl created the node.js project to use the Javascript language for server-side programming. This marked the birth of “Javascript modular programming”. Because in a browser environment, the lack of modules is not a particularly big problem, after all, web programs are limited in complexity; but on the server side, there must be modules that interact with the operating system and other applications, otherwise there is no way to program.
The module system of node.js is implemented in reference to the CommonJS specification. According to the CommonJS specification, a single file is a module. Each module is a separate scope, which means that variables defined inside that module cannot be read by other modules unless they are defined as properties of a global object. The best way to export module variables is to use the module.exports object.
The above code defines a function that bridges the communication between the outside of the module and the inside, using the module.exports
object. The module is loaded using the require
method, which reads a file, executes it, and returns the module.exports
object inside the file.
Once you have a server-side module, it is natural to want a client-side module. And ideally the two should be compatible, so that a module can run in both the server and the browser without modification. But here’s the problem.
The second line, math.add(2, 3)
, runs after the first line, require('math')
, so it must wait for math.js
to finish loading. That is, if it takes a long time to load, the whole application will just stop there and wait. This is not a problem for the server side, because all modules are stored on the local hard disk and can be loaded simultaneously, and the waiting time is the read time of the hard disk. However, for the browser, this is a big problem, because the modules are placed on the server side, the waiting time depends on the speed of the network, it may take a long time to wait, the browser is in a “false death” state. Therefore, the browser side of the module, you can not use “synchronous loading”, only “asynchronous loading”. This is the background of the birth of the AMD specification.
AMD
AMD stands for “Asynchronous Module Definition”, which means “Asynchronous Module Definition”. It loads the module asynchronously, so that the loading of the module does not affect the execution of the statements that follow it. All statements that depend on the module are defined in a callback function, which will not run until the loading is complete.
AMD also uses the require()
statement to load modules, but unlike CommonJS, it requires two arguments.
|
|
The first parameter module
is an array whose members are the modules to be loaded; the second parameter callback
is the callback function after the successful loading. If you rewrite the previous code in AMD form, it would look like this.
math.add()
is not synchronized with the loading of the math module and no false death occurs in the browser. So it is clear that AMD is better suited to the browser environment. Currently, there are two main Javascript libraries that implement the AMD specification: require.js and curl.js.
AMD is a specification for modular development on the browser side. Because it is not natively supported by JavaScript, page development using the AMD specification requires the use of a corresponding library function, the famous requireJS, which is actually the output of the specification of module definitions during the promotion of requireJS. requireJS was created to requireJS was created to solve two problems.
- multiple JavaScript files may have dependencies, and the dependent file needs to be loaded into the browser before the file that depends on it
- the browser stops rendering the page when JavaScript is loaded, and the more files are loaded, the longer the page loses response time
Let’s start with an example.
Define the module: (moduleA.js)
Load the module and call.
Syntax of requireJS.
requireJS defines a function define
, which is a global variable used to define the module.
|
|
id
is an optional parameter that defines the module’s identity, and if it is not provided, the script filename (minus the extension)dependencies
is an array of module names that the current module depends onfactory
is a factory method where the module initializes the function or object to be executed. If a function, it should be executed only once. If it is an object, this object should be the output value of the module
Load the module on the page using the require
function.
|
|
The require()
function accepts two arguments.
- the first argument is an array indicating the dependent modules
- The second argument is a callback function that will be called when all the previously specified modules have been loaded successfully. The loaded modules are passed into the function as arguments, so that they can be used inside the callback function
The require()
function loads dependent functions asynchronously, so that the browser does not lose response. Also it specifies a callback function that will run only after all the previous modules have been loaded successfully, solving the dependency problem.
CMD
CMD (Common Module Definition) is a specification that specifies the basic writing format and basic interaction rules for modules. This specification was developed in China. AMD is dependency front-loading, CMD is on-demand loading.
The CMD specification was developed in China, just like AMD has requireJS, CMD has a browser implementation of SeaJS, which solves the same problem as requireJS, except in the way modules are defined and loaded (so to speak, run, parse). SeaJS solves the same problem as requireJS, except that it differs in the way modules are defined and the timing of module loading (running, parsing, if you will).
Sea.js promotes one module, one file, and follows a uniform writing style
Module definition.
|
|
Because the CMD specification promotes a file as a module, the file name is often used as the module id. CMD advocates proximity of dependencies, so dependencies are generally not written in the define
argument. factory
is the constructor of the module. This constructor is executed to get the interface that the module provides to the outside. The factory
method is executed with three parameters by default: require
, exports
and module
.
require
is the first parameter of thefactory
function, a method that accepts the module identifier as the only parameter to get the interfaces provided by other modules.exports
is an object that is used to provide the module interface to the outside.module
is an object that stores some properties and methods associated with the current module.
The following is an example of a CMD-compliant specification.
AMD vs CMD Differences
The two specifications were originally developed for different purposes.
AMD is the output of the specification of module definitions made by RequireJS during the rollout CMD is the output of the specification of module definitions made by SeaJS during the rollout
For dependent modules, AMD preloads and CMD defers loading.
AMD promotes dependency preloading, declaring the dependent modules when defining the module CMD promotes proximity dependency, loading a module only when it is used
This difference has its advantages and disadvantages, it’s just a syntax difference, and both requireJS and SeaJS support each other’s writing style.
The biggest difference between AMD and CMD is that the timing of execution of dependent modules is handled differently, note that it is not the timing or manner of loading that is different.
Many people say that requireJS loads modules asynchronously and SeaJS loads them synchronously, but this understanding is actually inaccurate. In fact, both specifications load dependent modules asynchronously, except that AMD is “dependency-front”, where the factory can easily know what dependent modules are available, and CMD is “dependency-near”. You need to parse the module into a string when you use it to know which modules depend on it. This is one of the things that many people criticize about CMD, sacrificing performance to bring convenience to development, when in fact parsing modules takes so little time that it can be ignored.
Again, the modules are loaded asynchronously. AMD will execute the module after loading the module dependencies, and after all modules are loaded and executed it will enter the require
callback function and execute the main logic. The effect of this is that the order of execution of the dependencies and the order in which they are written are not necessarily the same, depending on the speed at which the modules are loaded and which one is executed first, but the main logic must be executed after all the dependencies have been loaded.
CMD does not execute a dependency module after it is loaded, it just downloads it. After all the dependency modules are loaded, it enters the main logic and executes the corresponding module only when it encounters the require
statement, so that the execution order of the modules and the writing order are exactly the same.
This is the reason why many people say that AMD has a good user experience because there is no delay and the dependent modules are executed in advance, and CMD has a good performance because it is executed only when the user needs it.
Future-proofing the ES6 modularity standard
Since the call for modular development is so high, the official ECMA must do something about it. In fact, ECMA has included modularity in the draft for a long time, and finally the standard specification for modularity was included in the ES6 official release in June 2015. However, probably due to the immaturity of the technology involved, ES6 removes the content about how modules are loaded/executed and only retains the syntax for defining and introducing modules, which requires no special work to define a module, since the role of a module is to provide APIs to the public. The module can be defined with the module
keyword, and the API that the module needs to provide to the public can be exported with exoprt
.
Use the import
keyword to load external modules.
The module requested from the server.
Dynamic loading of a module.
This is the basic usage of ES6 Module, which is really weak and needs a long time to be standardized, so it is a long way to go. It also has a problem that the new syntax keywords are not backward compatible (e.g. lower versions of IE browsers). Currently we can use some third-party modules to compile ES6 into working ES5 code, or AMD-compliant modules, such as the ES6 module transpiler. There is also a project that provides a way to load ES6 modules, such as es6-module-loader, but these are temporary solutions, so maybe once ES7 is released, there will be a standard for module loading, and browsers will be able to implement it, so there will be no use for these tools. The future is still worth looking forward to, from the language standard support modularity, Javascript can be more confident into large-scale enterprise-level development.