什么是模块化
- 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
- 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信
模块化的好处
- 解决命名冲突
- 依赖管理
- 提高可读性
- 提高复用性
- 提高可维护性
早期的模块
函数
将复杂功能封装到一个个函数中,一个函数就相当于一个模块,模块的调用即函数的调用。
function fn1() {
// do something
}
function fn2() {
// do something
}
优点:有模块的私有属性(函数内的变量,外部无法改变) 缺点:污染全局变量,依赖关系不能很容易的看出来
对象(又称namespace)
用对象的方式组织模块,一个对象就是一个命名空间。
var myModule = {
name: 'myModule',
sayName: function() {
console.log(this.name)
},
fn2: function() {}
}
优点: 降低了对全局变量的污染,但是还不能彻底避免,myModule这个变量 还是会污染全局变量
缺点: 会暴露所有模块成员,模块内的数据可以被意外修改
myModule.sayName() // myModule
myModule.name = '篡改的名字'
myModule.sayName() // 篡改的名字
IIFE(立即调用函数)
Immediately-Invoked Function Expression
利用闭包封装了模块的私有属性
var module1 = (function(){
var _count = 0;
var m1 = function(){
// ...
};
var m2 = function(){
//...
};
return {
m1 : m1,
m2 : m2
};
})();
优点: 模块有私有属性 缺点: 模块依赖问题,还是会污染全局变量。
IIFE优化
模块应该具有独立性,模块内部最好不与程序的其他部分直接交互,如果需要,则显示注入。
var module1 = (function(window, $){
var name 'myModule';
function sayName () {
console.log(this.name)
}
function fn2 () {
$('#app').css('color', 'red')
}
return {
sayName,
fn2
}
})(window, jquery) // 一眼就能看出模块内部需要使用全局变量,和jquery对象
优点:私有变量,模块依赖清晰
CommonJS
CommonJS是Node使用的一种模块规范
概述
CommonJS 每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。这一点和浏览器不同,浏览器都是在全局作用域中。
对外接口的导出
CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。
var name: 'myModule'; // 定义在模块作用域中,而不是全局
function sayName () {
console.log(name)
}
module.exports.sayName = sayName; // 使用module.exports对外暴露接口
为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。
var exports = module.exports;
上面的例子可写成下面这样
var name: 'myModule'; // 定义在模块作用域中,而不是全局
function sayName () {
console.log(name)
}
exports.sayName = sayName;
导入模块
使用require命令,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。
var myModule = require('./myModule.js');
myModule.sayName() // 'myModule'
同步性
CommonJS规范是为了服务于node的,在服务器端,文件读取通常都是在本地完成,速度快,所以采用同步方式,也就是说,只有加载并执行完require的模块,才能执行后面的操作,但是在浏览器中,模块的请求通常都是通过网络,速度慢,同步模式不适合。
模块缓存
CommonJS 的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象,以后再加载该模块,就直接从缓存取出该模块的module.exports属性
{
id: '...', // 模块名
exports: { ... }, // 模块输出的各个接口
loaded: true, // 模块的脚本是否执行完毕
...
}
动态加载(又称运行时加载)
CommonJS规范是动态加载,比如可以使用条件导出,缺点是无法进行静态分析
let moduleName = useA ? 'moduleA' : 'moduleB';
let module = require(moduleName) // require导入的模块是由上面的语句的执行结果决定的
总结
- CommonJS规范是同步的(浏览器端不适用)。
- 使用module.exports导出
- 使用require导入
- 模块缓存:只执行一次,缓存结果。
- 动态加载
AMD规范和RequireJS
CommonJS规范是同步加载,不适合用在浏览器,所以针对浏览器端,就诞生了AMD规范
AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
require.js是一个遵循了AMD规范的库,下面主要针对require.js进行介绍,除了require.js也有其他的库实现了AMD,如curl.js
模块定义:define
模块必须采用特定的define()函数来定义。如果一个模块不依赖其他模块,那么可以直接定义在define()函数之中。
第一个参数,是一个数组,指明该模块的依赖性。 define方法的第二个参数是一个函数,当前面数组的所有成员加载成功后,它将被调用。它的参数与数组的成员一一对应
这个函数必须返回一个对象,即模块的导出部分,供其他模块使用。
 // 定一个了一个模块,且该模块依赖了另外一个模块otherLib
