preface

A good day starts from NPM Run Dev. For the current front-end, from the contention of a hundred schools of thought to the gradual unification of auxiliary development tools has an irreplaceable role in the improvement of front-end efficiency, all of which must rely on the modular front-end. When the front end was still in the slash-and-burn era, the only way to achieve modularity was through closures, also known as IIFE. Now ES6 Modules may be the most commonly used module solution for the front end, so this article starts from IIFE to summarize the previous life of front-end modularity. Oh, by the way, I didn’t say why I was thinking about modularity, because modularity makes it easier to unpack code, avoid variable conflicts, and without modularity you want to develop a big project, well, not very good.

IIFE (Closure)

In the slash-and-burn era, it was natural to interpret a JS file as a module. See the following demo:

<! DOCTYPE html> <html> <head> <title></title> </head> <body> <script src="./main.js"></script> <script src="./app.js"></script> <script src="./api.js"></script> </body> </html>Copy the code

Main.js, app.js and api.js understand them as three modules, but how should the code in these three JS files be constructed? Because the js mechanism itself can only expose the code to the global scope, if not to avoid global variable pollution then such a module is meaningless and no group of future problems. Closures came naturally back in the slash-and-burn era, and we tried to build these three JS files to isolate the scope.

(function(){ var name = 'main.js' ... }) ()Copy the code

The reason we use anonymous functions instead of declaring a function and calling it is because anonymous functions are released soon after execution, and there is no way that function names pollute the global scope. This approach to anonymous functions is IIFE, and while it is effective in resolving naming conflicts, dependency management is not. Because the browser parser executes scripts from top to bottom, the order in which script tags are introduced must be manually maintained in order to ensure dependencies between scripts. Communication between modules can be passed in as parameters.

CommonJS (node. Js)

Node.js is a modular implementation based on the CommonJS specification. Node.js is a modular implementation based on the CommonJS specification.

Let’s take a look at what the CommonJS specification says:

  1. A single file is a module.
  2. Each module is a separate scope.

Node.js implements the CommonJS specification, so what does Node.js do?

  1. The Node.js implementation gives each file a Module object, which contains all the information describing the current module. Its exports property (module.exports) is the interface to the outside world. Loading a module loads the module.exports property of that module. Here’s a quick list of the properties that the Module object has:
  • Module. id The module’s identifier, usually the module’s file name with an absolute path.
  • Module. filename specifies the filename of the module, with an absolute path.
  • Module. loaded Returns a Boolean value indicating whether the module has completed loading.
  • Module.parent returns an object representing the module that called the module.
  • Module. children returns an array representing other modules used by this module.
  • Module. paths represents the search paths of modules, depending on the depth of the directory.
  • Module. exports indicates the value that the module exports.
  1. A require method is also provided for each file to load modules.
  2. To make it easier for Node.js to provide an exports variable for each module, pointing to module.exports. This is equivalent to declaring var exports = module.exports in the header of each module.

What is the difference between module. Exports and exports?

Exports = module.exports (var exports = module.exports); exports (var exports = module.exports);

exports.myName = 'tom'
exports.getMyName = function(){
  return 'tom'
}
Copy the code

Note that you can’t point the exports variable directly to a value, because that breaks the link between exports and module.exports. Exports no longer refers to module.exports.

exports = function(){
  return 'tom'
}
// or
exports = 'tom'
Copy the code

The loading mechanism for the require() method:

The require command is used to load module files. The basic function of the require command is to read and execute a JS file and then return the exports object of that module. If no specified module is found, an error is reported. The require method reads js files by default, so it can omit the js suffix and prefix the JS file name with a path, which can be relative or absolute. If you omit the path, Node.js will assume that you are loading an internal module, or one already installed in the local node_modules directory. If a directory is loaded, Node.js will first look for package.json in that directory and load the modules configured by the main field in that directory, otherwise it will look for index.js in that directory. Look for modules in the node_modules directory in the following path order when loading them.

/usr/local/lib/node/x.js

/home/user/projects/node_modules/x.js

/home/user/node_modules/x.js

/home/node_modules/x.js

/node_modules/x.js

The caching mechanism for the require() method:

When a module is first loaded, Node.js will cache it. After a module is loaded once, a copy of the module will be maintained in the cache. Duplicate modules are directly extracted from the cache if they are encountered, meaning that there is only one instance of each module in the cache at any one time. Modules are loaded in the order in which they appear in the code.

About runtime:

On the server side, modules are loaded synchronously at runtime. On the browser side, modules need to be pre-compiled for packaging (gulp, webpack).

Does require() load modules synchronously or asynchronously?

