什么是模块化?
模块化是组织代码的一种方式。将所有的js业务逻辑代码写在一个文件里面,不仅导致文件庞大,而且难以管理和维护。
比如:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>js</title>
</head>
<body>
<script>
...// 一些业务逻辑
</script>
</body>
</html>
为了方便维护,可以通过外部引入的方式:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>js</title>
</head>
<body>
...
<script type="javascript" src="...(file path)"></script>
</body>
</html>
这种方式个人看来也是一种模块化的方式,只不过这种方式存在许多弊端。
- 文件必须顺序引入。在大型项目开发中,由于引入的文件较多,文件之间的依赖关系也较为复杂,文件引入顺序难以明确。
// test.js是基于jQuery.js开发的,也就是说test.js是依赖于jQuery.js的,所以jQuery必须先于test.js引入。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>js</title>
</head>
<body>
...
<script type="javascript" src="jQuery.js"></script>
<script type="javascript" src="test.js"></script>
</body>
</html>
- 命名空间污染问题。
// a.js
var a=1;
// b.js
var a=2;
//test.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="a.js"></script>
<script src="b.js"></script>
<title>js</title>
</head>
<body>
<script>
alert('a的值为'+a);
</script>
</body>
</html>
为了解决这些问题,一些模块化的规范就出现了。
模块的发展历程
先想一想,为什么模块很重要?
因为有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块。
但是,这样做有一个前提,那就是大家必须以同样的方式编写模块,否则你有你的写法,我有我的写法,岂不是乱了套!考虑到 Javascript 模块现在还没有官方规范,这一点就更重要了。
命名空间
通过库名.类别名.方法名进行访问。
var NamaSpace = {}
NameSpace.type = NameSpace.type || {} // 避免命名空间被覆盖
NameSpace.type.method = function (){}
-
js不像其他高级语言有模块系统,标准库较少和更缺乏包管理系统 -
js起初只有全局对象的形式,通过一个个小函数来实现不同的模块功能function m1(){ //... } function m2(){ //... } -
渐渐发展,通过构建对象的形式,来武装不同的功能
var module1 = new Object ({ _count : 0, m1 : function (){ //... }, m2 : function (){ //... } });上面的函数
m1()和m2(),都封装在module1对象里。使用的时候,就是调用这个对象的属性:module1.m1();但是,这样的写法会暴露所有模块成员,内部状态可以被外部改写。比如,外部代码可以直接改变内部计数器的值。
module1._count = 5; -
继续发展,通过立即执行函数和闭包的形式来分离一个又一个的小组件
三、立即执行函数写法
使用"立即执行函数"(Immediately-Invoked Function Expression,IIFE),可以达到不暴露私有成员的目的。
var module1 = (function(){ var _count = 0; var m1 = function(){ //... }; var m2 = function(){ //... }; return { m1 : m1, m2 : m2 }; })();使用上面的写法,外部代码无法读取内部的_count 变量。
console.info (module1._count); //undefinedmodule1就是Javascript模块的基本写法。下面,再对这种写法进行加工。
放大模式( augmentation)
如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用"放大模式"(augmentation)。
var module1 = (function (mod){ mod.m3 = function () { //... }; return mod; })(module1);上面的代码为
module1模块添加了一个新方法m3(),然后返回新的module1模块。宽放大模式(Loose augmentation)
在浏览器环境中,模块的各个部分通常都是从网上获取的,有时无法知道哪个部分会先加载。如果采用上一节的写法,第一个执行的部分有可能加载一个不存在的空对象,这时就要采用"宽放大模式"。
var module1 = ( function (mod){ //... return mod; })(window.module1);与"放大模式"相比,"宽放大模式"就是"立即执行函数"的参数可以是空对象。
-
当对象多起来的时候,又开始通过命名空间,来实现分级管理
输入全局变量
独立性是模块的重要特点,模块内部最好不与程序的其他部分直接交互。
为了在模块内部调用全局变量,必须显式地将其他变量输入模块。
var module1 = (function ($, YAHOO) {
//...
})(jQuery, YAHOO);
上面的 module1 模块需要使用 jQuery 库和 YUI 库,就把这两个库(其实是两个模块)当作参数输入 module1。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显
- 最终,历经十多年,社区渐渐发展壮大,
commonJS规范的提出成了javascript历史上最重要的里程碑。 - Best Wishes : hope javascript can run everywhere!
主流的模块化的规范有:
commonJs规范AMD规范(Asynchronous Module Definition) (异步模块定义)CMD规范ES6 module
四大主流规范
CommonJS规范
CommonJS规范的使用
Node.js是commonJS规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:module、exports、require、global。实际使用时,用module.exports定义当前模块对外输出的接口(不推荐直接用exports),用require加载模块。
// 定义模块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);
// 引用核心模块时,不需要带路径
var http = require('http');
http.createService(...).listen(3000);
commonJS用**同步**的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。
commomJS的实现原理
commonJS简化版源码
function Module(id, parent){
this.id = id;
this.exports = {};
this.parent = parent;
this.filename = null;
this.loaded = false;
this.children = []
}
Module.prototype.require = function(path){
return Module._load(path, this)
}
//由此可知,require 并不是全局命令,而是每个模块提供的一个内部方法,也就是说,只有在模块内部才能使用require命令,(唯一的例外是REPL 环境)。另外,require 其实内部调用 Module._load 方法。
Module._load = function(request, parent, isMain) {
// 计算绝对路径
var filename = Module._resolveFilename(request, parent);
// 第一步:如果有缓存,取出缓存
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}
// 第二步:是否为内置模块
if (NativeModule.exists(filename)) {
return NativeModule.require(filename);
}
// 第三步:生成模块实例,存入缓存
var module = new Module(filename, parent);
Module._cache[filename] = module;
// 第四步:加载模块
try {
module.load(filename);
hadException = false;
} finally {
if (hadException) {
delete Module._cache[filename];
}
}
// 第五步:输出模块的exports属性
return module.exports;
};
module.exports = Module;
-
每个文件就是一个模块,每个模块都是
Module类的一个实例。 -
从上面图中,可以知道
module是global全局对象的一个属性。 -
可以观察到在
global对象也有一个全局函数require(),global对象的require()是对module.require()函数的进一步抽象和封装。
AMD规范
AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
为什么会有AMD规范
有了服务器端模块以后,很自然地,大家就想要客户端模块。而且最好两者能够兼容,一个模块不用修改,在服务器和浏览器都可以运行。
但是,由于一个重大的局限,使得CommonJS规范不适用于浏览器环境。还是上一节的代码,如果在浏览器中运行,会有一个很大的问题。
var math = require('math');
math.add(2, 3);
第二行math.add(2, 3),在第一行require('math')之后运行,因此必须等math.js加载完成。也就是说,如果加载时间很长,整个应用就会停在那里等。
这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。
因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。这就是AMD规范诞生的背景。
AMD规范的使用
AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:
require([module], callback);
第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。如果将前面的代码改写成AMD形式,就是下面这样:
require(['math'], function (math) {
math.add(2, 3);
});
math.add()与math模块加载不是同步的,浏览器不会发生假死。所以很显然,AMD比较适合浏览器环境。
目前,主要有两个Javascript库实现了AMD规范:require.js和curl.js。
AMD规范的写法
require.js 加载的模块,采用 AMD 规范。也就是说,模块必须按照 AMD 的规定来写。
具体来说,就是模块必须采用特定的 define() 函数来定义。如果一个模块不依赖其他模块。那么可以直接定义在 define() 函数之中。
假定现在有一个 math.js 文件,它定义了一个math模块。那么,math.js 就要这样写:
// math.js
define(function (){
var add = function (x,y){
return x+y;
};
return {
add: add
};
});
加载方法如下:
// main.js
require(['math'], function (math){
alert(math.add(1,1));
});
如果这个模块还依赖其他模块,那么 define() 函数的第一个参数,必须是一个数组,指明该模块的依赖性。
define(['myLib'], function(myLib){
function foo(){
myLib.doSomething();
}
return {
foo : foo
};
});
当 require() 函数加载上面这个模块的时候,就会先加载myLib.js文件。
加载非规范的模块
理论上,require.js 加载的模块,必须是按照 AMD 规范、用 define() 函数定义的模块。但是实际上,虽然已经有一部分流行的函数库(比如jQuery)符合 AMD 规范,更多的库并不符合。那么,require.js 是否能够加载非规范的模块呢?
回答是可以的。
这样的模块在用 require() 加载之前,要先用 require.config() 方法,定义它们的一些特征。
举例来说,underscore 和 backbone 这两个库,都没有采用 AMD 规范编写。如果要加载它们的话,必须先定义它们的特征。
require.config({
shim: {
'underscore': {
exports: '_'
},
'backbone': {
deps: ['underscore', 'jquery'],
exports: 'Backbone'
}
}
});
require.config() 接受一个配置对象,这个对象除了有前面说过的 paths 属性之外,还有一个 shim 属性,专门用来配置不兼容的模块。具体来说,每个模块要定义:
(1)exports 值(输出的变量名),表明这个模块外部调用时的名称;
(2)deps 数组,表明该模块的依赖性。
比如,jQuery 的插件可以这样定义:
shim: {
'jquery.scroll': {
deps: ['jquery'],
exports: 'jQuery.fn.scroll'
}
}
CMD 规范
CMD是Common Module definition的缩写,即通用模块定义。CMD规范是国内发展出来的,就像AMD有个requireJS,CMD有个浏览器的实现SeaJS,SeaJS要解决的问题和requireJS一样,只不过在模块定义方式和模块加载(可以说运行、解析)时机上有所不同
CMD规范的写法
- 全局函数
define,用来定义模块。 - 参数 factory 可以是一个函数,也可以为对象或者字符串。
- 当 factory 为对象、字符串时,表示模块的接口就是该对象、字符串。
在CMD中,一个模块就是一个文件,格式为:
define( factory );
- 定义
JSON数据模块:
define({ "foo": "bar" });
2.通过字符串定义模板模块:
define('this is `data`.');
-
factory 为函数的时候,表示模块的构造方法,执行构造方法便可以得到模块向外提供的接口。
-
require 是一个方法,接受模块标识作为唯一参数,用来获取其他模块提供的接口:
require(id)
define(function( require, exports ){ var a = require('./a'); a.doSomething(); });require是同步往下执行的,需要的异步加载模块可以使用require.async来进行加载:define( function(require, exports, module) { require.async('.a', function(a){ a.doSomething(); }); });require.resolve( id )可以使用模块内部的路径机制来返回模块路径,不会加载模块。
-
exports 是一个对象,用来向外提供模块接口
-
module 是一个对象,上面存储了与当前模块相关联的一些属性和方法
define( function(require, exports, module) { // 模块代码 }); -
示例:
// math.js
define(function(require,exports,module) {
exports.add = function() {
var sum = 0,i = 0,args = arguments,l = args.length;
while (i < l) {
sum += args[i++];
}
return sum;
};
});
// increment.js
define(function(require,exports,module) {
var add = require('math').add;
exports.increment = function(val) {
return add(val,1);
};
});
// program.js
define(function(require ,exports,module) {
var inc = require('increment').increment;
var a = 1;
inc(a); // 2
module.id == "program";
});
AMD规范和CMD规范的比较
- 对于依赖的模块,
AMD是提前执行,CMD是延迟执行。CMD推崇 as lazy as possible CMD推崇依赖就近,AMD推崇依赖前置。
// CMD
define(function(require, exports, module) {
var a = require('./a')
a.doSomething()
// 此处略去 100 行
var b = require('./b') // 依赖可以就近书写
b.doSomething()
// ...
});
// AMD 默认推荐的是
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
a.doSomething()
// 此处略去 100 行
b.doSomething()
// ...
})
- 两者的使用加载机制不同,也就导致了
AMD(requirejs)模块会提前执行**,用户体验好,**而CMD(seajs)性能好,因为只有在需要时候才执行。
ES6模块规范
ES6的语言规格中引入了模块化功能,也就很好的取代了之前的commonjs和AMD规范,成为了浏览器和服务器的通用的模块解决方案,在现今(vuejs,ReactJS)等框架大行其道中,都引入了ES6中的模块化(Module)机制。
Es6中模块导出的基本语法
模块的导出,export关键字用于暴露数据,暴露给其他模块
使用方式是,可以将export放在任何变量,函数或类声明的前面,从而将他们从模块导出,而import用于引入数据,例如如下所示:
将下面这些js存储到exportExample.js中,分别导出的是数据,函数,类:
exportExample.js
// 1. 导出数据,变量前面加上export关键字
export var name = "随笔川迹"; // 导出暴露name变量
export let weChatPublic = "itclanCoder"; // 暴露weChatPublic
export const time = 2018; // 暴露time
// 2. 导出函数,函数前面加上export关键字
export function sum(num1,num2){
return num1+num2;
}
/*
*
* 以上等价于
* function sum(num1,num2){
* return num1+num2;
* }
* export sum;
* 也可以这样:在定义它时没有马上导出它,由于不必总是导出声明,可以导出引用,因此下面这段代码也是可以运行的
*/
// 3. 导出类,类前面加上export关键字
export class People{
constructor(name,age){
this.name = name;
this.age = age;
}
info(){
return `${this.name}${this.age}岁了`;
}
}
注意:一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取,同样,任何未显示导出的变量,函数或类都是模块私有的,若没有用export对外暴露,是无法从模块外部访问的 例如:
function countResult(num1,num2){
return num1-num2;
}
// 没有通过export关键字导出,在外部是无法访问该模块的变量或者函数的
对应在另一个模块中通过import导入如下所示,模块命名为importExample.js
import { name, weChatPublic,time,sum,People} from "../modelTest1/exportExampleEs5.js"
var people = new People("小美",18); // 实例化perople对象
console.log(name);
console.log(weChatPublic);
console.log(time);
console.log(sum(1,2));
console.log(people.info());
注意:在上面的示例中,除了export关键字外,每一个声明与脚本中的一模一样,因为导出的函数和类声明需要有一个名称,所以代码中的每一个函数或类也确实有这个名称,除非用default关键字,否则不能用这个语法导出匿名函数或类。
Es6中模块导入的基本语法
如果想从一个文件(模块)访问另一个文件(模块)的功能,则需要通过import关键字在另一个模块中引入数据,import语句的两个部分组成分别是**:要导入的标识符和标识符应当从那个模块导入,**另外,导入的标识符的顺序可以是任意位置,但是导入的标识符(也就是大括号里面的变量)与export暴露出的变量名应该是一致的。具体的写法如下:
import {identifer1,indentifer2} from "./example.js" // import {标识符1,标识符2} from "本地
1. 导入单个绑定
// 只导入一个
import {sum} from "./example.js"
console.log(sum(1,2)); // 3
sum = 1; // 抛出一个错误,是不能对导入的绑定变量对象进行改写操作的
2. 导入多个绑定
如果想从示例模块中导入多个绑定,与单个绑定相似,多个绑定值之间用逗号隔开即可:
// 导入多个
import {sum,multiply,time} from "./exportExample.js"
console.log(sum(1,2)); // 3
console.log(multiply(1,2)); // 3
console.log(time); // 2018
在这段代码中,从exportExample.js模块导入3个绑定,sum,multiply和time之后使用它们,就像使用本地定义的一样 等价于下面这个: **不管在import语句中把一个模块写了多少次,该模块将只执行一次,导入模块的代码执行后,实例化过的模块被保存在内存中,**只要另一个import语句使用它就可以重复使用它.
import {sum} from "./exportExample.js"
import {multiply} from "./exportExample.js"
import {time} from "./exportExample.js
3. Es6中导入整个模块
特殊情况下,可以导入整个模块作为一个单一的对象,然后所有的导出都可以作为对象的属性使用,例如:
// 导入一整个模块
import * as example from "./exportExample.js"
console.log(example.sum(1,example.time));
consoole.log(example.multiply(1,2));// multiply与sum函数功能一样
在上面这段代码中,从本地模块的exportExample.js中导出的所有绑定被加载到一个被称作为example的对象中,指定的导出sum()函数,multiply()函数和time之后作为example的属性被访问,这种导入格式被称为命名空间导入,因为exportExample.js文件中不存在example对象,所以它被作为exportExample.js中所有导出成员的命名空间对象而被创建
Es6中如何给导入导出时标识符重命名
从一个模块导入变量,函数或者类时,我们可能不希望使用他们的原始名称,就是导入导出时模块内的标识符(变量名,函数,或者类)可以不用一一对应,保持一致**,**可以在导出和导入过程中改变导出变量对象的名称
使用方式①: 使用as关键字来指定变量,函数,或者类在模块外应该被称为什么名称,例如如下一函数:
function sum(num1,num2){
return num1+num2;
}
export {sum as add} // as后面是重新指定的函数名
如上代码,函数sum是本地名称,add是导出时使用的名称,换句话说,当另一个模块要导入这个函数时,必须使用add这个名称:
若在importExample.js一模块中,则导入的变量对象应是add而不是sum,是由它导出时变量对象决定的
import {add} from "./exportExample.js"
使用方式②: 使用as关键字来指定变量,函数,或者类在主模块内应该被称为什么名称,例如如下一函数:
// exportExample.js
export function sum(num1,num2){
return num1+num2;
}
// importExample.js
import {sum as add} from "./exportExample.js"
console.log(sum(1,2)); // 3
如上代码导入add函数时使用了一个导入名称来重命名sum函数,注意这种写法与前面导出export时的区别,使用import方式时,重新命名的标识符在前面,as后面是本地名称,但是这种方式,即使导入时改变函数的本地名称,即使模块导入了add函数,在当前模块中也没有add()标识符,如上对add的类型检测就是很好的验证.
ES6匿名方式的导入和导出
如果在不给导出的标识符(变量,函数,类)呢,那么可以通过导出default关键字指定单个变量,函数或者类, 在import的时候, 名字随便写, 因为每一个模块的默认接口就一个。
//a.js
let sex = "boy";
export default sex //(sex不能加大括号)
//原本直接export sex外部是无法识别的,加上default就可以了.但是一个文件内最多只能有一个export default。 其实此处相当于为sex变量值"boy"起了一个系统默认的变量名default,自然default只能有一个值,所以一个文件内不能有多个export default。
// b.js
//本质上,a.js文件的export default输出一个叫做default的变量,然后系统允许你为它取任意名字。所以可以为import的模块起任何变量名,且不需要用大括号包含
import any from "./a.js"
import any12 from "./a.js"
console.log(any,any12) // boy,boy