Javascript 模块化发展的历史精读 js 模块化发展
直接定义依赖 (1999): 由于当时 js 文件非常简单,模块化方式非常简单粗暴 —— 通过全局方法定义、引用模块。这种定义方式与现在的 commonjs 非常神似,区别是 commonjs 以文件作为模块,而这种方法可以在任何文件中定义模块,模块不与文件关联。
闭包模块化模式 (2003): 用闭包方式解决了变量污染问题,闭包内返回模块对象,只需对外暴露一个全局变量。
模版依赖定义 (2006): 这时候开始流行后端模版语法,通过后端语法聚合 js 文件,从而实现依赖加载,说实话,现在 go 语言等模版语法也很流行这种方式,写后端代码的时候不觉得,回头看看,还是挂在可维护性上。
注释依赖定义 (2006): 几乎和模版依赖定义同时出现,与 1999 年方案不同的,不仅仅是模块定义方式,而是终于以文件为单位定义模块了,通过 lazyjs 加载文件,同时读取文件注释,继续递归加载剩下的文件。
外部依赖定义 (2007): 这种定义方式在 cocos2d-js 开发中普遍使用,其核心思想是将依赖抽出单独文件定义,这种方式不利于项目管理,毕竟依赖抽到代码之外,我是不是得两头找呢?所以才有通过 webwpack 打包为一个文件的方式暴力替换为 commonjs 的方式出现。
Sandbox模式 (2009): 这种模块化方式很简单,暴力,将所有模块塞到一个 sanbox 变量中,硬伤是无法解决明明冲突问题,毕竟都塞到一个 sandbox 对象里,而 Sandbox 对象也需要定义在全局,存在被覆盖的风险。模块化需要保证全局变量尽量干净,目前为止的模块化方案都没有很好的做到这一点。
依赖注入 (2009): 就是大家熟知的 angular1.0,依赖注入的思想现在已广泛运用在 react、vue 等流行框架中。但依赖注入和解决模块化问题还差得远。
CommonJS (2009): 真正解决模块化问题,从 node 端逐渐发力到前端,前端需要使用构建工具模拟。
Amd (2009): 都是同一时期的产物,这个方案主要解决前端动态加载依赖,相比 commonJs,体积更小,按需加载。
Umd (2011): 兼容了 CommonJS 与 Amd,其核心思想是,如果在 commonjs 环境(存在 module.exports,不存在 define),将函数执行结果交给 module.exports 实现 Commonjs,否则用 Amd 环境的 define,实现 Amd。
Labeled Modules (2012): 和 Commonjs 很像了,没什么硬伤,但生不逢时,碰上 Commonjs 与 Amd,那只有被人遗忘的份了。
YModules (2013): 既然都出了 Commonjs Amd,文章还列出了此方案,一定有其独到之处。其核心思想在于使用 provide取代 return,可以控制模块结束时机,处理异步结果;拿到第二个参数 module,修改其他模块的定义(虽然很有拓展性,但用在项目里是个搅屎棍)。
ES2015 Modules (2015): 就是我们现在的模块化方案,还没有被浏览器实现,大部分项目已通过 babel 或 typescript 提前体验。
在1999年的时候,绝大部分工程师做JS开发的时候就直接将变量定义在全局,做的好一些的或许会做一些文件目录规划,将资源归类整理,这种方式被称为直接定义依赖但是,即使有规范的目录结构,也不能避免由此而产生的大量全局变量,这就导致了一不小心就会有变量冲突的问题。于是在2002年左右,有人提出了命名空间模式的思路,用于解决遍地的全局变量,将需要定义的部分归属到一个对象的属性上,不过这种方式,毫无隐私可言,本质上就是全局对象,谁都可以来访问并且操作,一点都不安全。
所以在2003年左右就有人提出利用IIFE结合Closures特性,以此解决私有变量的问题,这种模式被称为闭包模块化模式。IIFE可以形成一个独立的作用域,其中声明的变量,仅在该作用域下,从而达到实现私有变量的目的,在该IIFE外是不能直接访问和操作的,可以通过暴露一些方法来访问和操作,这就是所谓的Closures。同时,不同模块之间的引用也可以通过参数的形式来传递。虽然解决了依赖关系的问题,但是没有解决如何管理这些模块,或者说在使用时清晰描述出依赖关系,这点还是没有被解决。在实际项目中,得手动管理第三方的库和项目封装的模块,还是需要把所有需要的JS文件一个个按照依赖的顺序加载进来。
服务器端的模块化规范CommonJS
一套同步的方案,服务端Node.js的。CommonJS的核心思想就是通过 require 方法来同步加载所要依赖的其他模块,然后通过 exports 或者 module.exports 来导出需要暴露的接口。
CommonJS定义的模块分为:{模块引用(require)} {模块定义(exports)} {模块标识(module)}require()用来引入外部模块;exports对象用于导出当前模块的方法或变量,唯一的导出口;module对象就代表模块本身。
阮一峰《CommonJS规范》
CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用异步机制。
前端标准的模块化规范AMD/CMD
基于 AMD 的RequireJS
基于 CMD 的SeaJS
它们分别在浏览器实现了define
、require
及module
的核心功能,虽然两者的目标是一致的,但是实现的方式或者说是思路,AMD与CMD的本质区别就是:
AMD是加载完全部你所需要的文件
CMD是当你需要那个文件的时候他才加载
AMD用户体验好,因为没有延迟,依赖模块提前执行了
CMD性能好,因为只有用户需要的时候才执行
// CMD
define(function (require) {
var a = require('./a'); // <- 运行到此处才开始加载并运行模块a
var b = require('./b'); // <- 运行到此处才开始加载并运行模块b
// more code ..
})
// AMD
define(['./a', './b'], // 前置声明,也就是在主体运行前就已经加载并运行了模块a和模块b
function (a, b) {
// more code ..
}
)
AMD”Asynchronous Module Definition”异步模块定义
AMD标准中,定义了下面两个API:
1.require([module], callback)
2. define(id, [depends], callback)
即通过define
来定义一个模块,然后使用require
来加载一个模块。 并且,require还支持CommonJS的模块导出方式,但是不同于CommonJS,它要求两个参数:第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。
CMD规范,通用模块定义。实现js库为sea.js
。 它和requirejs非常类似,即一个js文件就是一个模块,但是CMD的加载方式更加优秀,是通过按需加载
的方式,而不是必须在模块开始就加载所有的依赖。缺点:依赖SPM打包,模块的加载逻辑偏重
ES6模块化
与CommonJS用require()
方法加载模块不同,在ES6中,import
命令可以具体指定加载模块中用export
命令暴露的接口(不指定具体的接口,默认加载export default
),没有指定的是不会加载的,因此会在编译时就完成模块的加载,这种加载方式称为编译时加载或者静态加载。而CommonJS的require()
方法是在运行时才加载的,将整个模块作为一个对象引入,然后再获取这个对象上的某个属性。
但是由于ES6目前无法在浏览器中执行,所以,我们只能通过babel
将不被支持的import编译为当前受到广泛支持的 require。
网友评论