This article was first published on vivo Internet technology wechat official account
Link: https://mp.weixin.qq.com/s/15sedEuUVTsgyUm1lswrKA
By Morrain
1, Foreword
Previous< Front end Popular Science Series (2): node JS view the world from another angle >, we talked about node JS, node JS has been in such a mess since its birth. It is inseparable from its mature modular implementation, node JS modularization is implemented on the basis of CommonJS specification. What is CommonJS?
Let's take a look at it first Wikipedia Definition on:
CommonJS is a project whose goal is to create module conventions for JavaScript outside the web browser. The main reason for creating this project was the lack of a generally acceptable form of JavaScript script module unit at that time. The module can be reused in a different environment from that provided by the regular web browser running JavaScript scripts.
As we know, JavaScript language did not have the concept of modularity for a long time, until node After the birth of JS and the introduction of JavaScript language to the server, modularity becomes indispensable in the face of complex business scenarios such as file systems, networks, operating systems, etc. So node JS and CommonJS specifications complement each other, complement each other, and come into the sight of developers together.
It can be seen that CommonJS initially served the server, so I say CommonJS is not the front end, but its carrier is the front-end language JavaScript, which has had a far-reaching impact on the popularity of front-end modularization and laid a solid foundation. CommonJS: not the front end, but the front end!
2, Why modularity is needed
1. What does the front end look like without modularization
In the previous Web: going all the way, we mentioned that JavaScript was only used as a scripting language at the beginning of its birth, doing some simple form verification, etc. Therefore, the amount of code is very small. At first, it is written directly to the <script> tag, as shown below:
// index.html <script> var name = 'morrain' var age = 18 </script>
With the further complexity of the business, after the birth of Ajax, the front end can do more and more things, and the amount of code is growing rapidly. Developers begin to write JavaScript into independent js files and decouple it from html files. Like this:
// index.html <script src="./mine.js"></script> // mine.js var name = 'morrain' var age = 18
Later, more developers participated and more js files were introduced:
// index.html <script src="./mine.js"></script> <script src="./a.js"></script> <script src="./b.js"></script> // mine.js var name = 'morrain' var age = 18 // a.js var name = 'lilei' var age = 15 // b.js var name = 'hanmeimei' var age = 13
It is not difficult to find that the problem has come! Before ES6, JavaScript had no module system and no concept of closed scope, so the variables declared in the above three js files will exist in the global scope. Different developers maintain different js files, so it is difficult to ensure that they do not conflict with other js files. Global variable pollution has become a nightmare for developers.
2. Modular prototype
In order to solve the problem of global variable pollution, developers began to use the namespace method. Since the naming would conflict, add the namespace, as shown below:
// index.html <script src="./mine.js"></script> <script src="./a.js"></script> <script src="./b.js"></script> // mine.js app.mine = {} app.mine.name = 'morrain' app.mine.age = 18 // a.js app.moduleA = {} app.moduleA.name = 'lilei' app.moduleA.age = 15 // b.js app.moduleB = {} app.moduleB.name = 'hanmeimei' app.moduleB.age = 13
At this point, there is already a vague concept of modularity, which is only implemented in namespaces. To some extent, this solves the problem of naming conflicts. The developers of the b.js module can easily app.moduleA.name To get the name in module A, but you can also use the app.moduleA.name ='rename'to arbitrarily change the name in module A, but module A doesn't know about this! This is clearly not allowed.
Smart developers began to use the function scope of JavaScript language and use the closure feature to solve the above problem.
// index.html <script src="./mine.js"></script> <script src="./a.js"></script> <script src="./b.js"></script> // mine.js app.mine = (function(){ var name = 'morrain' var age = 18 return { getName: function(){ return name } } })() // a.js app.moduleA = (function(){ var name = 'lilei' var age = 15 return { getName: function(){ return name } } })() // b.js app.moduleB = (function(){ var name = 'hanmeimei' var age = 13 return { getName: function(){ return name } } })()
Now the b.js module can
App Modulea Getname() to get the name of module A, but the names of each module are saved in their own functions and cannot be changed by other modules. Such A design has already had the shadow of modularization. Each module maintains private things internally and opens interfaces to other modules, but it is still not elegant and perfect. For example, in the above example, module B can get the contents of module A, but module A cannot get the contents of module B, because the above three modules are loaded in sequence and depend on each other. When the business scale of A front-end application is large enough, this dependency becomes extremely difficult to maintain.
To sum up, the front end needs modularization, which not only deals with global variable pollution and data protection, but also solves the maintenance of dependencies between modules.
3, Introduction to CommonJS specification
Since JavaScript needs modularization to solve the above problems, it is necessary to formulate modular specifications. CommonJS is the modular specification to solve the above problems. Specifications are specifications. There is no reason. It is the same as the syntax of programming language. Let's have a look.
1. CommonJS overview
Node JS applications are composed of modules. Each file is a module with its own scope. Variables, functions, and classes defined in one file are private and invisible to other files.
// a.js var name = 'morrain' var age = 18
In the above code, a.js is node A module in the a.js application, in which the declared variables name and age are private to a.js, and other files cannot be accessed.
The CommonJS specification also stipulates that there are two variables available in each module, require d and module.
require is used to load a module
Module represents the current module. It is an object that saves the information of the current module. Exports is an attribute on the module that stores the interfaces or variables to be exported by the current module. The value obtained by a module loaded with require is the value exported by that module using exports
// a.js var name = 'morrain' var age = 18 module.exports.name = name module.exports.getAge = function(){ return age } //b.js var a = require('a.js') console.log(a.name) // 'morrain' console.log(a.getAge())// 18
2. CommonJS exports
For convenience, node When implementing the CommonJS specification, JS provides a private variable of exports for each module, pointing to module Exports. You can understand it as node JS at the beginning of each module, add the following line of code.
var exports = module.exports
So the above code can also be written as follows:
// a.js var name = 'morrain' var age = 18 exports.name = name exports.getAge = function(){ return age }
It should be noted that exports is a private local variable in the module, which only points to the module Exports, so it is invalid to directly assign values to exports. This just makes exports no longer point to module Exports.
As follows:
// a.js var name = 'morrain' var age = 18 exports = name
If the external interface of a module is a single value, you can use module Exports export
// a.js var name = 'morrain' var age = 18 module.exports = name
3. CommonJS require
The basic function of the require command is to read in and execute a js file, and then return the exports object of the module. If the specified module is not found, an error will be reported.
When a module is loaded for the first time, node JS caches the module. When the module is loaded later, the module of the module is directly fetched from the cache The exports property returns.
// a.js var name = 'morrain' var age = 18 exports.name = name exports.getAge = function(){ return age } // b.js var a = require('a.js') console.log(a.name) // 'morrain' a.name = 'rename' var b = require('a.js') console.log(b.name) // 'rename'
As shown above, the second time module A is require d, module A is not reloaded and executed. Instead, it directly returns the result of the first request, that is, the module of module A Exports.
It should also be noted that the loading mechanism of the CommonJS module is that the require d is a copy of the exported value. That is, once a value is exported, changes within the module will not affect the value.
// a.js var name = 'morrain' var age = 18 exports.name = name exports.age = age exports.setAge = function(a){ age = a } // b.js var a = require('a.js') console.log(a.age) // 18 a.setAge(19) console.log(a.age) // 18
4, CommonJS implementation
After understanding the CommonJS specification, it is not difficult to find that when we write modules that conform to the CommonJS specification, we use three things: require, exports and module. Then a js file is a module. As follows:
// a.js var name = 'morrain' var age = 18 exports.name = name exports.getAge = function () { return age } // b.js var a = require('a.js') console.log('a.name=', a.name) console.log('a.age=', a.getAge()) var name = 'lilei' var age = 15 exports.name = name exports.getAge = function () { return age } // index.js var b = require('b.js') console.log('b.name=',b.name)
If we provide three parameters: require, exports, and module to an immediate execution function, the module code is placed in the immediate execution function. The exported value of the module is placed in module Exports, which enables the loading of modules. As follows:
(function(module, exports, require) { // b.js var a = require("a.js") console.log('a.name=', a.name) console.log('a.age=', a.getAge()) var name = 'lilei' var age = 15 exports.name = name exports.getAge = function () { return age } })(module, module.exports, require)
Knowing this principle, it is easy to convert project code that conforms to the CommonJS module specification into browser supported code. Many tools are implemented in this way. Starting from the entry module, all dependent modules are put into their own functions, and all modules are packaged into a js file that can run in the browser. For example, Browserify, webpack, and so on.
Let's take webpack as an example to see how to support the CommonJS specification. When we use webpack to build, we package the file contents of each module into a js file in the following format. Because it is an anonymous function that can be executed immediately, it can be run directly in the browser.
// bundle.js (function (modules) { // Implementation of module management })({ 'a.js': function (module, exports, require) { // a.js file content }, 'b.js': function (module, exports, require) { // b.js file content }, 'index.js': function (module, exports, require) { // Index JS file content } })
Next, we need to implement the content of module management according to the CommonJS specification. First of all, we know that the CommonJS specification states that loaded modules will be cached, so we need an object to cache the loaded modules, and then we need a require function to load the modules. When loading, we need to generate a module, and the module must have an exports attribute to receive the contents exported by the module.
// bundle.js (function (modules) { // Implementation of module management var installedModules = {} /** * Business logic implementation of loading module * @param {String} moduleName Module name to load */ var require = function (moduleName) { // If it has been loaded, return directly if (installedModules[moduleName]) return installedModules[moduleName].exports // If it is not loaded, a module is generated and placed in installedModules var module = installedModules[moduleName] = { moduleName: moduleName, exports: {} } // Execute module to load modules[moduleName].call(modules.exports, module, module.exports, require) return module.exports } return require('index.js') })({ 'a.js': function (module, exports, require) { // a.js file content }, 'b.js': function (module, exports, require) { // b.js file content }, 'index.js': function (module, exports, require) { // Index JS file content } })
As you can see, the specifications of the CommonJS core have been met in the above implementations. It's very simple. It's not as difficult as you think.
5, Other front-end modular solutions
We are very familiar with the CommonJS specification. The basic function of the require command is to read in and execute a js file, and then return the exports object of the module. This is feasible on the server, because the time consumption for the server to load and execute a file can be ignored. The module is loaded synchronously at runtime. After the require command is executed, the file will be executed, And successfully get the value exported by the module.
This specification is inherently not applicable to browsers because it is synchronous. It is conceivable that every time the browser loads a file, it needs to send a network request to get it. If the network speed is slow, it will be very time-consuming. The browser will have to wait for the request to return, and it will be stuck there all the time, blocking the execution of the following code, thus blocking the page rendering, making the page appear pseudo dead.
In order to solve this problem, many front-end modular specifications have been developed later, including CommonJS, which are roughly as follows:
1,AMD (Asynchronous Module Definition)
Before talking about AMD, get familiar with RequireJS.
The official website introduces it as follows:
"RequireJS is a JavaScript file and module loader. It is optimized for in-browser use, but it can be used in other JavaScript environments, like Rhino and Node. Using a modular script loader like RequireJS will improve the speed and quality of your code."
Translated roughly as follows:
RequireJS is a js file and module loader. It is very suitable for use in browsers, but it can also be used in other js environments, such as Rhino and Node. Loading modular scripts with RequireJS can improve the loading speed and quality of code.
It solves the problem that the CommonJS specification cannot be used on the browser side, and AMD is the standardized output of the module definition of RequireJS in the promotion process.
Let's take a look at the implementation of AMD specification:
<script src="require.js"></script> <script src="a.js"></script>
First, you need to introduce the require JS tool library, which provides functions such as defining modules and loading modules. It provides a global define function to define modules. Therefore, after introducing require JS file and other imported files can use define to define modules.
define(id?, dependencies?, factory)
id: an optional parameter used to define the module id. if this parameter is not provided, the js file name (excluding the extension name) is used. When only one module is defined for a js file, this parameter can be omitted. Dependencies: an optional parameter. It is an array that represents the dependencies of the current module. If there are no dependencies, the factory method can be omitted. The module initializes the function or object to be executed. If it is a function, it should only be executed once, and the return value is the value to be exported by the module. If it is an object, this object should be the output value of the module.
So module A can be defined as:
// a.js define(function(){ var name = 'morrain' var age = 18 return { name, getAge: () => age } }) // b.js define(['a.js'], function(a){ var name = 'lilei' var age = 15 console.log(a.name) // 'morrain' console.log(a.getAge()) // 18 return { name, getAge: () => age } })
It loads modules asynchronously, and the loading of modules does not affect the operation of the following statements. All statements that depend on this module are defined in the callback function. The callback function will not run until the loading is completed.
The basic idea of RequireJS is to define the code as a module through the define method. When the module is required, it starts to load the modules it depends on. When all dependent modules are loaded, it starts to execute the callback function. The return value is the value exported by the module. AMD is the abbreviation of "Asynchronous Module Definition", which means "Asynchronous Module Definition".
2,CMD (Common Module Definition)
Similar to AMD, CMD is sea JS is the standardized output of module definition in the promotion process. Sea JS was written by Ali's jade uncle. It was born after RequireJS. Yubo thinks that AMD specification is asynchronous, and the organization form of modules is not natural and intuitive. So he is pursuing a writing form like CommonJS. So there is CMD.
Sea JS official website introduces sea JS:
"Sea.js pursues simple and natural code writing and organization, with the following core features:"
"Simple and friendly module definition specification: Sea.js follows the CMD specification and can write module code like Node.js. Natural and intuitive code organization: automatic loading of dependencies and concise and clear configuration allow us to enjoy coding more."
Let's take a look at the implementation of CMD specification:
<script src="sea.js"></script> <script src="a.js"></script>
First, we need to introduce sea JS tool library, which provides functions such as defining modules and loading modules. It provides a global define function to define modules. So sea JS file and other imported files can use define to define modules.
// All modules are defined through define define(function(require, exports, module) { // Introducing dependencies through require var a = require('xxx') var b = require('yyy') // Provide external interfaces through exports exports.doSomething = ... // Or through module Exports provides the entire interface module.exports = ... }) // a.js define(function(require, exports, module){ var name = 'morrain' var age = 18 exports.name = name exports.getAge = () => age }) // b.js define(function(require, exports, module){ var name = 'lilei' var age = 15 var a = require('a.js') console.log(a.name) // 'morrain' console.log(a.getAge()) //18 exports.name = name exports.getAge = () => age })
Sea The secret that JS can write module code synchronously like CommonsJS is that when the b.js module is required, sea JS will scan the code of b.js, find the keyword require, extract all dependencies, and then load them. After all dependent modules are loaded, execute the callback function. At this time, execute the line of require('a.js'), and A.js has been loaded in memory
3,ES6 Module
CommonJS mentioned above serves the server, while AMD and CMD serve the browser. However, they all have one thing in common: the exported content can only be determined after the code is run, CommonJS implementation Can be seen in.
It should also be noted that AMD and CMD are module loading schemes formulated by community developers, not language level standards. Starting from ES6, modular functions have been realized at the level of language standards, and the implementation is quite simple. It can completely replace CommonJS, CMD and AMD specifications and become a common modular solution for browsers and servers.
In fact, as early as may2013, node Isaac Z. Schlueter, the author of the package manager NPM of JS, said CommonJS is out of date, node JS kernel developers have decided to scrap the specification . There are two main reasons. One is node JS itself does not completely adopt the CommonJS specification. For example, in CommonJS exports The exports attribute mentioned in is node JS, node JS decided not to follow the development of CommonJS. Second, node JS is also gradually replacing CommonJS with ES6 Module.
2017.9.12 node ES6 Module is supported in version 8.5.0 released by. JS. It's just an experiment. You need to add the --empirical-modules parameter.
November 21, 2019 node The --experimental-modules parameter has been canceled in the 13.2.0 version released by. JS, that is, from v13.2, node Es6.js has enabled ES6 Module support by default.
(1) ES6 Module syntax
Two issues that must be considered in any modularization are import dependency and export interface. The same is true for ES6 Module. The module function is mainly composed of two commands: export and import. The export command is used to export the external interface of the module, and the Import command is used to import the content exported by other modules.
Please refer to for specific grammar explanation Ruanyifeng's tutorial , for example:
// a.js export const name = 'morrain' const age = 18 export function getAge () { return age } //Equivalent to const name = 'morrain' const age = 18 function getAge (){ return age } export { name, getAge }
After defining the external interface of the module with the export command, other JavaScript files can load the module with the import command.
// b.js import { name as aName, getAge } from 'a.js' export const name = 'lilei' console.log(aName) // 'morrain' const age = getAge() console.log(age) // 18 // Equivalent to import * as a from 'a.js' export const name = 'lilei' console.log(a.name) // 'morrin' const age = a.getAge() console.log(age) // 18
In addition to specifying a certain output value to be loaded, you can also use overall loading, that is, an object is specified with an asterisk (*), and all output values are loaded on this object.
As can be seen from the above example, when using the import command, the user needs to know the name of the variable to be imported, which is sometimes troublesome. Therefore, ES6 Module specifies a convenient usage. Use the export default command to specify the default output for the module.
// a.js const name = 'morrain' const age = 18 function getAge () { return age } export default { name, getAge } // b.js import a from 'a.js' console.log(a.name) // 'morrin' const age = a.getAge() console.log(age) // 18
Obviously, a module can only have one default output, so the export default command can only be used once. At the same time, you can see that there is no need to use braces after the import command.
In addition to the basic syntax, you can learn the usage of as, the composite writing of export and import, export * from'a'and the dynamic loading of import().
The node mentioned earlier JS already supports ES6 Module by default, and the browser also fully supports ES6 Module. As for node JS and browsers can learn how to use ES6 Module by themselves.
(2) Differences between ES6 Module and CommonJS
CommonJS can only determine the exported interface at runtime. What is actually exported is an object. The design idea of ES6 Module is to be as static as possible, so that the dependencies of the module and the imported and exported variables can be determined at compile time, that is, the so-called "compile time loading".
Because of this, the Import command has a promotion effect. It will be promoted to the head of the entire module and executed first. The following code is legal because the execution of import is earlier than the call of getAge.
// a.js export const name = 'morrain' const age = 18 export function getAge () { return age } // b.js const age = getAge() console.log(age) // 18 import { getAge } from 'a.js'
Because the ES6 Module is loaded at compile time, expressions and variables cannot be used because these are syntax structures that can only get results at run time. As follows:
// Error reporting import { 'n' + 'ame' } from 'a.js' // Error reporting let module = 'a.js' import { name } from module
In front of CommonJS require It was mentioned that the require d is a copy of the exported value. That is, once a value is exported, changes within the module will not affect the value. Let's see what the ES Module looks like.
Let's review the previous examples:
// a.js var name = 'morrain' var age = 18 exports.name = name exports.age = age exports.setAge = function(a){ age = a } // b.js var a = require('a.js') console.log(a.age) // 18 a.setAge(19) console.log(a.age) // 18
Use the ES6 Module to implement this example:
// a.js var name = 'morrain' var age = 18 const setAge = a => age = a export { name, age, setAge } // b.js import * as a from 'a.js' console.log(a.age) // 18 a.setAge(19) console.log(a.age) // 19
ES6 Module is the specification for modules in ES6. ES6 is the abbreviation of ECMAScript 6.0. It is the next generation standard of JavaScript language and has been officially released in june2015. We mentioned in the first section of the Web: moving forward and forgetting the river. ES6 has been developed and released for more than ten years, and has introduced many new features and mechanisms. For developers, the learning cost is still very large.
Next, talk about ES6+ and Babel. Please look forward to
6, References
- CommonJS specification
- Syntax of ES Module
- Loading implementation of ES Module
- Front end modular development solution details
- webpack modular principle commonjs
For more information, please follow vivo Internet technology wechat official account
Note: please contact wechat: Labs2020 before reprinting the article.