define(['otherLib'], function(myLib){
function foo(){
otherLib.doSomething();
}
return {
foo : foo
};
});
模块导入:require
第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数
require(['math'], function (math) {
math.add(2, 3);
});
配置
require.js有一些配置项
- baseUrl参数指定本地模块位置的基准目录,即本地模块的路径是相对于哪个目录的。该属性通常由require.js加载时的data-main属性指定。
- paths:paths参数指定各个模块的位置。这个位置可以是同一个服务器上的相对位置,也可以是外部网址。可以为每个模块定义多个位置,如果第一个位置加载失败,则加载第二个位置,上面的示例就表示如果CDN加载失败,则加载服务器上的备用脚本。
- shim:有些库不是AMD兼容的,这时就需要指定shim属性的值。shim可以理解成“垫片”,用来帮助require.js加载非AMD规范的库。
require.config({
//baseUrl: '/', // 指定本地模块位置的基准目录
paths: { // paths参数指定各个模块的位置。
jquery: [
'//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js',
'lib/jquery'
]
},
shim: { //
"backbone": {
deps: [ "underscore" ],
exports: "Backbone"
},
"underscore": {
exports: "_"
}
}
});
插件
require.js还提供一系列插件,实现一些特定的功能
如 text和image插件,允许require.js加载文本和图片文件。
define(['text!review.txt','image!cat.jpg'], function(review,cat){
console.log(review);
document.body.appendChild(cat);
});
CMD规范和sea.js
CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在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();
}
});
ES6 Module
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
export和import
ES6 模块使用export定义导出接口,使用import导入其他模块
// math.js
let addNum = function (a, b) {
return a + b;
};
export { addNum };
// index.js
import { addNum } from './math';
addNuma(1, 2) // 3
}
使用export default默认导出
// math.js
let addNum = function (a, b) {
return a + b;
};
let math = {
addNum
}
export default math;
// index.js
import math from './math';
math.addNuma(1, 2) // 3
}
静态加载(又称编译时加载)
ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高
动态更新
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
上面代码输出变量foo,值为bar,500 毫秒之后变成baz。
这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新
动态加载:import()
动态加载解决方案,import()类似于CommonJs中的require,不用点在于require是同步的,而import()是异步的。
import()返回一个promise对象
ES6模块与CommonJS模块的差异
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是动态加载,ES6模块是编译时输出接口(使用import()方法也可以动态加载)。
- ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块
第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
在浏览器中使用ES6模块
使用type="module"标记的script,支持浏览器会将其当做ES6模块解析,不支持的浏览器会忽略。
<script type="module" src="module.mjs"></script>
<script nomodule src="fallback.js"></script>
nomodule属性提供了一种兼容方案,支持ES6模块的浏览器会将其忽略,不支持的浏览器将会执行。
type="module"的scripe标签,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了script标签的defer属性
如果网页有多个type="module"的标签,它们会按照在页面出现的顺序依次执行。
总结
- ES6 模块设计为前后端都可使用的方案,
- ES6 模块支持性差,node原生不支持,浏览器端支持也一般。
- import导入
- export导出
- import命令为静态导入
- import()动态导入,返回promise
- 多次导入,只执行一次
- 动态更新
webpack模块
webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)
什么是 webpack 模块
- ES2015 import 语句
- CommonJS require() 语句
- AMD define 和 require 语句
- css/sass/less 文件中的 @import 语句。
- 样式(url(...))或 HTML 文件(<img src=...>)中的图片链接(image url)
- webpack 通过 loader 可以支持各种语言和预处理器编写模块
即基于CommonJS/AMD/ES6 module规范的js文件,以及其他配置了loader的文件(如图片,css),都是webpack的模块。
使用webpack可以让你使用各种模块规范编写基于浏览器端的代码,webpack会帮你编译成浏览器可以识别和执行的代码。