一、概述
前端在发展初期就是为了实现简单的页面交互逻辑,通过script
标签来导入一个个的js文件,是没有模块化的概念的,随着网站功能逐渐丰富,网页中的js也变得越来越复杂和臃肿,原有通过script标签来导入一个个的js文件这种方式已经不能满足现在互联网开发模式,我们需要团队协作、模块复用、单元测试等一系列复杂的需求。这对于多人开发大型复杂的项目带来了巨大的困扰,协同开发和复用都很困难,当然效率也很低。java,Python有import,甚至连css都有@import,但是令人费解的是js居然没有这方面的支持。随着WEB2.0时代的到来,Ajax技术得到广泛应用,jQuery等前端库层出不穷,前端代码日益膨胀,此时在JS方面就会考虑使用模块化规范去管理。ES6出现之后才解决了这个问题,但在这之前,各大社区也都出现了很多解决方法,比较出色的被大家广为流传的就有AMD,CMD,commonjs,UMD,今天小编就带着大家揭晓这几个模块化的解决方案之谜。
二、什么是模块
当前端应用越来越复杂时,我们想要将代码分割成不同的模块,便于复用、按需加载等。简单的来讲,一个项目,有了模块化,我们就可以更方便地使用别人的代码,提高代码的耦合度和复用率,想要什么功能,就加载什么模块。要想使用别人的模块,我们就需要遵循一套规范。在ES6模块化出现之前,各大社区也都出现了很多解决方法,前端模块化规范是通过AMD,CMD,commonjs,UMD规范实现的,require
和 import
分别是不同模块化规范下引入模块的语句,下文将介绍这两种方式的不同之处。
出现时间
模块化 | 年份 | 出处 |
require/exports | 2009 | CommonJS |
import/export | 2015 | ECMAScript2015(ES6) |
不同端(客户端/服务器)的使用限制
platform | require/exports | import/export |
Node.js | 所有版本 | Node 9.0+(启动需加上 flag --experimental-modules) Node 13.2+(直接启动) |
Chrome | 不支持 | 61+ |
Firefox | 不支持 | 60+ |
Safari | 不支持 | 10.1+ |
Edge | 不支持 | 16+ |
### 模块化特点 |
- 原生浏览器不支持
require/exports
,可使用支持CommonJS
模块规范的Browsersify
、webpack
等打包工具,它们会将require/exports
转换成能在浏览器使用的代码。 import/export
在浏览器中无法直接使用**,我们需要在引入模块的 元素上添加type="module"
属性。- 即使
Node.js 13.2+
可以通过修改文件后缀为 .mjs 来支持 ES6 模块 import/export,但是Node.js
官方不建议在正式环境使用。目前可以使用 babel 将 ES6 的模块系统编译成 CommonJS 规范(注意:语法一样,但具体实现还是 require/exports)。 - 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
- 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信
- 安全的包装一个模块的代码,避免全局污
- 唯一标识一个模块
- 优雅的将模块api暴露出去
- 方便的使用模块
三、AMD、CMD、CommonJS、ES6的对比
CommonJS 是一种模块规范,最初被应用于 Nodejs,成为 Nodejs 的模块规范。运行在浏览器端的 JavaScript 由于也缺少类似的规范,在 ES6 出来之前,前端也实现了一套相同的模块规范 (例如: AMD、CMD),用来对前端模块进行管理。简单来说,它们都是用于在模块化定义中使用的,AMD、CMD、CommonJS是ES5中提供的模块化编程的方案,自 ES6 起,引入了一套新的 ES6 Module 规范,在语言标准的层面上实现了模块功能,而且实现得相当简单,有望成为浏览器和服务器通用的模块解决方案。但目前浏览器对 ES6 Module 兼容还不太好,我们平时在 Webpack 中使用的 export 和 import,会经过 Babel 转换为 CommonJS 规范。import/export是ES6中定义新增的。这些规范的目的都是为了 JavaScript 的模块化开发,特别是在浏览器端的。目前这些规范的实现都能达成浏览器端模块化开发的目的。
在浏览器端,因为其异步加载脚本文件的特性,CommonJS 规范无法正常加载。所以出现了 RequireJS、SeaJS 等(兼容 CommonJS )为浏览器设计的模块化方案。直到 ES6 规范出现,浏览器才拥有了自己的模块化方案 import/export。
- AMD 是 RequireJS 在推广过程中对模块定义的规范化产出
- CMD 是 SeaJS 在推广过程中对模块定义的规范化产出
- CommonJS Modules/2.0 规范,是 BravoJS 在推广过程中对模块定义的规范化产出
- 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行,不过从RequireJS 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)
- CommonJS 模块化方案
require/exports
是为服务器端开发设计的。服务器模块系统同步读取模块文件内容,编译执行后得到模块接口。(Node.js 是 CommonJS 规范的实现)。 - CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
- CommonJs 是单个值导出,ES6 Module可以导出多个
- CommonJs 是动态语法可以写在判断里,ES6 Module 静态语法只能写在顶层
- CommonJs 的 this 是当前模块,ES6 Module的 this 是 undefined
- CommonJs 的
require/exports
是运行时动态加载,import/export
是静态编译 - CommonJs 的
require/exports
输出的是一个值的拷贝,import/export
模块输出的是值的引用
四、 CommonJS模块规范
CommonJS
是以在浏览器环境之外构建 javaScript
生态系统为目标而产生服务端模块化规范,Nodejs
就采用了这个规范,主要是为了解决 javaScript
的作用域问题而定义的模块形式,可以使每个模块它自身的命名空间中执行,一个单独的文件就是一个模块,有自己的作用域,在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。该规范的主要内容是,模块必须通过 module.exports 导出对外的变量或者接口,通过 require
方法来导入其他模块的输出到当前模块的作用域中,该方法读取文件并执行,返回export对象;目前在服务器和桌面环境中,Nodejs应用由模块组成,遵循的是 CommonJS 的规范;
- NodeJS应用由模块组成,采用CommonJS模块规范
- CommonJS 对模块的加载时是同步的
- 每个文件就是一个模块,有自己的作用域
- 在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见
- CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的
exports
属性(即module.exports
)是对外的接口。加载某个模块,其实是加载该模块的module.exports
属性。 - CommonJS 加载的是一个对象(即 module.exports 属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
4.1 模块引用
CommonJS对模块的定义非常简单,主要分为模块引用,模块定义和模块标识3部分
example.js // 模块应用 const http = require('http'); var a = require('./a.js'); var b = require('config.js'); // 模块定义 var x = 5; var addX = function (value) { return value + x; }; // 模块标识 module.exports.a = a; module.exports.b = b; module.exports.x = x; module.exports.addX = addX;
上面代码通过module.exports输出模块a、b变量x和函数addX。也通过赋值方式可以看出exports
属性是一个对象;
4.2 模块加载
CommonJS 对模块的加载时是同步的,require方法用于加载模块
var example = require('./example.js'); console.log(example.x); // 5 console.log(example.addX(2)); // 7
4.3 exports 与 module.exports
为了方便,NodeJS为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令var exports = module.exports;
于是我们可以直接在 exports 对象上添加方法,表示对外输出的接口,如同在module.exports上添加一样。注意,因为NodeJS 模块是通过 module.exports 导出的,如果直接将exports变量指向一个值,就切断了exports与module.exports的联系,导致意外发生:
// a.js exports = function a() {}; // b.js const a = require('./a.js') // a 是一个空对象
我们可以修改为
// a.js exports.x = function() {}; // b.js const a = require('./a.js') // 调用函数 a.x()
注意: 如果还没有理解透彻,建议直接使用module.exports
更简单
五、AMD模块规范
AMD是RequireJS在推广过程中对模块定义的规范化产出,它是一个概念,RequireJS是对这个概念的实现,就好比JavaScript语言是对ECMAScript规范的实现。AMD是一个组织,RequireJS是在这个组织下自定义的一套脚本语言 AMD 主要是为前端 js 的表现指定的一套规范;而 CommonJS 是主要为了 js 在后端的表现制定的,它不适合前端;
AMD 是 Asynchronous Module Definition 的缩写,意思是 异步模块定义;采用的是异步的方式进行模块的加载,在加载模块的时候不影响后边语句的运行;
AMD 也是采用 require() 语句加载模块的,但是不同于 CommonJS ,它有两个参数;require(['模块的名字'],callBack);requireJs 遵循的就是 AMD 规范;
六、 CMD模块规范
CMD 是 Common Module Definition 的缩写,是 seajs 推荐的一套规范,CMD 也是通过异步的方式进行模块的加载的,不同于 AMD 的是,CMD 的加载是按照就近规则进行的,AMD 依赖的是前置;CMD 在加载的使用的时候会把模块变为字符串解析一遍才知道依赖了哪个模块;
CommonJS 其实也有浏览器端的实现,原理是先将所有模块都定义好并通过 id 索引方便的在浏览器环境中进行解析;
AMD 的实现其实是通过 define 函数定义在闭包中,例如:define(id?: String,dependencies?: String[],factory: Function | Object);
其中,id 是模块的名字,是一个可选的参数;dependencied 指定了所要依赖的模块列表,是一个数组,每个依赖的模块的输出将作为参数一次传入 factory 中;如果没有指定 dependencies 的话,那么默认的就是 ["require","exports","module"];factory 包括了模块的具体实现,它是一个函数或者对象;如果是函数,那么它的返回值就是模块的输出接口或者值;
七、 UMD模块规范
UMD是AMD和CommonJS的结合,AMD适用浏览器,CommonJS适用服务端,如果结合了两者就达到了跨平台的解决方案。UMD先判断是否支持AMD(define是否存在),存在用AMD模块的方式加载模块,再判断是否支持NodeJS的模块(exports是否存在),存在用NodeJS模块的方式,否则挂在window上,当全局变量使用。 这也是目前很多插件头部的写法,就是用来兼容各种不同模块化的写法。
(function(window, factory) { //amd if (typeof define === 'function' && define.amd) { define(factory); } //umd else if (typeof exports === 'object') { //umd module.exports = factory(); } else { window.jeDate = factory(); } })(this, function() { ...module..code... })