随笔-JS早期模块化的实现与缺点分析

本文探讨了早期JS模块化的起源,如何由单一JS文件转向模块化以解决体积过大、重复代码和依赖混乱等问题,以及ES6模块化和打包技术如何改进这些问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 早期人们对于JS的预期只是一个具备,在浏览器上实现校验用户输入,的能力的脚本化语言,但是随着WEB的发展,越来越多的工作交给了浏览器端,浏览器不得不将JS独立出来,为JS单独设立了JS引擎,让JS可以处理更加繁重地任务,而这也进一步加剧了JS文件的复杂度。

终于有一天,人们发现无法再忍耐一个JS文件包办一个网站的情况了,比如:

1、所有HTML文件的行为都放到了一个JS文件中,导致JS文件体量异常巨大

2、JS文件体量过于庞大,导致每个HTML在加载JS文件时,会造成很长的延迟,用户体验不好,同时对于服务器来说也造成了巨大的压力

3、JS文件变成了一个功能集合,所有功能都放在了一个JS文件中,导致HTML a文件可能只需要JS文件中的a功能,但是却不得不引入所有的功能。

所以,模块化被提上了JS语言设计的议程,但是可能JS语言被创造之初根本没有想到JS会需要模块化,所以官方一直没有给出原生的模块化支持,并且一直拖到了2015年ES6,才实现了原生JS的模块化支持。

所以在2015年之前,JS开发者,JS社区,大型网络公司,都推出了自己的JS模块化方案。

本文主要说一下早期JS开发者们是如何实现JS模块化的,并就实现的过程来探究模块的本质以及发现的问题

最简单的模块化

由于将所有网页的行为都定义在一个JS文件中,会导致一系列问题

如下a.html和b.html都引入一个体量很大的big.js

// a.html
<script src="big.js"></script>
// b.html
<script src="big.js"></script>

为了加快了网页加载速度和减轻了服务器压力,且不让网页引入自己不需要的功能,

JS开发者们决定为每一个网页定制只包含自己所需功能的JS文件,这样既减轻了引入的JS的体积,又避免了引入不需要的功能

如下a.js中包含了a.html所需的所有功能,b.js中包含了b.html所需的所有功能

// a.html
<script src="a.js"></script>
// b.html
<script src="b.js"></script>

但是网页与网页之间会有很多相同的功能,如果这些相同的功能在每个网页的独立的JS文件中都定义一份,则即浪费内存,又不利于后期的统一整改。

所以需要将每个网页相同的行为提取到一个公共的JS文件中,然后需要这个功能的网页就按需引入特定的公共JS文件即可

如下common1.js是a.html和b.html的公共功能的js文件

// a.html
<script src="common1.js"></script>
<script src="a.js"></script>
// b.html
<script src="common1.js"></script>
<script src="b.js"></script>

此时,JS模块的一个重要特性展现出来:低耦合

即每个JS模块都只完成特定功能,而不是完成所有功能,我们在设计JS模块时,尽量往粒度小的方向设计,便于后期复用和整改。

但是此时又出现了新问题,比如引入的js出现了重复的变量定义,会发生覆盖,以及污染全局变量

// common1.js
var a = 1
// a.js
var a = 2
// a.html
<script>
    console.log(a)
</script>

这些问题的原因就是,通过script标签引入的js文件中代码,相当于在全局作用域下,所以每个JS文件中定义在顶层的变量都会变成全局变量,即污染了全局变量

为了解决这个问题,只能为每个JS模块定义一个单独的作用域,让JS模块内部变量不会影响全局变量,所以自然而然想到了函数,因为ES6之前除了全局作用域就是函数作用域。

这里的函数有两种选择:

1、普通函数

2、立即执行函数

而我们一般会选择立即执行函数,因为普通函数还需要一行调用代码,这样才能保证在script标签引入JS模块后,执行JS模块中代码,对应的普通函数才能执行。而立即执行函数会在script标签引入JS模块后立即执行。

// common1.js
(function(){
    var a = 1
})()
// a.js
(function(){
    var a = 2
})()
// a.html
<script>
    console.log(a)  // undefined
</script>

 这就解决了JS模块变量污染全局的问题,且又提出了一个模块的重要特性:

模块需要具备自身的模块作用域,即高内聚,或者说封装性。模块内部设计不对外开放。

而说到封装性,自然而然又想到了getter和setter,即模块不能自己玩自己的,它需要和外部交流,它需要提供入口接收外部信息,需要提供出口反馈处理结果

当这个实现不难,因为现在JS模块当前是一个立即执行函数,函数的特点就是可以接收外部参数,可以返回值

