在日益复杂和多元的Web业务背景下,前端工程化这个概念经常被提及。“说说你对Web工程化的理解?” 相信很多初学者在面试时会经常遇到,而大多数人脑子会直接浮现出 Webpack,认为工程化就是 Webpack 做的那些事情儿,当然也不能说不对,准确说 Webpack 只是工程化背景下产生的工具。
工程化的目的是高性能、是稳定性、是可用性、是可维护性、是高效协同,只要是以这几个角度为目标所做的操作,都可称为工程化的一部分。工程化其实是软件工程中的一种思想。当下的前端工程化可以分为四个方面: 。
工程四部曲为题,此文我们以 为始来剖析前端工程化的概念,码字不易,先赞后看,养成习惯!
模块化其实是指解决一个复杂问题时 的过程,每个模块完成一个特定的子功能(单一职责),所有的模块按某种方法组装起来,成为一个整体,从而完成整个系统所要求的功能,不理解没关系,接着往下看。
早期在网页这个东西刚出现的时候,页面、样式都很简单,极少有交互以及设计元素,一个页面也不会依赖很多文件,逻辑代码非常少,就是静态页面那种,那个时候的前端叫网页设计。
随着 技术的发展,各种交互以及新技术等使网页变得越来越丰富,逐渐我们前端工程师登上了舞台,同时也使得我们前端同学的代码量急速上涨、复杂度在逐步增高,越来越多的业务逻辑和交互都放在 Web 层实现,代码一多,各种命名冲突、代码冗余、文件间依赖变大等等一系列的问题就出来了,甚至导致后期难以维护。
在这些问题上,其他如 java、php 等后端语言中早已有了很多实践经验,那就是模块化,因为小的、组织良好的代码远比庞大的代码更易理解和维护,于是前端也开启了模块化历程。
说到JS模块化,肯定少不了CommonJS、AMD、CMD、UMD、ESM,相信很多前端新同学甚至有些喜好摸鱼的老同学经常把它们搞混,如果你知道这些概念却不知道它们之间的联系,或者知道它们之间的联系却不了解核心实现,那请好好看此文。
早期前端三剑客中占据主导地位的 JS 不是一种模块化编程语言,规范中也没有模块(即module)的概念,所以模块的实现就显得很麻烦了,不过早期的前端工程师通过 JS 的语言特性来模拟实现了模块化。
早期JS模块化方案
普通函数
首先考虑到函数实现,因为 JS 中函数是有独立作用域的,并且函数中可以放任何代码,只需要在需要使用的地方调用即可,就比如下面代码:
可以看到这样做实现了代码分离及组织,看着挺清晰,其实是因为代码量小,如果函数过多,并且在多个文件中,还是无法保证它们不与其它模块发生命名冲突,而且模块成员之间看不出直接关系,还是会给后期的维护造成麻烦。
命名空间
在上面普通函数的方式中,很多变量和函数会直接在全局作用域下面声明,很容易产生命名冲突,于是,命名空间模式(namespace)就被提出了。
因为对象可以有属性,而它的属性既可以是数据,也可以是方法,刚好能够很好地满足需求,而且对象的属性通过对象名字来访问,相当于设定了一个命名空间。
我们来看看把模块写成一个对象,所有的模块成员都放到这个对象里面是怎么样的:
显然这是可行的,但是很快我们又发现了其缺点,对象内部属性全部会暴露出来,内部状态可以被外部更改,如下:
立即执行函数(IIFE)
尽管命名空间模式一定程度上解决了全局命名空间上的变量污染问题,但是它没办法解决代码和数据隔离的问题,大概在 2003 年,立即执行函数简称 出现了 ,它其实是利用函数闭包的特性来实现私有数据和共享方法,如下:
这样我们就可以通过 来获取 ,并且实现 属性的私有化,即外部调用不到:
那假如我们这个模块需要依赖其他模块呢?这时候就用到了引入依赖,即函数传参:
通过这种传参的形式,我们就可以在 模块中使用其他模块,从而解决了很多问题,这也是现代模块化规范的思想来源。
依赖注入
模块化发展的历程中还有模版定义依赖、注释定义依赖等方案,这些我觉得并不具有很强的学习性质,不再赘述。我们下面说说依赖注入(Dependency Indection, DI),说到这个,不得不提起三大框架之一的 ,它诞生于 2009 年,其核心特性之一就是依赖注入。
假如我们有两个原始模块 和 :
我们编写一个函数 ,想要使用上面两个模块,我们可以通过下面这种方式:
我们也知道,上面这样的代码无论从哪个角度看都很不灵活,我们不知道这段代码中有哪些依赖,也不能对引入的依赖进行二次修改因为会造成原函数的更改,这个时候我们要做的就是将依赖的函数作为参数显式的传入:
问题又来了,如果我们在很多地方都调用了函数 ,后面突然有需求需要调用第三个依赖项怎么办呢?难道要去修改调用处函数传入参吗?这样做也可以,但很不明智,那这里就需要一段代码帮助我们做这个事情,也就是所谓的依赖注入器,它需要帮我们解决下面这几个问题:
我们来简单实现一个依赖注册器,我们新建一个 对象,它是独立的,以便它能够在我们应用的各个部分都拥有同样的功能。
可以看到,这个对象非常简单,只有三个属性, 用来保存依赖, 用来添加依赖,最后的 用来注入依赖。
函数需要做的事情很简单,先检查 数组,然后在 对象中寻找依赖,依次添加至 数组中, 参数存在则指定其作用域,返回的函数中将其参数使用 的方法传入我们传递回去的 回调。
再来看使用:
调用时,我们也可以传入额外的参数:
由此,我们实现了一个简单的依赖注入,依赖注入并不是一个新的东西,它在其他语言中存在已久,它是一种设计模式,也可以说是一种风格。
早期的模块化演变过程中还有很多方案,就不一一写了。我们所说的模块化方案,并不是相互独立的,每种方案之间可能相互借鉴,就像依赖注入这种方式也用到了 ,一个好的模块化方案,无非就像是解决我们上面依赖注入提出的几个问题一样解决实际问题而存在。
随着前端发展对模块需求越来越大,社区中逐渐出现了一些优秀且被大多数人认同的模块化解决方案,慢慢演变成了通用的社区模块化规范,它们不仅解决了依赖注入的这些问题,还具备了很多独有的模块化特性,再到后面 ES6 的出现,也意味着官方(语言层面)的模块化规范 ESM 的落地。
JS模块化规范演进
CommonJS规范
简介
JS 标准定义的 API 只是为了构建基于浏览器的应用程序,并没有制定一个用于更广泛的应用程序的标准库。
而 规范的提出主要是为了弥补 JS 没有标准的缺陷,它由社区提出,终极目标就是提供一个类似 或 或语言的标准库,而不只是停留在脚本程序的阶段。
即用 编写出的应用不仅可利用 JS 来开发客户端应用,还可编写服务器端 JS 应用程序、命令行工具、桌面图形界面应用程序等。
2009 年,美国程序员 以 规范为基础创造了 项目,将 JS 语言用于服务器端编程,为前端奠基,从此之后 nodejs 就成为了 的代名词。
规范中规定每个文件就是一个独立的模块,有自己的作用域,模块的变量、函数、类都是私有的,外部想要调用,必须使用 主动暴露,而在另一个文件中引用则直接使用 即可,如下:
引用如下:
命令则负责读取并执行一个 JS 文件,并返回该模块的 对象,没找到的话就抛出一个错误。
上面也说过, 规范适用于服务端,也就是只适用于 ,其实简单来说就是 内部提供一个构造函数 ,所有模块都是构造函数 的实例,如下:
每个模块内部,都有一个 实例,该对象就会有下面几个属性:
总的来说 规范的特点有下面几个方面:
说了这么多,不如我们直接实现一个简单的。
核心实现
不多说,先上代码为敬,简单几十行代码,带大家体会一下 commonJS。
首先,我们创建一个 ,写如下代码:
有人会问: 明明是原生的,不是要手写实现吗?接着看。
新建一个 文件,全部代码如下,看一遍注释,后面再略微介绍下就 OK 了。
如上,附带注释也不过 80 行代码。
首先我们写了一个构造函数 ,其中 是唯一ID, 存文件的绝对路径, 存模块对应的导出结果。
我们还为 Module 添加了几个静态属性,其中 存放可解析模块扩展名,而在后面将扩展名作为 key,添加其解析方法。 则是缓存加载过的模块, 是一个数组,包含两个字符串项,两个字符串合起来就是一个函数字符串,它作为我们后面拼凑函数的数组。
其次还添加了一个静态方法 用于解析文件完整路径,还有一个比较核心的原型方法 ,用于加载模块。
平常我们使用 node 加载模块时,使用的是 方法,而我们手写则是用 方法,该方法传入一个文件路径(可省略后缀),方法中我们首先调用构造函数 Module 的 方法把传入的路径解析成一个绝对路径 ,接着校验 对象中是否存在以 路径为 key 的值,如果有,直接读取缓存。
如果缓存中没有,new 一个 Module 实例,再调用 方法加载模块。
最重要的是 的过程, 首先解析 字符串,拿到文件的后缀名,通过调用 中后缀名对应的方法加载对应文件,我们在代码中,已经为 添加了对应解析方法,也就是解析 js 后缀的文件。
文中的文件是 ,其后缀是 ,正好对应,调用该方法,传入 this(即 module 实例)。
目光来到 方法,其实也简单,首先通过 读取该文件内容,接着,开始拼凑一个方法,也就是下面这行代码:
此行代码拼凑出来的字符串 script 其实就是一个方法,只不过是字符串方法,如下:
再接下来,就是大家不太理解的 方法了,这里简单介绍下:
会创建一个独立的沙箱环境,执行对参数代码 的编译,运行并返回结果。该方法运行的代码没有权限访问本地作用域,但是可以访问 Global 全局对象。
这样说不理解的话,那大家总知道 吧!其实它和 类似,来看示例:
如上,使用 执行的字符串 code 并不会改变当前作用域,而 可以,仅此而已。
思绪回来, 把我们拼成的字符串方法,变成了一个可执行的方法,随后调用并传入参数:
由于使用了 方法,所以第一个参数是将转换后的 也就是函数 fn 的 this 指向变为 当前 实例,剩余三个即函数调用参数,回顾当时拼函数时这个函数的形参与当前函数调时传入值的对比:
三个参数分别是:
看到这里我想大家应该明白示例最开始的 中我们为什么可以直接使用 导出了,很明显因为在加载过程中,我们把整个 文件作为一块代码塞进了匿名的加载方法中,而这个加载方法在执行时,形参中存在 实例,所以我们就可以直接操作 实例,向其 属性中塞数据了!!!如此,一个非常简单的 commonJS 手写例子就结束了,你 Get 了吗?
最后总结,「简单点说,CommonJs 就是模块化的社区标准,而 Nodejs 就是 CommonJs 模块化规范的实现」,它对模块的加载是同步的,也就是说,只有引入的模块加载完成,才会执行后面的操作,在 服务端应用当中,模块一般存在本地,加载较快,同步问题不大,在浏览器中就不太合适了,你试想一下,如果一个很大的项目,所有的模块都同步加载,那体验是极差的,所以还需要异步模块化方案,所以 就此诞生。
AMD规范
简介
AMD(异步模块定义)是专门为浏览器环境设计的,它定义了一套异步加载标准来解决同步的问题
语法如下:
我们简单列举一些用法,如下,我们定义一个名为 的模块,依赖于 模块:
没有 ID 值的匿名模块,此时文件名就是它的标识名,通常都作为启动模块:
依赖多个模块:
模块输出:
模块内部引用依赖:
大家应该都知道 ,一个遵守 规范的工具库,用于客户端的模块管理。
它就是通过 方法,将代码定义为模块,通过 方法,实现代码的模块加载,使用时需要下载和导入,也就是说我们在浏览器中想要使用 规范时先在页面中引入 就可以了。
可以说 「 就是 的标准化实现」 。
核心实现
上面的用法大家可能都知道,我们接下来来简单实现一个 AMD 规范的模块加载器,类似 。
写之前,我们先把使用的例子写出来:
index.html 入口文件:
如上所示,大概就是引入 文件,然后使用它引入 和 两个依赖项并返回其相加的和。
我们来看模块 和 :
可以看到, 文件中返回或者说导出了变量 1。
而 文件确又依赖了模块 ,返回 的和。
最后的 模块文件则是直接返回了变量 2。
我们要做到的效果就是执行 文件,最终输出 5 即可。
接下来我们来手写,其实最主要的就是两个方法 ,还是先放代码再解释, 文件如下:
同样这也是简化版本,不超过 90 行代码,附带注释,大部分同学看一遍应该就懂了。不过分讲解,简单介绍一下流程。
调用 或者 方法,首先是根据依赖数组加载 js 文件,不同于 commonJS,AMD 基于浏览器,要读文件,我们只能动态创建 script 标签,所以 即创建script标签,用来加载文件模块。
script 引入文件加载完成后会触发 onload 事件,我们以此控制依赖的加载顺序。只有在 JS 模块加载完成后,才能执行其 callback 回调,但是我们引入的 JS 依赖项中都是使用 方法定义的,而 方法还可能会依赖某些 js 文件模块,但总有一个源头是不存在依赖的,如此,递归便派上了用场。
我们的目的是模块加载完成后执行 callbck 回调,但如果是 A 依赖 B,B 又依赖 C 等等的关系,我们想要执行 A 回调,那必须等 B 和 C 都加载完,所以我们使用一个栈(数组) 来存储 callback 回调,等所有依赖都加载完了,再依次执行,就和 Node 框架 koa 的洋葱模型一样。这是为了让 callback 回调函数的执行顺序正确。
大致如上,剩下还有一些校验,因为代码简单,不细说了。但可不要以为 requireJS 源码真的这么简单,并不是,真正的源码考虑了太多的东西,此代码只是为了方便大家理解,有兴趣自己看源码吧~
CMD规范
简介
的出现较为晚一些,它汲取了 和 规范的优点,也是专门用于浏览器的异步模块加载。
在 规范中,一个模块就是一个文件, 是一个全局函数,用来定义模块。
接受 参数, 可以是一个函数,也可以是一个对象或字符串。
为对象和字符串时,表示模块的接口就是该对象、字符串,如下:
为函数时,表示是模块的构造方法,执行该构造方法,可以得到模块向外提供的接口,即 :
为函数时,如下:
再来看看更多用法:
和上面 、 类似,「 是 在推广过程中对模块定义的规范化产出」 ,而 规范以及 在国内曾经十分被推崇,原因不只是因为它足够简单方便,更是因为 的作者是阿里的 大佬所写,同 Vue 一样的国人作者,堪称国人之光。
核心实现
对于 CMD 规范下的 SeaJS,同 AMD 规范下的 RequireJS 一样,都是浏览器端模块加载器,两者很相似,但又有明显不同,个人认为 SeaJS 的实现相对来说更精美一些,一度风靡前端圈,碍于篇幅,放在这里肯定是不合适的,后面有机会单独来介绍 SeaJS 的实现,此文我们先了解 CMD 与 AMD 区别即可。
CMD 与 AMD
对比 来说, 比较推崇 (尽可能的懒加载,也称为延迟加载,即在需要的时候才加载)。
对于依赖的模块, 是提前执行, 是延迟执行,两者执行方式不一样, 执行过程中会将所有依赖前置执行,也就是在自己的代码逻辑开始前全部执行,而 如果 引入了但整个逻辑并未使用这个依赖或未执行到逻辑使用它的地方前是不会执行的,不过 从 2.0 开始,也能改成延迟执行(根据写法不同,处理方式不同),另外一方面 推崇依赖就近,而 推崇依赖前置。
UMD规范
简介
UMD(Universal Module Definition),即通用模块定义,从名字就可以看出来,这东西是做大一统的。
它随着大前端的趋势所诞生,可以通过运行时或者编译时让同一个代码模块在使用 甚至是 的项目中运行,也就是说同一个 JavaScript 包运行在浏览器端、服务区端甚至是 APP 端都只需要遵守同一个写法就行了,那它是怎样实现的呢?
核心实现
我们来看看这样一段代码
可以看到, 是 语法,而 只在 中存在,你会发现它在定义模块的时候会检测当前使用环境和模块的定义方式,如果匹配就使用其规范语法,全部不匹配则挂载到全局对象上,我们看到传入的是一个 ,它在浏览器中指的就是 ,在服务端环境中指的就是 ,使用这样的方式将各种模块化定义都兼容。
其实社区形成的的规范还有很多,目的都是为了 JS 的模块化开发,只是我们上面说的这几个是最常用的。
截止到目前为止我们说的 、 、 等都只是社区比较认可的统一模块化规范,但并不是官方(JS语言层面)的,那接下来要说的这个就是 JS 的官方模块化规范了。
ES Module
2015年6月, 也就是我们说的 发布了,JS 终于在语言标准的层面上,实现了模块功能,使得在编译时就能确定模块的依赖关系,以及其输入和输出的变量,不像 、 之类的需要在运行时才能确定(例如 FIS 这样的工具只能预处理依赖关系,本质上还是运行时解析),成为浏览器和服务器通用的模块解决方案。
所以说在 ES6 之前 JS 是没有官方的模块机制的,ES6在语言标准的层面上,实现了模块化功能,而且实现的相当简单,旨在成为浏览器和服务器通用的模块化解决方案,其模块化功能主要由俩个命令构成:exports和import,export命令由于规定模块的对外接口,import命令用于输入其他模块的功能。ES6还提供了export default的命令。为模块指定默认输出。对应的import语句不需要大括号。这也更接近AMD的引用写法。
ES6 Module不是对象,import命令被JavaScript引擎静态分析,在编译的时候就引入模块代码。而不是在代码运行时加载,所以无法实现条件加载。也就使得静态分析成为可能。
export可以导出的是对象中包含多个属性、方法,export default只能导出一个可以不具名的函数。我们可以用import引入。同时我们也可以直接使用require使用,原因是webpack启用了server相关。
ES6模块运行机制与commonjs运行机制不一样。js引擎对脚本静态分析的时候,遇到模块加载指令后会生成一个只读引用。等到脚本真正执行的时候。才会通过引用模块中获取值,在引用到执行的过程中,模块中的值发生变化,导入的这里也会跟着发生变化。ES6模块是动态引入的。并不会缓存值。模块里总是绑定其所在的模块。
其实说白了,对于 JS 模块化,上述这些方案都在解决几个同样的问题:
不同的模块化手段都在致力于解决这些问题。前两个问题其实很好解决,使用闭包配合立即执行函数,高级一点使用沙箱编译,缓存输出等等。难点在于文件依赖关系梳理以及加载。CommonJS 在服务端使用 fs 模块同步读取文件,而在浏览器中,不管是 AMD 规范的 RequireJs 还是 CMD 规范的 SeaJs,其实都是使用动态创建 script 标签方式加载,在依赖加载完毕之后再执行,以此省去开发手动书写 script 标签还需关注加载顺序这一烦恼。
ESM 作为语言标准层面的模块化方案,不需要我们额外引入用于模块化的三方包,抛开兼容问题,绝对是最好的选择,也是未来趋势,这点在 Vite 上就足以证明。
读到这里,你是不是对模块化了解有了更清晰的认识呢?此文我们主要讲 JS 模块化,当然模块化并不是只有 JS,下文将会介绍 CSS 模块化的内容,欢迎关注!有问题评论区码字,也欢迎指错勘误!
Dependency injection in JavaScript[1]
【推荐看看】JavaScript模块化七日谈[2]
Reference
[1]Dependency injection in JavaScript: https://krasimirtsonev.com/blog/article/Dependency-injection-in-JavaScript
[2]【推荐看看】JavaScript模块化七日谈: https://github.com/Huxpro/js-module-7day

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/174092.html