前端工程化:前端模块化演变历程

前端模块化是指将一个大型的前端应用程序分解为小的、独立的模块,每个模块都有自己的功能和接口,可以被其他模块使用。

前端模块化的出现主要是为了解决以下几个问题:

  • 代码复用:通过模块化,可以在多个地方重复使用同一个模块,而不需要重复编写相同的代码。
  • 代码维护:模块化后的代码更加清晰,每个模块负责的功能明确,便于维护和升级。
  • 依赖管理:模块化可以很好地处理模块间的依赖关系,确保模块使用时其依赖已经被正确加载。
  • 私有化:模块内部具有私有化内容,对外只提供暴露的通信接口
  • 提高加载效率:模块化允许按需加载,只有需要的模块才会被加载,减少了不必要的资源加载,提高了页面的加载速度。
  • 隔离命名空间:每个模块都有自己的命名空间,避免了全局变量的污染,减少了命名冲突的可能性。

普通脚本与模块化的区别:

  • 普通脚本:只有一个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

欢迎加入前端筱园交流群:
描述文字
关注我的公众号【前端筱园】,不错过每一篇推送

描述文字
  • 56
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端筱园

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值