模块应该是职责单一、相互独立、低耦合的、高度内聚且可替换的离散功能块。
模块化通过分解复杂系统为独立的模块实现细粒度的精细控制,对于复杂系统的维护和管理十分有益。
为啥需要模块化?
首先是前端是基于多语言、多层次的编码和组织工作,其次前端产品的交付是基于浏览器,这些资源是通过增量加载的方式运行到浏览器端,如何在开发环境组织好这些碎片化的代码和资源,并且保证他们在浏览器端快速、优雅的加载和更新,就需要一个模块化系统。
模块化的好处
1、可维护性。 因为模块是独立的,一个设计良好的模块会让外面的代码对自己的依赖越少越好,这样自己就可以独立去更新和改进。
2、命名空间。 在 JavaScript 里面,如果一个变量在最顶级的函数之外声明,它就直接变成全局可用。因此,常常不小心出现命名冲突的情况。使用模块化开发来封装变量,可以避免污染全局环境。
3、重用代码。 我们有时候会喜欢从之前写过的项目中拷贝代码到新的项目,这没有问题,但是更好的方法是,通过模块引用的方式,来避免重复的代码库。我们可以在更新了模块之后,让引用了该模块的所有项目都同步更新,还能指定版本号,避免 API 变更带来的麻烦。
实现模块化的规范主要有:
- CommonJS
CommonJS 是以在浏览器环境之外构建 JavaScript 生态系统为目标而产生的项目,比如在服务器和桌面环境中。
采用同步加载模块的方式,也就是说只有加载完成,才能执行后面的操作。CommonJS 代表:Node 应用中的模块,通俗的说就是你用 npm 安装的模块。
它使用 require 引用和加载模块,exports 定义和导出模块,module 标识模块。使用 require 时需要去读取并执行该文件,然后返回 exports 导出的内容。
//定义模块 math.js
var random=Math.random()*10;
function printRandom(){
console.log(random)
}
function printIntRandom(){
console.log(Math.floor(random))
}
//模块输出
module.exports={
printRandom:printRandom,
printIntRandom:printIntRandom
}
//加载模块 math.js
var math=require('math')
//调用模块提供的方法
math.printIntRandom()
math.printRandom()
CommonJS主要用于服务端的模块化,不适用于前端模块化的原因在于:
-
服务端加载一个模块,直接就从硬盘或者内存中读取了,消耗时间可以忽略不计
-
浏览器需要从服务端下载这个文件,所以说如果用CommonJS的require方式加载模块,需要等代码模块下载完毕,并运行之后才能得到所需要的API。
-
如果我们在某个代码模块里使用CommonJS的方法require了一个模块,而这个模块需要通过http请求从服务器去取,如果网速很慢,而CommonJS又是同步的,所以将阻塞后面代码的执行,从而阻塞浏览器渲染页面,使得页面出现假死状态。
-
CMD
CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。
CMD语法:
- 定义暴露模块:
//定义没有依赖的模块
define(function(require, exports, module){
exports.xxx = value
module.exports = value
})
//定义有依赖的模块
define(function(require, exports, module){
//引入依赖模块(同步)
var module2 = require('./module2')
//引入依赖模块(异步)
require.async('./module3', function (m3) {
})
//暴露模块
exports.xxx = value
})
- 引入使用模块:
define(function (require) {
var m1 = require('./module1')
var m4 = require('./module4')
m1.show()
m4.show()
})
CMD的优缺点:
-
优点:依赖就近,延迟执行 可以很容易在 Node.js 中运行;
-
缺点:依赖 SPM 打包,模块的加载逻辑偏重;
-
AMD
异步模块定义,所谓异步是指模块和模块的依赖可以被异步加载,他们的加载不会影响它后面语句的运行。有效避免了采用同步加载方式中导致的页面假死现象。AMD代表:RequireJS。
RequireJS是一个工具库,主要用于客户端的模块管理。它的模块管理遵守AMD规范,RequireJS的基本思想是,通过define方法,将代码定义为模块;通过require方法,实现代码的模块加载。
它主要有两个接口:define 和 require。define 是模块开发者关注的方法,而 require 则是模块使用者关注的方法。
define() 函数:
define(id?, dependencies?, factory);
//id :可选参数,它指的是模块的名字。
//dependencies:可选参数,定义中模块所依赖模块的数组。
//factory:模块初始化要执行的函数或对象
require() 函数:
require([module], callback);
//module:一个数组,里面的成员就是要加载的模块.
//callback:模块加载成功之后的回调函数。
AMD的优缺点:
AMD 运行时核心思想是「Early Executing」,也就是提前执行依赖 AMD 的这个特性有好有坏:
-
首先,尽早执行依赖可以尽早发现错误。
-
另外,尽早执行依赖通常可以带来更好的用户体验,也容易产生浪费。
-
引用AMD的Javscript库: 目前,主要有两个Javascript库实现了AMD规范:require.js和curl.js
-
在浏览器环境中异步加载模块;并行加载多个模块;
-
开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅;不符合通用的模块化思维方式,是一种妥协的实现。
-
ES6模块
ES6模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。所以说ES6是编译时加载,
ES6模块不是对象,而是通过export命令显式指定输出的代码,输入时也采用静态命令的形式。
ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";。
严格模式主要有以下限制:
- 变量必须声明后再使用
- 函数的参数不能有同名属性,否则报错
- 不能使用with语句
- 不能对只读属性赋值,否则报错
- 不能使用前缀 0 表示八进制数,否则报错
- 不能删除不可删除的属性,否则报错
- 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
- eval不会在它的外层作用域引入变量
- eval和arguments不能被重新赋值
- arguments不会自动反映函数参数的变化
- 不能使用arguments.callee
- 不能使用arguments.caller
- 禁止this指向全局对象(ES6 模块之中,顶层的this指向undefined,即不应该在顶层代码使用this。)
- 不能使用fn.caller和fn.arguments获取函数调用的堆栈
- 增加了保留字(比如protected、static和interface)
语法:
模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
export
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个 JS 文件,里面使用export命令输出变量。
function cUl(){
let ulEle = document.createElement("ul");
for(let i = 0; i < 5; i++){
let liEle = document.createElement("li");
liEle.innerHTML = "无序列表" + i;
ulEle.appendChild(liEle);
}
return ulEle;
}
let ul = cUl();
export {ul};
import
使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。
import {table} from "../test/test_table.js";
import {div} from "../test/test_div.js" ;
import {ul} from "../test/test_ul.js" ;
export {table, div, ul};
ES6 模块与 CommonJS 模块的差异 它们有两个重大差异:
-
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
-
CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。