模块化就是将一个复杂的系统分解成多个独立的模块的代码组织方式。
前端模块化发展之路: IIFE(自执行函数)>>AMD(RequireJS实现)>>CMD(SeaJS实现)>>CommonJS(NodeJs)>>ES6 Modules(模块化直接成为了Javascript语言规范中的一部分)。
一、前端模块化发展简介
1.CommonJS(require / module.exports / exports)
2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。这标志”Javascript模块化编程”正式诞生。nodeJs中的模块,一律为CommonJS 格式。
1.1 语法风格
1 | //Math.js |
1 | //main.js |
1.2 同步加载
1.3 动态加载
1 | //main.js |
1.4 浏览器不支持CommonJS规范。
浏览器不兼容CommonJS的根本原因,在于缺少四个Node.js环境的变量。
- module
- exports
- require
- global
可以使用工具进行转换,例如:Browserify
2.AMD(require/define)
CommonJS是主要为了JS在后端的表现制定的,它是不适合前端的。
AMD是”Asynchronous Module Definition”的缩写,意思就是”异步模块定义”。
RequireJS实现了AMD规范。下面以RequireJS为例,了解一下AMD规范。
requireJS原理
require.js 的核心原理是通过动态创建 script 脚本来异步引入模块,然后对每个脚本的 load 事件进行监听,如果每个脚本都加载完成了,再调用回调函数。
2.1 语法风格
1 | //Math.js |
1 | //main.js |
2.2 异步加载
2.3 动态加载
2.4 依赖前置,提前执行
3.CMD
CMD是SeaJS 在推广过程中对模块定义的规范化产出。
3.1 语法风格
1 | // CMD |
3.2 AMD和CMD的区别
1) 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible(尽可能的懒加载,也称为延迟加载,即在需要的时候才加载)。
2) CMD 推崇依赖就近,AMD 推崇依赖前置。虽然 AMD 也支持 CMD 的写法,同时还支持将 require 作为依赖项传递,但 RequireJS 的作者默认是最喜欢上面的写法,也是官方文档里默认的模块定义写法。
3.3 推荐链接
es6(import/export)
require与import的区别
- require支持 动态导入,import不支持,正在提案 (babel 下可支持)
- require是 同步 导入,import属于 异步 导入
- require是 值拷贝,导出值变化不会影响导入值;import指向内存地址,导入值会随导出值而变化
二、Module
ES6中Module的特点
- 浏览器,服务器通用
- 静态加载
1. 基本语法
1.1 export
一个模块就是一个独立的文件。export
关键字用来输出该变量。可以输出变量,函数或类。
1 | // test.js |
1 | // test.js |
可以使用as
为输出变量重命名。
1 | var firstName = 'cheng'; |
需要特别注意的是,export
命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
1 | // 报错 |
export
语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
1 | export var foo = 'bar'; |
export
命令可以出现在模块的任何位置,只要处于模块顶层就可以。
1.2 import
其他 JS 文件通过import
命令加载模块。大括号里面的变量名,必须与被导入模块(test.js
)对外接口的名称相同。
1 | // main.js |
import
命令输入的变量都是只读的,因为它的本质是输入接口。
1 | import {a} from './xxx.js' |
如果a
是一个对象,改写a
的属性是允许的。和const一样。
1 | import {a} from './xxx.js' |
可以用星号(*
)指定一个对象,进行整体加载。
1 | // main.js |
import
命令具有提升效果,会提升到整个模块的头部,首先执行。
1 | foo(); |
由于import
是静态执行,所以不能使用表达式和变量。
1 | // 报错 |
import
语句会执行所加载的模块,因此可以有下面的写法。仅仅执行lodash
模块,但是不输入任何值。
1 | import 'lodash'; |
即使加载多次,也只会执行一次。也就是说,import
语句是 Singleton 模式。
1.3 单例模式解读
1 | //counter.js |
1 | //main.js |
1 |
|
1 | //main2.js |
1.4 export default
使用export default
可以不用关注输出模块中的变量名。
1 | // export-default.js |
1 | // import-default.js |
import
命令可以为该匿名函数指定任意名字。import
命令后面,不使用大括号。
1 | // export-default.js |
export default
的本质,就是输出一个叫做default
的变量或方法。
imort something from ..
.的本质,就是import {default as something} from ...
1 | // 正确 |
1 | // 正确 |
所以export default
是比较常用的方法:
1 | // MyClass.js |
1.5 import&export混合使用
1 | export { foo, bar } from 'my_module'; |
上面代码中,export
和import
语句可以结合在一起,写成一行。但需要注意的是,写成一行以后,foo
和bar
实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo
和bar
。
1 | // 接口改名 |
1.6 模块的继承
1 | //calculator.js |
1 | //calculatorPlus.js |
1 | //main.js |
1.7 import()
import()
提案是为了解决import
动态加载,和不能写在代码块中的问题。
1 | import(a + '.js') |
1 | if (condition) { |
2. Module补充
2.1 浏览器加载
html中加载 ES6 模块,也使用<script>
标签,但是要加入type="module"
属性。
1 | <script type="module" src="./myModule.js"></script> |
defer
:要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行。保证执行顺序。async
:一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。不能保证执行顺序。
对于外部的模块脚本,要注意:
- 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
- 模块脚本自动采用严格模式,不管有没有声明
use strict
。 - 模块之中,可以使用
import
命令加载其他模块(.js
后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export
命令输出对外接口。 - 模块之中,顶层的
this
关键字返回undefined
,而不是指向window
。也就是说,在模块顶层使用this
关键字,是无意义的。 - 同一个模块如果加载多次,将只执行一次。
2.2 循环加载
1 | // a.js |
1 | // b.js |
1 |
|
1 | // a.js |
1 | // b.js |
1 | //b.mjs |
因为函数具有提升作用。
2.3 ES6模块和CommonJS模块的差异
import
和export
是关键字,require
不是。CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。(详情见上方【单例模式解读】)
CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
因为 CommonJS 加载的是一个对象(即`module.exports`属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
1
2
3
4
5
6
7
8
9
10//counter.js
let counter = 2;
function addCounter(){
counter++;
}
addCounter();
module.exports = {
counter,
addCounter: addCunter
}1
2
3
4
5
6//main.js
var counter = require('./addCounter.js');
console.log('main:' + counter.counter);//3
counter.addCounter();
console.log('main:' + counter.counter);//3this
指向不同。ES6 模块之中,顶层的this
指向undefined
;CommonJS 模块的顶层this
指向当前模块,这是两者的一个重大差异。