In the last section, we introduced some modularity thinking, such as AMD, CommonJS, CMD, ES Module, etc. Before talking about packaging principles, we will first familiarize ourselves with how these modules are. In this section, we will select a few commonly used ones.

CommonJS

It is what we often call CJS, a set of standards including module, file, IO and console proposed in 2009. The familiar node.js implementation uses it as part of the standard, and makes some adjustments based on it.

So CommonJS is a part of Node. It was originally designed for the server, but Browserify is a package tool that packages modules running in the Node environment into a single file that the browser can run. Then CommonJS can also be used for client-side code.

Specify files as modules in CommonJS. If you want to use the script tag directly to import js files, you can use the script tag directly to import js files. If you want to use the script tag directly to import JS files, you can use the script tag directly to import js files. We can test this, as shown in the following code:

// module1.js
var moduleName = 'module1'
Copy the code
// module2.js
var moduleName = 'module2'
require('./module1.js')
console.log('The current module is :', moduleName)
Copy the code

Module1.js and module2.js, each of which has a variable called moduleName, are overwritten by module1 when module2 is introduced into module1 using script tags. CommonJS will not be affected and output the current module is: Module2, so it can be proved that they have their own scope.

So why is that?

As you can see from the Node source code, because the module code is packaged as self-executing functions, the entire module code is wrapped in self-executing functions, so it is isolated from the outside world. This is what jQuery does in the previous section. Why can you access require, exports, __dirname? Node has a Module constructor that defines many properties, such as exports, ID, parent, chilren, Loaded, path, etc. The general code is as follows:

(function (exports.require.module, __filename, __dirname) {})
Copy the code

export

If you have a module that you want to share with other modules, such as some common logic and methods, then you need to export the module through module.exports in CommonJS.

module.exports = {
  add: function(a, b) {
    return a + b
  },
  subtract: function(a, b) {
    return a - b
  },
  multiply: function(a, b) {
    return a*b
  }
}
Copy the code

Module. exports is used to specify the contents of the module. Exports can be exported directly from exports.

exports.add = function(a, b) {
  return a + b
}
exports.subtract = function(a, b) {
  return a - b
}
exports.multiply = function(a, b) {
  return a * b
}
Copy the code

Exports: module. Exports: module. Exports: module.

var module = {
  exports: {} // Start with an empty object
}
var exports = module.exports
// exports.add = {add: () => {}}
Copy the code

Exports cannot be assigned a value directly, which will cause the problem of invalidation because you changed the reference of exports and it will not be added to module.exports.

// Error
exports = {
  add: () = >{}}// Do not mix, as follows
exports.add = function(a, b) {
  return a + b
}

module.exports = {
  subtract: (a, b) = > {
    return a - b
  }
}

// The problem with mixing is that add-exports is lost, module.exports is reassigned later, so the last one is exported
Copy the code

In addition to this, we don’t have to write the exported code at the end, the code after it will be executed, but to improve readability, we recommend putting it at the end.

The import

Import modules using require. Let’s look at the code first:

// module1.js
module.exports = {
  add: (a, b) = > { return a + b }
}

// index.js
const module1 = require('./module1.js')
console.log(module1.add(1.2)) / / 3
Copy the code

As shown in the code above, we create two files, module1.js and index.js, export an object with an add method in it, and then call this method. The require method takes a module path as an argument, and it can also use expressions to dynamically specify the module load path.

When you use require a module, if the module is loaded the first time, the contents of the module will be executed and then exported, and the second time it is loaded, the cache will be used. The module object has a loaded identifier inside it to record whether the module has been loaded. The default is false, as mentioned above. Node has a Module constructor, which constructs a Module object that contains the loaded property. Loop dependencies are solved by using caching, more on this later.

What does loading a module do in Node when we call require?

There are roughly the following steps:

  • If there is a cache, the cache is taken
  • If it is a built-in module, it returns directly
  • If is. /or/or../At the beginning, the absolute path is determined according to its parent module, whenxIs the file name, will look forx.x.js.x.json.x.node. ifxIs the directory, will find the directorypackage.jsonIn themainField configuration
  • If the module does not have a path, the system determines the possible installation directories based on the parent module and loads them in the directoriesxfile
  • Cache it when you find it

For those who are interested in this section, look at the Node source code, which has a method called module. _findPath that does this.

ES6 Module

Let’s look at the code implementation

// module.js
export default {
  add: function (a, b) {
    return a + b
  }
}

// index.js
import module from './module.js'
console.log(module.add(1.2)) / / 3
Copy the code

ES6 Module also treats each file as a Module, and each Module has its own scope. The difference is that the import and export statements are different, and it turns on strict mode by default.

export

Use the export syntax in ES6 Module to export modules. It can be exported in two ways: named export and default export.

The named export is written as follows:

// Declare and export together
export const moduleName = 'moduleA'
export const version = '1.0.0'
export const add = function (a, b) { return a + b }

// Or declare first and export later
const moduleName = 'moduleA'
export const version = '1.0.0'
const add = function (a, b) { return a + b }
export{moduleName, version, add}Copy the code

When exporting, we can also rename the export by as, for example

