Modular development

Modular development

Overview

Modularization is one of the most important front-end development paradigms at the moment. With the increasing complexity of front-end applications, project code has gradually expanded to the point where it has to spend a lot of time to manage, and modularization is one of the most mainstream code organization methods , It improves our development efficiency and reduces our cost by dividing our complex code into different modules according to different functions.

As far as the term single modularization is concerned, it is only an idea or a theory, and does not contain specific implementations. Next, we will learn how to practice the idea of modularization in front-end projects, as well as the current mainstream tools in the industry.

summary

  • Modular evolution
  • Modular specification

Modular evolution

The early front-end technical standards did not anticipate that the front-end industry would have the same scale as it is today, so many remaining design problems have caused us to encounter many problems in implementing front-end modularization. Although these problems have been solved by some tools or standards, its evolution process is worth thinking about.

Stage1-file division method

This is the most primitive module system in the Web. The specific method is to store each function and its related state data separately in different files. Let's agree that each file is an independent module. When using this module, refer to the page, and one Script tag corresponds to one. Module, and then directly call the members of the module (variables, functions) in the code. module-a.js

//module a  

var name = 'module-a'

function method1 () {
  console.log(name + '#method1')
}

function method2 () {
  console.log(name + '#method2')
}
 

module-b.js

//module b  

var name = 'module-b'

function method1 () {
  console.log(name + '#method1')
}

function method2 () {
  console.log(name + '#method2')
}
 

index.html

<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
 // 
  method1()
 // 
  name = 'foo'
</script>
 

The disadvantages of this approach are obvious:

  • Pollution of the global scope: all modules work directly in the global, without independent private space, resulting in all members of the module can be arbitrarily accessed or modified outside the module;
  • Naming conflict problem: it is easy to cause naming conflicts when there are more modules;
  • Cannot manage module dependencies

In the early days, modularization relied entirely on conventions, and when the size of the project became larger, it would be completely useless.

Stage2-Namespace method

It is agreed that each module only exposes one global object, and all module members are mounted under this object. The specific method is based on the first stage, by "wrapping" each module as a global object, which is a bit similar to adding a "namespace" to the members in the module.

module-a.js

//module a  

var moduleA = {
  name: 'module-a',

  method1: function () {
    console.log(this.name + '#method1')
  },

  method2: function () {
    console.log(this.name + '#method2')
  }
}

 

module-b.js

//module b  

var moduleB = {
  name: 'module-b',

  method1: function () {
    console.log(this.name + '#method1')
  },

  method2: function () {
    console.log(this.name + '#method2')
  }
}
 

index.html

<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
  moduleA.method1()
  moduleB.method1()
 // 
  moduleA.name = 'foo'
</script>
 

"Namespace" reduces the possibility of naming conflicts, but there is also no private space, all module members can also be accessed or modified outside the module, and the dependencies between modules cannot be managed.

Stage3-IIFE

Use Immediately-Invoked Function Expression (IIFE: Immediately-Invoked Function Expression) to provide private space for the module. The specific method is to put each module member in a private scope provided by a function. With the concept of private members, private members can only be accessed in the form of closures within module members. Ensure the safety of private members. For the members that need to be exposed to the outside, it is implemented by hanging on the global object.

module-a.js

//module a  

;(function () {
  var name = 'module-a'
  
  function method1 () {
    console.log(name + '#method1')
  }
  
  function method2 () {
    console.log(name + '#method2')
  }

  window.moduleA = {
    method1: method1,
    method2: method2
  }
})()
 

module-b.js

//module b  

;(function () {
  var name = 'module-b'
  
  function method1 () {
    console.log(name + '#method1')
  }
  
  function method2 () {
    console.log(name + '#method2')
  }

  window.moduleB = {
    method1: method1,
    method2: method2
  }
})()
 

index.html

<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
  moduleA.method1()
  moduleB.method1()
 // 
  console.log(moduleA.name)//=> undefined
</script>
 

You can also use the parameters of the self-executing function as our dependency declaration to use, so that the dependency between the modules becomes obvious. For example, if you use jQuery in module A, you can accept a parameter in the immediate call function of the module, and pass the jQuery parameter when you call it immediately, so that you can clearly know that this module depends on jQuery when you maintain this module later.

module-a.js

//module a  

;(function ($) {
  var name = 'module-a'
  
  function method1 () {
    console.log(name + '#method1')
    $('body').animate({ margin: '200px' })
  }
  
  function method2 () {
    console.log(name + '#method2')
  }

  window.moduleA = {
    method1: method1,
    method2: method2
  }
})(jQuery)
 

The above stages are the early developers' implementation of modularization without tools and specifications. These methods have indeed solved the various problems of modularization in the front-end field, but there are still some unresolved problems. The problem, let's look at it below.

