前端模块化发展历史(commonJS、AMD、CMD、UMD、ES6不含webpack)

写在前面

JavaScript发展之初,只是为了解决基础的表单验证问题,以及基础的页面交互,代码很是简单,不存在模块化的概念和问题。可是随着ajax的发展,web进入2.0时代,JavaScript成为一门应用很是普遍的语言。html

这个时候js做为一门嵌入型语言,劣势就展现出来了,没有一个权威的规范,问题老是要解决,在前端发展的这几十年,也就顺势而为的产生了不少的js规范。前端

前端模块化

1、函数

在最先的js中,想要实现分模块开发,最简单的就是函数,由于函数能造成一个相对封闭的空间,经过函数来实现简单的模块化也是最先的解决方案vue

function model1 = {
    
}

function model2 = {

}
复制代码

缺点:

一、污染全局做用域
二、维护成本高(命名容易冲突)
三、依赖关系不明显
复制代码

2、对象

对象里面能够包含属性和方法,就至关于一个容器了,咱们能够把每一个模块的代码写到一个对象里面,从而实现模块化的目的node

var model1 = {
    age: 11,
    say() {
        console.log(age)
    }
}

var model2 = {
    age: 15,
    say() {
        console.log(age)
    }
}
复制代码

缺点

外部能够修改模块内部状态,能够随意修改每一个模块的某个属性,有至关的安全隐患
复制代码

3、自执行函数

IIFE(immediately invoked function expression),也就是咱们说的自执行函数,经过定义一个匿名函数,建立了一个“私有”的命名空间,该命名空间的变量和方法,不会破坏全局的命名空间jquery

var module = (function(){
  var age = 11
    var say = function(){
        console.log(age)
    }
    return {say};
})();

module.say();  //11
console.log(module.age)  //undefined
复制代码

缺点

外部没法访问内部私有变量
复制代码

4、commonJs

前端真正提出模块化的概念,就是从commonJs的诞生开始的, 由于js做为一门嵌入型语言,处理页面逻辑和交互,即便没有模块化也能运行,并不会出什么问题,可是服务端却必需要有模块的概念。因此commonJs的发扬光大和nodejs相关,尤为是近几年nodejs的应用愈来愈普遍,npm统治整个前端之后,commonJs规范所以被你们熟知。webpack

一、定义模块

根据CommonJS规范,一个单独的文件就是一个模块。每个模块都是一个单独的做用域,也就是说,在该模块内部定义的变量,没法被其余模块读取,除非定义为global对象的属性es6

二、模块输出

模块只有一个出口,module.exports对象,咱们须要把模块但愿输出的内容放入该对象web

三、加载模块

加载模块使用require方法,该方法读取一个文件并执行,返回文件内部的module.exports对象ajax

// model1.js
var age = 11

function say(){
	console.log(age);
}
module.exports = {
    say
}

// index.html
var wu = require('./index.js');

console.log(wu.say)
复制代码

四、优势

解决了依赖、全局变量污染的问题express

五、缺点

一、同步加载

CommonJS用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取很是快,因此这样作不会有问题。可是在浏览器端,限于网络缘由,CommonJS不适合浏览器端模块加载,合理的方案是使用异步加载。

二、浏览器不能用

5、AMD

AMD 即Asynchronous Module Definition,中文名是异步模块定义的意思。

CommonJS 规范主要是为服务器端的 NodeJS 服务,服务器端加载模块文件无延时,可是在浏览器上就大不相同了。AMD 便是为了在浏览器宿主环境中实现模块化方案的规范之一。

因为不是JavaScript原生支持,使用AMD规范进行页面开发须要用到对应的库函数,也就是大名鼎鼎RequireJS,实际上AMD 是 RequireJS 在推广过程当中对模块定义的规范化的产出。

官网地址不能用,能够直接在这个地址下载下来引用

requirejs.org/docs/releas…

一、引入依赖

<script src="js/require.js" data-main="./main"></script>
复制代码

二、模块定义

由 define 方法来定义,在 define API 中:

id:模块名称,或者模块加载器请求的指定脚本的名字;

dependencies:是个定义中模块所依赖模块的数组,默认为 [“require”, “exports”, “module”]

factory:为模块初始化要执行的函数或对象。若是为函数,它应该只被执行一次。若是是对象,此对象应该为模块的输出值;

// hello.js
define('hello', function (x, y){
  var add = function (x,y){
    console.log(x, y) // 1, 2
   return x+y;
 };
  return {
   add: add
 };
});
复制代码

三、模块引入

require()函数接受两个参数

