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:

  1. 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
  2. The code is messy, wrapped in an anonymous function in each file, and the outer functions in each module are redundant and repetitive
  3. 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 theNode exports are not exports at all, but module.exportsBecause 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?

  1. 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

      1. Check to see if X is a core module, if so, return directly to the core module, and stop looking
      2. If X is not a core module, gonode_modulesCheck to see if it is a third-party module, if it is in the current directorynode_modulesIf it does not exist, it will go to the next directorynode_modulesIn the middle, and so on, until/node_modules, this lookup rule is actually saved inmodule.pathIn 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))

      1. 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

          1. Find file X directly
          2. Find the x. js file
          3. Find the x.json file
          4. Find the X. ode file
      2. No corresponding file found, use X as a directory

        1. Find the X/index file
        2. Find the X/index.js file
        3. Find the X/index.json file
        4. 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 asThe 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:

  1. On the one hand, it uses the import and export keywords, not functions

  2. On the other hand, it uses static analysis at compile time, and also adds dynamic references

  3. 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

  1. 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

  2. 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

  1. 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 settype=moduleIs equivalent toThe script tagAlso addedasyncattribute
  • 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
  1. 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