弄懂前端模块化,CommonJS,AMD,CMD及ES6模块化

前言

之前一直对于前端的模块化理解的不太透彻,平常一直用的也只有ES6模块化,对于其他几种模块化规范用的也少,理解不够深,但是经常会见到一些面试题是问这几种模块化规范的区别,所以这次做一个整理总结

一、对于模块化的理解

  1. 什么是模块化?
    模块化开发是一种管理方式,是一种生产方式,一种解决问题的方案,一个模块就是实现特定功能的文件,有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块,但是模块开发需要遵循一定的规范,否则就都乱套了,因此,才有了后来大家熟悉的AMD规范,CMD规范,以及ES6自带的模块化规范
  2. 模块化带来的好处
  • 解决命名冲突
  • 提供复用性
  • 提高代码可维护性
  • 灵活架构,焦点分离,方便模块间组合、分解
  • 多人协作互不干扰
  1. 模块化规范的发展进程
  • 原始写法(全局function)
    模块就是实现特定功能的一组方法,只要把不同的函数(以及记录状态的变量)简单的放在一起,就算是一个模块。
function m1 () {
	// ...
}

function m2 () {
	// ...
}

这样写上面的函数 m1() 和 m2() ,组成了一个模块,在使用的时候直接调用就可以了。
但是带来的问题也很明显:“污染”了全局变量,无法保证不与其他模块发生变量名冲突,而且模块成员之间看不出直接关系

  • namespace写法(简单对象)
    为了解决上面写法带来的缺点,可以吧模块写成一个对象,所有的模块成员都放到这个对象里面,这样减少了全局变量,减少命名冲突
const myModule = new Object({
	count: 0,
	m1: function () {
		console.log('m1:' + this.count);
		
	},
	m2: function () {
		console.log('m2:' + this.count)
		
	}
})

// 调用
myModule.m1() // m1:0

// 改变模块内部状态
myModule.count = 5;
myModule.m1() // m1:5

上面的函数 m1 和 m2 ,都封装在myModule对象里。使用的时候,就是调用这个对象的属性。但是,这样的写法会暴露所有模块成员,内部状态可以被外部改写。比如,外部代码可以直接改变内部计数器的值。

  • 立即执行函数写法(匿名函数自执行 ,闭包)
const myModule = (function () {
	let count = 0;
	let m1 = function () {
		console.log('m1:' + count)
	};
	let m2 = function () {
		console.log('m2:' + count)
	}
	return { m1, m2 };
})()

// 这样写在外面就无法读取内部的 count 变量
console.log(myModule.count); // undefined
  • 立即执行函数增强(引入依赖)
const myModule = (function ($) {
	// 这里面就可以使用JQuery
})(JQuery)

// 这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显

二、模块化规范

1. CommenJS

(1)简介
Node 应用由模块组成,采用 CommonJS 模块规范。每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。

(2)特点

  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序。

(3)基本语法
暴露模块:module.exports = value或exports.xxx = value
引入模块:require(xxx),如果是第三方模块,xxx为模块名;如果是自定义模块,xxx为模块文件路径

CommonJS暴露的模块到底是什么?
CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。

// example.js
let count = 5;
let incrementCount = function () {
	return ++count
}
module.exports.count = count;
module.exports.incrementCount = incrementCount;
// require.js
const example = require('./example.js');
console.log(example.count); // 5
console.log(example.incrementCount()); // 6

require命令用于加载模块文件。require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。

(4)模块的加载机制
CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。这点与ES6模块化有重大差异(ES6输入的是值的引用)

// example.js
let count = 5;
let incrementCount = function () {
	return ++count
}

module.exports = { count, incrementCount };
// require.js
const example = require('./example.js');

console.log(example.count); // 5
example.incrementCount();
console.log(example.count); // 5

2. AMD

(1)简介

CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。此外AMD规范比CommonJS规范在浏览器端实现要来着早。

