技术创想 | 模块化的发展历史

文摘   2022-09-16 18:14  

随着前端技术的飞速发展,在前端项目中更加高效更加清晰维护前端项目中的每个资源成了所有前端工程师的一个目标,继而出现了大量的解决方案。今天就简单一起回顾一下前端模块化的一个发展历史。

本期作者:领创集团-Ginee 前端工程师 曾祥瑞

石器时代:文件划分方案

在最早期的时候,前端没有完整且成熟的方案,但是大家希望能够将各自的业务或者功能独立出来维护。

所以在项目开发中,大家进行了一个约定,使用文件来划分模块。每个文件相当于是一个独立的模块,在使用某个模块的时候就将该模块引入到页面中,然后使用的引入模块的变量/函数

举个🌰

└─ project-1        ├── a.js        ├── b.js        └── index.html

// a.jsvar name = 'a'function a (){ console.log('this is module a')}
// b.jsvar name = 'b'function b (){ console.log('this is module b')}
// index.html<!DOCTYPE html><html><head> <meta charset="UTF-8"> <title>石器时代:文件划分方案</title></head><body> <script src="a.js"></script> <script src="b.js"></script> <script> // 调用方法a a() // this is module a // 调用方法b b() // this is module b // 获取全局变量name console.log(name) // b</script></body></html>

缺点:

  • 虽然有文件进行区分,但是各个模块也都是挂载在全局下的,所以会有大量的全局污染和命名冲突;

  • 没有独立的空间,模块之间是可以随意相互访问或者进行修改

  • 没有办法管理模块之间的依赖关系;

  • 无法在代码层面进行检查,只能是人为约定,一旦项目变大或者人员变动,后续难以维护;

青铜时代:命名空间方案

大家觉得既然无法通过文件名称来区分,那就约定一下,我们每个独立的模块,只向全局暴露一个对象,然后将该模块的全部变量挂载在当前这个对象下。

// a.jswindow.moduleA={ name: 'a', fooA: function(){ console.log('this is module a') }}
// b.jswindow.moduleA={ name: 'b', fooB: function(){ console.log('this is module b') }}
// index.html<!DOCTYPE html><html><head> <meta charset="UTF-8"> <title>青铜时代:命名空间方案</title></head><body> <script src="a.js"></script> <script src="b.js"></script> <script> // 调用方法a moduleA.fooA() // this is module a // 调用方法b moduleB.fooB() // this is module b // 修改变量 moduleB.name = 'c' moduleA.name = 'd'</script></body></html>

这样的书写方式也会暴露所有的模块成员,内部状态还是可以被外部改写,除了解决掉命名冲突之外,其他所有的问题,都还依然存在。


铁器时代:IIFE

短暂的经历了上面两个时代之后,大家想到了使用IIFE进行管理,这样就可以避免将所有内容都存储到全局命名空间中,有助于解决变量冲突并使我们的代码更加私密。也可以通过传参的方式,来表现模块的依赖关系。
// a.js(function(){    var name = 'a'    windows.moduleA = {        name:name    }})()
// b.js(function($){ var name = 'b' windows.moduleB = { name:name }})(jQuery)
但是这样的书写方式也存在一定的问题,没有统一的规范,引入js 需要使用<script>标签,引入多个<script>就会发多个请求,导致我们的请求过多。

在使用依赖的时候需要判断依赖模块是否加载完成,不知道依赖之间的先后顺序。

模块之间的依赖关系无法清晰的表现出来,当模块的依赖变多之后依赖关系错综复杂,维护成本比较高。

蒸汽时代:模块化规范方案

如今前端的模块化加载方案已经发展的非常成熟了,下面就介绍一下我们目前日常开发会接触到的模块化规范方案。

  • 4.1 CommonJS

CommonJS是2009年诞生的,它出现的目的是希望JS可以运行到更多地方,主要是服务端,nodejs采用了这种规范。之前的nodeJS是只支持CommonJS来实现模块的。Node verison 13.2.0 起开始正式支持 ES Modules 特性。

当然CommonJS目前也可用于浏览器端,但是需要使用 Browserify 进行提前编译打包。

CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。

到此,我们可以看出来,CommonJs 规范解决了我们的最开始的痛点。

它具有模块变量作用域的能力,即使多个模块内部有相同的变量名称,也不会有冲突;

它有导出导入模块的方式,同时能够处理基本的依赖关系;

根据它的规范,可以得知模块之间的加载顺序;

特点:

  • 每个文件都是一个模块,有独立的作用域,模块内部的代码运行在自己的作用域下,不会造成全局污染。

  • 模块可以多次加载,但是只会在第一次加载的时候运行,将运行结果进行缓存,后续的加载都是进行缓存结果读取。

  • 模块加载的顺序,按照其在代码中出现的顺序。

举个🌰:

└─ project-4    ├── moduleA.js    ├── moduleB.js    └── index.js
// index.jsrequire("./moduleA");var moduleB = require("./moduleB");console.log(moduleB);
// moduleA.jsvar module = require("./moduleB");setTimeout(()=>{console.log(moduleB)},1000);
// moduleB.jsvar module = new Date().getTime();module.exports = module;// 执行结果是先打印出来一个时间戳,然后1s后打印一个相同的时间戳

到此,我们可以看出来,CommonJs 规范解决了我们的最开始的痛点。

  • 它具有模块变量作用域的能力,即使多个模块内部有相同的变量名称,也不会有冲突;

  • 它有导出导入模块的方式,同时能够处理基本的依赖关系;

  • 根据它的规范,可以得知模块之间的加载顺序;

