「前端考古系列」一个需求引发的前端模块化考古

零、故事的开始

从前有个流行说法是"全国 13 亿人,每人给我一块钱我就是亿万富翁"

现在老板觉得这个主意很棒,所以让张三来做个网页方便收钱,界面简单点如下所示就好~

image-20210131002533398

可以看到这里就两个逻辑,点击红色按钮开始打钱,点击蓝色链接触发举报。是不是很简单~

这时老板跟张三说:"唔使急,最紧要快~ 5分钟后我要看到这个网页",这时候的张三的情绪毫无波动,什么软件工程可维护性模块化直接抛之脑后,满脑子只剩下一句"老夫写代码就是一把梭".

1473308168_167070

一、最初的面条代码

五分钟后张三写完了代码直接 scp 传到了公司服务器某现有全静态前端项目的目录下,把链接发给老板后长出了一口气...

现在的代码大概是这样的:




    
    
    亿万富翁计划


    

帮助 Nodreame 成为亿万富翁

现在的支付逻辑函数 payLogic 看起来似乎还过得去,但是现在老板不满意了:只支持微信支付明显是不够的,要是有土豪就是想要用支付宝、网银、paypal甚至比特币打钱怎么办?

image-20210131013238793

没办法,和老板对线不是明智之举,只能肝上去继续一把梭了,然后代码就变成了这样:




...
 

随着支付方式的增加,引入的脚本、支付参数、支付逻辑也随之增加. 这个为小需求而生的网页开始变得复杂.

张三想起上次临时接收"祖传屎山"的通宵分析代码经历,决定为了方便以后对项目的维护,现在尝试一下对项目进行一些优化.

当下最重要的当然是分析一下项目当前存在的问题:

  • 无私有空间:各支付渠道的支付参数只需要在模块内可以访问即可;
  • 全局变量污染:不应该将操作接口暴露到全局;
  • 依赖管理:支付逻辑没有显式标记对应的依赖

OK,就从这三个点的优化开始吧~

二、模块化意识的觉醒

由于每个支付渠道都有对应支付参数和支付流程逻辑,所以第一个想到的是可以将不同的逻辑拆分到不同的文件中:

截屏2021-01-31_上午2_12_45

如果是 Java 或者 C# 这个写法确实是能解决 全局变量污染 & 无私有空间 的问题(class 包裹),但是在 JS 中使用上面的分文件写法其实完全没有解决任何问题,即使将支付参数、逻辑函数分到不同文件,它们依旧会被暴露到全局变量中.

为了解决 全局变量污染 & 无私有空间 的问题,有前辈提出了原生解法 -- 利用立即执行函数实现"伪模块". 这样外部就无法访问支付参数,所以 无私有空间 问题就此解决,全局变量污染问题 得到了一部分解决(参数不再暴露,方法依旧挂载到全局):

image-20210131022450619

另外如果对立即执行函数传入 依赖 作为参数(例如 lodash),那么"伪模块"就可以"显式"地依赖某个库了:

截屏2021-01-31_上午2_37_52

但是很明显的,这个"显式依赖"并没有真正解决了依赖管理的问题.

当前 index.html 引入lodash,而"伪模块"文件中无需引用就直接使用了 lodash,那么如果在 index.html 中删除了对 lodash 的引用,那么"伪模块"逻辑的执行必定报错. 故这里的问题在于:未在调用处显式声明依赖项.

为了更好的解决这些问题,张三决定查看到技术社区找找可选方案.

三、了解社区规范

张三到技术论坛看了一圈,了解到了一堆社区方案 CommonJS、AMD、CMD、UMD,决定逐个了解一下再做决定.

1. CommonJS规范

CommonJS规范是 NodeJS 实现模块化参考的标准,看看其模块定义 & 加载的写法:

image-20210131182742977

NodeJS 中一般通过 module.exports 或者 exports 定义模块,再通过 require 加载模块.

由于其模块加载的设计是 同步 的,这对服务端从内存或者硬盘读取模块并无影响,但对于需要通过网络异步下载模块的浏览器端就不太适用了(在网络加载较慢的情况下,模块加载速度过慢会导致长时间白屏)

