我们都知道当今前端世界里,前端工程化,模块化都已经是常态化了。模块化的诞生给我们带来了极大的便利,它可以将复杂的程序规范拆分成若干模块,一个模块包括输入和输出;同时模块的内部实现是私有的,对外暴露接口与其他模块通信,还有很多好处这里就不细说了。
那在没有CommonJS
、AMD
、CMD
、UMD
、EsModule
这些模块化规范之前,我们都是怎么做前端模块化的呢?下面笔者将分成四个阶段阐述。通过这四个阶段可以看出前端模块化的发展,以及没有以上模块化规范之前,我们是多么的不便利(难用!!)
一、第一阶段,全局函数
早期,我们都是将不同的功能模块拆分成不同的全局函数
//function.js
//前端调用后端接口
function api() {
return {
code: 0,
data: {
a: 1,
b: 2
}
}
}
//处理后端接口
function handle(data, key) {
return data.data[key]
}
//数据操作
function sum(a, b) {
return a + b;
}
const data = api();
const a = handle(data, 'a');
const b = handle(data, 'b');
const c = sum(a, b);
console.log(c);
复制代码
html
文件引入funciton.js
浏览器查看
这样做有一个较大的缺陷就是容易引发全局命名空间冲突,因为全局定义的函数是挂在到window
的全局属性上的
如果一不小心起了一个名字和Window
上属性一样的方法,就会被覆盖掉了,所以很快就引发了下面第二个阶段
二、第二阶段,namespace命名空间
既然名字容易冲突,那可以通过对象封装模块的方式,把所以函数都放在对象里面
//namespace.js
window.__Module = {
//前端调用后端接口
api() {
return {
code: 0,
data: {
a: 1,
b: 2
}
}
},
//处理后端接口
handle(data, key) {
return data.data[key]
},
//数据操作
sum(a, b) {
return a + b;
}
}
const m = window.__Module;
const data = m.api();
const a = m.handle(data, 'a');
const b = m.handle(data, 'b');
const c = m.sum(a, b);
console.log(c);
console.log(window)
复制代码
html
文件引入namespace.js
浏览器查看
输出结果还是3
,我们再看下window
里面的属性
通过这种方式,我们起了一个很难被覆盖的名字(__Module
),很大程度上降低了第一阶段说的问题,但是这种方式没有弊端吗?当然有!!!
我们可以通过外部能够修改模块内部的数据
假设__Module
里面有个变量x
window.__Module = {
x: 1,
api() {
//....
},
....
}
const m = window.__Module;
console.log(m.x); //1
m.x = 2;
console.log(m.x); //2
复制代码
如果是这样,就失去了模块化私有的特征了,所以又引发了第三阶段
三、第三阶段,函数作用域 + 闭包
首先说第三阶段之前,先看看函数作用域
funciton test() {
var a = 1;
}
console.log({
test
});
复制代码
我们是根本拿不到里面的变量a
的值的,因为外界无法访问,如果想要访问/修改,可以通过闭包的方式
function test() {
var a = 1;
return {
set(val) {
a = val
},
get() {
return a;
}
}
}
const t = test();
console.log(t.get()); //1
t.set(2);
console.log(t.get()); //2
复制代码
所以第三阶段,是通过IIFE,就是我们常说的立即执行函数创建闭包
//iife.js
(function (global) {
var x = 1;
//前端调用后端接口
function api() {
return {
code: 0,
data: {
a: 1,
b: 2,
},
};
}
//处理后端接口
function handle(data, key) {
return data.data[key];
}
//数据操作
function sum(a, b) {
return a + b;
}
//获取x
function getX() {
return x;
}
//设置x
function setX(val) {
x = val;
}
global.__Module = {
x,
api,
handle,
sum,
getX,
setX,
};
})(window);
const m = window.__Module;
const data = m.api();
const a = m.handle(data, "a");
const b = m.handle(data, "b");
const c = m.sum(a, b);
console.log(c);
console.log(m.x);
m.x = 2;
console.log(m.x);
复制代码
html
文件引入iife.js
浏览器查看
有些同学肯定发现了,刚才不是说通过函数作用域,是无法直接通过外部修改函数内部的值吗?
为什么这里m.x = 2
会生效呢?不是应该通过闭包,调用m.setX(2)
才会修改x
的值吗??
细心的同学应该知道了,在函数内部我把x
也放到了__Module
的对象中
global.__Module = {
x, //把x放在这里,相当于是浅拷贝,
api,
handle,
sum,
getX,
setX,
};
复制代码
这里就关系到了函数作用域和对象属性的区别了,我们调用的m.x = 2
实际修改的是对象属性,并非真正的函数内部的var x = 1
的值,想要修改函数内部的值,唯一的方法就是使用闭包!!
console.log(c); //3
console.log(m.x); //1 对象属性
m.setX(2);
console.log(m.getX()); //2 函数内部的x
console.log(m.x); //1 对象属性
复制代码
虽然这个阶段看上去已经近乎完美了,但是仍然是有缺陷的,就是无法解决模块间相互依赖的问题,从而引发了阶段四
四、第四阶段,IIFE模式增强,支持传入自定义依赖
什么是模块依赖呢?简单来说就是一个模块需要用到另外一个模块的属性或者方法。还是上面的例子,我们将原来的模块进行拆分
//iffe.js
(function (global, api) {
var x = 1;
global.__Module = {
x,
api
};
})(window, window.__Module_API);
const m = window.__Module;
const data = m.api.api();
const a = m.api.handle(data, "a");
const b = m.api.handle(data, "b");
const c = m.api.sum(a, b);
console.log(c); //3
console.log(m.x); //1
m.api.setX(2);
console.log(m.api.getX()); //2
复制代码
//iife_module.js
(function (global) {
//前端调用后端接口
function api() {
return {
code: 0,
data: {
a: 1,
b: 2,
},
};
}
//处理后端接口
function handle(data, key) {
return data.data[key];
}
//数据操作
function sum(a, b) {
return a + b;
}
//获取x
function getX() {
return x;
}
//设置x
function setX(val) {
x = val;
}
global.__Module_API = {
api,
handle,
sum,
getX,
setX,
};
})(window);
复制代码
html
文件需要引入iife.js
和iife_module.js
,并且需要注意引入顺序
这种方式,一定程度上支持模块的依赖,但是也存在其他的问题:
- 多依赖传入时,代码阅读困难
- 无法支持大规模的模块化开发
- 模块之间的引入顺序需要固定
- 无特定语法支持,代码简陋
五、总结
模块化的发展大致是经历了这四个阶段,在没有标准的模块化规范之前,前端想要实现模块化,可谓是举步维艰。直到它们的出现,前端工程化,打包工具,MVVM
框架等才逐渐发展壮大,才有百花齐放的前端世界。