Emergence of modular specifications

The above methods are based on the original module and implement modular code organization through agreed methods. These methods will be different for different developers when they are implemented. Therefore, in order to unify different developers and different projects We need a standardized modular implementation of the standard area. In addition, for the module loading problem in modularization, in these methods, each module used is manually introduced through the script tag, which means that the module loading is not controlled by the code, and it will be very troublesome to maintain once a long time. Imagine if the code relies on a module and forgets to introduce it in html, there will be problems, or remove the reference of a certain module in the code, and then forget to delete the reference of this module in html, these will cause great The problem, so we need some basic common code to realize automatic loading of modules, which means that we need modular standard + module loader.

CommonJS specification

It is a set of standards proposed in Node.js, and all modular codes in Node.js must follow the CommonJS specification. It agreed on the content

  • A file is a module
  • Each module has a separate scope
  • Export members through module.exports
  • Load the module through the require function

But when we want to use this specification on the browser side, there will be some problems, because CommonJS loads modules in a synchronous mode, and the mechanism of Node.js is to load modules at startup. There is no need to load during execution, only I will use modular fast, so there will be no problem in using this method in Node. However, if the browser side uses the CommonJS specification, it will inevitably lead to our inefficiency, because every page load will cause a large number of synchronous mode requests to appear, so the CommonJS specification was not selected in the early front-end modularization, but for the browser The characteristics of AMD have been redesigned with a specification.

AMD(Asynchronous Module Definition)

Asynchronous module definition specification. At the same time, a very well-known library Require.js was also launched, which implements the AMD specification, and it is also a powerful module loader.

It is agreed in AMD to define a module through the define function. By default, this function can accept two parameters or pass three parameters. If three parameters are passed, the first parameter is the name of the module, which can be loaded later When using; the second parameter is an array, used to declare the dependencies of the module; the third parameter is a function, the parameters of the function correspond to the previous dependencies one-to-one, and each item is a member exported by the dependency , The function of the function can be understood as the current module provides a private space, if it is necessary to export some members for the module, it can be realized by return.

// 
define('module1', ['jquery', './module2'], function ($, module2) {
  return {
    start: function () {
      $('body').animate({ margin: '200px' })
      module2()
    }
  }
})
 

In addition, it also provides a require function to automatically load a module, and its usage is similar to define. Once the module is loaded with require, it will automatically create a script tag inside to send the corresponding script file request and execute the corresponding module code.

// 
require(['module1'], funciton(){
  module1.start()
})
 

At present, most third-party libraries support AMD specifications, which means that the AMD ecosystem is relatively good, but it

  • It is relatively complicated to use: because in the process of writing the code, in addition to the business code, a lot of define and require operation module codes need to be used, which leads to an increase in the complexity of the code;
  • Frequent requests for module JS files: When the modules in the project are too detailed, the number of requests for JS files in the same page will be particularly large, resulting in low page efficiency

Therefore, I personally think that AMD can only be regarded as a step in the evolution of front-end modularity, a compromised implementation, and not a final solution. In addition, during the same period, Taobao launched Sea.js + CMD (Common Module Definition), which is similar to CommonJS, and is basically similar to Require.js in use. It can be regarded as a duplicate wheel, which was later compatible with Require.js. .

//  CMD   CommonJS  
define(function (require, exports, module) {
	//  require  
  var $ = require('jquery')
 //  exports   module.exports  
  module.exports = function () {
    console.log('module 2~')
    $('body').append('<p>module2</p>')
  }
})
 

Modular Standard Specification

Best practices for modularity

CommonJS in Node.js

CommonJS is a built-in module system that does not have any environmental problems and can directly follow the CommonJS specification. Use require to load modules and export modules through mudule.export.

ES Modules in Browers

ES Modules is the latest module system defined by ECMAScript 2015 (ES6), so there will be environmental compatibility issues. When this standard was first introduced, all mainstream browsers did not support this feature, but with the popularity of a series of packaging tools such as Webpack, this specification gradually began to become popular. So far, ES Modules is the most mainstream front-end modular solution.

Compared with the development specifications proposed by the AMD community, ES Modules implements modularization at the language level, so it is more complete. In addition, most browsers nowadays have begun to support the ES Modules feature, and native support means that we can directly use the development application. In conclusion, aiming at better use of ES Modules in different environments will become the focus of our next study.

ES Modules

Basic characteristics

By adding the attribute of type = module to the script, the JS code can be executed according to the ES Modules standard.

<script type="module">
  console.log('this is es module')
</script>
 

Compared with ordinary script tags, there will be new features:

  • Automatically adopt strict mode ('use strict' file header declaration)
<script type="module">
 // this this 
  console.log(this)//undefined
