前言:在JavaScript发展初期就是为了实现简单的页面交互逻辑,而如今CPU、浏览器性能得到了极大的提升,很多页面逻辑迁移到了客户端(表单验证等),随着web2.0时代的到来,Ajax技术得到广泛应用,jQuery等前端库层出不穷,前端代码日益膨胀,此时在JS方面就会考虑使用模块化规范去管理。模块化已经发展了有十余年了,不同的工具和轮子层出不穷,但总结起来,它们解决的问题主要有三个:
- 外部模块的管理;
- 内部模块的组织;
- 模块源码到目标代码的编译和转换;
什么是模块?
- 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件),并进行组合在一起;
- 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信;
模块化的进化过程
1. 全局function模式
方案:将不同的功能封装成不同的全局函数
- 编码:将不同的功能封装成不同的全局函数
- 问题:污染全局命名空间,容易引起命名冲突或数据不安全,而且模块成员之间看不出直接关系
function getValue(){
return document.getElementById('username').value;
}
2. namespace模式
方案: 简单对象封装
- 作用:减少了全局变量,解决命名冲突
- 问题:数据不安全(会暴露所有模块成员,内部状态可以被外部改写,外部可以直接修改模块内部的数据)
const tool = {
id: 'tool_1',
type: 'input',
value: '123',
getType() {
console.log(`type-${this.type}`);
return this.type;
}
getValue() {
console.log(`value-${this.value}`);
return this.value;
},
}
tool.type = 'checkbox' // 直接修改模块内部的数据
tool.getType() // 'checkbox'
3. IIFE模式
方案:匿名函数自调用(闭包、立即执行函数、依赖注入)
- 作用:数据是私有的, 外部只能通过暴露的方法操作
- 编码:将数据和行为封装到一个函数内部, 通过给window添加属性来向外暴露接口
- 问题:如果当前这个模块依赖另一个模块怎么办?
// index.html文件
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
myModule.foo()
myModule.bar()
console.log(myModule.data) //undefined 不能访问模块内部数据
myModule.data = 'xxxx' //不是修改的模块内部的data
myModule.foo() //没有改变
</script>
// tool.js文件
(function(window, $) {
let id = '#tool_1';
let type = 'input';
let value = '123';
let count = 0;
// 函数
function getType() {
console.log(`type-${this.type}`);
return type;
}
function getValue() {
console.log(`value-${$(id).val()}`);
return $(id).val();
}
function setValue(val) {
value = val;
}
function increase() {
count++;
}
// 私有方法
function resetValue() {
value = '123';
}
// 私有方法
function resetCount() {
count = 0;
}
function resetHandler() {
console.log('resetHandler');
resetValue();
resetCount();
}
// 暴露方法
window.tool = { getType, getValue, setValue, increase, resetHandler }
})(window, jQuery)
注意:引入js时必须保证顺序正确
<script type="text/javascript" src="jquery-1.7.2.js"></script>
<script type="text/javascript" src="tool.js"></script>
<script type="text/javascript">
tool.setValue('567');
</script>
模块化的好处
- 避免命名冲突(减少命名空间污染);
- 更好的分离, 按需加载;
- 更高复用性;
- 高可维护性;
引入多个
- 请求过多
- 首先我们要依赖多个模块,那样就会发送多个请求,导致请求过多;
- 依赖模糊
- 我们不知道他们的具体依赖关系是什么,也就是说很容易因为不了解他们之间的依赖关系导致加载先后顺序出错;
- 难以维护
- 以上两种原因就导致了很难维护,很可能出现牵一发而动全身的情况导致项目出现严重的问题。
模块化固然有多个好处,然而一个页面需要引入多个js文件,就会出现以上这些问题。而这些问题可以通过模块化规范来解决,因此才有了后续的commonjs, AMD, ES6, CMD规范。
CommonJS
- Node.js是commonJS规范的主要实践者,每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理
- 它有四个重要的环境变量为模块化的实现提供支持:
module
、exports
、require
、global
// 定义模块math.js
var basicNum = 0;
function add(a, b) {
return a + b;
}
module.exports = { //在这里写上需要向外暴露的函数、变量
add: add,
basicNum: basicNum
}
// 引用自定义的模块时,参数包含路径,可省略.js
var math = require('./math');
math.add(2, 5);
// 引用核心模块时,不需要带路径 nodejs
var http = require('http');
http.createService(...).listen(3000);
注意:
1、exports其实是指向module.exports的引用;
2、require() 返回的是 module.exports 而不是 exports;
3、module.exports 的初始值为一个空对象{},所以 exports也为空对象{};
4、module.exports对象不为空的时候exports对象就自动忽略;
*其实从上面我们可以推断出我们不能直接给exports赋值(注:exports=module.exports=samething这样的赋值除外;但可以给其添加属性,前提是module.exports对象为空),因为如果赋值后,exports和module.exports的指向关系就会断裂,导致易出错。
commonJS用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。
服务端实现
npm install uniq --save // 用于数组去重
//module1.js
module.exports = {
msg: 'module1',
foo() {
console.log(this.msg)
}
}
//module2.js
module.exports = function() {
console.log('module2')
}
//module3.js
exports.foo = function() {
console.log('foo() module3')
}
exports.arr = [1, 2, 3, 3, 2]
// app.js文件
// 引入第三方库,应该放置在最前面
let uniq = require('uniq')
let module1 = require('./modules/module1')
let module2 = require('./modules/module2')
let module3 = require('./modules/module3')
module1.foo() //module1
module2() //module2
module3.foo() //foo() module3
console.log(uniq(module3.arr)) //[ 1, 2, 3 ]
浏览器端实现
使用Browserify:Browserify 会对代码进行解析,整理出代码中的所有模块依赖关系,然后把相关的模块代码都打包在一起,形成一个完整的JS文件,这个文件中不会存在 require 这类的模块化语法,变成可以在浏览器中运行的普通JS
1. 创建项目结构
|-js
|-dist //打包生成文件的目录
|-src //源码所在的目录
|-module1.js
|-module2.js
|-module3.js
|-app.js //应用主源文件
|-index.html //运行于浏览器上
|-package.json
{
"name": "browserify-test",
"version": "1.0.0"
}
2. 下载browserify
- 全局: npm install browserify -g
- 局部: npm install browserify --save-dev
3. 定义模块代码
注意:index.html文件要运行在浏览器上,需要借助browserify将app.js文件打包编译,如果直接在index.html引入app.js就会报错!
4. 打包处理js
根目录下运行
browserify js/src/app.js -o js/dist/bundle.js
5. 页面使用引入
在index.html文件中引入
<script type="text/javascript" src="js/dist/bundle.js"></script>
AMD(Asynchronous Module Definition)和require.js
AMD规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。这里用require.js实现AMD规范的模块化:用require.config()
指定引用路径等,用define()
定义模块,用require()
加载模块。
首先我们需要引入require.js文件和一个入口文件main.js。main.js中配置require.config()
并规定项目中用到的基础模块。
/** 网页中引入require.js及main.js **/
<script src="js/require.js" data-main="js/main"></script>
/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
require.config({
baseUrl: "js/lib",
paths: {
"jquery": "jquery.min", // 实际路径为js/lib/jquery.min.js
"underscore": "underscore.min",
},
});
// 执行基本操作
require(["jquery","underscore"],function($,_){
// some code here
});
引用模块的时候,我们将模块名放在[]
中作为reqiure()
的第一参数;如果我们定义的模块本身也依赖其他模块,那就需要将它们放在[]
中作为define()
的第一参数。
// 定义math.js模块
define(function () {
var basicNum = 0;
var add = function (x, y) {
return x + y;
};
return {
add: add,
basicNum :basicNum
};
});
// 定义一个依赖underscore.js的模块
define(['underscore'],function(_){
var classify = function(list){
_.countBy(list,function(num){
return num > 30 ? 'old' : 'young';
})
};
return {
classify :classify
};
})
// 引用模块,将模块放在[]内
require(['jquery', 'math'],function($, math){
var sum = math.add(10,20);
$("#sum").html(sum);
});
CMD(Common Module Definition)和sea.js
/** AMD写法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
// 等于在最前面声明并初始化了要用到的所有模块
a.doSomething();
if (false) {
// 即便没用到某个模块 b,但 b 还是提前执行了
b.doSomething()
}
});
/** CMD写法 **/
define(function(require, exports, module) {
var a = require('./a'); //在需要时申明
a.doSomething();
if (false) {
var b = require('./b');
b.doSomething();
}
});
/** sea.js **/
// 定义模块 math.js
define(function(require, exports, module) {
var $ = require('jquery.js');
var add = function(a,b){
return a+b;
}
exports.add = add;
});
// 主文件
<script src="js/sea.js"></script>
<script>
// 加载模块
seajs.use(['math.js'], function(math){
var sum = math.add(1+2);
});
</script>
CMD是另一种js模块化方案,它与AMD很类似,不同点在于:
- 对依赖的处理:
- AMD 推崇依赖前置、提前执行(require.js在申明依赖的模块时会在第一时间加载并执行模块内的代码)
- CMD推崇依赖就近、延迟执行。此规范其实是在sea.js推广过程中产生的。
- 在本模块的对外输出:
- AMD推崇通过返回值的方式对外输出;
- CMD推崇通过给module.exports赋值的方式对外输出;
ES6 Module
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在成为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构成:
export
和import
。export
命令用于规定模块的对外接口,import
命令用于输入其他模块提供的功能。
/** 定义模块 math.js **/
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);
}
如上例所示,使用import
命令的时候,用户需要知道所要加载的变量名或函数名。其实ES6还提供了export default
命令,为模块指定默认输出,对应的import
语句不需要使用大括号。这也更趋近于ADM的引用写法。
/** export default **/
//定义输出
export default { basicNum, add };
//引入
import math from './math';
function test(ele) {
ele.textContent = math.add(99 + math.basicNum);
}
ES6 模块与 CommonJS 模块的差异
1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
- ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令
import
,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。原始值变了,import
加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
- 编译时加载: ES6 模块不是对象,而是通过
export
命令显式指定输出的代码,import
时采用静态命令的形式。即在import
时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。
CommonJS 加载的是一个对象(即module.exports
属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
UMD(Universal Module Definition)
是一种javascript通用模块定义规范,让你的模块能在javascript所有运行环境中发挥作用。
意味着要同时满足CommonJS, AMD, 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模块'
}
}))
总结
- CommonJS规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案;
- AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅;
- CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行;
- ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案;
- UMD为同时满足CommonJS, AMD, CMD标准的实现;