模块化

模块化是一种思想, 是将大工程拆成小的模块分治的思想.

服务端模块化

Nodejs和CommonJS的关系

这里要说一下Nodejs和CommonJS的关系。

  1. Nodejs的模块化能一种成熟的姿态出现离不开CommonJS的规范的影响
  2. 在服务器端CommonJS能以一种寻常的姿态写进各个公司的项目代码中,离不开Node的优异表现
  3. Node并非完全按照规范实现,针对模块规范进行了一定的取舍,同时也增加了少许自身特性

CommonJS规范简介

CommonJS对模块的定义非常简单,主要分为模块引用,模块定义和模块标识(require()函数)3部分

意义在于将类聚的方法和变量等限定在私有的作用域中,同时支持引入和导出将上下游模块无缝衔接,每个模块具有独立的空间,它们互不干扰。

Nodejs的模块化实现

Node模块在实现中并非完全按照CommonJS来,进行了取舍,增加了一些自身的的特性。

Node中一个文件是一个模块——module

一个模块就是一个Module的实例

Node模块分类

Node模块一般分为两种核心模块和文件模块。

核心模块——就是Node内置的模块比如http, path等。在Node的源码的编译时,核心模块就一起被编译进了二进制执行文件,部分核心模块(内建模块)被直接加载进内存中。

在Node模块的引入过程中,一般要经过一下三个步骤

  1. 路径分析 2. 文件定位 3. 编译执行

核心模块会省略文件定位和编译执行这两步,并且在路径分析中会优先判断,加载速度比一般模块更快。

文件模块——就是外部引入的模块如node_modules里通过npm安装的模块,或者我们项目工程里自己写的一个js文件或者json文件。

文件模块引入过程以上三个步骤都要经历。

前端模块化

前端模块化和服务端模块化有什么区别

服务端加载一个模块,直接就从硬盘或者内存中读取了,消耗时间可以忽略不计 浏览器需要从服务端下载这个文件,所以说如果用CommonJS的require方式加载模块,需要等代码模块下载完毕,并运行之后才能得到所需要的API。

为什么CommonJS不适用于前端模块

如果我们在某个代码模块里使用CommonJS的方法require了一个模块,而这个模块需要通过http请求从服务器去取,如果网速很慢,而CommonJS又是同步的,所以将阻塞后面代码的执行,从而阻塞浏览器渲染页面,使得页面出现假死状态。

因此后面AMD规范随着RequireJS的推广被提出,异步模块加载,不阻塞后面代码执行的模块引入方式,就是解决了前端模块异步模块加载的问题。

AMD(Asynchronous Module Definition) & RequireJS

AMD——异步模块加载规范 与CommonJS的主要区别就是异步模块加载,就是模块加载过程中即使require的模块还没有获取到,也不会影响后面代码的执行。

RequireJS——AMD规范的实现。其实也可以说AMD是RequireJS在推广过程中对模块定义的规范化产出。

//a.js
define(function(){
  console.log('a.js执行');
  return {
    hello: function(){
      console.log('hello, a.js');
    }
  }
});

//b.js
require(['a'], function(a){
  console.log('b.js 执行');
  a.hello();
  $('#b').click(function(){
    b.hello();
  });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

CommonJS 和AMD的对比:

CommonJS一般用于服务端,AMD一般用于浏览器客户端

CommonJS和AMD都是运行时加载

什么是运行时加载

CommonJS 和AMD模块都只能在运行时确定模块之间的依赖关系

require一个模块的时候,模块会先被执行,并返回一个对象,并且这个对象是整体加载的

CMD(Common Module Definition) & SeaJS

CMD——通用模块规范,由国内的玉伯提出。

SeaJS——CMD的实现,其实也可以说CMD是SeaJS在推广过程中对模块定义的规范化产出。

与AMD规范的主要区别在于定义模块和依赖引入的部分。AMD需要在声明模块的时候指定所有的依赖,通过形参传递依赖到模块内容中:

define(['dep1', 'dep2'], function(dep1, dep2){
  return function(){};
})

define(function(require, exports, module){
  //依赖模块a
  var a = require('./a');

  //调用模块a的方法
  a.method();
})
1
2
3
4
5
6
7
8
9
10
11

在依赖示例部分,CMD支持动态引入,require、exports和module通过形参传递给模块,在需要依赖模块时,随时调用require( )引入即可,

也就是说与AMD相比,CMD推崇依赖就近, AMD推崇依赖前置。

UMD(Universal Module Definition) 通用模块规范

(function (global, factory) {
   typeof exports === 'object' && typeof module !== 'undefined' 
       ? module.exports = factory()          // Node , CommonJS
       : typeof define === 'function' && define.amd  
         ? define(factory)                   //AMD CMD
         : (global.CodeMirror = factory());  //模块挂载到全局
}(this, (function () { 
   ...
})
1
2
3
4
5
6
7
8
9

可以看说所谓的兼容模式是将几种常见模块定义方式都兼容处理。

ES6模块

CommonJS和AMD都是运行时加载。ES6在语言规格层面上实现了模块功能,是编译时加载,完全可以取代现有的CommonJS和AMD规范,可以成为浏览器和服务器通用的模块解决方案。

ES6模块与CommonJS模块加载区别

ES6模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。所以说ES6是编译时加载,不同于CommonJS的运行时加载(实际加载的是一整个对象),ES6模块不是对象,而是通过export命令显式指定输出的代码,输入时也采用静态命令的形式:

//ES6模块
import { basename, dirname, parse } from 'path';

//CommonJS模块
let { basename, dirname, parse } = require('path');
1
2
3
4
5

以上这种写法与CommonJS的模块加载有什么不同?

  1. 当require path模块时,其实 CommonJS会将path模块运行一遍,并返回一个对象,并将这个对象缓存起来,这个对象包含path这个模块的所有API。以后无论多少次加载这个模块都是取这个缓存的值,也就是第一次运行的结果,除非手动清除。

  2. ES6会从path模块只加载3个方法,其他不会加载,这就是编译时加载。ES6可以在编译时就完成模块加载,当ES6遇到import时,不会像CommonJS一样去执行模块,而是生成一个动态的只读引用,当真正需要的时候再到模块里去取值,所以ES6模块是动态引用,并且不会缓存值。

因为CommonJS模块输出的是值的拷贝,所以当模块内值变化时,不会影响到输出的值。