Node.js is mainly used in the server, module files usually exist in the local hard disk, and modules with the cache mechanism mentioned above are often reused, so the loading is relatively fast, IO overhead can be ignored, do not consider the asynchronous loading mode. So the CommonJS specification is appropriate. In other words, the next operation cannot be performed until the load is complete. 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.

How is scope isolation implemented?

On the browser side, by looking at the packaged code, you can see that the essence of scoping isolation is still achieved through the function scope, which is also called closure.

AMD (RequireJS)

AMD is also a specification about modularity. RequireJS is a modular solution implemented based on the AMD specification. Said the CommonJS module load of specification is synchronous, so imagine in the browser environment module is present in the server, in the process of load module will lead to blocking, prevented the steps behind us proceed cause a long period of time the browser is blocked, may also perform an undefined method and result in error. In contrast to server-side modularity, the standard of modularity in the browser environment must meet a new requirement of “asynchronous module management”. In this context, RequireJS emerged.

RequireJS provides two methods:

  • Require () is used to introduce other modules
  • Define () defines the new module

The internal logic of RequireJS is to load the required dependent module through the function define(), retrieve the dependent (loaded module) in the callback and return a new value (module). All of our business code for the new module operates inside this function.

Let’s look at the following example:

Definition module

// Define (function() {return {... }}) // Define modules that depend on other modules (['./module1', './module2'], function(m1, M2) {// m1, m2 are './module1', './module2' loaded into the value return {... }})Copy the code

Load module

M1.fun () m2.fun()} require(['./module1', './module2'], function(m1, m2) {// m1, m2 also is './module1', './module2' loaded into value m1.fun() m2.fun()})Copy the code

CMD (Sea. Js)

CommonJS is the specification, AMD is the specification, CMD is of course the specification, cmd-based implementation is sea-.js. Through the analysis of RequireJS, we found that when declaring a module with RequireJS, we need to specify all dependencies in advance. These dependencies will be passed as parameters to define() method, which makes development more difficult, because dependent modules will be loaded first. Read all the dependencies first before reading the code (because they are initialized). Laziness is the primary goal of developers to improve efficiency, so if you can load a dependency module when it is used in the business code, you don’t have to read all the dependency module code in advance. In this way, sea.js appears, which can be understood as a combination of CommonJS and AMD in use.

define(function(require, exports, module) {
  let myHeader, myBody, myFooter
  if (status1) {
    var header = require('./header')
    myHeader = header.fun()
  }
  if (status2) {
    var body = require('./body')
    myBody = body.fun()
  }
  if (status3) {
    var footer = require('./footer')
    myFooter = footer.fun()
  }
  require.async('./module', function (m) { })

  module.exports = {
    header: myHeader,
    body: myBody,
    footer: myFooter
  }
})
Copy the code

Note that although the require() method is embedded in the business code, it is still loaded ahead of time. After loading a dependency module, it is not executed, it is simply downloaded. After loading all dependencies, the master logic is entered, and the corresponding module is executed when the require statement is encountered. The order in which modules are executed and written is exactly the same. Lazy loading of modules can be achieved by using require.async().

RequireJS also supports SeaJS, but dependent modules are still pre-loaded.

UMD (AMD + CommonJS)

CommonJS applies to the server, AMD, CMD applies to the Web, so modules that need to run on both ends can adopt UMD method, using this modular scheme, can be compatible with AMD, CommonJS syntax. UMD determines whether any module (exports) that supports Node.js exists and uses node.js module mode if it does. Then check whether AMD is supported (define exists). If yes, load the module in AMD mode. Due to the applicability of this generic module solution, many JS frameworks and libraries are packaged into this form of code.

ES6 Module

ES6 doesn’t have to be a special tool. ES6 supports modularity at the syntactic level. However, due to the browser implementation, if you want to run in the browser, you still need to compile with translation tools such as Babel. The ES6 provides the import and export commands, which correspond to the import and export functions of modules respectively. Es6 module related syntax has a lot of, will not expand to say.

Features:

  1. The syntax is static, and imports are automatically promoted to the top of the code.
  2. Variables imported using import are read-only, cannot be assigned, and are passed by reference. If your module changes the value of an exported variable during runtime, this will be reflected in the code that uses the module. Therefore, it is not recommended to modify exported values in modules; exported variables should be static.
  3. ES6 uses file-based modules, one file at a time.
  4. The ES6 module API is static, and once the module has been imported, you cannot add methods to the program as it runs.

Modular solutions in Webpack

With the rise of WebPack, we can use CommonJS, AMD, CMD, UMD, ES6 any method to develop and eventually webPack will be packaged as you need it.