为什么需要模块化
为方便文件管理、增加复用,我们需要细化JS文件,每个文件负责单一职能,称之为模块,明确每个文件的职能,当交互功能较复杂时,引用的文件也越加庞大,此时我们就需要模块化管理。
模块化管理可以避免全局变量污染、函数命名冲突、文件依赖等问题
如何模块化
为了实现JavaScript的模块化开发,需要遵循一定的规范,目前通用的规范分为服务端(CommonJS)和客户端(AMD/CMD)。
CommonJS
CommonJS是针对JavaScript的服务端而言的,NodeJS采用的就是这个规范。根据CommonJS规范,一个单独的文件就是一个模块,每一个模块都是一个单独的作用域,也就是说,在该模块内部定义的变量,无法被其他的模块读取,除非定义为global对象的属性。加载模块使用require方法,该方法读取一个文件并执行,最后返回文件内部的exports对象。
//引入其他模块
var example = require('./example.js');
function exportFunc(){
this.foo = function(){
console.log('foo');
}
}
//导出模块
exports.module1 = exportFunc();
NodeJS中使用require引入模块可以引入核心模块、自定义模块和路径形式的文件模块。其中核心模块(如http、fs、path等)是在NodeJS的源代码编译过程中已经编译为二进制代码,其加载过程最快。路径形式的文件模块是指以.
、./
标示的文件路径。在分析路径模块时,require()方法会将路径转换为真时路径,并以真实路径为索引,将编译执行后的结果存放到缓存中。由于文件模块给Node指明了确切的文件位置,所以在查找过程中可以节约大量时间,其加载时间鳗鱼核心模块。自定义模块指的是非核心模块,也不是路径形式的标识符。它是一种特殊的文件模块,可能是一个文件或者包的形式。这类模块的查找是最费时的,也是所有方式中最慢的一种。定位过程大概是先在当前文件目录下的node_modules
目录,然后在找父目录下的node_modules
目录,沿路径向上主机递归,知道根目录下的node_modules
目录
CommonJS规范加载模块是同步的,也就是说只有加载完成才能执行后面的操作。由于NodeJS主要用于服务器端编程,模块文件一般都已经存在与本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范
AMD(异步模块定义)规范
AMD定义了一套JavaScript模块依赖异步加载标准。
AMD模块的定义
define(id? :String,dependencies?:String[],factory:Function | Object);
id
是模块的名字,它是可选的参数
dependencies
指定了所要依赖的模块列表,它是一个数组,也是可选的参数,每个依赖的模块的输出将作为参数依次传入factory中。如果没有指定dependencies
,那么它的默认值就是 ['require','exports','module']
。
factory
是最后一个参数,它包裹了模块的具体实现,它是一个函数或者对象。如果是函数,那么它的返回值就是模块的输出接口或值
define('myModule',['jquery'],function($){
$('body').text('hello world');
});
//使用
define(['myModule'],function(myModule){});
AMD模块的加载
AMD采用require()语句加载模块,它要求两个参数
require([module],callback);
第一个参数是数组,每个项都是你需要依赖的模块,这里需要注意,由于AMD是异步加载,每个模块的加载顺序是不固定的,但是执行顺序是固定的,按照依赖声明的先后顺序执行。
目前,require.js实现了AMD规范。
ES6中的模块化
ES6模块的设计思想是尽量静态化,使得在代码运行前就能确定模块的依赖冠以,以及输入输出的变阿玲。CommonJS和AMD模块都只能在运行时确定这些东西。
* 模块常用命令*
ES6的模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
import {es6} from './someModle';
function setName(ele){
console.log(ele);
}
export setName;
另外,ES6还提供了module命令。该命令可以取代import语句,达到整体输入模块的作用
//circle.js
export function area(radius){
return Math.PI * radius * radius;
}
export function circum(radius){
return 2 * Math.PI *radius;
}
//main.js(写法一)
import { area,circum } from './circle';
console.log('圆面积:' + area(4));
console.log('圆周长:' + circum(14);
//main.js(写法二)
import * as circle from './circle';//as关键字可以重命名输出的模块
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circum(14);
//main.js(写法三)
module circle from './circle';//as关键字可以重命名输出的模块
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circum(14);
为了方便用户,使开发人员使用其他人编写的模块时不用阅读文档就都能加载模块,ES6提供了export default命令,为模块指定默认输出。但是一个模块只能由一个默认输出,export default命令只能使用一次。
//export-default.js
export default function(){
console.log('foo');
}
//import-default.js
import customName from '.export-default';
customName();//'foo'
模块加载实质
ES6模块加载的机制与CommonJS模块完全不同。CommonJS模块输出的是一个值的拷贝,而ES6模块输出的是值的引用。也就是说,通过CommonJS引入的模块,一旦输出一个值,模块内部的变化就影响不到这个值;而ES6遇到加载命令import时不回去执行模块,智慧生成一个动态的只读引用。等到真的需要用到时,再到模块中取值。
//Common.js实现
var counter = 3;
function incCounter(){
counter++;
}
module.exports = {
counter:counter,
incCounter:incCounter,
};
//main1.js
import{counter,incCounter} from '.lib';
console.log(counter);//3
incCounter();
console.log(counter);//3
//ES6实现
//lib.js
export let counter = 3;
export function incCounter(){
counter++;
}
//main1.js
import{counter,incCounter} from '.lib';
console.log(counter);//3
incCounter();
console.log(counter);//4