前端模块化是指将一个大型的前端应用程序分解为小的、独立的模块,每个模块都有自己的功能和接口,可以被其他模块使用。
前端模块化的出现主要是为了解决以下几个问题:
- 代码复用:通过模块化,可以在多个地方重复使用同一个模块,而不需要重复编写相同的代码。
- 代码维护:模块化后的代码更加清晰,每个模块负责的功能明确,便于维护和升级。
- 依赖管理:模块化可以很好地处理模块间的依赖关系,确保模块使用时其依赖已经被正确加载。
- 私有化:模块内部具有私有化内容,对外只提供暴露的通信接口
- 提高加载效率:模块化允许按需加载,只有需要的模块才会被加载,减少了不必要的资源加载,提高了页面的加载速度。
- 隔离命名空间:每个模块都有自己的命名空间,避免了全局变量的污染,减少了命名冲突的可能性。
普通脚本与模块化的区别:
- 普通脚本:只有一个index.js文件,所有的业务逻辑都在这个一个js文件中
- 模块化:以一个entry.js作为入口,然后去引用若干个其他模块
接下来就用一个简单的案例来讲解模块化的演变历程
第一阶段:函数调用
对于一个复杂业务,一般都会拆分成多个小任务,每个任务就编写成一个函数,下面列举一个简单的例子:
// 获取一个随机的坐标
function getCoordinate() {
return [Math.random() * 100, Math.random() * 100];
}
// 把横纵坐标向下整
function handleData(data) {
return [Math.floor(data[0]), Math.floor(data[1])];
}
// 求和
function sum(a, b) {
return a + b;
}
const coordinate = getCoordinate();
const data = handleData(coordinate);
const result = sum(data[0], data[1]);
console.log(result); // 99
这里的每个函数就可以看做是不同的模块,这些方法都是挂在全局window上的
这就会出现一个严重的问题,如果引入了其他的库,其他的库也在全局定义了同样的方法,尤其是那种比较通用的方法名。就会导致同名函数相互覆盖,最终只有一个是可用的。
缺点:容易引发全局命名冲突
第二阶段:全局namespace模式
其本质就是通过对象封装模块,在window上定义一个全局对象,然后把所有函数都挂到这个对象上
window.__Module = {
name: "module",
getCoordinate() {
return [Math.random() * 100, Math.random() * 100];
},
handleData(data) {
return [Math.floor(data[0]), Math.floor(data[1])];
},
sum(a, b) {
return a + b;
},
};
const module = window.__Module;
const coordinate = module.getCoordinate();
const data = module.handleData(coordinate);
const result = module.sum(data[0], data[1]);
这种方式可以大大的降低命名冲突的概率,以前有很多JS库都是用这种方式实现的。
缺点:对象里面的属性可以被外部修改,缺少了私有属性的功能
console.log(module.name); // module
module.name = "new_module";
console.log(module.name); // new_module
第三阶段:IIFE模式+函数作用域+闭包
关注我的公众号【前端筱园】,不错过每一篇推送
加入【前端筱园交流群】,与大家一起交流,共同进步!
IIFE(Immediately Invoked Function Expression),即立即调用函数表达式,是一种在定义后立即被执行的JavaScript函数。
IIFE的主要作用包括:
- 创建独立的作用域:IIFE可以创建一个独立的作用域,防止变量污染全局作用域。通过这种方式,可以在函数内部定义私有变量,而不影响外部环境。
- 避免变量提升:在JavaScript中,传统的函数声明会进行变量提升,即在代码执行前被提前至作用域顶部。而IIFE由于是表达式,不会被提升,因此可以避免变量提升带来的问题。
- 保持代码封装性:IIFE有助于保持代码的封装性,使得一些只需要在特定作用域内运行的代码得以隔离,减少全局命名空间的冲突。
- 模拟块级作用域:在ES6之前,JavaScript不支持块级作用域,IIFE常被用来模拟块级作用域的效果,尤其是在循环和条件语句中需要临时的变量时非常有用。
// 函数作用域+闭包
function fun() {
let name = "module";
return {
get() {
return name;
},
set(newName) {
name = newName;
},
};
}
console.log(name); // undefind
const Name = fun();
console.log(Name.get()); // module
如果要改变函数内属性的值,只有通过暴露出来的方法进行修改,否则无法修改,这就符合了模块化的标准
Name.set("new_module");
console.log(Name.get()); // new_module
接下来就使用闭包进行模块化的改造,创建一个自执行的闭包函数。
(() => {
let name = "module";
function getCoordinate() {
return [Math.random() * 100, Math.random() * 100];
}
function handleData(data) {
return [Math.floor(data[0]), Math.floor(data[1])];
}
function sum(a, b) {
return a + b;
}
function getName() {
return name;
}
function setName(newName) {
name = newName;
}
window.__Module = {
name,
getCoordinate,
handleData,
sum,
getName,
setName,
};
})();
const module = window.__Module;
const coordinate = module.getCoordinate();
const data = module.handleData(coordinate);
const result = module.sum(data[0], data[1]);
console.log(result); // 125
console.log(module.name); // module
module.name = "new_module";
console.log(module.name); // new_module
在上面的代码中,对 module.name 的值进行修改,然后打印发现结果为修改后的值,这与之前提到的私有行相矛盾:
module.name = "new_module";
console.log(module.name); // new_module
这个问题本质上是函数作用域与对象属性的区别,在闭包方法中,name 属性添加到了返回结果中,这里其实是对name的一个拷贝,而不是函数内部的name。
只有通过getName才能拿到内部属性name的值,也只有通过 setName 才能改变内部属性 name的值。
console.log(module.getName()); // module
module.setName("new_module")
console.log(module.getName()); // new_module
缺点:无法解决模块间相互依赖的问题
第四阶段:IIFE模式增强,支持传入自定义依赖
将模块进行拆分,不同的模块放在不同的自执行函数中
((global) => {
function handleData(data) {
return [Math.floor(data[0]), Math.floor(data[1])];
}
function sum(a, b) {
return a + b;
}
global.__Module_utils = {
handleData,
sum,
};
})(window);
iief_entry.js
((global, module) => {
function getCoordinate() {
return [Math.random() * 100, Math.random() * 100];
}
global.__Module = {
getCoordinate,
handleData: module.handleData,
sum: module.sum,
};
})(window, window.__Module_utils);
const module = window.__Module;
const coordinate = module.getCoordinate();
const data = module.handleData(coordinate);
const result = module.sum(data[0], data[1]);
console.log(result);
缺点:
- 传入了多个参数依赖,代码阅读变得困难
- 大规模的模块开发会非常的麻烦,很容易出错
- 无特定语法支持,代码简陋
经过这四个阶段的演变,前端模块化的标准逐步形成,和前面提到的自执行函数原理非常接近,下期讲解CommonJS规范(关注我的公众号,不错过推送哦)。
写在最后
欢迎到我的个人网站(www.dengzhanyong.com)
关注我的公众号【前端筱园】,不错过每一篇推送
加入【前端筱园交流群】,与大家一起交流,共同进步!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。