所有的大型应用都会使用模块化开发,模块化就是将复杂系统分解成多个独立模块,便于之后维护。
- 高可维护性:很长一段时间里,前端通过
- 命名冲突:js所创建的对象都会变成全局对象,这样没有命名空间的方式,会造成很多问题,引入第三方库还有可能命名的覆盖
- 高复用性
上面都是非模块化存在问题,所以我们如何解决这些问题,使前端支持模块化开发
1.命名空间模式
简单的来说,我们要通过js对象来模拟命名空间的概念
-
这样我们把模拟的namespace 看作是一个 模块module 通过对象的方式减少了全局变量,解决命名冲突问题
-
**但是没有解决外部还是可以解决内部变量的问题,name 也暴露出来可以随意改变,这不是模块化想要的
var namespace = { name: 'namespace', getName: function() { return this.name } } namespace.name namespace.getName()
**
2.IIFE
-
js 为了避免全局变量污染,提供了闭包的方式提供模块化方案,我们通过自执行函数来避免变量泄露到全局作用域中
-
数据是私有作用域的,只通过方法来操作私有变量
(function(window) { let name = 'private' var module = { getName: function() { return name }, setName: function() { return name + 'Set' } } window.module = module })(window)
如上 将module 挂载在window下,这个模块只暴露了getName, setName 方法,内部的name不能修改
-
但是这种模式也面临一个问题,如果我想引入其他依赖的库,还是不能引用全局的依赖库,比如我们想引入jQuery,这个时候我们要向模块中注入依赖
-
这种模式也叫IIFE模式增强版
-
这样做实现了模块化,并使得模块的依赖关系变得清楚一些
(function(window, jQuery) { let name = 'private' var module = { getName: function() { return name }, setName: function() { return name + 'Set' } } window.module = module })(window, jQuery)
3. CommonJS规范
随着js在服务端的应用,急需标准化的模块化方案,CommonJS 规范提出后Node.js就是应用了这个规范,
- 每个文件就是一个模块,都有自己的作用域
- 一个文件里的变量,函数,类都是私有的,对其他文件不可见
- node中模块加载是运行时候同步加载的
1.用法是 用 module 暴露对象,require 引入模块
//定义模块 module.js
module.exports = {
a: 1
}
// 加载模块
var moduleA = require('./module.js')
2.模块的加载机制
require到的对象是对 module.exports 暴露对象的拷贝,但是观察到一个现象,如果两个模块共同引入了一个 module,如果其中一个模块扩展了当前模块,其他模块引入时候除了module 初始化定义的key的值不变,其他新加的key ,也会在各个模块间相互影响
3. 在浏览器端使用CommonJS规范,可以用 Browserify ,gulp, webpack等解析后引入到页面中去,这里主要介绍 Browserify
//定义模块A moduleA.js
module.exports = {
a: 1
}
// 定义模块B moduleB.js
var moduleB = require('./moduleA.js')
console.log(moduleB)
module.exports = {
b: 2
}
定义好之后安装Browserify
npm install --save-dev Browserify
打包命令 将打包命名放到 package.json 的scripts 中
"scripts":{
"build": "browserify moduleB.js -o build.js"
}
npm run build
将打包好的build.js 文件引入到html文件中就是使用了
4.AMD (Asynchronous Module Definition)
这也是一种js模块化规范,CommonJS采用同步加载方式,只有模块全部加载完成才能进行之后的操作,AMD主要提供了异步加载功能,需要RequireJS来实现模块化编写,浏览器一般需要从服务端加载模块,所有采用异步加载会比较好
1.基本语法
定义一个模块: 通过define 方法将代码定义成模块
// moduleA.js
define(function(){
return {
...module
}
})
引用一个模块:通过require方法加载一个模块
require(['moduleA'], function(moduleA){
// 这里拿到 moduleA 的返回
})
2. 浏览器使用AMD的好处
- 模块定义清晰,不用再写自执行函数了, 并且不会污染全局环境
- 清楚的展示依赖关系
5.CMD( Common Module Definition)
CMD 规范专门用于浏览器端,模块加载是异步的,模块使用时候才会加载执行,CMD整合了CommonJS 和AMD规范的特点,对应的库是SeaJS,它跟requireJS一样,都是解决异步加载问题,只是在使用方法和加载时机上不同
- CMD 加载完某个模块依赖后并不执行,只是下载,在所有依赖模块加载完成后进入主逻辑
- 遇到require语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序完全一致,如果使用require.async()方法,可以实现模块的懒加载
1.基本使用
//引入Sea.js
// 定义 模块A, moduleA.js
define(function(require, exports, module) {
let a = 'aaa'
module.exports = a
})
//定义模块B, moduleB.js
define(function(require, exports, module){
var a = require('./a') // 同步引入
var aa = require.async('./a', function(MA) {
})// 异步引入
var b = 'bbb'
console.log(a, '我是B模块引入的A模块')
module.exports = b
})
//定义入口 main.js
define(function(require){
var b = require('./moduleB.js')
})
// sea.js引入入口文件
seajs.use('./main')
6.UMD
上面说了CommonJS常用在node端,AMD用在浏览器端,都是针对特定平台,如果想要跨平台方案要引入UMD方案(Universal Nodule Definition),能够很好的兼容AMD, CommonJS,很多JS框架和类库都会打包成这种形式,UMD的实现如下
- 首先判断是否支持CommonJs模块(exports是否存在),存在则使用CommonJs模块化格式
- 在判断是否支持AMD(define)是否存在,存在则用AMD方式加载
- 判断define(factory)是一个全局函数,用来定义模块,存在就是AMD或者CMD环境
- 都不存在的话就将模块挂在到全局(window或者global)
1.基本语法
(
function(name, factory){
// 检测 是否包含 AMD 和 CMD
var hasDefine = typeof define === 'function'
var hasExports = typeof module != 'undefined' && module.exports
if(hasDefine) {
define(factory)
}else if(hasExports) {
module.exports = factory()
}else{
this[name] = factory()
}
}
)('xxx',function(){})
7.ES6 Module
有了ES6之后,不必再用闭包和函数进行模块化,需要一个转义工具Babel等工具进行编译,
它的思想就是尽量静态化,编译阶段就能确定这些东西,而CommonJS和AMD,都只能再运行时候确定
1.基本语法
// export 用于定义模块,import 用于引入模块 // ModuleA.js
var a = 'a'
export {a: a}
import {a} from './ModuleA.js'
- ES6使用基于文件的模块,必须一个文件一个模块,不能将多个模块合并到单个文件中
- ES6模块API是静态的,一旦模块导入后,就不能在程序中增添方法
- ES6采用引用绑定(指针),这个和CommonJS中的值不用,如果你的模块在运行过程中修改了导出的变量值,就会反映到使用模块的代码中去
- ES6模块采用的是单例模式,每次对同一个模块的导入其实指向同一个实例