模块化是指将一个复杂的程序分解为多个模块,方便编码javascript
function m1(){
// xxx
}
function m2(){
// xxx
}
复制代码
上面的函数m一、m2就至关于一个模块,使用的时候,直接调用就能够了。css
可是这种作法缺点也很明显:因为函数是直接挂载在window(全局)对象下,"污染"了全局变量,没法保证不与其余模块发生变量名冲突,并且模块成员之间看不出直接关系。html
既然window对象的可命名属性名就那么多,那我再在window(全局)对象上面声明一个对象,而后把全部的模块成员都放到这个对象里面。前端
var module = {
count: 0,
function m1(){
// xxx
}
function m2(){
// xxx
}
}
复制代码
上面的函数m1()和m2(),都封装在module1对象里。使用的时候,就是调用这个对象的属性。vue
module.m1();
复制代码
可是,这样的写法会暴露全部模块成员,内部状态能够被外部改写。好比,外部代码能够直接改变内部计数器的值。java
module1._count = 5;
复制代码
为了防止内部成员被暴露出去,咱们用当即执行函数能够实现私有化变量。node
const module2 = (function() {
let _money = 100
const m1 = () => {
console.log(123)
}
const m2 = () => {
console.log(456)
}
return {
f1: m1,
f2: m2
}
})()
复制代码
使用上面的写法,外部代码没法读取内部的_count变量。react
console.info(module2._count); //undefined
复制代码
不过虽然这样function内部的变量就对全局隐藏了,达到是封装的目的。可是这样仍是有缺陷的,Module2这个变量仍是暴漏到全局了,随着模块的增多,全局变量仍是会愈来愈多。jquery
<script type="text/javascript" src="a.js"></script>
<script type="text/javascript" src="b.js"></script>
<script type="text/javascript" src="c.js"></script>
<script type="text/javascript" src="d.js"></script>
复制代码
缺点:git
(1)加载的时候会中止渲染网页,引入的js文件越多,网页失去响应的时间越长;
(2)会污染全局变量;
(3)js文件之间存在依赖关系,加载是有顺序的,依赖性最大的要放到最后去加载;当项目规模较大时,依赖关系变得错综复杂。
(4)要引入的js文件太多,不美观,代码难以管理。
使用函数写法会致使全局变量污染,并有可能致使命名冲突
使用命名空间会致使内部属性被暴露,能够致使内部成员被改写
使用当即执行函数能够实现私有化变量,能够达到必定的防护做用。是早期较好的模块化方案
使用Script来引用JS模块会致使文件关系错综复杂,难以管理
2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。这标志"Javascript模块化编程"正式诞生。由于老实说,在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限;可是在服务器端,必定要有模块,与操做系统和其余应用程序互动,不然根本无法编程。而node.js的模块系统,就是参照CommonJS规范实现的。
3.1.一、CommonJS特色
3.1.二、基本语法
module.exports = value
或exports.xxx = value
require(xxx)
,若是是第三方模块,xxx为模块名;若是是自定义模块,xxx为模块文件路径。此处咱们有个疑问:CommomJS暴露的模块究竟是什么?CommonJS规范规定,每一个模块内部,module变量表明当前模块,这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,实际上是加载该模块的module.exports属性。
// a.js
module.exports = {
a: 1
}
// or
exports.a = 1
// b.js
var module = require('./a.js')
module.a // -> log 1
复制代码
上面的写法很好用,可是 module.exports 和 exports 是咋回事?为啥这几句代码就实现模块化了,让咱们来看一下基础的实现
先说 require 吧
var module = require('./a.js')
module.a
// 这里其实就是包装了一层当即执行函数,这样就不会污染全局变量了,
// 重要的是 module 这里,module 是 Node 独有的一个变量
module.exports = {
a: 1
}
// 基本实现
var module = {
id: 'xxxx', // 我总得知道怎么去找到他吧
exports: {} // exports 就是个空对象
}
// 这个是为何 exports 和 module.exports 用法类似的缘由
var exports = module.exports
var load = function (module) {
// 导出的东西
var a = 1
module.exports = a
return module.exports
};
// 而后当我 require 的时候去找到独特的
// id,而后将要使用的东西用当即执行函数包装下,over
复制代码
再来讲说module.exports
与exports
的区别。
exports
是指向的module.exports
的引用module.exports
初始值为一个空对象{},因此exports
初始值也是{},可是不能对exports
直接赋值,不会有任何效果,,看了上面代码的同窗确定明白为何了。require()
返回的是module.exports
而不是exports
3.1.三、模块的加载机制
CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。 这点与ES6模块化有重大差别(下文会介绍),请看下面这个例子:
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
复制代码
上面代码输出内部变量counter和改写这个变量的内部方法incCounter。
// main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;
console.log(counter); // 3
incCounter();
console.log(counter); // 3
复制代码
上面代码说明,counter输出之后,lib.js模块内部的变化就影响不到counter了。这是由于counter是一个原始类型的值,会被缓存。除非写成一个函数,才能获得内部变更后的值。
CommonJS规范是 Node 独有的,若是浏览器想使用该规范,就须要用到 Browserify 解析了。
3.1.四、Browserify
Browserify 可让你使用相似于 node 的 require() 的方式来组织浏览器端的 Javascript 代码,经过 预编译 让前端 Javascript 能够直接使用 Node NPM 安装的一些库。 -- 来自百度百科
①下载
②打包编译 将须要打包编译的JS文件经过 运行代码browserify app.js -o bundle.js
将路径下的app.js文件编译output到bundle.js文件中
③页面使用引入 最后从新在页面文件中引入bundle.js
文件<script type="text/javascript" src="./bundle.js"></script>
3.1.五、总结
见名知意,就是异步模块定义。上面已经介绍过,CommonJS是服务器端模块的规范,主要是为了JS在后端的表现制定的,不太适合前端。而AMD就是要为前端JS的表现制定规范。因为不是JavaScript原生支持,使用AMD规范进行页面开发须要用到对应的库函数,也就是require.js(还有个js库:curl.js)。实际上AMD 是 require.js在推广过程当中对模块定义的规范化的产出。 AMD采用异步方式加载模块,模块的加载不影响它后面语句的运行。全部依赖这个模块的语句,都定义在一个回调函数中,等到加载完成以后,这个回调函数才会运行。
3.2.一、模块的定义和使用
// 定义一个模块
define('module', ['dep'], function (dep) {
return exports;
});
// id 可选参数,用来定义模块的标识,若是没有提供该参数,默认脚本文件名(去掉拓展名)
// dependencies 是一个当前模块用来的模块名称数组,(所依赖模块的数组)
// factory 工厂方法,模块初始化要执行的函数或对象,若是为函数,它应该只被执行一次,若是是对象,此对象应该为模块的输出值。
复制代码
require.js也采用require()语句加载模块,可是不一样于CommonJS,它要求二个参数
//导入和使用模块
require([module], callback);
// 第二个参数[module],是一个数组,里面的成员就是要加载的模块;
// 第二个参数callback,则是加载成功以后的回调函数
// 等到前面的module加载完成以后,这个回调函数才被调用。
// 加载的模块会以参数形式传入该函数,从而在回调函数内部就可使用这些模块
复制代码
3.2.二、看个例子
// demo.html
<body>
//引入所依赖文件和主入口文件
<script src="./require.js" data-main = './demo.js'></script>
</body>
// modules/m1.js
define(function(){
var name = 'm1-amd';
function getName(){
return name;
}
return {getName} //暴露出的模块
})
// modules/m2.js
//在m2模块中,引用了m1模块
define(['m1'],function(m1){
var msg = 'm2-amd';
function show(){
console.log(msg,m1.getName());
}
return {show} //暴露的模块
})
//demo.js
(function(){
//配置每一个变量对应的模块路径
require.config({
paths: {
m1: './modules/m1',
m2: './modules/m2',
}
})
require(['m2'],function(m2){
m2.show(); //结果:m2-amd m1-amd
})
})()
复制代码
默认状况下,require.js假定这加载的模块与main.js在同一个目录,而后自动加载。若是不在同一目录,咱们可使用require.config()方法对模块的加载行为进行自定义
在上面的例子中也能够引用第三方库,只需在上面代码的基础稍做修改:
//demo.js
(function(){
//配置每一个变量对应的模块路径
require.config({
paths: {
m1: './modules/m1',
m2: './modules/m2',
jquery:'./jquery-3.3.1'
}
})
require(['m2','jquery'],function(m2,$){
m2.show(); //结果:m2-amd m1-amd
$('body').css('backgroundColor','#000');
})
})()
复制代码
不过须要注意的是:jquery对模块化作了各类不一样的规范,对每一个不一样模块都有暴露出的接口名字,对AMD暴露出的接口名字是小写jquery,所以不能把jquery写成大写的jQuery,这样会报错
//jquery 3.3.1.js
if(typeof define === 'function' && define.amd){
define('jquery',[],function(){
return jQuery;
})
}
复制代码
3.2.三、AMD特色:
依赖前置:必须等到全部依赖的模块加载完成以后才会执行回调,即便在回调里根本没用到该模块。(在定义模块的时候就要声明其依赖的模块),不过目前在AMD2.0也能够动态加载模块了
requireJS优缺点
优势:
一、适合在浏览器环境中异步加载模块
二、能够并行加载多个模块
缺点:
一、提升了开发成本,代码的阅读和书写比较困难,模块定义方式的语义不畅
二、不符合通用的模块化思惟方式,是一种妥协的实现
即通用模块定义,对应SeaJS,是阿里玉伯团队首先提出的概念和设计。跟requireJS解决一样问题,只是运行机制不一样。
3.3.一、CMD与AMD的不一样的在于
(1)AMD推崇依赖前置;CMD推崇依赖就近,只有在用到某个模块的时候再去require:
通俗来讲:
AMD
在加载完成定义(define
)好的模块就会当即执行,全部执行完成后,遇到require
才会执行主逻辑。(提早加载)
CMD
在加载完成定义(define
)好的模块,仅仅是下载不执行,在遇到require
才会执行对应的模块。(按需加载)
AMD
用户体验好,由于没有延迟,CMD
性能好,由于只有用户须要的时候才执行。
CMD
为何会出现,由于对node.js的书写者友好,由于符合写法习惯,就像为什么vue会受人欢迎的一个道理。
3.3.二、CMD语法
Sea.js 推崇一个模块一个文件,遵循统一的写法。
define(id?, deps?, factory)
由于CMD推崇一个文件一个模块,因此常常就用文件名做为模块id CMD推崇依赖就近,因此通常不在define的参数中写依赖,在factory中写 factory有三个参数
function(require, exports, module)
定义暴露模块
//定义没有依赖的模块
define(function(require, exports, module){
exports.xxx = value
module.exports = value
})
复制代码
//定义有依赖的模块
define(function(require, exports, module){
//引入依赖模块(同步)
var module2 = require('./module2')
//引入依赖模块(异步)
require.async('./module3', function (m3) {
})
//暴露模块
exports.xxx = value
})
复制代码
引入使用模块
define(function (require) {
var m1 = require('./module1')
var m4 = require('./module4')
m1.show()
m4.show()
})
复制代码
sea.js 简单使用教程
①下载sea.js, 并引入
官网: seajs.org/
github : github.com/seajs/seajs
而后将sea.js导入项目: js/libs/sea.js
②建立项目结构
|-modules
|-m1.js
|-m2.js
|-m3.js
|-m4.js
|-index.html
|-main.js
|-sea.js
复制代码
③定义sea.js的模块代码
// index.html
<body>
<script src="./sea.js"></script> //引入依赖文件
<script>
seajs.use('./main.js'); //设置主入口文件
</script>
</body>
// modules/m1.js
define(function(require,exports,module){
var msg = 'm1';
function foo(){
console.log(msg);
}
module.exports = { //暴露的接口
foo:foo
}
});
// modules/m2.js
define(function(require,exports,module){
var msg = 'm2';
function bar(){
console.log(msg);
}
module.exports = bar; /暴露的接口
});
// modules/m3.js
define(function(require,exports,module){
var msg = 'm3';
function foo(){
console.log(msg);
}
exports.m3 = { foo:foo} /暴露的接口
});
// modules/m4.js
define(function(require,exports,module){
var msg = 'm4';
// 同步引入
var m2 = require('./m2');
m2();
// 异步引入
require.async('./m3',function(m3){
m3.m3.foo();
});
function fun(){
console.log(msg);
}
exports.m4 = fun; /暴露的接口
})
//main.js
define(function(require,exports,module){
var m1 = require('./modules/m1');
m1.foo();
var m4 = require('./modules/m4');
m4.m4();
})
复制代码
最后获得结果以下
优势: 一样实现了浏览器端的模块化加载。 能够按需加载,依赖就近。
缺点: 依赖SPM打包,模块的加载逻辑偏重
ES6 模块的设计思想是尽可能的静态化,使得编译时就能肯定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时肯定这些东西。好比,CommonJS 模块就是对象,输入时必须查找对象属性。
3.4.1 语法
在 ES6 中,使用export关键字来导出模块,使用import关键字引用模块。可是浏览器尚未彻底兼容,须要使用babel转换成ES5的require。
// 导出
export function hello() { };
export default {
// ...
};
// 导入
import { readFile } from 'fs';
import React from 'react';
复制代码
使用import导入模块时,须要知道要加载的变量名或函数名。
在ES6中还提供了export default,为模块指定默认输出.对应导入模块import时,不须要使用大括号。
//math.js
var num = 0;
var add = function (a, b) {
return a + b;
};
export { num, add };
//导入
import { num, add } from './math';
function test(ele) {
ele.textContent = add(1 + num);
}
复制代码
3.4.二、ES6与CommonJS的区别
CommonJS
对于基本数据类型,属于复制。即会被模块缓存。同时,在另外一个模块能够对该模块输出的变量从新赋值。
对于复杂数据类型,属于浅拷贝。因为两个模块引用的对象指向同一个内存空间,所以对该模块的值作修改时会影响另外一个模块。
当使用require命令加载某个模块时,就会运行整个模块的代码。
当使用require命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,CommonJS模块不管加载多少次,都只会在第一次加载时运行一次,之后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
循环加载时,属于加载时执行。即脚本代码在require的时候,就会所有执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
ES6模块
ES6模块中的值属于【动态只读引用】。
对于只读来讲,即不容许修改引入变量的值,import的变量是只读的,不管是基本数据类型仍是复杂数据类型。当模块遇到import命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
对于动态来讲,原始值发生变化,import加载的值也会发生变化。不管是基本数据类型仍是复杂数据类型。
循环加载时,ES6模块是动态引用。只要两个模块之间存在某个引用,代码就可以执行。
3.4.三、优缺点
优势:
一、容易进行静态分析
二、面向将来的 EcmaScript 标准
缺点:
一、浏览器尚未彻底兼容,必须经过工具转换成标准的 ES5 后才能正常运行。
二、全新的命令字,新版的 Node.js才支持
CommonJS规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,由于同步意味着阻塞加载,浏览器资源是异步加载的,所以有了AMD CMD解决方案。
AMD规范在浏览器环境中异步加载模块,并且能够并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不畅。
CMD规范与AMD规范很类似,都用于浏览器编程,依赖就近,延迟执行,能够很容易在Node.js中运行。不过,依赖SPM 打包,模块的加载逻辑偏重
ES6 在语言标准的层面上,实现了模块功能,并且实现得至关简单,彻底能够取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。