模块一般是指编程语言所提供的代码组织机制,利用此机制可将程序拆解为独立且通用的代码单元。所谓模块化主要是解决代码分割、做用域隔离、模块之间的依赖管理以及发布到生产环境时的自动化打包与处理等多个方面。html
CommonJS 最开始是 Mozilla 的工程师于 2009 年开始的一个项目,它的目的是让浏览器以外的 JavaScript (好比服务器端或者桌面端)可以经过模块化的方式来开发和协做。前端
在 CommonJS 的规范中,每一个 JavaScript 文件就是一个独立的模块上下文(module context),在这个上下文中默认建立的属性都是私有的。也就是说,在一个文件定义的变量(还包括函数和类),都是私有的,对其余文件是不可见的。node
须要注意的是,CommonJS 规范的主要适用场景是服务器端编程,因此采用同步加载模块的策略。若是咱们依赖3个模块,代码会一个一个依次加载它们。express
该模块实现方案主要包含 require 与 module 这两个关键字,其容许某个模块对外暴露部分接口而且由其余模块导入使用。npm
编程//sayModule.js
jsonfunction SayModule () {
segmentfaultthis.hello = function () {
数组console.log('hello');
浏览器};
this.goodbye = function () {
console.log('goodbye');
};
}
module.exports = SayModule;
//main.js 引入sayModule.js
var Say = require('./sayModule.js');
var sayer = new Say();
sayer.hello(); //hello
做为一个服务器端的解决方案,CommonJS 须要一个兼容的脚本加载器做为前提条件。该脚本加载器必须支持名为 require 和 module.exports 的函数,它们将模块相互导入导出。
Node.js
Node 从 CommonJS 的一些创意中,创造出本身的模块化实现。因为Node 在服务端的流行,Node 的模块形式被(不正确地)称为 CommonJS。
Node.js模块能够分为两大类,一类是核心模块,另外一类是文件模块。
核心模块 就是Node.js标准的API中提供的模块,如fs、http、net等,这些都是由Node.js官方提供的模块,编译成了二进制代码,能够直接经过require获取核心模块,例如require('fs'),核心模块拥有最高的加载优先级,若是有模块与核心模块命名冲突,Node.js老是会加载核心模块。
文件模块 是存储为单独的文件(或文件夹)的模块,多是JavaScript代码、JSON或编译好的C/C++代码。在不显式指定文件模块扩展名的时候,Node.js会分别试图加上.js、.json、.node(编译好的C/C++代码)。
加载方式
若是require参数一"/"开头,那么就以绝对路径的方式查找模块名称,若是参数一"./"、"../"开头,那么则是以相对路径的方式来查找模块。
若是require参数不以"/"、"./"、"../"开头,而该模块又不是核心模块,那么就要经过查找node_modules加载模块了。咱们使用的npm获取的包一般就是以这种方式加载的。
加载缓存
Node.js模块不会被重复加载,这是由于Node.js经过文件名缓存全部加载过的文件模块,因此之后再访问到时就不会从新加载了。
注意: Node.js是根据实际文件名缓存的,而不是require()提供的参数缓存的,也就是说即便你分别经过require('express')和require('./node_modules/express')加载两次,也不会重复加载,由于尽管两次参数不一样,解析到的文件倒是同一个。
Node.js 中的模块在加载以后是以单例化运行,而且遵循值传递原则:若是是一个对象,就至关于这个对象的引用。
模块载入过程
加载文件模块的工做,主要由原生模块module来实现和完成,该原生模块在启动时已经被加载,进程直接调用到runMain静态方法。
例如运行: node app.js
Module.runMain = function () {
// Load the main module--the command line argument.
Module._load(process.argv[1], null, true);
};
//_load静态方法在分析文件名以后执行
var module = new Module(id, parent);
//并根据文件路径缓存当前模块对象,该模块实例对象则根据文件名加载。
module.load(filename);
具体说一下上文提到了文件模块的三类模块,这三类文件模块之后缀来区分,Node.js会根据后缀名来决定加载方法,具体的加载方法在下文require.extensions
中会介绍。
.js
经过fs模块同步读取js文件并编译执行。.node
经过C/C++进行编写的Addon。经过dlopen方法进行加载。.json
读取文件,调用JSON.parse解析加载。接下来详细描述js后缀的编译过程。Node.js在编译js文件的过程当中实际完成的步骤有对js文件内容进行头尾包装。以app.js为例,包装以后的app.js将会变成如下形式:
//circle.js
var PI = Math.PI;
exports.area = function (r) {
return PI * r * r;
};
exports.circumference = function (r) {
return 2 * PI * r;
};
//app.js
var circle = require('./circle.js');
console.log( 'The area of a circle of radius 4 is ' + circle.area(4));
//app包装后
(function (exports, require, module, __filename, __dirname) {
var circle = require('./circle.js');
console.log('The area of a circle of radius 4 is ' + circle.area(4));
});
//这段代码会经过vm原生模块的runInThisContext方法执行(相似eval,只是具备明确上下文,不污染全局),返回为一个具体的function对象。最后传入module对象的exports,require方法,module,文件名,目录名做为实参并执行。
这就是为何require并无定义在app.js 文件中,可是这个方法却存在的缘由。从Node.js的API文档中能够看到还有__filename
、__dirname
、module
、exports
几个没有定义可是却存在的变量。其中__filename
和__dirname
在查找文件路径的过程当中分析获得后传入的。module
变量是这个模块对象自身,exports
是在module的构造函数中初始化的一个空对象({},而不是null)。
在这个主文件中,能够经过require方法去引入其他的模块。而其实这个require方法实际调用的就是module._load方法。
load方法在载入、编译、缓存了module后,返回module的exports对象。这就是circle.js文件中只有定义在exports对象上的方法才能被外部调用的缘由。
以上所描述的模块载入机制均定义在lib/module.js中。
require 函数
require 引入的对象主要是函数。当 Node 调用 require() 函数,而且传递一个文件路径给它的时候,Node 会经历以下几个步骤:
require.extensions 来查看对三种文件的支持状况
能够清晰地看到 Node 对每种扩展名所使用的函数及其操做:对 .js 文件使用 module._compile;对 .json 文件使用 JSON.parse;对 .node 文件使用 process.dlopen。
文件查找策略
尽管原生模块与文件模块的优先级不一样,可是优先级最高的是从文件模块的缓存中加载已经存在的模块。
原生模块的优先级仅次于文件模块缓存的优先级。require方法在解析文件名以后,优先检查模块是否在原生模块列表中。以http模块为例,尽管在目录下存在一个http
、http.js
、http.node
、http.json
文件,require(“http”)
都不会从这些文件中加载,而是从原生模块中加载。
原生模块也有一个缓存区,一样也是优先从缓存区加载。若是缓存区没有被加载过,则调用原生模块的加载方式进行加载和执行。
当文件模块缓存中不存在,并且不是原生模块的时候,Node.js会解析require方法传入的参数,并从文件系统中加载实际的文件,加载过程当中的包装和编译细节在前面说过是调用load方法。
··
当 Node 遇到 require(X) 时,按下面的顺序处理。
(1)若是 X 是内置模块(好比 require('http'))
a. 返回该模块。
b. 再也不继续执行。
(2)若是 X 以 "./" 或者 "/" 或者 "../" 开头
a. 根据 X 所在的父模块,肯定 X 的绝对路径。
b. 将 X 当成文件,依次查找下面文件,只要其中有一个存在,就返回该文件,再也不继续执行。
X
X.js
X.json
X.node
c. 将 X 当成目录,依次查找下面文件,只要其中有一个存在,就返回该文件,再也不继续执行。
X/package.json(main字段)
X/index.js
X/index.json
X/index.node
(3)若是 X 不带路径
a. 根据 X 所在的父模块,肯定 X 可能的安装目录。
b. 依次在每一个目录中,将 X 当成文件名或目录名加载。
(4) 抛出 "not found"
模块循环依赖
//建立两个文件,module1.js 和 module2.js,而且让它们相互引用
// module1.js
exports.a = 1;
require('./module2');
exports.b = 2;
exports.c = 3;
// module2.js
const Module1 = require('./module1');
console.log('Module1 is partially loaded here', Module1);
在 module1 彻底加载以前须要先加载 module2,而 module2 的加载又须要 module1。这种状态下,咱们从 exports 对象中能获得的就是在发生循环依赖以前的这部分。上面代码中,只有 a 属性被引入,由于 b 和 c 都须要在引入 module2 以后才能加载进来。
Node 使这个问题简单化,在一个模块加载期间开始建立 exports 对象。若是它须要引入其余模块,而且有循环依赖,那么只能部分引入,也就是只能引入发生循环依赖以前所定义的这部分。
AMD 是 Asynchronous Module Definition 的简称,即“异步模块定义”,是从 CommonJS 讨论中诞生的。AMD 优先照顾浏览器的模块加载场景,使用了异步加载和回调的方式。
AMD 和 CommonJS 同样须要脚本加载器,尽管 AMD 只须要对 define 方法的支持。define 方法须要三个参数:模块名称,模块运行的依赖数组,全部依赖均可用以后执行的函数(该函数按照依赖声明的顺序,接收依赖做为参数)。只有函数参数是必须的。define 既是一种引用模块的方式,也是定义模块的方式。
// file lib/sayModule.js
define(function (){
return {
sayHello: function () {
console.log('hello');
}
};
});
//file main.js
define(['./lib/sayModule'], function (say){
say.sayHello(); //hello
})
main.js 做为整个应用的入口模块,咱们使用 define 关键字声明了该模块以及外部依赖(没有生命模块名称);当咱们执行该模块代码时,也就是执行 define 函数的第二个参数中定义的函数功能,其会在框架将全部的其余依赖模块加载完毕后被执行。这种延迟代码执行的技术也就保证了依赖的并发加载。
RequireJS
RequireJS 是一个前端的模块化管理的工具库,遵循AMD规范,经过一个函数来将全部所须要的或者说所依赖的模块实现装载进来,而后返回一个新的函数(模块),咱们全部的关于新模块的业务代码都在这个函数内部操做,其内部也可无限制的使用已经加载进来的以来的模块。
<script data-main='scripts/main' src='scripts/require.js'></script>
//scripts下的main.js则是指定的主代码脚本文件,全部的依赖模块代码文件都将从该文件开始异步加载进入执行。
defined用于定义模块,RequireJS要求每一个模块均放在独立的文件之中。按照是否有依赖其余模块的状况分为独立模块和非独立模块。
一、独立模块 不依赖其余模块。直接定义
define({
methodOne: function (){},
methodTwo: function (){}
});
//等价于
define(function (){
return {
methodOne: function (){},
methodTwo: function (){}
};
});
二、非独立模块,对其余模块有依赖
define([ 'moduleOne', 'moduleTwo' ], function(mOne, mTwo){
...
});
//或者
define( function( require ){
var mOne = require( 'moduleOne' ),
mTwo = require( 'moduleTwo' );
...
});
如上代码, define中有依赖模块数组的 和 没有依赖模块数组用require加载 这两种定义模块,调用模块的方法合称为AMD模式,定义模块清晰,不会污染全局变量,清楚的显示依赖关系。AMD模式能够用于浏览器环境而且容许非同步加载模块,也能够按需动态加载模块。
CMD(Common Module Definition),在CMD中,一个模块就是一个文件。
全局函数define,用来定义模块。
参数 factory 能够是一个函数,也能够为对象或者字符串。
当 factory 为对象、字符串时,表示模块的接口就是该对象、字符串。
定义JSON数据模块:
define({ "foo": "bar" });
factory 为函数的时候,表示模块的构造方法,执行构造方法即可以获得模块向外提供的接口。
define( function(require, exports, module) {
// 模块代码
});
SeaJS
sea.js 核心特征:
seajs.use
用来在页面中加载一个或者多个模块
// 加载一个模块
seajs.use('./a');
// 加载模块,加载完成时执行回调
seajs.use('./a',function(a){
a.doSomething();
});
// 加载多个模块执行回调
seajs.use(['./a','./b'],function(a , b){
a.doSomething();
b.doSomething();
});
AMD和CMD最大的区别是对依赖模块的执行时机处理不一样,注意不是加载的时机或者方式不一样。
不少人说requireJS是异步加载模块,SeaJS是同步加载模块,这么理解其实是不许确的,其实加载模块都是异步的,只不过AMD依赖前置,js能够方便知道依赖模块是谁,当即加载,而CMD就近依赖,须要使用把模块变为字符串解析一遍才知道依赖了那些模块,这也是不少人诟病CMD的一点,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到能够忽略。
为何说是执行时机处理不一样?
一样都是异步加载模块,AMD在加载模块完成后就会执行该模块,全部模块都加载执行完后会进入回调函数,执行主逻辑,这样的效果就是依赖模块的执行顺序和书写顺序不必定一致,看网络速度,哪一个先下载下来,哪一个先执行,可是主逻辑必定在全部依赖加载完成后才执行。
CMD加载完某个依赖模块后并不执行,只是下载而已,在全部依赖模块加载完成后进入主逻辑,遇到require语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是彻底一致的。
统一模块定义(UMD:Universal Module Definition )就是将 AMD 和 CommonJS 合在一块儿的一种尝试,常见的作法是将CommonJS 语法包裹在兼容 AMD 的代码中。
(function(define) {
define(function () {
return {
sayHello: function () {
console.log('hello');
}
};
});
}(
typeof module === 'object' && module.exports && typeof define !== 'function' ?
function (factory) { module.exports = factory(); } :
define
));
该模式的核心思想在于所谓的 IIFE(Immediately Invoked Function Expression),该函数会根据环境来判断须要的参数类别
ES6 的模块自动采用严格模式,无论有没有在模块头部加上"use strict";。
严格模式主要有如下限制。
一个模块,就是一个对其余模块暴露本身的属性或者方法的文件。
做为一个模块,它能够选择性地给其余模块暴露(提供)本身的属性和方法,供其余模块使用。
// profile.js
export var firstName = 'qiqi';
export var lastName = 'haobenben';
export var year = 1992;
//等价于
var firstName = 'qiqi';
var lastName = 'haobenben';
var year = 1992;
export {firstName, lastName, year}
一、 一般状况下,export输出的变量就是原本的名字,可是可使用as关键字重命名。
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
//上面代码使用as关键字,重命名了函数v1和v2的对外接口。重命名后,v2能够用不一样的名字输出两次。
二、 须要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量创建一一对应关系。
// 报错
export 1;
// 报错
var m = 1;
export m;
//上面两种写法都会报错,由于没有提供对外的接口。第一种写法直接输出1,第二种写法经过变量m,仍是直接输出1。1只是一个值,不是接口。
/ 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};
//上面三种写法都是正确的,规定了对外的接口m。其余脚本能够经过这个接口,取到值1。它们的实质是,在接口名与模块内部变量之间,创建了一一对应的关系。
三、最后,export命令能够出如今模块的任何位置,只要处于模块顶层就能够。若是处于块级做用域内,就会报错,接下来讲的import命令也是如此。
function foo() {
export default 'bar' // SyntaxError
}
foo()
做为一个模块,能够根据须要,引入其余模块的提供的属性或者方法,供本身模块使用。
一、 import命令接受一对大括号,里面指定要从其余模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。若是想为输入的变量从新取一个名字,import命令要使用as关键字,将输入的变量重命名。
import { lastName as surename } from './profile';
二、import后面的from指定模块文件的位置,能够是相对路径,也能够是绝对路径,.js路径能够省略。若是只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。
三、注意,import命令具备提高效果,会提高到整个模块的头部,首先执行。
foo();
import { foo } from 'my_module';
//上面的代码不会报错,由于import的执行早于foo的调用。这种行为的本质是,import命令是编译阶段执行的,在代码运行以前。
四、因为import是静态执行,因此不能使用表达式和变量,这些只有在运行时才能获得结果的语法结构。
/ 报错
import { 'f' + 'oo' } from 'my_module';
// 报错
let module = 'my_module';
import { foo } from module;
// 报错
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
五、最后,import语句会执行所加载的模块,所以能够有下面的写法。
import 'lodash';
//上面代码仅仅执行lodash模块,可是不输入任何值。
每一个模块支持咱们导出一个
没有名字的变量,使用关键语句export default来实现.
export default function(){
console.log("I am default Fn");
}
//使用export default关键字对外导出一个匿名函数,导入这个模块的时候,能够为这个匿名函数取任意的名字
//取任意名字都可
import sayDefault from "./module-B.js";
sayDefault();
//结果:I am default Fn
一、默认输出和正常输出的比较
// 第一组
export default function diff() { // 输出
// ...
}
import diff from 'diff'; // 输入
// 第二组
export function diff() { // 输出
// ...
};
import {diff} from 'diff'; // 输入
//上面代码的两组写法,第一组是使用export default时,对应的import语句不须要使用大括号;第二组是不使用export default时,对应的import语句须要使用大括号。
export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,所以export default命令只能使用一次。因此,import命令后面才不用加大括号,由于只可能对应一个方法。
二、由于export default本质是将该命令后面的值,赋给default变量之后再默认,因此直接将一个值写在export default以后。
/ 正确
export default 42;
// 报错
export 42;
//上面代码中,后一句报错是由于没有指定对外的接口,而前一句指定外对接口为default。
三、若是想在一条import语句中,同时输入默认方法和其余变量,能够写成下面这样。
import _, { each } from 'lodash';
//对应上面代码的export语句以下
export default function (){
//...
}
export function each (obj, iterator, context){
//...
}
若是在一个模块之中,先输入后输出同一个模块,import语句能够与export语句写在一块儿。
export { foo, bar } from 'my_module';
// 等同于
import { foo, bar } from 'my_module';
export { foo, bar };
/ 接口更名
export { foo as myFoo } from 'my_module';
// 总体输出
export * from 'my_module';
注意事项
一、声明的变量,对外都是只读的。可是导出的是对象类型的值,就可修改。
二、导入不存在的变量,值为undefined。
ES6 中,imports 是 exprts 的只读视图,直白一点就是,imports 都指向 exports 本来的数据,好比:
//------ lib.js ------
export let counter = 3;
export function incCounter() {
counter++;
}
//------ main.js ------
import { counter, incCounter } from './lib';
// The imported value `counter` is live
console.log(counter); // 3
incCounter();
console.log(counter); // 4
// The imported value can’t be changed
counter++; // TypeError
所以在 ES6 中处理循环引用特别简单,看下面这段代码:
//------ a.js ------
import {bar} from 'b'; // (1)
export function foo() {
bar(); // (2)
}
//------ b.js ------
import {foo} from 'a'; // (3)
export function bar() {
if (Math.random()) {
foo(); // (4)
}
}
假设先加载模块 a,在模块 a 加载完成以后,bar 间接性地指向的是模块 b 中的 bar。不管是加载完成的 imports 仍是未完成的 imports,imports 和 exports 之间都有一个间接的联系,因此老是能够正常工做。
//---module-B.js文件---
//导出变量:name
export var name = "cfangxu";
moduleA模块代码:
//导入 模块B的属性 name
import { name } from "./module-B.js";
console.log(name)
//打印结果:cfangxu
批量导出
//属性name
var name = "cfangxu";
//属性age
var age = 26;
//方法 say
var say = function(){
console.log("say hello");
}
//批量导出
export {name,age,say}
批量导入
//导入 模块B的属性
import { name,age,say } from "./module-B.js";
console.log(name)
//打印结果:cfangxu
console.log(age)
//打印结果:26
say()
//打印结果:say hello
重命名导入变量
import {name as myName} from './module-B.js';
console.log(myName) //cfangxu
总体导入
/使用*实现总体导入
import * as obj from "./module-B.js";
console.log(obj.name)
//结果:"cfangxu"
console.log(obj.age)
//结果:26
obj.say();
//结果:say hello