什么是模块化
- 模块化其实是指解决一个复杂问题时
自顶向下逐层把系统划分成若干模块的过程,每个模块完成一个特定的子功能(单一职责),所有的模块按某种方法组装起来,成为一个整体。 - 模块的内部数据与实现是
私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信。
模块化解决了什么问题
随着Web技术的发展,各种交互以及新技术等使网页变得越来越丰富,复杂度在逐步增高,代码一多,各种命名冲突、代码冗余、文件间依赖变大等一系列的问题就出来了,甚至导致后期难以维护。模块化的出现解决了以下问题:
- 避免命名冲突(减少命名空间污染)
- 更好的分离, 按需加载
- 更高复用性, 可维护性
- 更方便管理模块间依赖关系
模块化的进化过程
一、文件划分方式,每个文件就是一个模块
- 模块直接在全局工作, 大量模块成员污染全局作用域。
- 没有私有空间,模块内的成员都可被外部访问或者修改。
- 一旦模块增多,容易产生命名冲突。
- 无法管理模块间的依赖关系。
- 在维护过程中很难分辨每个成员的所属模块。
二、namespace命名空间
- 约定每个模块只允许暴露一个全局对象,所以模块成员要被挂到这个全局模块当中。
- 减少了全局变量,解决了命名冲突,但是外部可以直接修改模块内部数据,不安全。
三、IIFE立即执行函数(闭包)
- 为模块提供一个私有空间,暴露到外部的成员可以挂载到全局对象的方式实现,数据是私有的,外部只能通过暴露的方法操作。
- 解决了全局作用域污染和命名冲突问题。
- 模块间的依赖问题没有解决。
四、IIFE模式增强:引入依赖
- 通过参数声明模块所依赖的模块,让模块间的依赖关系更加明显。
以上是早期没有工具和规范下对模块化实现的落地方式(模块的加载还没解决,都是通过script的形式将模块引入到页面中。
- 引入多个script后会出现请求过多的问题。
- 不了解模块间的依赖关系导致加载先后顺序出错。
- 模块的加载不受代码控制的,时间久了维护起来很麻烦。
更为理想的方式是在页面中引入一个js入口文件,其余用到的模块都可以通过代码控制,按需加载 为了统一不同开发者,不同项目之间的差异,需要制定一个行业标准去规范模块化的实现方式。 针对模块加载问题要实现2个需求:
- 一个统一的模块化标准规范
- 一个可以自动加载模块的基础库
模块化规范
commonJS规范
nodejs中遵循的模块化规范:一个文件就是一个模块,每个模块都有单独作用域(变量、函数、类都是私有的,外部不可见)。在服务器端,模块的加载是运行时同步加载的。对于服务器端,所有模块都存在本地硬盘,等待模块加载的时间就是读取硬盘文件的时间,是很速的;但是对于浏览器而言,它需要从服务端加载模块就会涉及到网速,代理等问题,一旦时间过长,浏览器会处于等待状态。所以在浏览器端直接使用这个规范会出现一些问题。
(1)特点
- 所有代码都运行在模块作用域,不会污染全局作用域。
- 模块能多次加载,第一次加载运行后,结果就缓存了,以后再加载,就直接读取缓存。要想让模块再次运行,必须清除缓存。
- 模块加载的顺序,按照其在代码中出现的顺序。
- CommonJS是动态的(动态是指对于模块的依赖关系建立在代码执行阶段,静态是指对于模块的依赖关系建立在代码编译阶段)
(2)语法
- 暴露模块:
module.exports = value或exports.xxx = value - 引入模块:
require(xx),如果是第三方模块,xx为模块名;如果是自定义模块,xx为模块文件路径
CommonJS暴露的模块到底是什么?
require第一次加载脚本时会执行整个脚本,然后在内存中生成一个对象。
{
id: '模块名称', // 模块的名称
exports: {}, // 该模块导出的接口
loaded:true // 表示模块是否加载完毕
}
CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。
(3)模块的加载机制
CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
counter输出以后,a.js模块内部的变化就影响不到counter了。这是因为counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。
AMD规范
由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。
require.js是AMD模块化规范的实现,也是很强大的模块加载器。
(1)AMD语法
- define函数接收2个参数,第一个参数是模块所依赖的模块路径,第二个参数是函数,为模块提供私有空间。
- 如果想向外暴露某些成员可以通过return的方式实现。
- requirejs提供一个require函数自动加载模块,用法和define函数类似,区别在于require只能加载模块,define还可以定义模块。
- require加载模块时候会自动创建script标签,请求并且执行对应的模块代码。
目前绝大多数第三方的库都支持AMD模式的加载,但是这种方式使用起来比较复杂,当项目中,模块划分比较细的时候,会出现同一个页面对js请求次数过多的情况,导致应用运行效率低,但在当时的情况下,AMD确实做为一个规范,为前端模块化提供了一个标准,但只是一个妥协方案。 现如今模块化发展的比较成熟,在nodejs中遵循commonjs规范来组织模块,浏览器环境中遵循ESModules规范。最新的nodejs中表示node环境也会逐渐趋向ESModules规范。
CMD(Common Module Definition通用模块定义)
专门用于浏览器端,模块的加载是异步的,模块使用时才会被加载执行,CMD规范整合了CommonJS和AMD规范的特点。在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范。
(1)CMD语法
UMD
umd是CommonJS和AMD的产物,AMD适用于浏览器的异步加载模块,CommonJS适用于服务器的同步加载模块。UMD是解决跨平台的方案。UMD会先判断是否支持Node.js的模块exports是否存在,存在就使用Node.js模块模式,否则判断是否支持AMD,支持就使用AMD方式加载模块
ESModules
是ECMAScript2015(ES6)中才定义的模块系统,存在环境兼容问题,随着webpack等一系列打包工具的流行这一规范才开始逐渐被普及。经过5年的迭代,ESModule已发展为现今最主流的前端模块化标准。
(1)ES6模块化语法
- 每个模块只加载一次,后面再加载相同目录下文件,直接从缓存中读取,一个模块就是一个单例或者一个对象
- 每个模块内声明的变量都是局部变量,不会污染全局作用域
- 模块内部的变量或者函数可以通过export带出
- 一个模块可以导入别的模块
CMD和AMD的区别
- 对于依赖的模块,AMD是提前执行,CMD是延迟执行,不过requirejs从2.0开始,也改成延迟执行。
- AMD推崇依赖前置,CMD推崇依赖就近。
ES6模块和CommonJS模块的差异
它们有两个重大差异:
① CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
对基本类型,一旦输出,模块内部的变化影响不到这个值。对引用类型,效果同引用类型的赋值操作。
ES6 模块是动态关联模块中的值,输出的是值得引用。原始值变了,import 加载的值也会跟着变。
② CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
-
运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
-
编译时加载: ES6 模块不是对象,而是通过
export命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。
另外,ES6 模块的运行机制与 CommonJS 不一样。ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。