在JavaScript发展初期就是为了实现简单的页面交互逻辑,寥寥数语即可;如今CPU、浏览器性能得到了极大的提升,很多页面逻辑迁移到了客户端(表单验证等),随着web2.0时代的到来,Ajax技术得到广泛应用,jQuery等前端库层出不穷,前端代码日益膨胀
这时候JavaScript作为嵌入式的脚本语言的定位动摇了,JavaScript却没有为组织代码提供任何明显帮助,甚至没有类的概念,更不用说模块(module)了,JavaScript极其简单的代码组织规范不足以驾驭如此庞大规模的代码
前人栽树,后人乘凉,以下为学习笔记。
早期模块化
一个模块就是实现特定功能的文件,有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块。
函数封装
函数一个功能就是实现特定逻辑的一组语句打包,而且JavaScript的作用域就是基于函数的,所以把函数作为模块化的第一步是很自然的事情
在一个文件里面编写几个相关函数就是最开始的模块了
function fn1(){
statement
}
function fn2(){
statement
}
这种做法的缺点很明显:污染了全局变量,无法保证不与其他模块发生变量名冲突,而且模块成员之间没什么关系。
对象封装
var myModule = {
var1: 1,
var2: 2,
fn1: function(){
},
fn2: function(){
}
}
这样避免了变量污染,只要保证模块名唯一即可,同时同一模块内的成员也有了关系。看似不错的解决方案,但是也有缺陷,外部可以随意修改内部成员。
立即执行函数 IIFE
var myModule = (function(){
var var1 = 1;
var var2 = 2;
function fn1(){
}
function fn2(){
}
return {
fn1: fn1,
fn2: fn2
};
})();
或者引入依赖
// module.js文件
(function(window, $) {
let data = 'www.baidu.com'
function foo() {
console.log(`foo() ${data}`)
$('body').css('background', 'red')
}
function bar() {
console.log(`bar() ${data}`)
otherFun() //内部调用
}
function otherFun() {
//内部私有的函数
console.log('otherFun()')
}
//暴露行为
window.myModule = { foo, bar }
})(window, jQuery)
// index.html文件
<!-- 引入的js必须有一定顺序 -->
<script type="text/javascript" src="jquery-1.10.1.js"></script>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
myModule.foo()
</script>
现代规范
CommonJS规范
- CommonJS规范
在网页端没有模块化编程,只是页面JavaScript逻辑复杂,但也可以工作下去,在服务器端却一定要有模块,所以虽然JavaScript在web端发展这么多年,第一个流行的模块化规范却由服务器端的JavaScript应用带来,CommonJS规范是由NodeJS发扬光大,这标志着JavaScript模块化编程正式登上舞台。
根据CommonJS规范,一个单独的文件就是一个模块。加载模块使用require方法,该方法读取一个文件并执行,最后返回文件内部的exports对象。
//模块定义 myModel.js
var name = 'Byron';
function printName(){
console.log(name);
}
function printFullName(firstName){
console.log(firstName + name);
}
module.exports = {
printName: printName,
printFullName: printFullName
}
//加载模块
var nameModule = require('./myModel.js');
nameModule.printName();
CommonJS规范加载模块是同步的。模块系统需要同步读取模块文件内容,并编译执行以得到模块接口。像Node.js主要用于服务器的编程,加载的模块文件一般都已经存在本地硬盘,所以加载起来比较快,不用考虑异步加载的方式。
但如果是浏览器环境,要从服务器加载模块,这是就必须采用异步模式。浏览器端,加载JavaScript最佳、最容易的方式是在document中插入script 标签。但脚本标签天生异步,传统CommonJS模块在浏览器环境中无法正常加载。
要么需要提前编译打包处理, 借助Browserify等打包处理。要么使用 AMD CMD 解决方案。
AMD规范
AMD 即Asynchronous Module Definition,中文名是异步模块定义的意思。它是一个在浏览器端模块化开发的规范。浏览器环境要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。此外AMD规范比CommonJS规范在浏览器端实现要来着早。
由于不是JavaScript原生支持,使用AMD规范进行页面开发需要用到对应的库函数,也就是大名鼎鼎RequireJS,实际上AMD 是 RequireJS 在推广过程中对模块定义的规范化的产出
语法
requireJS定义了一个函数 define,它是全局变量,用来定义模块
define(id?, dependencies?, factory);
//id:可选参数,用来定义模块的标识,如果没有提供该参数,默认脚本文件名(去掉拓展名)
//dependencies:是一个当前模块依赖的模块名称数组
//factory:工厂方法,模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值
在页面上使用require函数加载模块
require([dependencies], function(dependencies){});
//第一个参数是一个数组,表示所依赖的模块
//第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块
例子
// 定义模块 myModule.js
define(function(){
var name = 'Byron';
function printName(){
console.log(name);
}
return {
printName: printName
};
});
// 加载模块
require(['myModule'], function (myModule){
myModule.printName();
});
// main.js文件
(function() {
require.config({
baseUrl: 'js/', //基本路径 出发点在根目录下
paths: {
//映射: 模块标识名: 路径
myModule: './modules/myModule', //此处不能写成alerter.js,会报错
}
})
require(['myModule'], function (myModule){
myModule.printName();
});
})()
// index.html文件
<body>
<!-- 引入require.js并指定js主文件的入口 -->
<script data-main="js/main" src="js/libs/require.js"></script>
</body>
AMD模式可以用于浏览器环境并且允许非同步加载模块,也可以按需动态加载模块。
CMD规范
CMD 即Common Module Definition通用模块定义,CMD规范是国内发展出来的,和AMD要解决的问题一致,只不过在模块定义方式和模块加载(可以说运行、解析)时机上有所不同
CMD与AMD的区别
对于依赖的模块AMD是提前执行,CMD是延迟执行。不过RequireJS从2.0开始,也改成可以延迟执行(根据写法不同,处理方式不通过)。
CMD推崇依赖就近,AMD推崇依赖前置。
//AMD
define(['./a','./b'], function (a, b) {
//依赖一开始就写好
a.test();
b.test();
});
//CMD
define(function (requie, exports, module) {
//依赖可以就近书写
var a = require('./a');
a.test();
...
//软依赖
if (status) {
var b = requie('./b');
b.test();
}
});
语法
define(id?, deps?, factory)
//factory 有三个参数 function(require, exports, module)
//require 是一个方法,接受 模块标识 作为唯一参数,用来获取其他模块提供的接口
//exports 是一个对象,用来向外提供模块接口
//module 是一个对象,上面存储了与当前模块相关联的一些属性和方法
AMD也支持CMD写法,但依赖前置是官方文档的默认模块定义写法。
例子
// 定义模块 myModule.js
define(function(require, exports, module) {
var $ = require('jquery.js')
$('div').addClass('active');
//exports.xxx = value
//module.exports = value
});
// 加载模块
seajs.use(['myModule.js'], function(my){
});
UMD规范
UMD是AMD和CommonJS的糅合。
AMD模块以浏览器第一的原则发展,异步加载模块。
CommonJS模块以服务器第一原则发展,选择同步加载,它的模块无需包装(unwrapped modules)。
这迫使人们又想出另一个更通用的模式UMD (Universal Module Definition)。希望解决跨平台的解决方案。
UMD先判断是否支持Node.js的模块(exports)是否存在,存在则使用Node.js模块模式。
再判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。
(function (window, factory) {
if (typeof exports === 'object') {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
define(factory);
} else {
window.eventUtil = factory();
}
})(this, function () {
//module ...
});
ES6模块化规范
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
export 命令用于规定模块的对外接口,import 命令用于输入其他模块提供的功能。
例子
//module1.js文件
// 分别暴露
export function foo() {
console.log('foo() module1')
}
export function bar() {
console.log('bar() module1')
}
//module2.js文件
// 统一暴露
function fun1() {
console.log('fun1() module2')
}
function fun2() {
console.log('fun2() module2')
}
export { fun1, fun2 }
//module3.js文件
// 默认暴露 可以暴露任意数据类项,暴露什么数据,接收到就是什么数据
export default () => {
console.log('默认暴露')
}
// app.js文件
import { foo, bar } from './module1'
import { fun1, fun2 } from './module2'
import module3 from './module3'
与CommonJS 差异
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。