第 26 题: 介绍模块化发展历程
cleverboy32 opened this issue · 15 comments
很cool
模块化主要是用来抽离公共代码,隔离作用域,避免变量冲突等。
IIFE: 使用自执行函数来编写模块化,特点:在一个单独的函数作用域中执行代码,避免变量冲突。
(function(){
return {
data:[]
}
})()
AMD: 使用requireJS 来编写模块化,特点:依赖必须提前声明好。
define('./index.js',function(code){
// code 就是index.js 返回的内容
})
CMD: 使用seaJS 来编写模块化,特点:支持动态引入依赖文件。
define(function(require, exports, module) {
var indexCode = require('./index.js');
});
CommonJS: nodejs 中自带的模块化。
var fs = require('fs');
UMD:兼容AMD,CommonJS 模块化语法。
webpack(require.ensure):webpack 2.x 版本中的代码分割。
ES Modules: ES6 引入的模块化,支持import 来引入另一个 js 。
import a from 'a';
(function(){
return {
data:[]
}
})()
define('./index.js',function(code){
// code 就是index.js 返回的内容
})
define(function(require, exports, module) {
var indexCode = require('./index.js');
});
var fs = require('fs'):
import a from 'a';
模块化
模块化的作用
模块化是为了处理全局污染和依赖管理混乱的问题
模块化
因为一开始js本身没有提供模块化的机制,所以才会衍生出commonJS、AMD、CMD和UMD这么多模块化规范。js在ES6时原生提供了import和export模块化机制
commonJS
定义
文件即模块,每个文件通过module来表示,用require来引用其他依赖,用module.exports来导出自身
机制
通过require去引用文件时,会将文件执行一遍后,将其执行结果通过浅克隆的方式,写入全局内存。后续再require该路径,就直接从内存里取出,不需要重新执行对应的文件
特点
commonJS是服务器编程范式,因为服务器上所有文件都在硬盘里,通过同步加载的方式即可,所以该规范是同步加载规范。同时它是在运行时加载,也就是你可以在require里拼接变量,在加载时会自动识别出最终的实际路径
AMD
定义
define(module, [dep1, dep2], callback)
机制
通过require加载时,它会先加载对应的依赖,等依赖资源加载完之后,会执行回调函数,将依赖作为入参,执行对应的业务逻辑
特点
AMD机制是浏览器编程范式,它是在客户端使用的,由于资源都是在服务器上,所以它是异步加载。同时,它最大的特点是强调依赖前置。
CMD
定义
机制和AMD类似,最大的区别就是CMD强调延迟加载,对应的依赖等到回调函数里执行具体依赖语句,才会去加载,但是AMD在后续版本里也支持了延迟加载的写法
机制
同上
特点
同上
UMD
定义
CommonJS、AMD、CMD并行的状态下,就需要一种方案能够兼容他们,这样我们在开发时,
就不需要再去考虑依赖模块所遵循的规范了,而UMD的出现就是为了解决这个问题。
ES6
定义
通过import引入依赖,通过export导出依赖
机制
ES6的模块机制在依赖模块时并不会先去预加载整个脚本,而是生成一个只读引用,并且静态解析依赖,等到执行代码时,再去依赖里取出实际需要的模块
特点
编译时加载,不允许在里边引用变量,必须为真实的文件路径。可以通过调用import()语句,会生成一个promise去加载对应的文件,这样子就是运行时加载,可以在路径里边编写变量
AMD,CMD用的不多,主要讲一下CommonJS和ESModule
模块的特性
- 为创建一个内部作用域而调用了一个包装函数。
- 包装函数的返回值至少包含一个对内部函数的引用,这样才会创建涵盖整个包装函数内部作用域的闭包。
CommonJS
特点: require
、module.exports
、exports
CommonJS 一般用在服务端或者Node用来同步加载模块,它对于模块的依赖发生在代码运行阶段,不适合在浏览器端做异步加载。
exports
实际上是一个对module.exports
的引用:
exports.add = function add () {/* 方法 */}
// 等同于
module.exports.add = function add () {/* 方法 */}
但注意,不能给exports
赋值,否则会断开与module.exports
的连接。
ES6 Module
特点: import
、export
ES6模块化不是对象,import
会在JavaScript引擎静态分析,在编译时就引入模块代码,而并非在代码运行时加载,因此也不适合异步加载。
在HTML中如果要引入模块需要使用
<script type="module" src="./module.js"></script>
ESModule的优势:
- 死代码检测和排除。我们可以用静态分析工具检测出哪些模块没有被调用过。比如,在引入工具类库时,工程中往往只用到了其中一部分组件或接口,但有可能会将其代码完整地加载进来。未被调用到的模块代码永远不会被执行,也就成为了死代码。通过静态分析可以在打包时去掉这些未曾使用过的模块,以减小打包资源体积。
- 模块变量类型检查。JavaScript属于动态类型语言,不会在代码执行前检查类型错误(比如对一个字符串类型的值进行函数调用)。ES6 Module的静态模块结构有助于确保模块之间传递的值或接口类型是正确的。
- 编译器优化。在CommonJS等动态模块系统中,无论采用哪种方式,本质上导入的都是一个对象,而ES6 Module支持直接导入变量,减少了引用层级,程序效率更高。
二者的差异
CommonJS模块引用后是一个值的拷贝,而ESModule引用后是一个值的动态映射,并且这个映射是只读的。
- CommonJS 模块输出的是值的拷贝,一旦输出之后,无论模块内部怎么变化,都无法影响之前的引用。
- ESModule 是引擎会在遇到
import
后生成一个引用链接,在脚本真正执行时才会根据这个引用链接去模块里面取值,模块内部的原始值变了import
加载的模块也会变。
CommonJS运行时加载,ESModule编译阶段引用。
- CommonJS在引入时是加载整个模块,生成一个对象,然后再从这个生成的对象上读取方法和属性。
- ESModule 不是对象,而是通过
export
暴露出要输出的代码块,在import
时使用静态命令的方法引用指定的输出代码块,并在import
语句处执行这个要输出的代码,而不是直接加载整个模块。
可从IIFE、AMD、CMD、CommonJS、UMD、webpack(require.ensure)、ES Module、<script type="module">
这几个角度考虑。
很酷
第一阶段——无模块化
将所有JS文件都放在一块,代码执行顺序就按照文件的顺序执行。
缺点是污染全局作用域。每一个模块都是暴露在全局中的,容易产生命名冲突。
还有要手动处理各代码的依赖关系。
第二阶段——commonJS规范
是一个JavaScript模块化的规范,一个文件就是一个模块,内部定义的变量就属于这个模块里的,不会对外暴露,所以不会污染全局变量。
- 通过require引入模块
- 通过module.exports导出模块
//a.js
var num = 100;
var add = function(val){
return val + num
}
module.exports.num = num;
module.exports.add = add ;
//b.js
var moduleA = require('./a.js')
var fn = moduleA.add;
- 同步加载模块,等当前模块加载完成了才进行下一步,服务器端文件都是保存在硬盘上,所以同步加载没问题。但是浏览器上,需要把文件从服务器端请求过来,比较慢,所以同步加载不适合用在浏览器上
第三阶段——AMD规范
因为commonJS规范不适用于浏览器,因为要从服务器加载文件,不能用同步模式,所以有了AMD规范,该规范的实现,就是requireJs了。
define(function () {
var alertName = function (str) {
alert("I am " + str);
}
var alertAge = function (num) {
alert("I am " + num + " years old");
}
return {
alertName: alertName,
alertAge: alertAge
};
});
//引入模块:
require(['alert'], function (alert) {
alert.alertName('JohnZhu');
alert.alertAge(21);
});
依赖前置,require([dep1, dep2],callback),先加载依赖再执行回调函数
优点是可以在浏览器环境中异步加载模块,而且可以并行加载多个模块
第四阶段——CMD规范
和requirejs非常类似,即一个js文件就是一个模块,但是可以通过按需加载的方式,而不是必须在模块开始就加载所有的依赖。
define(function(require, exports, module) {
var $ = require('jquery');
var Spinning = require('./spinning');
exports.doSomething = ...
module.exports = ...
})
第五阶段——ES6的模块化
使用ES6的语法,export和import实现模块化,用的比较多就不介绍了。缺点是浏览器暂不支持,需要babel编译过
特殊规范——UMD
参考这篇文章吧前端科普系列-CommonJS:不是前端却革命了前端
1、CommonJS
CommonJS 规范概述了同步声明依赖的模块定义。这个规范主要用于在服务器端实现模块化代码组 织,但也可用于定义在浏览器中使用的模块依赖。CommonJS 模块语法不能在浏览器中直接运行。
CommonJS 模块定义需要使用 require()指定依赖,而使用 exports 对象定义自己的公共 API。如下例:
无论一个模块在 require()中被引用多少次,模块永远是单例。模块第一次加载后会被缓存,后续加载会取得缓存的模块。
如下例,moduleA只会被打印一次,这是因为无论请求多少次,ModuleA只会被加载一次。
module.exports 对象非常灵活,有多种使用方式。如下例:
只导出一个实体,也可以到处多个对象。
2、异步模块定义(AMD):由于CommonJS以服务器端为目标环境,能够一次性把所有模块都加载到内存,而异步模块定义(AMD)的模块定义系统则以浏览器为目标执行环境,这需要考虑网络延迟的问题。
AMD的一般策略:让模块声明自己的依赖,而运行在浏览器中的模块系统会按需获取依赖,并在依赖加载完成后立即执行依赖它们的模块。
AMD模块实现的核心是用函数包装模块定义:这样可以防止声明全局变量,并允许加载器库控制何时加载模块。
与CommonJS不同,AMD支持可选的为模块指定字符串标识符。
例子:
AMD 也支持 require 和 exports 对象,通过它们可以在 AMD 模块工厂函数内部定义 CommonJS
风格的模块。这样可以像请求模块一样请求它们,但 AMD 加载器会将它们识别为原生 AMD 结构
3、通用模块定义UMD:为了统一CommonJS和AMD生态系统,通用模块定义(UMD,Universal Module Definition)规范应运而生。UMD可用于创建这两个系统都可以使用的模块代码。
本质上:UMD定义的模块会在启动时检测要使用哪个模块系统,然后进行适当配置,并把所有逻辑包装在一个立即调用的函数表达式(IIFE)中。
此模式有支持严格 CommonJS 和浏览器全局上下文的变体。不应该期望手写这个包装函数,它应该
由构建工具自动生成。开发者只需专注于模块的内由容,而不必关心这些样板代码。
4、使用ES6模块:ES6 最大的一个改进就是引入了模块规范。这个规范全方位简化了之前出现的模块加载器,原生浏
览器支持意味着加载器及其他预处理都不再必要。从很多方面看,ES6 模块系统是集 AMD 和 CommonJS
之大成者。
ECMAScript 6 模块是作为一整块 JavaScript 代码而存在的。带有 type="module"属性的<script> 标签会告诉浏览器相关代码应该作为模块执行,而不是作为传统的脚本执行。模块可以嵌入在网页中, 也可以作为外部文件引入:
嵌入的模块定义代码不能使用 import 加载到其他模块。只有通过外部文件加载的模块才可以使用import 加载。因此,嵌入模块只适合作为入口模块。
模块化
脚本顺序依赖
全局变量冲突 数据污染
cmd
amd
Commonjs
es6
Commmonjs 引入是值的拷贝,多次引入会缓存
es6 引入是值的引用,静态分析