概念
模块化其实是指解决一个复杂问题时 自顶向下逐层把系统划分成若干模块 的过程,每个模块完成一个特定的子功能(单一职责),所有的模块按某种方法组装起来,成为一个整体,从而完成整个系统所要求的功能
早期js模块化方案
普通函数
因为 JS 中函数是有独立作用域的,所以也可以让变量私有化,而在多个文件中使用时,无法保证它们不与其它模块发生命名冲突,
而且模块成员之间看不出直接关系.此时便有了命名空间这个概念
命名空间
命名空间模式是房子变量函数在定义时发生命名从图的一种模式
把模块写成一个对象,所有的模块成员都放到这个对象里面:
textvar myModule = { name: "a1ex", getName: function (){ console.log(this.name) } } myModule.getName() // a1ex
但是这样写你会发现,我们可以通过i对象内属性重新赋值而改变内部状态
立即执行函数
它是利用函数闭包的特性来实现私有数据和共享方法
textvar myModule = (function() { var name = 'a1ex' function getName() { console.log(name) } return { getName } })()
此时只能通过getName区获取内部name,实现了name私有化
JS模块化规范演进
CommonJS规范
CommonJS 规范中规定每个文件就是一个独立的模块,有自己的作用域,模块的变量、函数、类都是私有的,外部想要调用,必须使用 module.exports 主动暴露,而在另一个文件中引用则直接使用 require(path)
require 命令则负责读取并执行一个 JS 文件,并返回该模块的 exports 对象
CommonJS 规范适用于服务端,也就是只适用于 NodeJS ,其实简单来说就是 Node 内部提供一个构造函数 Module,所有模块都是构造函数 Module 的实例
textfunction Module(id, parent) { this.id = id this.exports = {} this.parent = parent // ... }
每个模块内部,都有一个 module 实例,该对象就会有下面几个属性:
- module.id 模块的识别符,通常是带有绝对路径的模块文件名
- module.filename 模块的文件名,带有绝对路径
- module.loaded 返回一个布尔值,表示模块是否已经完成加载
- module.parent 返回一个对象,表示调用该模块的模块
- module.children 返回一个数组,表示该模块要用到的其他模块
- module.exports 表示模块对外输出的值
CommonJS 规范的特点:
- 所有代码都运行在模块作用域,不会污染全局作用域
- 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果,要想让模块再次运行,必须清除缓存
- 模块加载的顺序,按照其在代码中出现的顺序
简单来说他会通过字符串拼接的方式去组成一个函数,把文件内容放到函数内部执行
text'function(exports,require,module){ 文件内容 }'
通过vm.runInThisContext(code) 会创建一个独立的沙箱环境,执行对以上的编译,运行并返回结果。该方法运行的代码没有权限访问本地作用域,但是可以访问 Global 全局对象。
三个参数分别为:
- module 实例的 exports 对象
- require 模块导入方法
- require查找规则 require(X)
- X是一个node核心模块 比如path、http => 直接返回核心模块并停止查找
- X是一个路径 => 先查找该名字文件,然后依次查找.js,.json,.node文件
- 如果还是没有,则再该名字的文件夹下的index.js文件,然后依次查找.js,.json,.node文件
- X既不是路径也不是模块 => 从当前目录的node_modules文件夹查找
- module 实例本身
当模块内由多个module.export时,导出的对象为最后一个module.export
exports就相当与module.export ,模块默认导出module.export
textexports = { c: 2 }; // 这样之后exports指向新地址,它不知想module.export
CommonJs 就是模块化的社区标准,而 Nodejs 就是 CommonJs 模块化规范的实现,它对模块的加载是同步的,也就是说,只有引入的模块加载完成,才会执行后面的操作,在 Node 服务端应用当中,模块一般存在本地,加载较快,同步问题不大,在浏览器中就不太合适了,你试想一下,如果一个很大的项目,所有的模块都同步加载,那体验是极差的,所以还需要异步模块化方案,所以 AMD规范 就此诞生。
AMD规范
专门为浏览器环境设计的,它定义了一套异步加载标准来解决同步的问题
textdefine(id?: String, dependencies?: String[], factory: Function|Object)
- id 即模块的名字,字符串,可选
- dependencies 指定了所要依赖的模块列表,它是一个数组,也是可选的参数,每个依赖的模块的输出将作为参数一次传入factory中。如果没有指定dependencies,那么它的默认值是 ["require", "exports", "module"]
- factory 包裹了模块的具体实现,可为函数或对象,如果是函数,返回值就是模块的输出接口或者值
text// 定义依赖 myModule,该模块依赖 JQ 模块 define('myModule', ['jquery'], function($) { // $ 是 jquery 模块的输出 $('body').text('test') }) // 引入依赖 require(['myModule'], function(myModule) { // todo... })
它就是通过 define 方法,将代码定义为模块,通过 require 方法,实现代码的模块加载,使用时需要下载和导入,也就是说我们在浏览器中想要使用 AMD 规范时先在页面中引入 require.js 就可以了。
CMD规范
CMD 的出现较为晚一些,它汲取了 CommonJS 和 AMD 规范的优点,也是专门用于浏览器的异步模块加载。
textdefine(function(require, exports, module) { // 同步引入 var a = require('./a') // 异步引入 require.async('./b', function (b) { }) // 条件引入 if (status) { var c = requie('./c') } // 暴露模块 exports.aaa = 'hahaha' })
CMD和AMD
| 规范 | 推崇 | 代表 | | ------ | ------ | --------- | | AMD | 依赖前置 | requirejs | | CMD | 依赖就近 | seajs |
AMD 执行过程中会将所有依赖前置执行,也就是在自己的代码逻辑开始前全部执行,而 CMD 如果 require 引入了但整个逻辑并未使用这个依赖或未执行到逻辑使用它的地方前是不会执行的
ES Module
2015年6月,ECMAScript2015 也就是我们说的 ES6 发布了,JS 终于在语言标准的层面上,实现了模块功能,使得在编译时就能确定模块的依赖关系,以及其输入和输出的变量,不像 CommonJS 、AMD 之类的需要在运行时才能确定(例如 FIS 这样的工具只能预处理依赖关系,本质上还是运行时解析),成为浏览器和服务器通用的模块解决方案。
其模块化功能主要由俩个命令构成:exports和import,export命令由于规定模块的对外接口,import命令用于输入其他模块的功能。ES6还提供了export default的命令。为模块指定默认输出。对应的import语句不需要大括号。这也更接近AMD的引用写法。
ES6 Module不是对象,import命令被JavaScript引擎静态分析,在编译的时候就引入模块代码。而不是在代码运行时加载,所以无法实现条件加载。也就使得静态分析成为可能。
export可以导出的是对象中包含多个属性、方法,export default只能导出一个可以不具名的函数。我们可以输用import引入。同时我们也可以直接使用require使用,原因是webpack启用了server相关。
导入:
textimport { fn } from './xxx' // export导出的方式 import fn from 'xx' // export default方式
ES6模块运行机制与commonjs运行机制不一样。js引擎对脚本静态分析的时候,遇到模块加载指令后会生成一个只读引用。等到脚本真正执行的时候。才会通过引用模块中获取值,在引用到执行的过程中,模块中的值发生变化,导入的这里也会跟着发生变化。ES6模块是动态引入的。并不会缓存值。模块里总是绑定其所在的模块。