为了借鉴 CommonJS 的思想来解决浏览器端的模块化问题,大神们提出了 AMD 和 CMD 这两个 "异步加载模块" 的浏览器模块规范. 两者分别是 RequireJS 和 SeaJS 在推广过程中对模块定义的规范化产出.

2. AMD规范

1)概述

  • AMD(Asynchronous Module Definition) 即异步模块定义,是为了解决浏览器端模块化问题提出的规范.
  • 实现库:require.js
  • 主要 API:模块定义 define & 模块加载 require
  • 特点:推崇依赖"依赖前置"

2)实践

张三看完文档和教程后觉得方案可行,于是将支付模块基于 AMD 规范重构了,最新文件目录如下(pay/lib 为第三方提供的支付库):

image-20210201022444719

将原本的 js 逻辑移入 main.js 中,html 中只留下 require.js 的引入(requirejs会自动完成 main.js 的加载):

 

然后在 main.js 中配置路径(方便引入依赖) & 通过 require(依赖数组,回调函数)的方式写入原先的逻辑,并将支付相关逻辑放入对应的文件并根据 AMD 规范完成定义:

image-20210201022133266

从上面的 define 和 require 可以看出,AMD规范推崇"依赖前置",也就是定义模块 & 编写逻辑前先声明依赖的模块.

完成后刷新页面,可以看到 wx.js 的回调函数中的打印语句已经执行,说明AMD规范的 "依赖前置"会使依赖提前加载并执行其回调函数:

image-20210201022708486

3)小结

AMD 规范通过 define & require 实现了模块的定义和引用,解决了全局污染和私有性的问题.

同时也以"依赖前置"的形式实现了对模块的显式管理,但是写法是相对繁琐的.

3. CMD规范

1)概述

  • CMD(Common Module Definition)即通用模块定义,和 AMD 的目标相似,不过推崇的理念和模块处理的方法与 AMD 不同.
  • 实现库:sea.js
  • 主要 API:模块定义 define & 模块加载 require & use
  • 特点:推崇依赖就近 & 懒执行

2)写法

sea.js 的写法其实和 require.js 的写法很相似所以就不重写了,官方给出了下面的代码:

define(function(require, exports) {
  var a = require('./a'); // 获取模块 a 的接口, 就近书写
  a.doSomething();
}); 

从上面可以看出 CMD 推崇的写法不同于 AMD 的依赖前置,而是在使用到某模块功能附近才对模块进行 require 引用,称为"依赖就近".

CMD define方法里的函数被称为 factory ,包含三个参数 require, exports, modules,用于在 factory 中引用模块和暴露接口.

4. UMD

UMD 全称 Universal Module Definition,见名知义,该规范的目标就是平台通用.

当需要同时支持浏览器端和 NodeJS 端使用的时候一般会选用 UMD 规范打包的代码,例如 Vue:

image-20210201182209023

UMD 规范的代码特点是会通过一些对于 exports 和 define 的判断确定环境,例如 vue.js 中的:

image-20210201182504976

社区方案的提出就是在 JS尚未支持模块化的时期解决模块化问题,虽然够用但是还是略显繁琐,这时张三想起上次去某网站下软件导致整个电脑都是大天使之剑后来养成了凡是信官网的习惯,所以决定还是先看看官方模块化方案再做决定.

四、官逼同

image-20210201183224816

TC39 将 Modules 加入到 ECMAScript 2015 中,将文件区分成 脚本 Scripts & 模块Modules,使用 import & export 实现模块的导入导出,用起来也比 AMD/CMD 直观,ES Module 成为浏览器和服务器通用的模块解决方案。

张三看了ES6文档和大神文章,觉得这个由官方规定的标准肯定是以后的大趋势,以后现在学一波顺便在项目中练练手肯定不亏,于是马上行动了起来.

image-20210201215851498

一切都是那么顺畅美好,并且所有模块都能成功加载,只是在浏览器打开 index.html 运行时出现一点错误:

image-20210203010956967

这是在 index.html 中的