前言
JavaScript 语言诞生至今,模块规范化之路曲曲折折。社区先后出现了各种解决方案,包括 AMD、CMD、CommonJS 等,而后 ECMA 组织在 JavaScript 语言标准层面,增加了模块功能(因为该功能是在 ES2015 版本引入的,所以在下文中将之称为 ES6 module)。
今天我们就来聊聊,为什么会出现这些不同的模块规范,它们在所处的历史节点解决了哪些问题?
何谓模块化?
或根据功能、或根据数据、或根据业务,将一个大程序拆分成互相依赖的小文件,再用简单的方式拼装起来。
全局变量
演示项目
为了更好的理解各个模块规范,先增加一个简单的项目用于演示。
# 项目目录:
├─ js # js文件夹
│ ├─ main.js # 入口
│ ├─ config.js # 项目配置
│ └─ utils.js # 工具
└─ index.html # 页面html
Window
在刀耕火种的前端原始社会,JS 文件之间的通信基本完全依靠window
对象(借助 HTML、CSS 或后端等情况除外)。
// config.js
var api = 'https://github.com/ronffy';
var config = {
api: api,
}
// utils.js
var utils = {
request() {
console.log(window.config.api);
}
}
// main.js
window.utils.request();
小贼先生:【深度全面】JS模块规范进化论
IIFE
浏览器环境下,在全局作用域声明的变量都是全局变量。全局变量存在命名冲突、占用内存无法被回收、代码可读性低等诸多问题。
这时,IIFE(匿名立即执行函数)出现了:
;(function () {
...
}());
用IIFE重构 config.js:
;(function (root) {
var api = 'https://github.com/ronffy';
var config = {
api: api,
};
root.config = config;
}(window));
IIFE的出现,使全局变量的声明数量得到了有效的控制。
命名空间
依靠window
对象承载数据的方式是“不可靠”的,如window.config.api
,如果window.config
不存在,则window.config.api
就会报错,所以为了避免这样的错误,代码里会大量的充斥var api = window.config && window.config.api;
这样的代码。
这时,namespace
登场了,简约版本的namespace
函数的实现(只为演示,不要用于生产):
function namespace(tpl, value) {
return tpl.split('.').reduce((pre, curr, i) => {
return (pre[curr] = i === tpl.split('.').length - 1
? (value || pre[curr])
: (pre[curr] || {}))
}, window);
}
用namespace
设置window.app.a.b
的值:
namespace('app.a.b', 3); // window.app.a.b 值为 3
用namespace
获取window.app.a.b
的值:
var b = namespace('app.a.b'); // b 的值为 3
var d = namespace('app.a.c.d'); // d 的值为 undefined
app.a.c
值为undefined
,但因为使用了namespace
, 所以app.a.c.d
不会报错,变量d
的值为undefined
。
AMD/CMD
随着前端业务增重,代码越来越复杂,靠全局变量通信的方式开始捉襟见肘,前端急需一种更清晰、更简单的处理代码依赖的方式,将 JS 模块化的实现及规范陆续出现,其中被应用较广的模块规范有 AMD 和 CMD。
面对一种模块化方案,我们首先要了解的是:1. 如何导出接口;2. 如何导入接口。
AMD
异步模块定义规范(AMD)制定了定义模块的规则,这样模块和模块的依赖可以被异步加载。这和浏览器的异步加载模块的环境刚好适应(浏览器同步加载模块会导致性能、可用性、调试和跨域访问等问题)。
本规范只定义了一个函数define
,它是全局变量。
/**
* @param {string} id 模块名称
* @param {string[]} dependencies 模块所依赖模块的数组
* @param {function} factory 模块初始化要执行的函数或对象
* @return {any} 模块导出的接口
*/
function define(id?, dependencies?, factory): any
RequireJS
AMD 是一种异步模块规范,RequireJS 是 AMD 规范的实现。
接下来,我们用 RequireJS 重构上面的项目。
在原项目 js 文件夹下增加 require.js 文件:
# 项目目录:
├─ js # js文件夹
│ ├─ ...
│ └─ require.js # RequireJS 的 JS 库
└─ ...
// config.js
define(function() {
var api = 'https://github.com/ronffy';
var config = {
api: api,
};
return config;
});
// utils.js
define(['./config'], function(config) {
var utils = {
request() {
console.log(config.api);
}
};
return utils;
});
// main.js
require(['./utils'], function(utils) {
utils.request();
});