</script>
 
  • Each ESM module is a separate private scope
<script type="module">
  var foo = 100
  console.log(foo)
</script>
<script type="module">
  console.log(foo)//foo is not defined
</script>
 
  • ESM requests external JS modules through CORS
<script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script>
 

Note that the requested address must support CORS

  • ESM's script tag will delay the execution of the script

demo.js

alrt('hello')
 
<script defer src="demo.js"></script>
<p> </p>
<!--   -->
<!-- defer   -->
 

Import and export

Basic usage

  • exportExport
  • importImport
export var name = 'foo module'

export function hello () {
  console.log('hello')
}

export class Person {}
 

It can also be exported together at the end, which can more intuitively describe which members the module provides.

var name = 'foo module'

function hello () {
  console.log('hello')
}

class Person {}

export { name, hello, Person }
 

Can be asrenamed with keywords before exporting

export {
  name as fooName,
  hello as fooHello
}
 

When the export member is renamed default, then this member is the member exported by the module by default, and the member must be renamed when importing

export {
  name as default,
  hello as fooHello
}
 
import { defelut as fooName } from './xxx'
 

Pass the export default+ variable directly , the variable is the default export of the module, and the member that is exported by default is received by directly importing the variable name when importing

export defalut name
 
import fooName from './xxx'
 

Precautions

  1. {} Is a fixed syntax, not an object literal, nor is it deconstructed (just extract the exported members of the module directly)
var name = 'jack'
var age = 18

export { name, age }
//  import { name, age } from 'xxx' 
 

Here it is { name, hello }not an object literal, it's just only grammatical rules.

var name = 'jack'
var age = 18

export default { name, age }
 

export defaultThe {name, age} derived above is an object with two attributes.

  1. When exporting, what is exported is the reference address of the member, not a copy

Through a small example

module.js

var name = 'jack'
var age = 18
export { name, age }

setTimeout(function () {
  name = 'ben'
}, 1000)
 

app.js

import { name, age } from './module.js'
console.log(name, age)
setTimeout(function () {
  console.log(name, age)
}, 1500)

=> jack 18
1.5s  =>  ben 18
 
  1. Import module member variables are read-only
import { name, age } from './module.js'
name = 'tom'//  Uncaught TypeError: Assignment to constant variable.
 

If the imported object is an object, the read and write properties of the object will not be affected

import usage

  • The file extension .js cannot be omitted
//import { name } from './module'
import { name } from './module.js'
 
  • Can not omit index.js (CommonJS can be loaded into the directory)
//import { lowercase } from './utils'
import { lowercase } from './utils/index.js'
 

For the file path name, the operation of Shen Lue extension and index.js can be realized when using the packaging tool to package the module later.

  • Cannot be omitted./(beginning with a letter will think it is loading a third-party module)
  1. . Start relative path
  2. / Start with an absolute path, from the project root directory
  3. Use the full url to load the module, which means you can directly use the file on the cdn
//import { name } from 'module.js'
import { name } from './module.js'
import { name } from '/04-import/module.js' 
import { name } from 'http://localhost:3000/04-import/module.js'
 
  • Only need to execute a module, do not need to extract the members of the module

Very useful in importing some submodules that do not require external control

import {} from './module.js'
  import './module.js'
 
  • Extract all the members in the module, asput all the members into an object in a way

There are many members exported in the module, and they will all be used when importing

import * as mod from './module.js'
console.log(mod)
 
  • Dynamic import module

You cannot use the import keyword to from a variable, and import can only appear at the top level

//var modulePath = './module.js'
//import { name } from modulePath
//console.log(name)

//if (true) {
//  import { name } from './module.js'
//}
 

ESM provides a global import function for dynamically importing modules

import('./module.js').then(function (module) {
  console.log(module)
})
 
  • Some named members are exported in the same module and a default member is exported
var name = 'jack'
var age = 18

export { name, age }

export default 'default export'
 
import { name, age, default as title } from './module.js'
  import abc, { name, age } from './module.js'
 

Export and import members

export { foo, bar } from './module.js'
 

Import members foo and bar cannot be used in the current scope

The scattered files are usually exported in the index

export { Button } from './button.js'
export { Avatar } from './avatar.js'
 

ES Modules browser environment Polyfill compatible solution

<script src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"></script>
<script src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
<script src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
 

The ES Modules that are not recognized by the browser are handed over to Babel for conversion, the files that need to be imported are requested through Ajax, and the code returned by the request is converted through Babel to support ESM.

For browsers that support ES Modules, the above solution will execute the code of the module twice (browser-es-module-loader executes once, and the browser executes once) causing the script to be executed repeatedly. This problem can be solved with the new attribute of script (nomodule), which is a boolean attribute. If nomodule is added to the script tag, the content in the script will only be executed in browsers that do not support ES Modules.

