模块化,其实在日常开发中到处都在用。但我对这个词,其实没有一个系统的、全面的理解。这篇文章,就参考各种文章稍微整理一下~
一、什么是模块化
什么是模块
- 将一个复杂的程序,依据一定的规则(规范)封装成几个块(文件),并组合在一起,用来实现特定的功能
- 块的内部数据/实现是私有的,只通过向外暴露的一些接口(方法)与外部通信
- 简单来说,模块,就是实现特定功能的一组方法
模块的组成
- 数据(内部的属性)
- 操作数据的行为(内部的函数)
什么是模块化
将 JavaScript 程序,拆分为可按需导入的单独模块的机制。
二、模块化的发展历史
原始写法
将不同函数及记录状态的变量放在一起,算一个模块。使用时,直接调用某个函数即可。
var count = 1
function add () {
count++
}
function minus () {
count--
}
复制代码
【缺点】:
- 污染了全局变量(无法保证与其他模块不发生变量名冲突);
- 模块之间看不出直接关联
对象写法
将模块作为一个对象,模块的所有成员放在对象里。使用时,通过调用对象的属性。
var module1 = {
_count: 0; // _ 是约定俗成的用来声明私有属性的前缀
add: function () {
_count++
},
minus: function () {
_count--
}
}
复制代码
命名空间
上面示例的方法,在 ES5 中叫:命名空间。
命名空间下,又可以声明子命名空间。
下面看一个示例:
// 若全局空间中,已有同名对象,则不覆盖该对象;否则,创建一个新的命名空间
var MYNAMESPACE = MYNAMESPACE || {}
复制代码
<!doctype html>
<html lang="en">
<meta charset="UTF-8">
<title>命名空间的用法</title>
<script src="1.js"></script>
<script src="2.js"></script>
<script src="3.js"></script>
<head>
<body>
<script>
sayMyName() // 'Jane Doe'
Aubrey.sayMyName() // 'Aubrey'
Gabriel.sayMyName() // 'Gabriel'
</script>
</body>
</head>
</html>
复制代码
// 1.js
function sayMyName () {
console.log('Jane Doe')
}
复制代码
// 2.js
var Aubrey = Aubrey || {}
Aubrey.sayMyName = function () {
console.log('Aubrey')
}
复制代码
// 3.js
var Gabriel = Gabriel || {}
Gabriel.sayMyName = function () {
console.log('Gabriel')
}
复制代码
【优点】:
- 对象写法及命名空间,避免了全局变量污染
- 同时易于组织代码模块的逻辑性、扩展性、可读性及可维护性。
【缺点】:
- 暴露了所有模块成员,内部状态(如:上面示例中的_count)可以被外部改写
立即执行函数(Immediately-Invoked Function Expression, IIFE)
通过 IIFE,可以隐藏模块的私有成员。
下面示例的 add() 和 _count,minus() 和 _count 形成了两个闭包。
var module1 = (function () {
var _count = 1
var add = function () {
_count++
}
var minus = function () {
_count--
}
return {
add,
minus
}
})()
复制代码
上面的示例中,变量 _count 在 module1 外部无法再被访问,只能通过 module1.add 和 module1.minus 访问。
// 放大模式(argumentation):用于将一个模块拆分成几个部分,或者需要继承另一个模块
var module1 = (function (mod) {
mod.subMode = function () {}
return mod
})(module1)
复制代码
// 宽放大模式(loose argumentation):在浏览器环境中,无法确保模块的哪个部分会先加载完,为了避免第一个执行的部分加载一个不存在的对象
var module1 = (function (mod) {
mod.subMode = function () {}
return mod
})(window.module1 || {})
复制代码
// 引入依赖:模块内需要调用全局变量时,为了保持模块的独立性,使用显式传参方式
var module1 = (function (mod, $) {
mod.subMode = function () {
$('#id").onclick = function () {}
}
return mod
})(window.module1 || {}, jQuery)
复制代码
【优点】:
- 避免命名冲突
- 隐藏私有变量
- 保持了模块的独立性,使模块之间的依赖关系变得清晰
- 高可复用性、维护性
- 模块可按需加载
三、模块的规范
JS 中的模块规范主要有这几种:CommonJS、AMD、CMD、UMD、ES6 Modules
(一)CommonJS
参考文章:CommonJS规范(阮一峰)
Node 模块是遵循的 CommonJS 规范。
CommonJS规范规定,每个模块内部,
module
变量代表当前模块。这个变量是一个对象,它的exports
属性(即module.exports
)是对外的接口(exports 也是个对象)。require
方法用于加载模块,加载某个模块,其实是加载该模块的module.exports
属性。
CommonJS 模块的特点:
- 所有代码都运行在模块作用域,不会污染全局作用域;
- 模块可以多次加载,但只会在第一次加载时运行一次,然后运行结果就被缓存了,后续加载,直接读取缓存结果。如果要重新运行模块,需要清除缓存。
- 模块的加载顺序,按照其在代码中出现的顺序
CommonJS 模块的语法:
const fn1 = function (value) {
}
const fn2 = function (value) {
}
const fn3 = function (value) {
}
// 写法一:
module.exports.fn1 = fn1
module.exports.fn2 = fn2
module.exports.fn3 = fn3
// 写法二:
module.exports = {
fn1,
fn2: fn2,
fn3
}
// 写法三:在 Node 中,可以省略 module,exports 指向 module.exports
exports.fn1 = fn1
exports.fn2 = fn2
exports.fn3 = fn3
// 写法四:在 Node 中,可以省略 module,exports 指向 module.exports
exports = {
fn1,
fn2,
fn3
}
复制代码
引入util.js
// 使用方法一:
let util = require('util')
util.fn1()
// 使用方法二:
let { fn1, fn2 } = require('util')
fn1()
fn2()
复制代码
上面的代码实质加载的是整个 util.js,尽管我们没有使用 fn3。CommonJS 的这种模块加载方式,叫“运行时加载”。
CommonJS 的缺点:
- 运行时加载,依赖无法静态优化
- 模块加载同步,只有模块加载完,才能执行后面的操作
AMD(Asynchronous Module Definition:异步模块定义)
AMD 采用异步方式加载模块,所有依赖这个模块的语句,都定义在一个回调函数中。
AMD 语法:
- 定义暴露模块: define([依赖模块名], function () { return 模块对象 })
- 引入模块:require([‘moduleA’, ‘moduleB’, ‘moduleC’], function (moduleA, moduleB, moduleC) {} )
AMD 示例:
// index.js
require(['moduleA', 'moduleB'], function(moduleA, moduleB) {
console.log(moduleB);
});
// moduleA.js
define(function(require) {
var m = require('moduleB');
setTimeout(() => console.log(m), 1000);
});
// moduleB.js
define(function(require) {
var m = new Date().getTime();
return m;
});
复制代码
要使用 AMD 规范,需要先添加一个符合 AMD 规范的加载器脚本在页面中,比如 require.js 和 curl.js
<html>
<!-- 此处必须加载 require.js 之类的 AMD 模块化库之后才可以继续加载模块-->
<script src="/require.js"></script>
<!-- 只需要加载⼊⼝模块即可 -->
<script src="/index.js"></script>
</html>
复制代码
require.js 优点:
- 实现 JS 文件的异步加载,避免网页失去响应
- 管理模块之前的依赖性(加载顺序),便于代码的编写和维护
CMD (Common Module Definition:通用模块定义)
在 AMD 规范中,在 require 的第一个参数中,就要引入所有依赖。
在 CMD 规范中,依赖是就近,用的时候再 require 的。
可以通过 引入 Sea.js 使用 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()
})
复制代码
UMD(Universal Module Definition)
UMD是AMD和CommonJS的糅合。AMD模块以浏览器第一的原则发展,异步加载模块。CommonJS模块以服务器第一原则发展,选择同步加载,它的模块无需包装(unwrapped modules)。这迫使人们又想出另一个更通用的模式UMD (Universal Module Definition)。希望解决跨平台的解决方案。
UMD先判断是否支持Node.js的模块(exports)是否存在,存在则使用Node.js模块模式。在判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。
(function (window, factory) {
if (typeof exports === 'object') {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
define(factory);
} else {
window.eventUtil = factory();
}
})(this, function () {
//module ...
});
复制代码
ES6 Modules
ES6 的模块,不是对象,而是通过 export 命令显式指定输出代码,再通过 import 命令输入。
// 只加载了显式声明的 3 个方法,没有加载 fs 模块的其他方法
import { stat, exists, readFile } from 'fs';
复制代码
ES6 的这种加载,称为“编译时加载”,或者“静态加载”。即,ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。但这也导致了没办法引用 ES6 模块本身,因为它不是对象。
ES6 模块的优点:
- 编译时加载,使静态分析成为可能。比如,引入宏、类型检测等功能
- 不再需要 UMD 模块格式
- 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者
navigator
对象的属性 - 不再需要对象作为命名空间(比如
Math
对象),未来这些功能可以通过模块提供
ES6 模块注意点:
ES6 中,模块自动启动严格模式(’use strict’),严格模式主要有以下限制:
- 变量必须声明后再使用
- 函数的参数不能有同名属性,否则报错
- 不能使用
with
语句 - 不能对只读属性赋值,否则报错
- 不能使用前缀 0 表示八进制数,否则报错
- 不能删除不可删除的属性,否则报错
- 不能删除变量
delete prop
,会报错,只能删除属性delete global[prop]
eval
不会在它的外层作用域引入变量eval
和arguments
不能被重新赋值arguments
不会自动反映函数参数的变化- 不能使用
arguments.callee
- 不能使用
arguments.caller
- 禁止
this
指向全局对象 - 不能使用
fn.caller
和fn.arguments
获取函数调用的堆栈 - 增加了保留字(比如
protected
、static
和interface
)
尤其是:ES6 模块中,顶层的 this 指向 undefined。
export 命令
- ES6 中,一个模块就是一个独立的文件。文件内部的除了使用 export 命令输出的变量外,所有变量,外部无法获取。
// util.js
// 写法一:
export const firstName = 'Aurey'
export const fn1 = function (value) {}
export function fn2 (value) {}
// 写法二(推荐):
const firstName = 'Aubrey'
const fn1 = function (value) {}
export {
firstName,
fn1 as function1 // 可以使用 as 关键字重命名
}
复制代码
- ES6 中,export 语句输出的接口,与其对应的值是动态绑定关系。(即,输出的值会动态更新)
- ES6 中,export 语句可以处于模块顶层的任何位置,但不能在块级作用域内
import 命令
- import 命令也可以使用 as 关键字重命名输入变量
import { firstName, lastName as surname } from './profile.js';
复制代码
- import 命令输入的变量都是只读的。
但是对象除外,对象的属性是可以更改的。但不建议修改,因为很难查错
3. import 命令具有提升效果,会将代码提升至整个模块的顶部,首先执行
4. 由于 import 是静态执行,所以不能使用表达式和变量
5. import 语句是 Singleton 模式,只会执行一次相同模块加载
模块的整体加载
import * as moduleName from './module1'
复制代码
export default 命令
export default 命令,用来为模块指定默认输出。default 其实是输出的变量的名称。
一个模块只能有一个默认输出,一个模块中 export default 命令只能使用一次。
// export-default.js
export default function () { // 也可以给匿名函数,添加名称(但这个名称只在模块内部有效)
console.log('foo');
}
// 也可以写成
function foo () {
console.log('foo');
}
export { foo as default };
// import-default.js
import customName from './export-default'; // 注意 import 后面不使用 {}
customName(); // 'foo'
// 也可以写成
import { default as add } from './export-default';
// export default 也可以输出类
export default class {}
复制代码
import() 动态异步加载
const main = document.querySelector('main');
import(`./section-modules/${someVariable}.js`)
.then(module => { // module 是对象,可以解构赋值
module.loadPageInto(main);
})
.catch(err => {
main.textContent = err.message;
});
复制代码
import () 可以配合 Promise、async/await 语句使用。
// 配合 Promise.all,实现同时依赖多个模块的情况
Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
])
.then(([module1, module2, module3]) => {})
// 也可以在 async 函数中使用
async function main() {
const myModule = await import('./myModule.js');
const {export1, export2} = await import('./myModule.js');
const [module1, module2, module3] =
await Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
]);
}
main();
复制代码
更多关于 ES6 Modules 的内容,请参考ES6 Modules
总结
JS 模块化规范主要有:
- CommonJS:module.exports/require,用于服务端,同步加载,Node 遵循此规范;用在浏览器会导致代码阻塞
- AMD:define/require, 用于浏览器,异步加载,使用回调函数方式实现,依赖前置,需要先声明(require)所有需要引入的模块
- CMD:define,用于浏览器,异步加载,使用回调函数方式实现,依赖就近,使用前引入依赖模块即可
- UMD:先判断是否支持 Node 的 模块引入方式(CommonJS),有就使用,没有再判断是否支持 AMD 模块规范,有就使用
- ES6 Modules:export/import,静态加载模块,按需加载,浏览器支持,加载速度快
模块化带来的好处:
- 避免命名冲突
- 代码分离,按需加载
- 更高可复用性
- 更高可维护性