// common1.js
var moduleC = (function(window){
    var a = 1
    return {a}
})(window)
// a.js
var moduleA = (function(window){
    var a = 2
    return {a}
})(window)

 以上就是一种实现方案,即立即执行函数接收window,返回一个对象

我们需要思考的是,为什么需要接收window,如果不接受window的话,立即执行函数内部可以拿到window吗?

当然可以,我们知道被script标签引入的JS代码,都处于全局作用域下,所以此时立即执行函数也相当于在全局作用域下,如果立即执行函数内部需要window,则可以沿着作用域链找到全局作用域拿到window,但是这样比较浪费性能。

最好地方式就是在全局作用域下调用立即执行函数时传入window作为立即执行函数的参数,这样的效率要比沿着作用域链查找快。

其次我们需要思考的是,为什么立即执行函数返回的是一个对象,而不是变量本身?
这其实取决于你的模块到底需要返回什么,如果你的模块返回值确实只是单个变量,则return a没有问题,但是大部分情况下,模块返回都不是一个简单的变量,可能是多个变量,或多个变量和函数以及对象,以及模块后期拓展功能,相应的返回值也需要拓展,而对象的拓展性要比变量和函数好的多,所以绝大数情况下,我们将一个对象作为模块的返回值。

最后,我们思考下,将模块的返回值赋值给一个全局变量,会不会造成全局变量污染?

会,但是这是没有办法的,因为模块对外暴露时,总要有个载体,且能被外部访问到,所以只能将模块返回值定义到一个全局变量上,此时我们只能尽量避免在全局代码中使用模块对外暴露的变量,另一方面,我们对于模块暴露的变量命名需要尽可能的特殊化,不要使用常规变量名,最著名的例子就是jQuery的$符号

以上完成了模块的输入输出定义,那么模块的输入是否可以是另一个模块呢?

答案是可能的,因为模块之间的依赖的很普遍的行为,比如模块a 依赖于模块b,模块b 依赖于 模块c,所以必须要先产生c模块,才能产生b模块,最后才能产生a模块

所以模块之间想要依赖,则必须要先产生模块。

而浏览器端,JS模块产生,只有一个途径,script标签导入JS文件,所以模块的依赖和script标签导入JS文件的顺序形成了强关联。

<script src="c.js"></script>
<script src="b.js"></script>
<script src="a.js"></script>

上面引入顺序一旦发生改变,则会报错,比如

<script src="a.js"></script>
<script src="b.js"></script>
<script src="c.js"></script>

在script引入a.js时就会报错,找不到b(b.js的输出) 

以上示例,说明一个问题:模块之间的依赖,无法写在模块中,只能在一个HTML文件的script标签实现。

这就导致了一个致命的问题:

一旦HTML文件引入了较多的JS文件,你将无法从中显示地识别出模块之间依赖关系,只能依靠自身经验来发现依赖关系,且非常容易通过变更script引入js文件地顺序导致模块依赖报错。

<script src="aa.js"></script>
<script src="ca.js"></script>
<script src="a1.js"></script>
<script src="c.js"></script>
<script src="b.js"></script>
<script src="a.js"></script>
<script src="zz.js"></script>
<script src="er.js"></script>

比如上面代码,如果你不知道c,b,a之间存在依赖关系,一眼看去根本无法直观地识别出哪些模块有依赖。

另外,还可以引出一个模块化产生地性能问题,在模块化之前,上面代码引入地js文件都写在一个js文件中,所以网页只需要一个script标签就能完成加载,即底层只需要一次HTTP请求。

而现在经过模块化,需要多个script标签加载,且它们是同步加载的(异步加载需要给script标签加defer或async属性),即必须等上一个script标签加载完成,才能加载下一个script标签,这是因为DOM构建遇到script标签就会被阻塞,需要等待script标签加载js文件,且执行完js文件代码后,才能继续构建,即继续执行下一个script标签。这导致了网页加载被延迟。

另外,多个script标签意味着多次HTTP请求,我们需要知道HTTP请求不是一个简单的动作,每次HTTP请求之前,需要经过TCP三次握手,每次HTTP请求结束,需要TCP四次挥手,且HTTP请求过程中如果发生阻塞,如物理层,数据链路层,那么会造成极大的延迟。

还有每次HTTP请求都需要服务器处理,多次HTTP请求意味着服务器的压力变大。

所以,JS模块化解决了一些问题,但是带来了更大的问题:

1、模块之间依赖关系不清晰

2、多模块加载带来的网页加载延迟和服务器处理压力代价

当然随着ES6模块化到来与打包技术的产生,这两个问题已经得到了解决

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

程序员阿甘

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

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

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

打赏作者

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

抵扣说明:

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

余额充值