<script nomodule src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"></script>
<script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>
<script nomodule src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>
 

The above solution can only be used in the development stage, because the script is dynamically parsed in the runtime stage, and the efficiency will be very poor. The production environment should still be compiled into the code supported by the production environment in advance.

ES Modules in Node.js

Support situation

ES Modules, as a modularization standard at the JavaScript language level, will gradually unify the modularization requirements of JS applications. Node.js, as a very important field of JS, has gradually supported this feature. Since the Node 8.5 version, internal experimental features have been implemented. Ways to support ESM. But there is a big gap between the original CommonJS specification and ESM, so this feature is still in a transitional state.

Confirm Node version>8.5

There are two things you need to do to use ESM in Node.js

  1. Change the file extension from .js to .mjs
  2. Need to add additional startup --experimental-modulesparameters, represent the experimental characteristics enable the ESM

module.mjs

export const foo = 'hello'

export const bar = 'world'
 

index.mjs

import { foo, bar } from './module.mjs'

console.log(foo, bar)
 
$ node --experimental-modules index.mjs
=>node:23052) ExperimentalWarning: The ESM module loader is experimental.
=>hello world
 

At this point, we can also load the built-in module through esm

import fs from 'fs'
fs.writeFileSync('./foo.txt', 'es module working')
 

For third-party NPM modules can also be loaded through esm

import _ from 'lodash'
_.camelCase('ES Module')
 

Does not support the extraction of third-party module members, because third-party modules are export default members, { camelCase }not deconstruction

import { camelCase } from 'lodash'
console.log(camelCase('ES Module'))
 

The members in the module can be extracted directly, the built-in module is compatible with the method of extracting members of ESM, and the members are exported separately

import { writeFileSync } from 'fs'
writeFileSync('./bar.txt', 'es module working')
 

Interact with CommonJS modules

  • CommonJS modules can be imported in ES Modules

commonjs.js

module.exports = {
  foo: 'commonjs exports value'
}
//(module.exports ) exports.foo = 'commonjs exports value'
 

es-module.mjs

import mod from './commonjs.js'
console.log(mod)

=> (node:39354) ExperimentalWarning: The ESM module loader is experimental.
=> { foo: 'commonjs exports value' }
 
  • When exporting with ESM, the CommonJS module will always export only one default member

es-module.mjs

import { foo } from './commonjs.js'
console.log(foo)
//    import  
 
  • Cannot load ES Module through require in CommonJS module

es-module.mjs

export const foo = 'es module export value'
 

commonjs.js

const mod = require('./es-module.mjs')
console.log(mod)
// 
 

Differences with CommonJS modules

  • There is no module global member in ESM

commonjs.js

// 
console.log(require)

// 
console.log(module)

// 
console.log(exports)

// 
console.log(__filename)

// 
console.log(__dirname)
 

es-module.js

Require, module, exports are naturally replaced by import and export

__filename and __dirname are obtained through the meta attribute of the import object

//  url   fileURLToPath  
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
console.log(__filename)
console.log(__dirname)
 
New version further supports ESM

node v12.10.0

After adding the type field in package.json and setting it to mudule, all js files of the project will work as ESM by default, and there is no need to change the extension to .mjs. At this time, the files that use CommonJS specification need to be processed separately and modified to .cjs extension.

Babel compatible solution

Early versions of Node.js can be compatible with Bebel. Babel is currently the earliest JavaScript compiler that can be used to compile new feature codes into codes supported by the current environment.

node v8.0.0

index.js

import { foo, bar } from './module.js'

console.log(foo, bar)
 
$ yarn add @babel/node @bebel/core @bebel/preset-env --dev
$ yarn bebel-node index.js
 

However, if you run directly as above, you will still get an error (import is not supported). The reason is that Babel is implemented based on the plug-in mechanism. Its core module does not transform our code. Specifically, we need to transform every feature in our code through Plug-in implementation.

preset-env is a collection of plug-ins, including all the new features in the latest JS standard.

So you can directly convert ES Modules with the help of preset.

$ yarn bebel-node index.js --presets=@bebel/preset-env
 

It is troublesome to pass in this parameter every time, you can choose to put it in the configuration file.

.babelrc

{
  "presets": ["@bebel/preset-env"]
}
 

So you can run it directlyyarn bebel-node index.js without adding parameters.

In fact, what helps us convert is that a plug-in is not a preset (just a collection). So try to remove the preset first yarn remove @bebel/preset-env, install the plug-in yarn add @babel/plugin-transform-modules-commonjs, and then change the configuration file to

{
  "plugins": [
    "@babel/plugin-transform-modules-commonjs"
  ]
}