4.2 AMD

AMD是2010由RequireJS模块定义在其推广过程中的规范输出。AMD全称Asynchronous Module Definition 的意思就是“异步模块加载”。AMD 本身就是为了浏览器而设计的,CommonJS是'运行时同步加载模块',就是这个原因,可能会在加载大量资源的时候,相应时间很长,造成浏览器“假死”现象。

Require.js 实现了AMD 的规范,我们下面主要就是通过Require.js来举例。

特点:

  • 用异步方式加载模块,模块的加载不影响它后面语句的运行;

  • 前置依赖,需要所有依赖加载完成后,会执行定义好的回调函数;

  • 可以一次定义多个依赖模块;

  • 可在不转换代码的情况下直接在浏览器运行;

举个🌰:

└─ project-5    ├── moduleA.js    ├── moduleB.js    ├── index.js    └── index.html
// index.html<!DOCTYPE html><html><head>  <meta charset="UTF-8">  <title>AMD</title></head><body>  <!-- 需要先加载require.js -->  <script src="require.js"></script>  <script src="./index.js"></script></body></html>
// index.jsrequire(['./moduleA','./moduleB'],function(moduleA,moduleB){ console.log(moduleB)})
// moduleA.jsdefine(function(require){ var moduleB = require('./moduleB') setTimeout(()=>{console.log(moduleB)},1000);})
// moduleB.jsdefine(function(){ var date = new Date().getTime(); return date})// 执行结果也是先打印出一个时间戳,然后在1s后打印一相同时间戳

其实整体使用下来发现,AMD 和 CommonJS 都可以解决掉之前说的问题,目前绝大多数的第三方库都会支持AMD规范。

AMD的默认异步以及需要在回调函数中来定义模块内容,使得它在使用中会相对复杂一些,当项目中模块划分维度过于细致的时候,会出现一个页面对JS文件请求次数过多的情况。导致效率降低。

4.3 CMD

CMD 是 sea.js 在推广过程中对模块定义的规范化产出。

由于CMD很多行为和AMD非常类型,就不再赘述了。

AMD和CMD区别点:

  • 对于依赖模块:AMD是前置执行,CMD是延迟执行,不过RequireJS 从2.0开始,也已经支持可以延迟执行了。

  • CMD 推行的是依赖就近,按需加载;AMD是依赖前置。

4.4 UMD

如果我们书写的模块,需要同时运行在浏览器端也需要运行在Node.js 中,我们怎么来处理?

分别写一份AMD + 一份CommonJS 规范,分别运行在各自环境中的做法,实在是不可取。

所以它出现了,UMD(Universal Module Definition) 解决跨平台的方案。其实就是AMD和CommonJS的糅合。

其实UMD的原理比较简单,就是在运行时进行判断当前模块的类型。

CommonJS判断依据:node.js 的exports是否存在。

AMD判断依据:define 是否存在。

根据判断的结果来使用不同的方法进行导出即可。

4.5 ES Modules规范

ES Modules 是 ECMAScript 2015(ES6)中才定义的模块系统。在此之前,JavaScript 一直是没有模块体系的。

前面讲到的CommonJS、AMD、CMD都是在没有ES Modules之前由各个社区提出的解决方案,

但是随着官方推出ES Modules,模块化的“江山一统”指日可待!

特点:

  • 编译时进行加载,完全静态化的方式进行模块加载;

  • 模块输出的是值的引用,会随着原始值变化而变化;

  • export的导出的都是接口。import 引入的也是接口,不能对其进行修改;

  • import() 可以动态使用,加载模块;

ES Modules模块导入导出,主要是 import 和 export 这两个关键字。有多种组合的情况。

简单举个🌰

// 导入import './moduleA.js'import moduleA from './moduleA.js'import {name} from './moduleA.js'import moduleA,{name} from './moduleA.js'import * as module from './moduleA.js'import {name as moduleName} from './moduleA.js'
// 导出export let name = 'name'export { name1, name2, nameN };export { variable1 as name1, variable2 as name2, …, nameN };export default expression;

整篇文章是为了让大家对模块化的一个过程有所了解,如果需要具体的模块化使用方式,可以查询对应库的官网。

参考文献:https://developer.mozilla.org/zh-CN

领创集团知乎官方账号已开通!
每周一个技术干货分享!
期待你的关注
感谢阅读「技术创想」第61期文章
领创集团正在招聘中
期待你的加入
点击文末阅读原文
获取更多招聘信息



关于领创集团
(Advance Intelligence Group)
领创集团成立于 2016年,致力于通过科技创新的本地化应用,改造和重塑金融和零售行业,以多元化的业务布局打造一个服务于消费者、企业和商户的生态圈。集团旗下包含企业业务和消费者业务两大板块,企业业务包含 ADVANCE.AI 和 Ginee,分别为银行、金融、金融科技、零售和电商行业客户提供基于 AI 技术的数字身份验证、风险管理产品和全渠道电商服务解决方案;消费者业务 Atome Financial 包括亚洲领先的先享后付平台 Atome 和数字金融服务。2021年 9月,领创集团宣布完成超4亿美元 D 轮融资,融资完成后领创集团估值已超 20亿美元,成为新加坡最大的独立科技创业公司之一。
领创集团Advance Group
领创集团是亚太地区AI技术驱动的科技集团。
 最新文章