背景:Hackathon
最近团队在参加一场国际 Hackathon 活动,虽然名义上我也是项目的成员,但我不是主要参加者,主要负责人是一位架构师大佬。
经过讨论后,我需要帮助大佬完成一些他不太擅长的前端工作。
这个项目模板是从一个开源 VR 项目中拉下来的。
我打开后,发现目录结构如下:
没有使用 node.js 和 npm,所以我称它是纯 umd 项目。
可能研究 VR 的大佬不怎么喜欢研究前端吧,或者对前端工程化完全不感兴趣。
在 index.html 中一堆包直接导入就好了。
这种用法一时间仿佛让我回到了十年前,但我不打算深究这个问题。
团队经常参加 Hackathon 活动来为我们的开源项目增加知名度,但是 Hackathon 项目一般都不会维护,开发完拿过去演示一下,基本上就再也不会碰了。所以我也没必要再去折腾它的前端工程化。
我现在要做的事,就是在 2 天之内为这个项目增加几个功能。
它的作用就是用 three 画个地板空间,空间的一端有两个屏幕。一个是老师的摄像头,一个是老师的屏幕。
学生会蹲在地板上听老师讲课。
和普通的直播项目不一样的是,你可以戴着 VR 参加直播。
虽然项目的前端看上去有些落后,可是技术含量一点儿都不低。
一切似乎很顺利。
问题出现了!
可是当我需要为项目引入两个包时,却碰到了问题。
- nanoid:id 生成器。
- presencejs:p2p 通信。
由于我必须生成唯一的 nanoid,所以要使用 nanoid 这个库。
可是 nanoid 的官网并没有介绍通过 umd 的方式使用,于是我查了下 issues。
果然有人碰到了和我一样的问题,但是 nanoid 的作者回复,说 umd 已老,我们不考虑支持 umd 了!
至于 presencejs,本来就是我维护的,v1 版本确实也不支持 umd。
umd 和 esm 混用!
那该怎么办呢?
两种选择:
- 折腾前端工程化,把项目用 npm 管理起来,通过打包工具构建项目。
- 通过 esm 的方式导入第三方库。
很明显后者成本更低,但它也有个问题,就是不支持老浏览器。
我立马询问了架构师这个项目的运行环境,是在最新版本的 Chrome 中运行。
那这个问题也不用考虑了。
具体用法就是在 esm 中把 umd 需要的 API 挂载到 window 对象上,伪代码:
<script type="module"> import { nanoid } from "https://unpkg.com/nanoid@3.1.23/nanoid.js"; window.nanoid = nanoid; </script>
不过在 esm 和 umd 混合使用的过程中又碰到了些问题。
执行顺序问题
esm 是异步加载、异步执行。umd 是立即加载、立即执行。
如果 umd 依赖 esm 怎么办?
比如我加载一个 umd 文件,立马要通过 nanoid 生成一个 id。但是此时 nanoid 还没有加载出来。
umd 有一个 async 和 defer 属性,可以让 js 文件以异步或者延迟的方式加载。
但是仍然没有用,因为 esm 和 umd 都是异步执行,没办法保证哪个先执行哪个后执行。
这个问题也有两个办法可以解决:
- 把 umd 改造成 esm。
- 在 esm 中等第三方库加载结束再调用 umd。
因为项目中有大量的 umd 文件,第一种方案风险明显更高。
那么选择第二种方案,伪代码如下:
定义 import UMD 函数。
function importUMD(url) { return fetch(url) .then(response => response.text()) .then(txt => eval(txt)) }
<script type="module"> import { nanoid } from "https://unpkg.com/nanoid@3.1.23/nanoid.js"; window.nanoid = nanoid; importUMD('./my.js') </script>
依赖问题
解决了 nanoid 的问题之后,我又发现了另一个问题。
presencejs 又依赖了一些 peer 级别的库。
在 esm 文件的开头会有一堆导入:
import { interval, Subject } from 'rxjs'; import { distinctUntilChanged, filter, map, takeWhile } from 'rxjs/operators'; import { WebSocketSubject } from 'rxjs/webSocket';
解决方案就是导入 rxjs 的 umd CDN。
<script src="https://unpkg.com/rxjs@^7/dist/bundles/rxjs.umd.min.js"></script>
然后把 presencejs 的依赖项改为如下这种写法:
const { interval, Subject, operators, webSocket } = window.rxjs const { distinctUntilChanged, filter, map, takeWhile } = operators const { WebSocketSubject } = webSocket
所幸 presencejs 依赖的第三方库只有 rxjs 一个,如果依赖了很多 peer 级别的第三方库的话,那需要导入的内容就多了去了。
JavaScript 模块规范的发展
我发现最近这几年,越来越多的库都在渐渐不支持 UMD 规范。原因大致相同,大家都认为这种用法已经过时了,应该被淘汰,大家应该拥抱官方的 ESM 规范。
这种想法是对的,但是没有向后兼容。
一段 JavaScript 代码的运行环境有非常多种,比如浏览器脚本、浏览器模块、Nodejs、deno、bun 等。理想状态是我们编写一份代码,就可以在全平台运行。当然依赖某些平台特性的 API,比如浏览器的 DOM API、Nodejs 的 process 等除外。
但现实情况根本不现实,必须通过编译工具对代码进行编译,输出多份文件,分别对应不同的环境。
最初 UMD 就是为了解决上述问题而产生的规范,一份 UMD 的编译结果,可以在所有平台上运行,这也是被大家认可和接受的。但如今它似乎已经是一个即将被时代淘汰的过度品,社区越来越拥抱 ESM 了。