export { moduleName as name, add } // Import is name and add
Copy the code

The default export mode is as follows:

export default {
  moduleName: 'moduleA'.version: '1.0.0'.add: function(a, b) { return a + b}
}
Copy the code

Export default can be understood as the output of a default variable. The default export method is commonly used, for example, when we write script in Vue files.

The import

Use the import syntax in ES6 Modules to import modules. It is written as follows:

// index.js
import { add } from './module.js'
console.log(add(1.2)) / / 3
Copy the code

As you can see, when importing a module with a named export, the import is followed by a curly bracket enclosing the name of the exported variable (the same name). The import is equivalent to declaring these variables (add) in the current scope and is read-only. As with exports, variables can be renamed using AS, for example:

import { add as sumFn } from './module.js'
Copy the code

When multiple named exports need to be imported, we can use * to uniformly import and use as, which is equivalent to one object. In this way, we use exported variables to reduce naming conflicts and pollution.

The default import mode is as follows:

import module from './module.js'
module.add(1.2) / / 3
Copy the code

In fact, the previous export default can be understood as output of a default variable, so the above code can be regarded as:

import { default as module } from './module.js'
Copy the code

In React, we often use a mix of two imports, such as:

import React, { Component } from 'react';
Copy the code

React should be placed in front of braces, otherwise a syntax error will be reported.

CommonJS and ES6 Modules

Static and dynamic

CommonJS module dependencies are created at runtime and can be loaded dynamically using expressions, so explicit dependencies cannot be determined until the module is executed.

The ES6 Module Module dependencies to build is a compile time, it does not support the expression way of import, and import and export statement must be in the Module’s top scope, can not be put in the if, therefore belongs to a kind of static Module structure, then we can use this to analyze the dependencies, can do the following things:

  • Check for useless code:tree-sharkingThat’s what they do, they get rid of useless code, that’s what they useES6 ModuleThe static structure feature analyzes which modules are not being used, thus reducing the volume of packaging.
  • Type detection of module variables: Static module structure helps ensure that the values or interface types passed between modules are correct.
  • Compiler optimization:CommonJSThis dynamic structure, no matter which export is used, is essentially the exported object, such as module object, while the static module structure is the direct output, reducing the reference level and making it more efficient.

Value copy and reference

When importing a Module, CommonJS imports a shallow copy of the exported value, whereas ES6 Module is a reference to the value and is read-only.

That is, in CommonJS, when we import a module, we modify the contents of the module without affecting the contents of the module. We import a copy, as opposed to a separate copy, so we can modify the imported values.

The code tests are as follows:

// module.js
var base = 1
module.exports = {
  base,
  add: (a, b) = > a + b + base
}

// index.js
var base = require('./module.js').base
var add = require('./module.js').add

console.log(base) / / 1
console.log(add(1.2))/ / 4
base = 2
console.log(add(1.2))/ / 4
Copy the code

So if we want to export content dynamically, we can use a function to get it dynamically, export this function.

ES6 Module import variable is for reference, the original when exporting the content changes of the inside of the Module, the import will get together, but we can’t modify the imported, because is read-only, you can imagine the content of the import and export you is like a mirror, you will also move, you move the mirror and the mirror does not take the initiative to move you, Because it’s just a mapping to you.

Circular dependencies

Circular dependencies occur when module one depends on module two, and module two depends on module one. This circular dependency is easy to find, but in a real world scenario where there may be many modules, the dependencies end up in a closed loop, which is difficult to find and can lead to problems.

CommonJS loop – dependent case performance

// module1.js
let module2 = require('./module2.js') 
console.log('Module 2:', module2)
module.exports = 'I'm module 1'

// module2.js
let module1 = require('./module1.js')
console.log('Module 1:',  module1)
module.exports = 'I'm module 2'

// index.js
require('./module1.js')
Copy the code

Run in the console and the result is as follows

We can see that the output is not the desired result of module 1: I am module 1, but an empty object, and the warning explicitly tells us that the loop is dependent.

Why is that?

Let’s take a look at the Webpack code for the packaged results (with the bits that don’t need this attention removed)

/ * * * * * * / (function(modules) { // webpackBootstrap
/ * * * * * * / 	// The module cache
/ * * * * * * / 	var installedModules = {};
/ * * * * * * /
/ * * * * * * / 	// The require function
/ * * * * * * / 	function __webpack_require__(moduleId) {
/ * * * * * * /
/ * * * * * * / 		// Check if module is in cache
/ * * * * * * / 		if(installedModules[moduleId]) {
/ * * * * * * / 			return installedModules[moduleId].exports;
/ * * * * * * / 		}
/ * * * * * * / 		// Create a new module (and put it into the cache)
/ * * * * * * / 		var module = installedModules[moduleId] = {
/ * * * * * * / 			i: moduleId,
/ * * * * * * / 			l: false./ * * * * * * / 			exports: {}
/ * * * * * * / 		};
/ * * * * * * /
/ * * * * * * / 		// Execute the module function
/ * * * * * * / 		modules[moduleId].call(module.exports, module.module.exports, __webpack_require__);
/ * * * * * * /
/ * * * * * * / 		// Flag the module as loaded
/ * * * * * * / 		module.l = true;
/ * * * * * * /
/ * * * * * * / 		// Return the exports of the module
/ * * * * * * / 		return module.exports;
/ * * * * * * / 	}
/ * * * * * * /
/ * * * * * * / 	// Load entry module and return exports
/ * * * * * * / 	return __webpack_require__(__webpack_require__.s = "./test1/src/index.js");
/ * * * * * * /({})"./test1/src/index.js": (function(module, __webpack_exports__, __webpack_require__) {}),
    "./test1/src/utils/add.js": (function(module, __webpack_exports__, __webpack_require__){})});Copy the code

