0. 为什么要模块化
模块化是解决应用系统与技术平台越来越复杂,越来越庞大问题的一个重要途径。无论是开发人员还是产品最终用户,都不希望为了系统中一小块的功能而不得不下载、安装、部署及维护整套庞大的系统。站在整个软件工业化的高度来看,模块化是建立各种功能的标准化的前提。
1. 什么是模块化
模块化是将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并组合在一起。块(文件)的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信。
在没有模块化之前,前端将不同的功能封装到不同的全局函数中,如下:
function f1(){ ....}
function f2(){.....}
那么带来的问题就是:
- 全局的命名空间污染,容易导致命名冲突
- 不能保证和其他模块起冲突
- 模块成员之间看不出直接关系。
为了解决这个问题,出现了namespace模式,如下:
let myNamespace = {
data:"www.baidu.com",
f1() {... }
}
//通过namespace避免污染命名空间
myNamespace.f1()
但是以上方法仍然存在问题: 数据不安全。例如,可以直接修改里面的data
myNamespace.data = 'other data'
为了解决这个问题,出现了IIFE(Immediately-invoked function expression 立即执行函数表达式)模式,如下:
// index.html文件
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
myModule.foo()
myModule.bar()
console.log(myModule.data) //undefined 不能访问模块内部数据
myModule.data = 'xxxx' //不是修改的模块内部的data
myModule.foo() //没有改变
</script>
// module.js文件
(function(window) {
let data = 'www.baidu.com'
//操作数据的函数
function foo() {
//用于暴露有函数
console.log(`foo() ${data}`)
}
function bar() {
//用于暴露有函数
console.log(`bar() ${data}`)
otherFun() //内部调用
}
function otherFun() {
//内部私有的函数
console.log('otherFun()')
}
//暴露行为
window.myModule = { foo, bar } //ES6写法
})(window)
由于module中的data没有暴露,所以是不可修改的,代码myModule.data只会在window.myModule这个对象上新增一个data的属性。
但是以上方法仍然存在一个问题,如果module模块需要依赖其他模块怎么办? 需要对IIFE进行增强,如下:
// index.html文件
<!-- 引入的js必须有一定顺序 -->
<script type="text/javascript" src="jquery-1.10.1.js"></script>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
myModule.foo()
</script>
// module.js文件
(function(window, $) {
let data = 'www.baidu.com'
//操作数据的函数
function foo() {
//用于暴露有函数
console.log(`foo() ${data}`)
$('body').css('background', 'red')
}
function bar() {
//用于暴露有函数
console.log(`bar() ${data}`)
otherFun() //内部调用
}
function otherFun() {
//内部私有的函数
console.log('otherFun()')
}
//暴露行为
window.myModule = { foo, bar }
})(window, jQuery)
以上就是前端的模块化
2. 什么是模块化规范
IIFE增强版的模块化实际上已经实现了前端的模块化,它带来的好处包括
- 避免了命名冲突(减少了全局命名空间的污染)
- 更好地分离代码和数据安全
- 更高的可维护性和可复用性
但是IIFE增强版的模块化带来了新的问题,比如:
- 请求过多,首先我们要依赖多个模块,那样就会发送多个请求,导致请求过多
- 依赖模糊,我们不知道他们的具体依赖关系是什么,也就是说很容易因为不了解他们之间的依赖关系导致加载先后顺序出错。
为了解决这两个问题,出现了模块化规范,比如 CommonJS, AMD, ES6, CMD规范。
3. 模块化规范--CommonJS
基本语法:
//暴露模块
module.export = value 或 export.xxx = value
//引用模块,如果是第三方模块,xxx为模块名;如果是自定义模块,xxx为模块文件路径
require("xxx")
CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。例:
// example.js
var x = 5;
var addX = function (value) {
return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
上面代码通过module.exports输出变量x和函数addX。
//如果参数字符串以“./”开头,则表示加载的是一个位于相对路径
var example = require('./example.js');
console.log(example.x); // 5
console.log(example.addX(1)); // 6
require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。
CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。这点与ES6模块化有重大差异(下文会介绍)
3. 模块化规范-- AMD
CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是非同步加载模块,允许指定回调函数。由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。
基本语法:
//定义没有依赖的模块
define(function(){
return 模块
})
//定义有依赖的模块
define(['module1', 'module2'], function(m1, m2){
return 模块
})
//引用模块使用
require(['module1', 'module2'], function(m1, m2){
使用m1/m2
})
AMD规范的典型实现是RequireJS,它是一个工具库,主要用于客户端的模块管理。通过define方法,将代码定义为模块;通过require方法,实现代码的模块加载。
4. 模块化规范-- CMD
CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD规范整合了CommonJS和AMD规范的特点。在 Sea.js 中,所有 JavaScript 模块都遵循 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()
})
5. 模块化规范-- ES6
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系、输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
基本语法:
export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
return a + b;
};
export { basicNum, add };
/** 引用模块 **/
import { basicNum, add } from './math';
function test(ele) {
ele.textContent = add(99 + basicNum);
}
如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。
// export-default.js
export default function () {
console.log('foo');
}
// import-default.js
import customName from './export-default';
customName(); // 'foo'
模块默认输出, 其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。
ES6 模块与 CommonJS 模块的差异
它们有两个重大差异:
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
ps: RequireJs 和 Sea.js 的实现其输出也是一个值的拷贝
6.总结
- CommonJS规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案。
- AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅。
- CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行。不过,依赖SPM 打包,模块的加载逻辑偏重
- ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
参考自:
1. 前端模块化详解(完整版)
2. Module 的语法
3. Module 的加载实现
4.【前端工程化系列】简谈前端模块化开发与开发规范
5. 前端模块化开发 - haoxl - 博客园
6. 详解JavaScript模块化开发 - trigkit4 - SegmentFault 思否
7. 前端模块化:CommonJS,AMD,CMD,ES6 - 掘金
8. 浅谈前端模块化- 腾讯Web前端IMWeb 团队社区| blog | 团队博客
9. 前端工程之模块化- FEX
10. 前端模块化** - 谦行 - 博客园
11 前端模块化和组件化理解- Counting Stars - SegmentFault 思否
12. js模块化历程