Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.
What is modular development
- In fact, the ultimate goal of modular development is to divide programs into smaller structures
- This structure writes its own logic code, has its own scope, does not affect other structures
- This structure can export variables, functions, objects, and so on that it wishes to expose to its structure for use
- You can also import variables, functions, objects, etc. from another structure in some way
The structures mentioned above are modules; The process of dividing development procedures according to this structure is the process of modular development
IIFE
The lack of modularity in the early days caused many problems: naming conflicts, for example
So initially to solve the above problem, we needed to use the IIFE (Immediately Invoked Function Expression) to create an independent Function scope
var moduleFoo = (() = > {
var name = 'Klaus'
/* IIFE can use two methods to expose data: 1. Return an object and receive it externally. 2. Mount to global objects to make it easier to use */ globally
return {
name
}
})()
Copy the code
But we’ve actually created a new problem:
- I have to remember the name of the returned object in each module so that I can use it correctly in the use of other modules. The more modules there are, the more difficult it is to find and name module names
- The code is messy, wrapped in an anonymous function in each file, and the outer functions in each module are redundant and repetitive
- In the absence of a proper specification, everyone and every company can have arbitrary names, even the same module names
Because we need to have some kind of specification that binds everyone to write modular code
There should be two core functions in this specification:
- Each module has its own independent scope
- The module itself can export exposed properties, and the module can import its own required properties
In order to solve the above problems, the JavaScript community emerged a series of good specifications, such as CJS, AMD, etc
commonjs
CommonJS is a specification that was originally proposed to be used outside of the browser and was named ServerJS. Later, to reflect its universality, it was changed to CommonJS, which is usually referred to as CJS for short
- Node is a representative implementation of CommonJS on the server
- Browserify is an implementation of CommonJS in a browser
- The WebPack packaging tool has CommonJS support and conversion
Therefore, CommonJS is supported and implemented in Node, so that we can easily carry out modular development in the process of developing Node
-
Each JS file in Node is a separate module
-
This module contains the core variables of the CommonJS specification :exports, module.exports, require
-
Exports and module.exports by default point to the same object and default to an empty object
-
We can use these variables to facilitate modular development
-
Exports and module.exports are responsible for exporting the contents of modules
-
The require function can help you import content from other modules (custom modules, system modules, third-party library modules).
-
/ / export
const name = 'Klaus'
const sayHello = () = > console.log('Hello World')
// Export properties and methods
exports.name = name
exports.sayHello = sayHello
Copy the code
/ / import
The return value of the require method is an object equal to the exports object of the foo module
const { name, sayHello } = require('./foo')
console.log(name) // => Klaus
sayHello() // => Hello World
Copy the code
So, common.js is essentially a shallow copy of an object (reference assignment)
The module exports and exports
- There is no module. Exports concept in CommonJS
- Module classes are used in Node to achieve modularity, and each Module is an instance of a Module, that is, a Module
- So in the
Node exports are not exports at all, but module.exports
Because Module is the real implementer of the export - The Module object’s exports property is a reference to the exports object (
module.exports === exports => true
)
/ / export
const name = 'Klaus'
exports.name = name
// At this point, the exported properties of exports are meaningless
// Because the export actually uses the module.exports object
module.exports = {
age: 23
}
Copy the code
/ / import
const foo = require('./foo')
console.log(foo.age) / / = > 23
console.log(foo.name) // => undefined
Copy the code
The search order of require
Require is a function that helps us import objects from a file (module). So, what are the lookup rules for require?
-
Check to see if the require method’s argument (assuming a value of X) is an absolute path
-
If it’s not an absolute path, it’s a name
- Check to see if X is a core module, if so, return directly to the core module, and stop looking
- If X is not a core module, go
node_modules
Check to see if it is a third-party module, if it is in the current directorynode_modules
If it does not exist, it will go to the next directorynode_modules
In the middle, and so on, until/node_modules
, this lookup rule is actually saved inmodule.path
In the
console.log(module) /* Module {id: '.', // The id of the entry file is., and the id of the other modules is the Module path: '/ Users/Klaus/Desktop/node - demo/foo/baz/bar', / / modules folder by exports: {}, actual export / / module content parent: null, filename: '/Users/klaus/Desktop/node-demo/foo/baz/bar/index.js', loaded: false, children: [], paths: [// Import module, If no path is not the core module search path array '/ Users/Klaus/Desktop/node - demo/foo/baz/bar/node_modules', '/Users/klaus/Desktop/node-demo/foo/baz/node_modules', '/Users/klaus/Desktop/node-demo/foo/node_modules', '/Users/klaus/Desktop/node-demo/node_modules', '/Users/klaus/Desktop/node_modules', '/Users/klaus/node_modules', '/Users/node_modules', '/node_modules' ] } */ Copy the code
-
If is a relative path (X with./ or.. / or /(root))
-
Look for X as a file in the corresponding directory
-
If the file name extension exists, locate the file based on the file name extension format
-
If there is no suffix, the search is performed in the following order
- Find file X directly
- Find the x. js file
- Find the x.json file
- Find the X. ode file
-
-
No corresponding file found, use X as a directory
- Find the X/index file
- Find the X/index.js file
- Find the X/index.json file
- Find the X/index.node file
-
If the resource is not found, an error message “Not found” is displayed
That’s why, when introducing modules in the Node environment, we can omit the JS suffix
-
Module loading process
- When a module is first introduced, the JS code in the module is run once
- Modules are cached when they are introduced multiple times, and eventually only loaded (run) once
- Each module object module has a property: Loaded
- False indicates that the load is not complete, and true indicates that the load is complete
- In a project, the interdependence of modules can be thought of as
The graph structure
- In the process of traversal, there are DFS (depth first search) and BFS (breadth first search).
- Node uses a depth-first algorithm
Therefore, in the figure above, the order of module introduction is main -> AAA -> CCC -> DDD -> EEE -> BBB
CJS features:
CommonJS loads modules synchronously
-
Synchronous means that the content in the current module cannot be run until the corresponding module is loaded
-
This will not be a problem on the server, because the js files loaded by the server are local files, very fast loading
-
The browser must download the JS file from the server before loading it
-
Then using synchronous means that subsequent JS code will not work properly, even some simple DOM operations
-
So in browsers, we generally don’t use the CommonJS specification
-
But early browsers didn’t have ESM, so the community provided EITHER AMD or CMD
-
AMD and CMD have been used very little, so here is a simple walkthrough
AMD
AMD is primarily a modular specification for browsers
AMD stands for Asynchronous Module Definition
It uses an asynchronous loading module
In fact, AMD’s specification predates CommonJS, but CommonJS is still in use and AMD uses it less
AMD is a modular specification, the specification just defines how the code should be written, only with a concrete implementation can be applied
The most common libraries implemented by AMD are require.js and curl.
index.html
<! Js automatically loads and executes files configured in data-main.
<script src="https://unpkg.com/[email protected]/bin/r.js" data-main="./index.js"></script>
Copy the code
Main entry file
// There should be an IIFE in the main entry file
(() = > {
// configure the corresponding module mapping table in require.js
require.config({
// baseUrl => baseUrl + paths[' module name ']
baseUrl: ' '.paths: {
// Note: the values corresponding to modules are not quoted
// require.js is automatically quoted when compiled
// No need to add, otherwise the js suffix will be repeatedly added
'foo': './foo'}})// Import modules
// Argument 1: array of imported modules
// Argument 2: a callback function that takes the imported module object
// There are several modules in the argument
// The order corresponds to the order of the modules in the parameter 1 array
require(['foo'].foo= > {
console.log(foo.name)
foo.sayHello()
})
})()
Copy the code
The module
// Define the module
// An array of modules introduced by parameter 1
// Argument 2 callback function -- argument is the imported module
define([], () = > {
const name = 'Klaus'
const sayHello = () = > console.log('Hello World')
// If a module needs to expose data, it simply returns it as the return value of the callback function
return {
name,
sayHello
}
})
Copy the code
CMD
The CMD specification is also a modular specification that applies to browsers
CMD stands for Common Module Definition
It also uses asynchronous loading modules, but it absorbs the benefits of CommonJS
CMD also has its own excellent implementation: sea-.js
index.html
<! Sea-.js -->
<script src="https://unpkg.com/[email protected]/dist/sea.js"></script>
<! -- Set main entry file -->
<script>
seajs.use('./index.js')
</script>
Copy the code
Main entry file
// Define the module
define((require.exports.module) = > {
const foo = require('./foo')
console.log(foo.name)
foo.sayHello()
})
Copy the code
Module file
// Define the module
define((require.exports.module) = > {
const name = 'Klaus'
const sayHello = () = > console.log('Hello World')
module.exports = {
name,
sayHello
}
})
Copy the code
ESM
There are some differences between ES Module and CommonJS modularity:
-
On the one hand, it uses the import and export keywords, not functions
-
On the other hand, it uses static analysis at compile time, and also adds dynamic references
-
Using ES Module automatically adopts strict mode: Use Strict
ES Module modules use export and import keywords to achieve modularity:
- Export is responsible for exporting the contents of the module
- Import is responsible for importing content from other modules
Tip: Note that using ESM modularity must be run as a local service
If you load Html files locally (such as a file:// path file), you will get CORS errors because of Javascript module security requirements
<! Type ="module" means that the js file will be parsed and loaded as a module.
<script src="./index.js" type="module"></script>
Copy the code
export
Method 1: Export variables one by one. Write the keyword export before each variable to be exported
export const name = 'Klaus'
export const sayHello = () = > console.log('Hello World')
Copy the code
Method 2: Export the data in a unified manner
const name = 'Klaus'
const sayHello = () = > console.log('Hello World')
// Note that the curly braces after export are not a list of objects, but rather a list of references to variables that need to be exported
export {
name,
sayHello
}
Copy the code
So when we export variables, we can alias variables, and objects do not have this function
const name = 'Klaus'
const sayHello = () = > console.log('Hello World')
// Note: If the alias is used, only fName and fSayHello will exist in the exported list. Name and sayHello will not exist
// If other modules need to be introduced, use fName and fSayHello instead of name and sayHello
export {
name as fName,
sayHello as fSayHello
}
Copy the code
The import
// Note that the curly braces introduced here are also not object destructions
import { name, sayHello } from './foo.js'
console.log(name)
sayHello()
Copy the code
// So we can still alias variables when importing them
import { name as fName, sayHello as fSayHello } from './foo.js'
console.log(fName)
fSayHello()
Copy the code
All import
// foo is an alias for *
// Note that foo is an object, not a reference list
import * as foo from './foo.js'
console.log(foo.name)
foo.sayHello()
Copy the code
Export and import are used together
When developing and encapsulating a feature library, it is often desirable to put all exposed interfaces into a single file and export them uniformly
This makes it easy to specify a uniform interface specification and easy to read. In this case, we can use export and import together
// Just change the original import to export
// The name and sayHello variables cannot be used in the transitionfile
export { name, sayHello } from './foo.js'
Copy the code
default
The previous exports we used were named exports (named exports)
- You need to specify a name when exporting export
- You need to know the specific name when importing an import
Another export is called a default export.
- You can export export without specifying a name by default
- You don’t need to use {} when importing, and you can specify the name yourself
- It also makes it easy for us to interact with existing specifications such as CommonJS
There can only be one default export in a module
The default export and named export can exist together
/ / export
export const name = 'Klaus'
export default() = >console.log('Hello World')
Copy the code
/ / import
// You can import both the named export and the default export. The default export is written before the named export
import sayHello, { name } from './foo.js'
sayHello()
console.log(name)
Copy the code
The import function
Loading a module with import cannot be placed in logical code
Import loading must be in the top-level scope of the code (global scope)
Since import is a keyword, dependency diagrams between modules need to be parsed at compile time,
Whether or not a module in logical code is loaded is not known until run time, and cannot be determined during parsing
But in some cases, we do want to load a module dynamically:
For example, according to different conditions, dynamically select the path to load the module
At this point we need to use the import() function to load dynamically
function foo() {
// Import is loaded asynchronously and returns what is actually a promise
// The res in the then method is actually what the module exports to us
import('./foo.js').then(res= > {
// The res returned is not a list of references, but an ordinary object, so it can be deconstructed
// Named exported variables are directly exported using the defined names
// The exported variable is automatically named default
const { name, default: sayHello } = res
console.log(name)
sayHello()
}).catch(err= > {
console.log(err)
})
}
foo()
Copy the code
Module loading process
CJS
-
The CommonJS module loads js files at runtime and synchronously
-
Run-time loading means that the JS engine loads modules during the execution of THE JS code
-
Synchronous means that the following code will not be executed until a file is loaded
-
-
CommonJS exports an object through module.exports
- Exporting an object means that references to this object can be assigned to other variables in other modules
- But eventually they all point to the same object, so if a variable modifies the properties of the object, everything gets modified
Modify the exported variable value after the export
/ / export
let info = { age: 23 }
let name = 'Klaus'
setTimeout(() = > info.age = 18.1000)
module.exports = {
info,
age
}
// -----------------------------------------
/ / import
const { info, name } = require('./foo')
// Export and import refer to the same object
// So the value of the reference type was successfully modified, but the value of the non-basic data type cannot be modified
setTimeout(() = > {
console.log(info.age) / / = > 18
console.log(name) // => Klaus
}, 2000)
Copy the code
In the imported JS file, reverse modify the exported variables
/ / export
let info = { age: 23 }
// Since both exports and imports are essentially one object, the imported file can also modify the INFO object
setTimeout(() = > console.log(info.age), 2000) / / = > 18
module.exports = {
info
}
// ------------------------------------------------
/ / import
const { info } = require('./foo')
setTimeout(() = > {
info.age = 18
}, 1000)
Copy the code
ESM
- The ES Module loads js files at compile (parse) time and asynchronously
- Loaded at compile time (parsing), meaning that imports cannot be used with run-time related content
- ES Modules are resolved statically, not dynamically or at run time
- Asynchronous means that the JS engine will fetch the import file when it encounters it, but the retrieval process is asynchronous and does not block the main thread from continuing
- Which means it’s set
type=module
Is equivalent toThe script tag
Also addedasync
attribute - The JS files and code corresponding to the ES Module would not block their execution if we had normal script tags followed by the corresponding code
<script src="./index.js" type="module"></script>
<script>
console.log('This code is going to get executed first.')
</script>
Copy the code
- The ES Module exports references to the variables themselves
- Export When exporting a variable, the JS engine will parse the syntax and create a Module environment record.
- Module environment records are bound to variables, and the binding is real-time
- In the import place, we can get the latest binding value in real time
So, if changes are made in the exported module, the latest variables are available in real time from where they were imported
Modify the exported variable value after the export
/ / export
export let age = 23
export const info = {
name: 'Klaus'
}
setTimeout(() = > {
info.name = 'Alex'
age = 18
}, 1000)
/ / import
import { info, age } from './foo.js'
// Unlike CJS, info and age are actually real-time reference addresses for exported variables
// So both basic data types
setTimeout(() = > {
console.log(info.name) // => Alex
console.log(age) / / = > 18
}, 2000)
Copy the code
Modify exported variables in reverse from the import file
/ / export
export let age = 23
export const info = {
name: 'Klaus'
}
setTimeout(() = > {
console.log(info.name)
console.log(age)
}, 2000)
// ------------------------
/ / import
import { info, age } from './foo.js'
setTimeout(() = > {
info.name = 'Alex' // => Reference variables can be modified normally
// age = 18 // => The basic data type cannot be modified
}, 1000)
Copy the code