MEMEPh. ideas that are worth sharing...

Front-end modularization (full version)

Foreword


In the early days of JavaScript development, it was just to implement simple page interaction logic, just a few words; now the performance of CPU and browser has been greatly improved, and many page logics have been migrated to the client (form validation, etc.), with the development of web 2.0 With the advent of the era, Ajax technology has been widely used, jQuery and other front-end libraries are emerging one after another, and the front-end code is expanding day by day. At this time, JS will consider using modular specifications to manage.


The content of this article mainly includes understanding of modularization, why it is necessary to modularize, the advantages and disadvantages of modularization, and modularization specifications, and introduces the most popular CommonJS, AMD, ES6, and CMD specifications in development. This article attempts to introduce these boring concepts in an easy-to-understand style from the perspective of Xiaobai. I hope that after reading, you will have a new understanding and understanding of modular programming!

 

An understanding of modularity


1. What is a module?

 

2. Modular evolution process

function m1(){
  //...
}

function m2(){
  //...
}
let myModule = {
  data: 'www.rendc.com',
  foo() {
    console.log(`foo() ${this.data}`)
  },
  bar() {
    console.log(`bar() ${this.data}`)
  }
}
myModule.data = 'other data' //Can directly modify the data inside the module
myModule.foo() // foo() other data

 

This way of writing exposes all module members, and the internal state can be rewritten externally.

// index.html document
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
    myModule.foo()
    myModule.bar()
    console.log(myModule.data) //undefined //Cannot access module internal data
    myModule.data = 'xxxx' //not inside the modified module data
    myModule.foo() //No change
</script>
// module.js document
(function(window) {
  let data = 'www.rendc.com'

// function to manipulate data
  function foo() {
// used to expose a function
    console.log(`foo() ${data}`)
  }

  function bar() {
// used to expose a function
    console.log(`bar() ${data}`)
    otherFun() //internal call
  }

  function otherFun() {
//Internal private function
    console.log('otherFun()')
  }

// expose behavior
  window.myModule = { foo, bar } //ES6
})(window)

 

IIFE Mode Enhancement: Introducing Dependencies

This is the cornerstone of modern module implementations

// module.js document

(function(window, $) {
  let data = 'www.rendc.com'
// function to manipulate data
  function foo() {
    //for exposing functions
    console.log(`foo() ${data}`)
    $('body').css('background', 'red')
  }
  function bar() {
// used to expose a function
    console.log(`bar() ${data}`)
    otherFun() //Internal call
  }

  function otherFun() {
//Internal private function
    console.log('otherFun()')
  }

  //exposed behavior
  window.myModule = { foo, bar }
})(window, jQuery)
 // index.html document
<!-- The imported js must be in a certain order -->
  <script type="text/javascript" src="jquery-1.10.1.js"></script>
  <script type="text/javascript" src="module.js"></script>
  <script type="text/javascript">
    myModule.foo()
  </script>

The above example uses the jquery method to change the background color of the page to red, so the jQuery library must be introduced first, and this library is passed in as a parameter. In addition to ensuring the independence of modules, this also makes the dependencies between modules obvious.

 

3. The benefits of modularity

 

4. <script>Problems occur after the introduction of multiple

First, we have to rely on multiple modules, which will send multiple requests, resulting in too many requests

We don't know what their specific dependencies are, which means that it is easy to cause errors in the loading order because of not understanding the dependencies between them.

The above two reasons make it difficult to maintain, and it is likely to cause serious problems in the project.
Of course, modularization has many advantages, but a page needs to introduce multiple js files, and the above problems will occur. And these problems can be solved by modular specifications. The most popular commonjs, AMD, ES6, CMD specifications in development are introduced below.

 

2. Modular specification


1.CommonJS

(1) Overview

Node applications are composed of modules, using the CommonJS module specification. Each file is a module and has its own scope. Variables, functions, and classes defined in a file are private and invisible to other files. On the server side, modules are loaded synchronously at runtime; on the browser side, modules need to be compiled and packaged in advance.

 

(2) Features

 

(3) Basic grammar

Here we have a question: What exactly is the module exposed by CommonJS? The CommonJS specification stipulates that within each module, the module variable represents the current module. This variable is an object whose exports property (ie module.exports) is the external interface. Loading a module is actually loading the module.exports property of the module .

// example.js
var x = 5;
var addX = function (value) {
  return value + x;
};
module.exports.x = x;
module.exports.addX = addX;

The above code outputs the variable x and the function addX through module.exports.

var example = require('./example.js');//If the parameter string starts with "./", it means that a relative path is loaded
console.log(example.x); // 5
console.log(example.addX(1)); // 6