When we refer to module1.js in index.js, the __webpack_require__ method is executed for the Webpack code, and module1.js is loaded for the first time and stored in installedModules. Module1.js is not yet exported to module.exports. {} is returned from module1.js.

__webpack_require__ __webpack_require__ __webpack_require__ __webpack_require__ __webpack_require__.

The following detailed analysis of the code execution process, as a whole:

  • First, index.js passesrequireThe importmodule1And gave the executive powermodule1.
  • Into themodule1After middle, first rowrequireThe importmodule2.module1The executive power of themodule2, pay attention tomodule1It’s not done,module.exportsor{}
  • Into themodule2After middle, first rowrequireThe importmodule1At this time, a cyclic dependency is formed. I’m going to go ahead and do it because that’s what I saidmodule1themodule.exportsIs an empty object, then printsModule 1: {}, and finally exportI'm module 2
  • aftermodule2Executive power is returned tomodule1, proceed to printModule 2: I am Module 2, and finally exportI'm module 1
  • module1After execution, the execution authority is returned to the index file

Let’s look at the PERFORMANCE of the ES6 Module

// module1.js
import module2 from './module2.js'
console.log('Module 2:', module2)
export default 'I'm module 1'

// module2.js
import module1 from './module1.js'
console.log('Module 1:',  module1)
export default 'I'm module 2'

// index.js
import module1 from './module1.js'
Copy the code

The result of this execution is:

The result is the same as CommonJS.

The essential difference between CommonJS and CommonJS in solving circular dependencies is the second point that can be used. CommonJS uses value copy import, while ES6 Module is reference, which can be mapped dynamically.

To solve this problem, we can use function wrapping, because functions promote, but do not write function expressions, because function expressions do not promote.

The code is as follows:

// module1.js
import { module2 } from './module2.js'
console.log('Module 2:', module2())
function module1() {
    return 'I'm module 1'
}

export { module1 }

// module2.js
import { module1 } from './module1.js'
console.log('Module 1:', module1())
function module2() {
    return 'I'm module 2' 
}
export { module2 }

// index.js
import module1 from './module1.js'
Copy the code

Execution Result:

Other modularity

Old project files

We often maintain older projects that don’t use modularity, so we use script tags like jQuery. /jquery.min.js. Note that when Webpack is packaged, there is a layer of function scope for each file to avoid global contamination, so if you want to mount to the global, you need to pay attention to this.

AMD

It is an acronym for asynchronous Module definition, a standard used to support browser modularity, and it loads modules asynchronously. Use as follows:

// module.js
define('sum'['calculator'].function(math) {
  return function(a, b) { return a + b }
})

// index.js
require(['sum'].function(sum) {
  sum(1.2) / / 3
})
Copy the code

In AMD, the define function is used to define a module. It takes three parameters, namely the module name of the current module, the dependency of the current module, and the description of the exported content of the module (can be a function and an object type, the function needs to return a value).

The module is loaded using the require function, which takes two parameters: the module to load and the callback function to execute when it is finished loading.

The advantage of asynchronously loading modules is that they are non-blocking and do not block the execution of subsequent code.

The disadvantages are that the syntax is more complex and the loading order is not clear, which can lead to callback hell.

UMD

When we encounter more complex modularity, we may need to accommodate multiple modularity approaches. Using it is a common module standard that determines which module environment we are in based on the values in the current global object. The priority is to determine the AMD environment first, then CommonJS, and non-module environment. The specific code is as follows:

// module.js
(function (global, main) {
  if (typeof define === 'function' && define.amd) {
    // AMD
    define(...)
  } else if (typeof exports= = ='object') {
      // CommonJS
      module.exports = {...}
  } else {
    	// Non-module environment
    	global.xxx = ....
  }
})(this.function() {
  // Define the module body
  return. })Copy the code

We can also replace the order ourselves and export it in a preferred CommonJS way.

conclusion

Above, I mainly studied CommonJS and ES6 Module, two commonly used modular standards, as well as some other modular standards. The following will summarize the two main modular standards through the table.

CommonJS ES6 Module
The import require(‘moduleName’) import moduleName from ‘module’
export module.exports= {} export default {}
The establishment of module relationships At runtime, At compile time
Modular structure dynamic static
The type of the imported value Static type, value copy (shallow copy), can be modified Dynamic type, value reference, cannot be modified
Number of imports Require at will, except for the first time, subsequent ones from the cache Import in the header