第一个参数是一个数组,表示所依赖的模块

第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可使用这些模块

// main.js

require.config({
    'baseUrl': './js',
    'paths': {
        'hello': './hello'
    }
})

define('main', function() {
    require(['hello'], function(hello) {
        console.log(hello.add(1, 2)) // 3
    })
})
复制代码

require()函数在加载依赖的函数的时候是异步加载的,这样浏览器不会失去响应,它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。

6、CMD

CMD 全称为 Common Module Definition,是 Sea.js 所推广的一个模块化方案的输出。在 CMD define 的入参中,虽然也支持包含 id, deps 以及 factory 三个参数的形式,但推荐的是接受 factory 一个入参,而后在入参执行时,填入三个参数 require、exports 和 module:

一、定义模块

require是能够把其余模块导入进来的一个参数;

而exports是能够把模块内的一些属性和方法导出的;

module 是一个对象,上面存储了与当前模块相关联的一些属性和方法。

define(function(require, exports, module) {
  // 每一个函数单独导出
  exports.add = function(x, y) {
    return x + y;
  }
});
复制代码

二、引用模块

define(function(require, exports, module) {
    var hello = require('hello');
    console.log(hello.add(2,3));
  
    // 单独导出
    exports.init = function init() {
      console.log('init');
    }
});
复制代码

三、html调用

<script src="./js/sea.js"></script>
<script>
seajs.config({
  base: './js', // 后续引用基于此路径
  alias: {  // 别名,能够用一个名称 替代路径(基于base路径)
    hello: './js/hello.js'
  },
});

// 加载入口模块
seajs.use("./main.js", function(main) {
  main.init(); // init
});
</script>
复制代码

四、AMD和CMD的区别

关于这两种的区别网上有不少版本,大致意思差很少:

AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块

CMD推崇就近依赖,只有在用到某个模块的时候再去require
复制代码

因此从这一点上来看,二者在性能上并无太多差别。由于最影响页面渲染速度的固然是资源的加载速度,既然都是预加载,那么加载模块资源的耗时是同样的(网络状况相同时)。

7、UMD

UMD,全称 Universal Module Definition,即通用模块规范。既然 CommonJs 和 AMD 风格同样流行,那么须要一个能够统一浏览器端以及非浏览器端的模块化方案的规范。

如今主流框架的源码都是用的UMD规范,由于它既能够兼容浏览器端又能够兼容node。

UMD的实现:

先判断是否支持 AMD(define 是否存在),存在则使用 AMD 方式加载模块;

再判断是否支持 Node.js 模块格式(exports 是否存在),存在则使用 Node.js 模块格式;

前两个都不存在,则将模块公开到全局(window 或 global);

全局对象挂载属性

(function(root, factory) {
    console.log('没有模块环境,直接挂载在全局对象上')
    console.log(factory())
    root.umdModule = factory();
}(this, function() {
    return {
        name: '我是一个umd模块'
    }
}))
复制代码

咱们把factory写成一个匿名函数,利用IIFE(当即执行函数)去执行工厂函数,返回的对象赋值给root.umdModule,这里的root就是指向全局对象this,其值多是window或者global,视运行环境而定。

兼容AMD环境

(function(root, factory) {
    if (typeof define === 'function' && define.amd) {
        // 若是环境中有define函数,而且define函数具有amd属性,则能够判断当前环境知足AMD规范
        console.log('是AMD模块规范,如require.js')
        define(factory)
    } else {
        console.log('没有模块环境,直接挂载在全局对象上')
        root.umdModule = factory();
    }
}(this, function() {
    return {
        name: '我是一个umd模块'
    }
}))
复制代码

兼容commonJs和CMD

(function(root, factory) {
  if (typeof module === 'object' && typeof module.exports === 'object') {
      console.log('是commonjs模块规范,nodejs环境')
      module.exports = factory();
  } else if (typeof define === 'function' && define.amd) {
      console.log('是AMD模块规范,如require.js')
      define(factory)
  } else if (typeof define === 'function' && define.cmd) {
      console.log('是CMD模块规范,如sea.js')
      define(function(require, exports, module) {
          module.exports = factory()
      })
  } else {
      console.log('没有模块环境,直接挂载在全局对象上')
      root.umdModule = factory();
  }
}(this, function() {
  return {
      name: '我是一个umd模块'
  }
}))
复制代码

jQuery 模块如何用 UMD 规范:

(function (factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(['jquery'], factory);
    } else if (typeof module === 'object' && module.exports) {
        // Node/CommonJS
        module.exports = function( root, jQuery ) {
            if ( jQuery === undefined ) {
                // require('jQuery') returns a factory that requires window to
                // build a jQuery instance, we normalize how we use modules
                // that require this pattern but the window provided is a noop
                // if it's defined (how jquery works)
                if ( typeof window !== 'undefined' ) {
                    jQuery = require('jquery');
                }
                else {
                    jQuery = require('jquery')(root);
                }
            }
            factory(jQuery);
            return jQuery;
        };
    } else {
        // Browser globals
        factory(jQuery);
    }
}(function ($) {
    $.fn.jqueryPlugin = function () { return true; };
}));
复制代码

vue源码UMD规范:

(function (global, factory) {
  // 遵循UMD规范
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  typeof define === 'function' && define.amd ? define(factory) :
  (global = global || self, global.Vue = factory());
}(this, function () { 'use strict';
  ···
  // Vue 构造函数
  function Vue (options) {
    // 保证了没法直接经过Vue()去调用,只能经过new的方式去建立实例
    if (!(this instanceof Vue)
    ) {
      warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options);
  }
  return Vue
})
复制代码

8、es6模块规范

前端的模块化发展如此复杂,ECMAScript 标准的起草者 TC39 委员会不能再坐视不理,推出了ES2015 Modules(import、export),最后有了ES6模块化规范。

导入的值也是只读不可变对象,不像CommonJS是一个内存的拷贝,看一个栗子就能明白es6相比commonJs优势在什么地方。

commonJs代码

// 模块定义代码:lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};
// 模块使用代码:main.js
var mod = require('./lib');
console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3
var mod2 = require('./lib');
console.log(mod2.counter);  // 3
复制代码

为何都是3?

CommonJS 规范是一种动态加载、拷贝值对象执行的模块规范。每一个模块在被使用时,都是在运行时被动态拉取并被拷贝使用的,模块定义是惟一的,但有几处引用便有几处拷贝。因此,对于不一样的 require 调用,生成的是不一样的运行时对象。

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  get counter() {
    return counter
  },
  incCounter: incCounter,
};
// main.js
var mod = require('./lib');
console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 4
复制代码

为何加了一个getter函数就行了?

这是因为 CommonJS 的拷贝机制形成的。因为 CommonJS 规范的拷贝运行机制,在 lib.js 中使用 module.exports 输出的对象,是从 lib 模块内拷贝而得,当时 counter 的值是几,便拷贝了几。不管执行 incCounter 多少次,改变的都不是输出对象的 counter 变量。

而当定义了 getter 属性以后,该属性指向了被 incCounter 方法以闭包形式囊括的 counter 变量,这个变量是输出的模块对象的一部分。

ES6模块化

/** 定义模块 hello.js **/
var age = 10;
var add = function (a, b) {
    return age + a + b
};
export { age, add };

/** 引用模块 **/
import { age, add } from './js/hello.js';
function test() {
    console.log(age)
    return add(20, age);
}
console.log(test())
复制代码

在 ES6 模块规范中,只有 export 与 import 两个关键字。

也可使用default关键字

/** 定义模块 hello.js **/
var age = 10;
var add = function (a, b) {
    return age + a + b
};
export default { age, add };

/** 引用模块 **/
import hello from './js/hello.js';
function test() {
    console.log(hello.age)
    return hello.add(20, hello.age);
}
console.log(test())
复制代码

ES6 模块规范与 CommonJS 规范不一样:

(1)ES6 模块规范是解析(是解析不是编译)时静态加载、运行时动态引用,全部引用出去的模块对象均指向同一个模块对象。在上面使用 CommonJS 规范声明的 lib 模块,若是使用 ES6 模块规范声明,根本不会出现 counter 变量含糊不清的问题。

(2)CommonJS 规范是运行时动态加载、拷贝值对象使用。每个引用出去的模块对象,都是一个独立的对象。

写在最后

前端模块化有这么多的标准,为何咱们实际开发中用的那么少呢,由于后来出了webpack这个神器,把模块化编程须要作的都帮咱们解决了。

webpack 本身实现了一套模块机制,不管是 CommonJS 模块的 require 语法仍是 ES6 模块的 import 语法,都可以被解析并转换成指定环境的可运行代码。随着webpack打包工具的流行,ES6语法普遍手中,后来的开发者对于 AMD CMD的感知愈来愈少。

参考连接

https://juejin.cn/post/6844903927104667662
https://zhuanlan.zhihu.com/p/55407719
https://www.cnblogs.com/dolphinX/p/4381855.html
https://juejin.cn/post/6844903848511799303
复制代码

公众号:小Jerry有话说