The require command is used to load module files. The basic function of the require command is to read and execute a JavaScript file, and then return the exports object of the module. If the specified module is not found, an error will be reported.

 

(4) Loading mechanism of modules

The loading mechanism of CommonJS modules is that the input is a copy of the value that is output. That is, once a value is output, changes within the module cannot affect this value. This is a major difference from ES6 modularity (described below), see the following example:

// lib.js

var counter = 3;
function incCounter() {
  counter++;
}

module.exports = {
  counter: counter,
  incCounter: incCounter,
};

 

The above code outputs the internal variable counter and the internal method incCounter that overrides this variable.

// main.js

var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;
console.log(counter);  // 3
incCounter();
console.log(counter); // 3

The above code shows that after the counter is output, changes inside the lib.js module will not affect the counter. This is because counter is a primitive value and will be cached. Unless it is written as a function, the value after the internal change can be obtained.

 

(5) Server-side implementation

① Download and install node.js

② Create a project structure

Note: When using npm init to automatically generate package.json, the package name (package name) cannot have Chinese and uppercase

|-modules
  |-module1.js
  |-module2.js
  |-module3.js
|-app.js
|-package.json

  {
    "name": "commonJS-node",
    "version": "1.0.0"
  }

③ Download third-party modules

npm install uniq --save // for array deduplication

④Define the module code

//module1.js
module.exports = {
  msg: 'module1',
  foo() {
    console.log(this.msg)
  }
}
//module2.js
module.exports = function() {
  console.log('module2')
}
//module3.js
exports.foo = function() {
  console.log('foo() module3')
}
exports.arr = [1, 2, 3, 3, 2]
// app.js document
// Introduce third-party libraries, which should be placed at the top
let uniq = require('uniq')
let module1 = require('./modules/module1')
let module2 = require('./modules/module2')
let module3 = require('./modules/module3')


module1.foo() //module1
module2() //module2
module3.foo() //foo() module3
console.log(uniq(module3.arr)) //[ 1, 2, 3 ]

⑤ Run app.js through node

Command line input node app.js, run JS file

(6) Browser-side implementation (with Browserify)

① Create a project structure

|-js
  |-dist //The directory where the generated files are packaged
  |-src //The directory where the source code is located
    |-module1.js
    |-module2.js
    |-module3.js
    |-app.js // application main source file
|-index.html // run on the browser
|-package.json
  {
    "name": "browserify-test",
    "version": "1.0.0"
  }

② Download browserify

③ Define the module code (same as the server side)

Note: index.htmlTo run the file on the browser, you need to app.jspackage and compile the file with browserify. If it is index.htmlimported directly, an app.jserror will be reported!

④ Packaging and processing js

run in the root directory browserify js/src/app.js -o js/dist/bundle.js

⑤ Page usage introduction

Introduced in the index.html file <script type="text/javascript" src="js/dist/bundle.js"></script>

 

2.AMD

The CommonJS specification loads modules synchronously, that is, only after the loading is complete, the subsequent operations can be performed. The AMD specification is to load modules asynchronously, allowing callback functions to be specified. Since Node.js is mainly used for server programming, the module files generally already exist on the local hard disk, so the loading is faster, and the asynchronous loading method does not need to be considered, so the CommonJS specification is more applicable. However, if it is a browser environment, to load modules from the server side, the asynchronous mode must be used at this time, so the browser side generally adopts the AMD specification. In addition, the AMD specification was implemented earlier than the CommonJS specification on the browser side.

(1) AMD specification basic syntax

Define the exposed module :

//Define a module with no dependencies
define(function(){
   return module
})
//Define dependent modules
define(['module1', 'module2'], function(m1, m2){
   return module

})

Import using modules :

require(['module1', 'module2'], function(m1, m2){
use m1/m2
})

 

(2) Not using AMD specification and using require.js

By comparing the implementation methods of the two, the benefits of using the AMD specification are illustrated.

Not using AMD specs

// dataService.js document

(function (window) {
  let msg = 'www.rendc.com'
  function getMsg() {
    return msg.toUpperCase()
  }
  window.dataService = {getMsg}
})(window)
// alerter.js document
(function (window, dataService) {
  let name = 'Tom'
  function showMsg() {
    alert(dataService.getMsg() + ', ' + name)
  }
  window.alerter = {showMsg}
})(window, dataService)
// main.js document
(function (alerter) {
  alerter.showMsg()
})(alerter)
// index.html document
<div><h1>Modular Demo 1: Unused AMD(require.js)</h1></div>
<script type="text/javascript" src="js/modules/dataService.js"></script>
<script type="text/javascript" src="js/modules/alerter.js"></script>
<script type="text/javascript" src="js/main.js"></script>