(2)基本语法

/**
* @param id
* @param dependencies
* @param factory
*/

define(id?: String, dependencies?: String[], factory: Function|Object);

id 是模块的名字,它是可选的参数。
dependencies 指定了所要依赖的模块列表,它是一个数组,也是可选的参数,每个依赖的模块的输出将作为参数一次传入 factory 中。如果没有指定 dependencies,那么它的默认值是 [“require”, “exports”, “module”]。
factory 是最后一个参数,它包裹了模块的具体实现,它是一个函数或者对象。如果是函数,那么它的返回值就是模块的输出接口或值。

(3)用例

定义一个名为 myModule 的模块,它依赖 jQuery 模块:

define('myModule', ['jquery'], function($) {
    // $ 是 jquery 模块的输出
    $('body').text('hello world');
});

// 使用
require(['myModule'], function(myModule) {});

依赖多个模块的定义:

define(['jquery', './math.js'], function($, math) {
    // $ 和 math 一次传入 factory
    $('body').text('hello world');
});

模块输出:

define(['jquery'], function($) {

    var HelloWorldize = function(selector){
        $(selector).text('hello world');
    };

    // HelloWorldize 是该模块输出的对外接口
    return HelloWorldize;
});

在模块定义内部引用依赖:

define(function(require) {
    var $ = require('jquery');
    $('body').text('hello world');
});

3. CMD

(1)简介

CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD规范整合了CommonJS和AMD规范的特点。在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范。

(2)基本语法

/**
* @param {String} id
* @param {Array} dependencies
* @param {Function | Object | String} factory
*/
define(id?, dependencies?, factory)

factory 为对象、字符串时,表示模块的接口就是该对象、字符串。

define({ "foo": "bar" });

define('I am a template. My name is {{name}}.');

factory 为函数时,表示是模块的构造方法。执行该构造方法,可以得到模块向外提供的接口。
factory 方法在执行时,默认会传入三个参数:require、exports 和 module:

define(function(require, exports, module) {

  // 模块代码

});

(3)简单用例

定义没有依赖的模块:

define(function(require, exports, module){
  	exports.xxx = value
  	module.exports = value
})

定义有依赖的模块:

define(function(require, exports, module){
  //引入依赖模块(同步)
  var module2 = require('./module2')
  //引入依赖模块(异步)
    require.async('./module3', function (m3) {
    })
  //暴露模块
  exports.xxx = value
})

4. ES6模块化

(1) 简介
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

(2) 基本语法

export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

/** 定义模块 **/
var basicNum = 0;
var add = function (a, b) {
    return a + b;
};

export { basicNum, add };

/** 引用模块 **/
import { basicNum, add } from './math';

function test(ele) {
    ele.textContent = add(99 + basicNum);
}

导出模块还可以有种默认导出的写法 module.export :

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

// 模块默认输出, 其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。
// import-default.js
import customName from './export-default'; 
customName(); // 'foo'

(2) ES6 模块与 CommonJS 模块的差异

1. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
2. CommonJS 模块输出的是一个值的拷贝, ES6 模块输出的是一个值的引用

差异一:是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

差异二:

// export.js
export let num = 5;
export let incrementNum = function() {
  ++num;
};

// import.js
import { num, num2, incrementNum } from '../export.js';

console.log(num); // 5
console.log(incrementNum());
console.log(num); // 6

从上面我们不难看出 ES6 模块化 与上面CommonJS不一样的地方(不记得的同学可以往上翻哦),ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。CommonJS输入的是值的拷贝(原始类型),如果是函数、对象,也是用的引用

三、总结

  • CommonJS规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案。
  • AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅。
  • CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行。不过,依赖 SPM 打包,模块的加载逻辑偏重
  • ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

参考文章

前端模块化详解(完整版)
Javascript模块化编程(二):AMD规范
CMD 模块定义规范
JavaSript模块规范 - AMD规范与CMD规范介绍

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值