1.引入
1.1 模块的概念
将一个复杂的程序依据一定的规范封装成几个文件,并将它们组合在一起。
1.2 模块化的发展史
(1)全局function模式
将不同的功能封装成不同的全局函数。
缺点在于污染全局命名空间,容易引起冲突,而且模块成员间关系不紧密。
function m1() {
//...
}
function m2() {
//...
}
(2)对象封装模式
减少了全局变量,解决了命名冲突。
缺点在于数据不安全,外部可以直接修改模块内部的数据。
let myModule = {
data : 123,
getData : function(){
console.log(this.data);
}
}
myModule.data = 456;
myModule.getData() //456
(3)IIFE模式(立即执行函数闭包)
实现了数据私有,通过给window添加属性来向外暴露接口。
缺点是当一个模块依赖另外一个模块时不好操作。
// module.js文件
(function (window) {
let data = 'www.baidu.com'
function foo() {
console.log(`foo() ${data}`)
}
function bar() {
console.log(`bar() ${data}`)
otherFun()
}
function otherFun() {
//内部私有的函数
console.log('otherFun()')
}
//暴露行为
window.myModule = { foo, bar } //ES6写法
})(window)
2.CommonJS
2.1 概念和特点
采用CommonJS规范,每个文件就是一个模块,有自己的作用域。 在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。
有以下特点:
- 所有代码运行在模块作用域,不会污染全局作用域
- 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,之后再加载,就直接读取缓存结果。要想让模块再次运行需要清除缓存。
- 模块加载的顺序按照其在代码中出现的顺序
2.2 基本语法
导出模块:
- module.exports = value
- exports.xxx = value
导入模块:
- require(xxx)
如果是第三方模块,xxx为模块名;如果是自定义模块,xxx为模块文件路径
CommonJS规定,每个模块内部,module代表当前模块,这个变量是一个对象;module.exports属性是对外的接口,也就是说加载某个模块,其实就是加载该模块的module.exports属性。
下面两段代码分别写在两个js文件里
module.exports输出变量和函数
let x = 5;
let addX = val => val + x;
module.exports.x = x;
module.exports.addX = addX;
利用vscode的codeRunner插件就能运行js文件里的代码
require命令读入并执行一个js文件,然后返回该模块的exports对象。
var example = require('./example.js');
console.log(example.x); // 5
console.log(example.addX(1)); // 6
2.3 模块的加载机制
CommonJS模块的加载机制是,输入的是被输出的值的拷贝。 也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。这和ES6的模块化有着重大的差异。
比如这个例子,counter是个原始类型的值,输出后会被缓存。除非写成一个函数,才能得到内部变动后的值。
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
var counter = require('./example').counter;
var incCounter = require('./example').incCounter;
console.log(counter); // 3
incCounter();
console.log(counter); // 3
2.4 实例
module1
module.exports = {
msg : 'module1',
foo(){
console.log(this.msg);
}
}
module2
module.exports = function(){
console.log('module2')
}
module3
exports.foo = function(){
console.log('module3')
}
exports.arr = [1,2,2,3,4,5];
app.js
//用于数组去重的第三方模块 需要npm install uniq --save
let uniq = require('uniq');
let m1 = require('./module1');
let m2 = require('./module2');
let m3 = require('./module3');
m1.foo();//module1
m2();//module2
m3.foo();//module3
console.log(uniq(m3.arr));//[1,2,3,4,5]
3.AMD
CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是非同步加载模块,允许指定回调函数。
由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。
但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。此外AMD规范比CommonJS规范在浏览器端实现要来得早。
3.1 基本语法
导出模块
//定义没有依赖的模块
define(function(){
return 模块
})
//定义有依赖的模块
define(['module1', 'module2'], function(m1, m2){
return 模块
})
导入模块
require(['module1', 'module2'], function(m1, m2){
使用m1/m2
})
3.2 实例
首先创建这么一个目录结构
|-js
|-libs
|-require.js
|-modules
|-alerter.js
|-dataService.js
|-main.js
|-index.html
dataService.js
该模块作用就是返回msg的大写形式
//定义没有依赖的模块
define(function(){
let msg = 'yoimiya'
function getMsg(){
return msg.toUpperCase();
}
return {getMsg};
})
alerter.js
该模块依赖于dataService模块,返回一个拼接出来的字符串。
//定义有依赖的模块
define(['dataService'] , function(dataService){
let name = 'gorgeous'
function showMsg(){
console.log(dataService.getMsg() + ' is ' + name);
}
return {showMsg}
})
main.js
主配置文件,通过html中引入script时data-main的设置使得它对所有页面起到配置作用。
// main.js文件
(function() {
require.config({
baseUrl: 'js/', //基本路径 出发点在根目录下
paths: {
//模块标识名: 路径
alerter: './modules/alerter', //此处不能写成alerter.js,会报错
dataService: './modules/dataService'
}
})
//依赖并调用alerter中的showMsg()
require(['alerter'], function(alerter) {
alerter.showMsg()
})
})()
index.html
<!DOCTYPE html>
<html>
<head>
<title>Modular Demo</title>
</head>
<body>
<!-- 引入require.js并指定js主文件的入口 -->
<script data-main="js/main" src="js/libs/require.js"></script>
</body>
</html>
最终效果:
4.ES6 Module
4.1 基本概念
es6之前,基本是CommonJS用于服务器,AMD用于浏览器。es6新推出的module实现了二者的统一,较好地实现了模块的功能。(在node中使用Module需要通过babel转换)
Module的思想是尽量地静态化,使得编译时就能确定模块的依赖关系(CommonJS和AMD都只能在运行时确定依赖关系)。
比如CommonJS模块就是对象,输入时必须查找对象属性:
下面代码的实质是即加载fs的所有方法,生成一个对象_fs,然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
// CommonJS模块
let { stat, exists, readfile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
ES6模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
下面代码的实质是直接从fs模块加载3个方法,其他方法不加载。 这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。
import { stat, exists, readFile } from 'fs';
4.2 export
export命令用于规定模块的对外接口。
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。
var basicNum = 0;
var add = function (a, b) {
return a + b;
};
export { basicNum, add };
另外,export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。(这一点和CommonJS是截然相反的,common输出的是值的缓存,不会改变)
export var foo = 'bar';
//500ms后foo的值变为baz
setTimeout(() => foo = 'baz', 500);
4.3 import
使用export命令定义了模块的对外接口以后,其他js文件就可以通过import命令加载这个模块。
import { basicNum, add } from './math';
function test(ele) {
ele.textContent = add(99 + basicNum);
}
可以看到,使用import命令时,用户需要知道所要加载的变量名或函数名。否则无法加载。
为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。
导出
var basicNum = 0;
var add = function (a, b) {
return a + b;
};
export default{ basicNum, add };
导入时不再需要大括号里面写上一堆变量名
import math from './math';
function test(ele) {
ele.textContent = math.add(99 + math.basicNum);
}
4.4 特征
- Module自动采用严格模式
- import的属性是只读的,不能赋值
- import/export不能位于块级作用域内,模块内的import/export会变量提升到模块顶部
4.5 CommonJS和Module的差异
(1)CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
CommonJS模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
Module是动态绑定,原始值变了,import加载的值也会跟着变,并不会缓存值,模块里的变量绑定其所在模块。
比如这个上面出现在CommonJS里的例子,可以发现这一次incCounter()函数影响到了counter的值。
// lib.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
(2)CommonJS模块是运行时加载,Module是解析时加载。
CommonJS加载的是一个对象(即module.exports属性),即在输入时先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”,对象只有在脚本运行完才会生成。
ES6模块不是对象,export提供的对外接口只是一种静态定义,在代码静态解析阶段就会生成。import采用静态命令的形式。可以直接加载需要的方法,而不是加载整个模块,这种加载称为“解析时加载”。
(3)CommonJS主要针对服务端,Module主要针对浏览器端。
虽然Module其实两端都挺好,只是相对而言。
主要说一下针对两端的区别在哪:
服务器端一般采用同步加载文件,也就是说需要某个模块,服务器端便停下来,等待它加载再执行。
浏览器端要保证效率,需要采用异步加载,这就需要一个预处理,提前将所需要的模块文件并行加载好。