The disadvantages of this method are obvious: firstly, multiple requests will be sent, and secondly, the sequence of the introduced js files cannot be wrong, otherwise an error will be reported!

RequireJS is a tool library mainly used for client-side module management. Its module management complies with the AMD specification. The basic idea of ​​RequireJS is to define the code as a module through the define method, and realize the module loading of the code through the require method.
Next, we introduce the steps of AMD specification implementation in browsers:

① Download require.js, and import

Then import require.js into the project: js/libs/require.js

② Create a project structure

|-js
  |-libs
    |-require.js
  |-modules
    |-alerter.js
    |-dataService.js
  |-main.js
|-index.html

③ Define the module code of require.js

// dataService.js document
// define a module with no dependencies
define(function() {
  let msg = 'www.rendc.com'
  function getMsg() {
    return msg.toUpperCase()
  }
  return { getMsg } // exposed module
})
//alerter.js document
// define dependent modules
define(['dataService'], function(dataService) {
  let name = 'Tom'
  function showMsg() {
    alert(dataService.getMsg() + ', ' + name)
  }
// expose the module
  return { showMsg }
})

 
// main.js document
(function() {
  require.config({
    baseUrl: 'js/', //Base path The starting point is in the root directory
    paths: {
//map: module identifier: path
      alerter: './modules/alerter', //It cannot be written as alerter.js here, it will report an error
      dataService: './modules/dataService'
    }
  })
  require(['alerter'], function(alerter) {
    alerter.showMsg()
  })
})()
// index.html document
<!DOCTYPE html>
<html>
  <head>
    <title>Modular Demo</title>
  </head>
  <body>
<!-- Introduce require.js and specify the entry of the main js file -->
    <script data-main="js/main" src="js/libs/require.js"></script>
  </body>
</html>

④ The page introduces the require.js module:

Introduced in index.html <script data-main="js/main" src="js/libs/require.js"></script>

**In addition, how to introduce third-party libraries in the project? **Just a slight modification on the basis of the above code:

// alerter.js document
define(['dataService', 'jquery'], function(dataService, $) {
  let name = 'Tom'
  function showMsg() {
    alert(dataService.getMsg() + ', ' + name)
  }
  $('body').css('background', 'green')

// expose the module
  return { showMsg }
})
// main.js document
(function() {
  require.config({
    baseUrl: 'js/', //Base path The starting point is in the root directory
    paths: {

//custom module
      alerter: './modules/alerter', //cannot be written here ,alerter.js will report an error
      dataService: './modules/dataService',

// third party library module
      jquery: './libs/jquery-1.10.1' //Notice:Written as jQuery will report an error
    }
  })

  require(['alerter'], function(alerter) {
    alerter.showMsg()
  })
})()

The above example is to introduce the jQuery third-party library in the alerter.js file, and the main.js file also has the corresponding path configuration.
Summary: Through the comparison of the two, it can be concluded that the method defined by the AMD module is very clear, does not pollute the global environment, and can clearly display the dependencies . AMD mode can be used in a browser environment and allows modules to be loaded asynchronously or dynamically on demand.

 

3. CMD

The CMD specification is specially used on the browser side. The loading of modules is asynchronous, and the modules are loaded and executed when they are used. The CMD specification combines the features of the CommonJS and AMD specifications. In Sea.js, all JavaScript modules follow the CMD module definition specification.

(1) Basic syntax of CMD specification

Define the exposed module:

//Define a module with no dependencies
define(function(require, exports, module){
  exports.xxx = value
  module.exports = value
})
//Define dependent modules
define(function(require, exports, module){
//Introduce dependent modules (synchronized)
  var module2 = require('./module2')
//Introduce dependent modules (asynchronous)
    require.async('./module3', function (m3) {
    })
// expose the module
  exports.xxx = value
})

Import using modules:

define(function (require) {
  var m1 = require('./module1')
  var m4 = require('./module4')
  m1.show()
  m4.show()
})

 

(2) simple use tutorial of sea.js

① Download sea.js, and import

Official website: http://seajs.org/

github : https://github.com/seajs/seajs

Then import sea.js into the project: js/libs/sea.js

② Create a project structure

|-js
  |-libs
    |-sea.js
  |-modules
    |-module1.js
    |-module2.js
    |-module3.js
    |-module4.js
    |-main.js
|-index.html

③Define the module code of sea.js

