javascript模块化的前世今生

235 阅读4分钟

前端为什么要用模块

  • 减少全局变量,有效避免命名污染
  • 更好的分离和按需加载
  • 提高复用性和可维护性

模块之初

一开始我们使用各种方式来代表模块

定义各种各样的全局函数

  function module(){} 
  function module1(){}

使用命名空间

 var module={
    a:2,
    b:1,
  }

使用自执行函数(闭包)

  (function (){
    //自执行函数 利用闭包 
    var a =1 ;
    window.module = { a }
  })()

使用自执行函数添加参数(添加依赖)

  (function ($){
    //自执行函数 添加依赖
    var a = $('.a');
    window.module = { a }
  })(jquery)

以上定义模块的方式,有缺陷

  • 更复杂的应用,更多的模块,难免引入不止一个<script>,这样的话,就会出现其他的问题:
    • 请求过多
    • 模糊依赖
    • 等等...

CommonJS

node 使用 CommonJS

核心思想

每单个文件就是一个模块require方法,同步加载依赖模块,然后exports或者module.exports导出需要暴露的接口。

问题

加载模块是同步的。

因为 Node.js 主要用于服务器编程。一般加载的模块文件都保存在本地硬盘中,无需考虑异步加载方式即可快速加载。因此,CommonJS规范更适用。

但是,它不适合浏览器环境。同步意味着阻塞加载,浏览器资源是异步加载的。针对浏览器的情况,为了解决上述同步加载问题,实现异步加载依赖模块,就出现了AMD

AMD

RequireJS 使用 AMD

核心思想

AMD规范是异步加载模块,它允许我们指定回调函数

其核心接口如下:define(id?, dependencies?, factory),dependencies并作为形参传入factory,在声明一个模块时指定所有的依赖,而这些依赖会提前执行

AMD的异步加载解决了加载和性能的阻塞问题,模块之间的依赖关系可以清晰的展现出来。

  // AMD代码示例 可与下面的cmd对比
  define(["a", "b", "c", "d", "e", "f"], 
    function(a, b, c, d, e, f) { 
      // 等于在最前面声明并初始化了要用到的所有模块
      a.doSomething();
      if (false) {
        // 即便没用到某个模块 b,但 b 还是提前执行了
        b.doSomething()
      } 
  });

CMD

SeaJS 使用 CMD

CMD标准与AMD标注非常类似,但解决同一问题的运行机制不同

AMD推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行

  // cmd代码示例
  define(function(require, exports, module) {
      var a = require('./a'); //在需要时申明
      a.doSomething();
      if (false) {
          var b = require('./b');
          b.doSomething();
      }
  });

es6模块

ES6(ESM)模块的设计思想是尽可能的静态化,这样在编译时就可以确定模块的依赖和输入输出变量。CommonJS 和 AMD 模块只能在运行时确定这些东西。

ESM 与 CommonJS区别

  • CommonJS 模块输出一个值的副本,而 ES6 模块输出一个值的引用

CommonJS示例

  // module1.js
  var data = 5;
  var doSomething = function () {
    data++;
  };
  module.exports.data = data;
  module.exports.doSomething = doSomething;
  
  // index.js
  var example = require('./module1.js');
  console.log(example.data); // 5
  example.doSomething(); 
  console.log(example.data);// 5 !这里的5不会变,因为common输出的是一个值的副本

ES6示例

  // module2.js
  let data = 5;
  function doSomething() {
    data++;
  }
  export { data, doSomething }
  // index.js
  import { data, doSomething } from './module2';
  console.log(data); // 5
  doSomething();
  console.log(data); // 6 ! 这里变了,因为es6模块输出的是一个引用地址
  • CommonJS 模块是在运行时加载的,ES6 模块是编译时的输出接口。

总结

  • CommonJS 模块的输出是一个值的副本,在运行时加载模块,CommonJS 规范主要用于服务器编程,加载模块是同步的,同步意味着阻塞加载,而浏览器资源是异步加载的,所以 AMD 和 CMD是可用的解决方案。
  • AMD 在RequireJS的推广过程中,定义的模块标准化输出。AMD规范是在浏览器环境中的应用异步加载模块并且可以并行加载多个模块。AMD的API是One for many依赖模块,AMD提倡提前执行(依赖前端)
  • CMD是SeaJS在推广过程中,定义的模块标准化输出。CMD 的 API 严格区分并推崇单一职责,加载模块是异步的,CMD 提倡依赖就近、延迟执行。
  • ES6模块的输出是引用地址,ES6模块是编译时输出,ES6在语言标准层面上实现了模块功能,简单,可以作为浏览器和服务器的通用模块解决方案。目前新版本的node(14+)也是可以直接使用es6模块的