
Most likely, when Brendan Ike designed JavaScript, he could not imagine how his project would evolve twenty years later. At the moment, there are already six main specifications of the language, and the work on its improvement is still ongoing.
Let's not be cunning: JavaScript has never been an ideal programming language. One of the weaknesses in JS was modularity, or rather its absence. Indeed, why in the scripting language, which animates the snowflakes falling on the page and validates the form, to take care of code isolation and dependencies? After all, everything can live beautifully and communicate with each other in one global area - the window.
')
Over time, JavaScript was transformed into a general purpose language, so it began to be used to build complex applications in various environments (browser, server). At the same time, it was impossible to rely on the old approaches to the interaction of program components through the global area: as the code grew, the application became very fragile. As a result, various modularity implementations were created to simplify the development process.
This article appeared as a result of communication with TC39 participants and framework developers, as well as reading source codes, blogs and books. We consider the following approaches / formats: Namespace, Module, Detached Dependency Definitions, Sandbox, Dependency Injection, CommonJS, AMD, UMD, Labeled Modules, YModules, and ES2015 Modules. In addition, we will restore the historical context of their appearance and development.
Content
Terms Used
Modularity solves the
following tasks : providing support for code isolation, determining dependencies between modules, and delivering code to the runtime environment. Some of the approaches listed above solved only one or two of these problems — we will call such solutions “patterns”, and those where all three tasks are solved — “modular systems”.
The specific structure of the source code with the definition of imported and exported entities (the second type includes objects, functions, etc.) will be called the "format of modules".
The term “detached definition of dependencies” (detached dependency definition, DDD) will be understood as such dependency definition approaches that can be used independently of modular systems.
More about problems
Before diving into the world of modularity, let's look more closely at what problems we face.
Name collision
Since its inception, JavaScript has used the global window object as a repository of all defined variables without the
var
keyword. In 1995-1999, it was very convenient because of the small amount of client JavaScript code on the pages. But with the increase in the amount of code, this feature began to lead to frequent errors due to the name collision. Let's look at an example:
When
greeting.js
connected to the page and then
hello.js
, we will receive the message “The script is broken” instead of a greeting because of a collision.
Obviously, in large projects this can cause a lot of headaches. Moreover, you cannot be sure that third-party scripts connected to the page will break nothing in your application.
Large code base support
Another uncomfortable point when using out-of-the-box javascript to build large applications is the need to explicitly specify the included scripts using the script tag.
If you make sure that the source code is convenient for support, you break it down into independent parts. Thus, there may be a lot of code files. With a large number of files, manual management of scripts (that is, the definition of connected scripts via the script tag) is very complicated: first, you need to remember to connect the necessary scripts to the page, and second, to distribute the sequence of script tags so that all dependencies between files were allowed.
Directly Defined Dependencies (1999)
The first attempt to introduce a modular structure in JavaScript, as well as the first implementation of a separate definition of dependencies, was the direct definition of dependencies in code (directly defined dependencies).
Erik Arvidsson (now a member of TC39) was one of the first to use this pattern back in 1999.
Eric then worked in a startup where a platform was created for launching GUI applications in the browser — WebOS (note: this is not about Palm’s webOS). WebOS was a proprietary platform — I could not get its source code. Therefore, we consider the implementation of the pattern on the example of the Dojo library, which since 2004 has been developed by Alex Russell and Dylan Skiemann.
The essence of the direct definition of dependencies is to load the code of the modules (in terms of Dojo - resources) with the explicit execution of the function
dojo.require
(which, in addition, initialized the loaded module). With this approach, dependencies are determined by the place of demand, right in the code.
Let's redo our example using Dojo 1.6:
Here we see that modules are defined using the
dojo.provide
function, and the code itself is loaded when the
dojo.require
function is
dojo.require
. This is a fairly simple approach that was used in Dojo before version 1.7 and is still used in the
Google Closure Library .
Namespace Pattern (2002)
The first attempt to solve the problem of name collision was to introduce agreements. For example, one could add a certain prefix to the names of all variables and functions:
myApp_
(
myApp_address
,
myApp_validateUser()
) or some other. Of course, this did not fundamentally change the situation, so that later developers began to use a key feature of JavaScript: the functions in it are first-class objects.
An object of the first class is an entity that can be assigned to variables and properties of objects and returned from other functions. That is, you can create objects with the same function properties (methods) as for the document and window objects (
document.write()
,
window.alert()
).
One of the first significant projects where this feature was used is the Bindows UI element library. Erik Arvidsson, already familiar to us, began working on it in 2002. Instead of prefixes in the names of functions and variables, he used a global object whose properties contained the data and logic of the library. Thus, pollution of the global area has been significantly reduced. Now a similar pattern for organizing code is known as the Namespace Pattern.
Let's put this idea to our example.
As we can see, the logic and data are now contained in the properties of the
app
object. This reduces the pollution of the global area, but we continue to have access to the necessary parts of the application from different files.
To date, the Namespace Pattern is probably the most popular pattern that can be found in JS. After Bindows, similar logic appeared in Dojo (2005), YUI (2005) and many other libraries and frameworks. It is worth noting that Eric
does not consider himself the author of this approach, but unfortunately he could not recall which project inspired him to use the pattern.
Module Pattern (2003)
Thanks to Namespace, the organization of the code became a little less erratic, but it was obvious that this was not enough: the problem of isolating code and data had not yet been solved.
The pioneer in solving this problem is the “Module” pattern. The idea behind the pattern is to encapsulate data and code using a closure and make it accessible through methods that, in turn, are accessible from the outside. Here is a basic example of such a module:
var greeting = (function () { var module = {}; var helloInLang = { en: 'Hello world!', es: '¡Hola mundo!', ru: ', !' }; module.getHello = function (lang) { return helloInLang[lang]; }; module.writeHello = function (lang) { document.write(module.getHello(lang)) }; return module; }());
Here we see a self-calling function — one that runs immediately after the declaration. It returns a
module
object, where there is a
getHello()
method that accesses the
helloInLang
object through a closure. Thus,
helloInLang
becomes inaccessible from the outside world and we get an atomic piece of code that can be inserted into any other script without a name conflict - even if the
helloInLang
object
helloInLang
been declared
helloInLang
.
The first mention of this approach appeared on the network in 2003, when Richard Cornford gave
an example of a module in the comp.lang.javascript group as an illustration of the use of closures. In 2005–2006, this approach was adopted by the developers of the YUI framework from Yahoo! under the direction of Douglas Crockford. But the greatest impulse for its distribution was given in 2008, when Douglas described the “Module” pattern in his book JavaScript JavaScript Good Parts.
But that's not all. In the article
"JavaScript Module Pattern: In-Depth" (here is a
translation in Habrahabr ) there are many different options for implementing a module. I recommend to look.
Template Defined Dependencies (2006)
The template definition of dependencies is the next pattern in the family of separate definitions. The earliest project I found where this approach is involved is
Prototype 1.4 (2006), but I suspect that it was used in earlier versions of the library.
Prototype has been developed since 2005 by
Sam Stephensen as a client part of the Ruby on Rails framework. Since Sam worked a lot with Ruby, it's no wonder that for managing dependencies between files, he chose regular templating with erb.
If you try to summarize, you can say that dependencies in this pattern are determined by including special labels in the target file. Both common templates (erb, jinja, smarty) and special build tools, such as
borshik, can be used
here .
When using template dependencies - in contrast to the previously considered patterns of separate definition - a preliminary assembly step is required.
Let's transform our example using the described style. To do this, use borshik:
Here the app.tmp.js file defines the included scripts and their order. If you reflect on the example, it becomes clear that this approach does not fundamentally change the life of the developer. Instead of using script tags, other labels in the js file are simply used. In other words, we can still forget something or confuse the order of the connected scripts. Therefore, the main purpose of this approach is to ensure the assembly of a single js-file from many others.
Comment Defined Dependencies (2006)
Another subtype of separate definition of dependencies is the definition of dependencies using comments. It is very similar to the direct definition, but in this case, comments are used instead of language constructions that contain information about the dependencies of the module.
An application using this approach should be pre-built - as was done in 2006 to build
MooTools , which
Valerio Proietti developed, - or it should parse the source code at the execution stage and then load all dependencies that were defined using comments. On the basis of the latter approach, the
LazyJS library of
Nicholas Bevacqua is implemented .
This is how our example will look like if you rewrite it using LazyJS:
The most famous library where this approach is used is MooTools. LazyJS was an interesting experiment, but it appeared after CommonJS and AMD and therefore did not receive much attention from the developers.
Externally Defined Dependencies (2007)
Let's look at the last pattern in the DDD family. When externally defining dependencies, they are all defined outside the main context - for example, in the configuration file or in the code as an object or an array. At the same time, there is a stage of preliminary preparation, during which the application is initialized with loading all dependencies in the correct order based on the information about them.
The earliest use of this approach, which I could find, dates back to 2007. We are talking about the library
MooTools 1.1 .
In the simplest case, we can implement our example using this pattern as follows (as a sample I will use my own experimental implementation of the
loader , where the desired pattern is involved):
The
deps.json
file is the external context where all dependencies are defined. When the application starts, the loader gets the file, reads dependencies from it, which are defined as an array, loads them and connects to the page in the correct order.
At the moment, this approach is used in some libraries to create custom assemblies, for example, in
lodash .
Sandbox Pattern (2009)
The Yahoo! programmers who worked on the new modular system YUI3 solved the problem of using different versions of the library on the same page. Before YUI3, the modular system in the framework was implemented by a combination of the Module and Namespace patterns. Obviously, with such a scheme, the root object containing the library code could be only one and, therefore, it was difficult to use several versions at once.
To solve this problem, one of the developers of YUI3,
Adam Moore, suggested using the Sandbox. A simple implementation of modularity using this pattern might look like this:
The essence of the approach is in two words - instead of a global object, a global constructor is used, and modules in turn can be defined as its properties.
The sandbox served as an interesting solution to the problem of modularity, but was not particularly popular outside of YUI3. If you want to learn more about Sandbox, I recommend the article
"Javascript Sandbox Pattern" , as well as the official YUI documentation about creating
new library
modules .
Dependency Injection (2009)
In 2004, Martin Fowler introduced the concept of “
dependency injection ” (DI) to describe the new component communication mechanism in Java. The main point is that all dependencies "come" from outside the component. In other words, the component is not responsible for initializing its dependencies, but only using them.
Five years later,
Mishko Heveri , a former employee of Sun and Adobe (where he was involved, including development in Java), started designing a JavaScript framework for his startup, where dependency injection was a key mechanism for interconnecting components. The idea of ​​a business has not proven its effectiveness, but the source code of the framework was decided to be put on the getangular.com startup domain. Most people know what happened next: Google took Mischko and his project under the wing and now Angular is one of the most well-known JavaScript frameworks.
Modules in Angular are implemented using the DI mechanism. However, modularity is not the primary purpose of DI: Mishko also clearly speaks about this in response to the
corresponding question .
To illustrate the approach, let's rewrite our example using the first version of Angular (yes, the example was extremely synthetic):
If you open a page with an example in the browser, then the code will
magically work out and we will see the result on the page.
Now the DI mechanism is used in the
Angular 2 and
Slot frameworks. There are also a large number of libraries that simplify the use of this approach in applications that are independent of any frameworks.
CommonJS Modules (2009)
Together with browser-based JavaScript engines, even before the advent of Node.js,
server development platforms were developed using JavaScript as the main language. Due to the lack of relevant specifications, server solutions did not provide a unified API for working with the operating system and the external environment (file system, network, environment variables, etc.), thus creating problems with code distribution. For example, scripts written for the old Netscape Enterprise Server did not work in Rhino and vice versa.
In 2009, a turning point came - Mozilla employee
Kevin Dangur published a
post about server-side JavaScript issues. In a post, he called on all interested to join an informal committee to discuss and develop a server-side JavaScript API. The project that started the work was named ServerJS; but later it was renamed CommonJS.
Work has begun to boil. Developers received the most attention from the specification of the format of modules in JavaScript -
CommonJS Modules (sometimes called CJS or just CommonJS), which was ultimately implemented in Node.js.
To demonstrate the CommonJS module, let's adapt our module as follows:
Here we see that for the implementation of modularity, new entities of
require
and
exports
(aliases on
module.exports
) are
module.exports
, which make it possible to load the module and provide its interface to the outside world. It is worth noting that neither
require
, nor
exports
, nor module are the keywords of the language - they appear in Node.js due to the wrapper, where all modules are wrapped, before going to run in a JavaScript engine:
(function (exports, require, module, __filename, __dirname) {
The CommonJS specification defines only the minimum necessary for interoperability of modules in various environments. So, CommonJS mechanisms can be expanded. For example, Node.js does this by adding to the
require
property a
main , which indicates the
module
if the file with the module was launched directly.
Babel also expands the
require
for a module with a default export in the format of ES2015 Modules (we will talk about this modular system at the end of the article):
export default something;
Babel converts a similar export to a CommonJS module, where the default value is exported using the corresponding property. That is, simply, the following code is obtained:
exports.default = something;
The webpack build system also uses various extensions, such as
require.ensure
,
require.cache
,
require.context
, but discussing them lies outside the context of the article.
Today CommonJS is the most common format for modules. This format is used not only in Node.JS - it can also be used when developing client web applications, collecting all modules into a single file using
Browserify or
webpack .
AMD (2009)
When the development of the CommonJS specification was well under way, there was occasional
vigorous discussion in the mailing list about adding asynchronous module loading capabilities to the specification. It would allow to speed up the loading of web applications consisting of a large number of files and eliminate the need to build to deliver code to users.
Kevin's fellow worker at Mozilla,
James Burke, was one of the most active supporters of asynchronous loading in all discussions. James could be an expert because he was the author of an asynchronous modular system in the Dojo 1.7 framework. In addition, it was he who in 2009 developed the
require.js loader.
, , — ( , ); . , AMD (Asynchronous Module Definition).
AMD, :
hello.js .
define
, . . ,
define
, , . .
2011 : AMD, CommonJS
.
, AMD , npm bower AMD .
UMD (2011)
, AMD CommonJS Modules. AMD , . CommonJS Modules - Node.js Browserify .
, . AMD- , CommonJS Modules (Node.js), CommonJS , AMD: RequireJS, curl.js. , RequireJS CommonJS-, . UMD —
Universal Module Definition .
— . UMD GitHub
.
, , ,
Q .
Q : ( script) Node.js Narwhal ( CommonJS Modules). , Q AMD. , UMD. UMD.
CommonJS AMD:
(function(define) { define(function () { var helloInLang = { en: 'Hello world!', es: '¡Hola mundo!', ru: ', !' }; return { sayHello: function (lang) { return helloInLang[lang]; } }; }); }( typeof module === 'object' && module.exports && typeof define !== 'function' ? function (factory) { module.exports = factory(); } : define ));
, . :
function (factory) { module.exports = factory(); }
CommonJS-. AMD-, define. .
, , Node.js, UMD. UMD ,
moment.js lodash .
Labeled Modules (2012)
2010 39 JavaScript. ES6 Modules. 2012 , . ,
( React), . , , ES3, , , .
Labeled Modules .
—
(labels). import export , : — exports, — require.
, , .
, Labeled Modules,
.
, . 2012 CommonJS AMD, . , Labeled Modules
webpack , JS- .
YModules (2013)
;). - , CommonJS AMD? , , CommonJS, AMD .
. API ( API .) , JS- . ,
, — .
2013 ,
dfilatov .
YModules :
YModules AMD. —
provide
,
return
.
«» , . , greeting.js - ( —
setTimeout
), , , :
, YModules . .
,
sayHello
greeting
, . , YModules (,
module
).
YModules .
i-bem.js .
ES2015 Modules (2015)
ECMAScript (TC39), , , JavaScript. , .
2010
Mozilla
. , . asm.js, emscripten, servo.
, 2015 ES2015, , ,
. :
,
import
export
.
- , , , Module Loader API, , , .
, ES2015 . , ES5,
Babel : .
Total
JS. ,
,
, -
. — , - . , , , - , .
« JavaScript » , , .
GitHub .