// module1.js document
define(function (require, exports, module) {

//internal variable data

  var data = 'rendc.com'

//internal function
  function show() {
    console.log('module1 show() ' + data)
  }

// expose to the outside
  exports.show = show
})
// module2.js document
define(function (require, exports, module) {
  module.exports = {
    msg: 'I Will Back'
  }
})
// module3.js document
define(function(require, exports, module) {
  const API_KEY = 'abc123'
  exports.API_KEY = API_KEY
})
// module4.js document
define(function (require, exports, module) {

//Introduce dependent modules (synchronized)
  var module2 = require('./module2')

  function show() {
    console.log('module4 show() ' + module2.msg)
  }

  exports.show = show

//Introduce dependent modules (asynchronous)
  require.async('./module3', function (m3) {
    console.log('Asynchronously import dependent modules3  ' + m3.API_KEY)
  })
})
// main.js document
define(function (require) {
  var m1 = require('./module1')
  var m4 = require('./module4')
  m1.show()
  m4.show()
})

④ Introduce in index.html

<script type="text/javascript" src="js/libs/sea.js"></script>
<script type="text/javascript">
  seajs.use('./js/modules/main')
</script>

 

4. ES6 Modularity

The design idea of ​​ES6 modules is to be as static as possible, so that module dependencies, as well as input and output variables can be determined at compile time. Both CommonJS and AMD modules can only determine these things at runtime. For example, CommonJS modules are objects, and object properties must be looked up on input.

(1) ES6 modular syntax

The export command is used to specify the external interface of the module, and the import command is used to import the functions provided by other modules.

/** define module  math.js **/
var basicNum = 0;
var add = function (a, b) {
    return a + b;
};

export { basicNum, add };

/** reference module **/
import { basicNum, add } from './math';
function test(ele) {
    ele.textContent = add(99 + basicNum);
}

As shown in the above example, when using the import command, the user needs to know the name of the variable or function to be loaded, otherwise it cannot be loaded. To make it easier for users to load modules without reading the documentation, the export default command is used to specify the default output for the module.

// export-default.js
export default function () {
  console.log('foo');
}
// import-default.js
import customName from './export-default';
customName(); // 'foo'

The module is output by default. When other modules load the module, the import command can specify any name for the anonymous function.

 

(2) Differences between ES6 modules and CommonJS modules

They have two major differences:

① CommonJS modules output a copy of a value, and ES6 modules output a reference to the value.

② The CommonJS module is loaded at runtime, and the ES6 module is the output interface at compile time.

The second difference is because CommonJS loads an object (ie the module.exports property), which is not generated until the script finishes running. The ES6 module is not an object, its external interface is just a static definition, which will be generated in the static analysis stage of the code.

Let’s focus on explaining the first difference. Let’s take the example of the loading mechanism of the CommonJS module above:

// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

ES6 modules work differently than CommonJS. ES6 modules are dynamically referenced and do not cache values. Variables in modules are bound to the module where they are located.

 

(3) ES6-Babel-Browserify tutorial

In a nutshell: use Babel to compile ES6 to ES5 code, and use Browserify to compile and package js .

① Define the package.json file

 {
   "name" : "es6-babel-browserify",
   "version" : "1.0.0"
 }

② Install babel-cli, babel-preset-es2015 and browserify

③Define the .babelrc file 

 {
    "presets": ["es2015"]
  }

④Define the module code

//module1.js document
// expose separately

export function foo() {
  console.log('foo() module1')
}

export function bar() {
  console.log('bar() module1')
}
//module2.js document
// unified exposure
function fun1() {
  console.log('fun1() module2')
}

function fun2() {
  console.log('fun2() module2')
}

export { fun1, fun2 }
//module3.js document
// Default exposure can expose any data class item, what data is exposed, what data is received
export default () => {
  console.log('default exposed')
}
// app.js document
import { foo, bar } from './module1'
import { fun1, fun2 } from './module2'
import module3 from './module3'
foo()
bar()
fun1()
fun2()
module3()

⑤ Compile and import in index.html

Compile ES6 to ES5 code using Babel (but includes CommonJS syntax): babel js/src -d js/lib

Compile js with Browserify: browserify js/lib/app.js -o js/lib/bundle.js

Then introduce it in the index.html file

 <script type="text/javascript" src="js/lib/bundle.js"></script>

 

In addition, how to introduce third-party libraries (take jQuery as an example) ?
First install the dependencies npm install jquery@1
and then introduce them in the app.js file

//app.js document

import { foo, bar } from './module1'
import { fun1, fun2 } from './module2'
import module3 from './module3'
import $ from 'jquery'

foo()
bar()
fun1()
fun2()
module3()
$('body').css('background', 'green')

 

3. Summary


 

postscript


It took a long time (>10h) to finally explain "JS modularization" clearly, and my understanding of modularization has deepened. In fact, it is not difficult to understand one thing, but how to popularize it Share it with others, and let others also gain something, I have always asked myself this way! If there are any mistakes or inaccuracies in the article, corrections and criticisms are welcome. At the same time, I also hope that you will support me a lot. I will have greater creative motivation!