JavaScript前期的代码还不是很复杂,我们可以通过代码简单实现,但随着项目越来越大、程序所包含的功能越来越多、开发人员原来越多的时候,我们就需要一个统一的模块化规范来编写代码,以求我们的代码的可读性、可维护性更高,同时也方便其他人来使用我们的实现。
什么是模块化?
模块化开发是一种管理方式,是一种生产方式,一种解决问题的方案,一个模块就是实现特定功能的文件,有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块。
解决目标
- 提高代码的可维护性,降低重构成本
- 解决全局变量污染和变量重名等问题
- 依赖管理
缺点
- 系统分层,调用链会很长
- 模块间通信,模块间发送消息会很耗性能
模块化的演进
随着需求的不断增加,前端的模块化技术也是一直处于不断演进的状态。
全局function模式(1999)
将不同的功能封装成不同的全局函数,这会导致Global被污染了, 很容易引起命名冲突。
function sum(a,b){
return parseInt(a) + parseInt(b);
}
function reduce(a,b){
return parseInt(a) - parseInt(b);
}
命名空间
通过将参数、方法挂载在对象上,实现的简单的模块化隔离。
var MYNAMESPACE = MYNAMESPACE || {};
MYNAMESPACE.person = function(name) {
this.name = name;
};
MYNAMESPACE.person.prototype.getName = function() {
return this.name;
};
// 使用方法
var p = new MYNAMESPACE.person("doc");
p.getName(); /
IIFE执行函数(闭包模式)
IIFE: Immediately Invoked Function Expression,意为立即调用的函数表达式,也就是说,声明函数的同时立即调用这个函数。我看可以通过自执行函数实现数据的私有化隔离(函数上下文)。匿名函数自身不污染全局环境,同时为内部变量提供作用于环境空间,且提供闭包环境,可以做闭包想做的事情。
var a = 2;
(function IIFE(global){
var a = 3;
console.log(a); // 3
console.log(global.a); // 2
})(window);
console.log(a); // 2
CommonJS
2009年发布,Node 应用由模块组成,采用 CommonJS 模块规范。commonJS规范加载是同步的,也就是说,加载完成才执行后面的操作。CommonJS遵循Modules/1.0(http://wiki.commonjs.org/wiki/Modules/1.0)规范,该规范首次提出了JS的模块化实现应该遵循的规则(nodejs)。规范指出:
- 模块上下文(require)
- 在模块中,有一个自由变量“ require”,即一个函数
- 在模块中,有一个称为“ exports”的自由变量,该变量是模块执行时可以向其添加API的对象。
- 模块必须使用“导出”对象作为唯一的导出方法。
- 模块标识符
- 模块标识符是由正斜杠分隔的“条款”字符串。
- 术语必须是驼峰标识符“。”或“ ..”。
- 模块标识符可能没有文件扩展名,例如“ .js”。
- 模块标识符可以是“相对”或“顶级”。如果第一项为“”,则模块标识符为“相对”。或者 ”..”。
- 顶级标识符从概念模块名称空间根目录解析。
- 相对标识符相对于在其中写入和调用“ require”的模块的标识符进行解析。
- 未指定
该规范保留了以下未指明的互操作性要点:
- 模块是与数据库,文件系统或工厂功能一起存储的,还是与链接库可互换的。
- 模块加载程序是否支持PATH来解析模块标识符。
遵循commonjs规范的代码:
//math.js
exports.add = function() {
var sum = 0, i = 0, args = arguments, l = args.length;
while (i < l) {
sum += args[i++];
}
return sum;
};
//increment.js
var add = require('math').add;
exports.increment = function(val) {
return add(val, 1);
};
//program.js
var inc = require('increment').increment;
var a = 1;
inc(a); // 2
Modules/1.0规范源于服务端,无法直接用于浏览器端,原因表现为:
- 外层没有function包裹,变量全暴漏在全局。
- 资源的加载方式与服务端完全不同。
浏览器模块化
基于Modules/1.0的基础上,commonJs社区讨论关于浏览器端的模块化规范的时候,内部发生了比较大的分歧,分裂出了三个主张,渐渐的形成三个不同的派别:
Modules/Transport
这一波人认为,在现有基础上进行改进即可满足浏览器端的需要,既然浏览器端需要function包装,需要异步加载,那么新增一个方案,能把现有模块转化为适合浏览器端的就行了。基于这个主张,制定了Modules/Transport(http://wiki.commonjs.org/wiki/Modules/Transport)规范,提出了先通过工具把现有模块转化为复合浏览器上使用的模块,然后再使用的方案。
AMD
AMD是”Asynchronous Module Definition”的缩写,意思就是”异步模块定义”。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。他们认为浏览器与服务器环境差别太大,不能沿用旧的模块标准。既然浏览器必须异步加载代码,那么模块在定义的时候就必须指明所依赖的模块,然后把本模块的代码写在回调函数里。模块的加载也是通过下载-回调这样的过程来进行,这个思想就是AMD的基础,由于“革新派”与“保皇派”的思想无法达成一致,最终从CommonJs中分裂了出去,独立制定了浏览器端的js模块化规范AMD(Asynchronous Module Definition)(https://github.com/amdjs/amdjs-api/wiki/AMD)
// 规定采用require语句加载模块,但是不同于CommonJS,它要求两个参数
require([module], callback);
// 在定义模块的时候需要使用define函数定义:
define(id?, dependencies?, factory);
- 动态并行加载js,依赖前置,一个模块的回调函数必须得等到所有依赖都加载完毕之后,才可执行。无需再考虑js加载顺序问题。
- 规范化输入输出,使用起来方便。
- 对于不满足AMD规范的文件可以很好地兼容。
Modules/Wrappings
这一波人有点像“中间派”,既不想丢掉旧的规范,也不想像AMD那样推到重来。他们认为,Modules/1.0固然不适合浏览器,但它里面的一些理念还是很好的,(如通过require来声明依赖),新的规范应该兼容这些,AMD规范也有它好的地方(例如模块的预先加载以及通过return可以暴漏任意类型的数据,而不是像commonjs那样exports只能为object),也应采纳。最终他们制定了一个Modules/Wrappings(http://wiki.commonjs.org/wiki/Modules/Wrappings)规范,此规范指出了一个模块应该如何“包装”,包含以下内容:
- 全局有一个module变量,用来定义模块
- 通过module.declare方法来定义一个模块
- module.declare方法只接收一个参数,那就是模块的factory,次factory可以是函数也可以是对象,如果是对象,那么模块输出就是此对象。
- 模块的factory函数传入三个参数:require,exports,module,用来引入其他依赖和导出本模块API
- 如果factory函数最后明确写有return数据(js函数中不写return默认返回undefined),那么return的内容即为模块的输出。
UMD
为了支持一个模块同时兼容AMD和CommonJs规范,适用于 同时支持浏览器端和服务端引用的第三方库,提出了 UMD规范。UMD是一个时代的产物,当各个环境最终实现ES harmony的统一的规范后,它也将退出历史舞台。
// 定义一个模块
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery', 'underscore'], factory);
} else if (typeof exports === 'object') {
// Node, CommonJS之类的
module.exports = factory(require('jquery'), require('underscore'));
} else {
// 浏览器全局变量(root 即 window)
root.returnExports = factory(root.jQuery, root._);
}
}(this, function ($, _) {
// 方法
function a(){}; // 私有方法,因为它没被返回 (见下面)
function b(){}; // 公共方法,因为被返回了
function c(){}; // 公共方法,因为被返回了
// 暴露公共方法
return {
b: b,
c: c
}
}));
CMD
CMD 是 “Common Module Definition”的缩写,意思是通用模块规范。CMD专门用于浏览器端, 模块的加载是异步的 ,模块使用时才会加载执行。在 CMD 规范中,一个模块就是一个文件。规范地址: https://github.com/cmdjs/specification/blob/master/draft/module.md
// 定义一个模块
define(function(require, exports, module) {
....
})
//sea.js:
define(function(require, exports, module) {
var mod_A = require("dep_A");
var mod_B = require("dep_B");
var mod_C = require("dep_C");
});
ES2015 Module(es6初版)
ES6是ECMA的为JavaScript制定的第6个版本的标准,标准委员会最终决定,标准在每年的 6 月份正式发布一次,作为当年的正式版本。ECMAscript 2015 是在2015年6月份发布的ES6的第一个版本。
// file lib/greeting.js 定义一个模块
const helloInLang = {
en: 'Hello world!',
es: '¡Hola mundo!',
ru: 'Привет мир!'
};
// 对外输出
export const greeting = {
sayHello: function (lang) {
return helloInLang[lang];
}
};
// file hello.js 引入一个模块
import { greeting } from "./lib/greeting";
const phrase = greeting.sayHello("en");
document.write(phrase);
webpack(构建工具)
webpack 自己实现了一套模块机制,无论是 CommonJS 模块的 require 语法还是 ES6 模块的 import 语法,都能够被解析并转换成指定环境的可运行代码。随着webpack打包工具的流行,ES6语法广泛手中,后来的开发者对于 AMD CMD的感知越来越少。