英文:Lea Verou,翻译:前端大全 / 1bite前端
今天早上,我在构思写一个库来包装及增强 querySelectorAll 时忽然想到,相比直接引入 Parsel,更好的作法是先检测它是否已经加载,若是已经加载,就用它来作解析;若是没有加载,就用本身手撸的正则表达式作解析(反正根据我这个库要作的事情来看,这个方案足以覆盖大部分场景)。正则表达式
之前,因为每一个库都会加载到全局名字空间中,因此用如下代码就能搞定:缓存
if (window.Parsel) { let ast = Parsel.parse(); // 根据AST能够正确的重写选择器 } else { // 正则表达式方案 }
然而,在 ESM 模块系统里,彷佛没有办法检测某个库是否已经导入过了,除非你本身的代码显式导入过。网络
为这事我还专门发过一条推:框架
ESM 模块系统(及其它模块系统)的一个缺点是,你没法指定无关紧要的依赖项。编辑器
使用全局名字空间,你能够先检查某个全局变量是否存在,而后走相应的分支。而使用 ESM 模块,就没法检测某个库是否已经导入过,除非你已经在某处导入过。ide
— Lea Verou (@LeaVerou) 2020/11/19性能
我觉得这个情形很常见,你们都应该能理解这种作法的好处。然而,当我发现网上有很多人并不理解我要干啥时,我仍是很惊讶的。他们中大部分人觉得我想作的是模块条件导入或者模块导入失败后的错误恢复。测试
我想了一下,多是由于我是从库做者的角度思考如何写 JS 的。做为一个库做者,我是没法控制宿主环境的。但对于大多数开发者而言,他们是以开发某个具体的 APP 或者网站的角度去思考如何写 JS 的。网站
在 Kyle Simpson 要求我详细阐述使用场景后,就有了这篇文章。
我描述的情形本质上是一种 “渐进式加强”(实际上,我曾经还想过把这篇博文取名为 “JS 之渐进式加强”)。若库 X 已经被其它代码加载了,则用它完成更复杂但覆盖了全部边界状况的功能;不然只保留一些基本功能。这套方案针对的是那些本身的代码不真正“依赖”的依赖项,也就是那些锦上添花的依赖项。
咱们常常会看到这样的现象,有些模块功能虽然很是完备,但就是使用了一堆依赖项。把这样的库添加到即便是最简单的项目中,也会让项目忽然变得很庞大。究其缘由,是这些库须要考虑各类意外状况,而咱们的项目可能并不关心这些边界状况,甚至这些边界状况都不会出如今咱们项目中。咱们也常常看到另一种现象,有些模块虽然是零依赖的,但又重造了不少现有的轮子,或者干脆缺失某些功能。
而我提出的这种范式,则兼具上述两个优势:零依赖(或者少依赖),又能利用系统中已经导入的模块增强本身尚未反作用。
使用这种范式,依赖项的大小就再也不是个问题,由于它们如今是无关紧要的同版本依赖,今后你能够尽情挑选合适的库,不再用被包体积束手束脚。并且,同时用多个也能够!不止一个库能助你实现需求,若是系统中已经存在一些更庞大、更完备的库,就直接用它们;而若是它们尚未加载,就回退至那些微型库。
再举几个场景:
若是系统中已存在 Prism , Markdown 转 HTML 的转换器就能够支持代码语法高亮,甚至支持多个语法高亮器。
若是系统中已存在 Icrementable ,代码编辑器就能够用它实现箭头键控制数值自增。
若是系统中已存在 Dragula ,模板库就能够用它来实现列表条目拖拽排序。
若是系统中已存在 Tippy ,测试框架就能够用它来实现更友好的提示信息弹出框。
若是已经加载了某个能计算代码体积的库,代码编辑器就能够用它来显示代码体积(以 KB 为单位);若是 gzip 库能用,则这个代码编辑器能够用它来显示经 gzip 压缩事后的代码体积。
若是能用某自定义元素,UI 库就能够先尝试使用该自定义元素,不然,就使用功能相近的原生元素(好比某个超炫酷日期选择器 vs <input type="date">)。又好比,系统中已存在 Awesomplete ,则能够用它来实现自动补全,不然就使用简单的 。
这个模式甚至能够与条件加载相结合,例如:咱们能够检查全部已知的语法高亮器,若是都没有加载,再加载 Prism。
回顾一下,这个模式优点主要体如今如下方面:
* 效率方面:好比使用网络加载模块,而 HTTP 请求代价很高的时候;好比直接打包到包里,又会增长包体积的时候。就算包体积不是问题,若是在不须要的时候走虽然周全但相对较慢的路径,也会影响运行时性能,由于这时简单的逻辑就够用了。
* 选择性方面:比起一个功能就选用一个库,如今能够支持多库备用。例如:多款语法高亮器,多款 Markdown 语法解析器等等。若是你要完成的功能必定得要某个库,也能够在其它能支持的库都没有加载的状况下再加载它。
弱依赖是反模式吗?
这篇文章发布后,我收到了一些这样的反馈:“弱依赖是一种反模式,由于你没法预测使用了这种模式的模块的行为。若是你引入了某个库,又不想其它库使用它,这时该怎么办?这种状况下,使用参数注入来显式提供这些库的引用会更好。”
对此,我有几点不一样的见解。
首先,若是弱依赖项运用得当,它们只会被用来增强缺省/基础行为,因此不太可能不使用弱依赖项而回退到缺省行为。
其次,弱依赖项与参数注入的方式并不冲突。它们能够一块儿使用,并互相完善。好比弱依赖项能够用来选择更合理的缺省库,而后再使用参数注入方式作进一步调整(或者彻底禁用)。只保留参数注入会给使用库带来高昂的前期认知成本(参考 约定优于配置)。好的 API 让复杂的事变简单,让简单的事变容易。 常见的例子是,若是加载了某语法高亮模块,你确定但愿用它来作语法高亮;若是加载了某解析器模块,你确定是首选它作解析,而不会选择正则表达式。而那些不太常见的边界情形,好比你不想作语法高亮或者想用另一个解析器模块,仍然能用参数注入方式实现,但并不表明其它方式就不能实现。
最后,最终开发者可能并不知道已经加载了全部库,也就是说,开发者彻底有可能由于别的缘由引入了某个库却浑然不知。而弱依赖项模式是有能用的库就用,没有就不用,因此不存在引入多余库或者须要提早准备好相关库的这类问题。
这种模式如何与 ESM 兼容?
仍是有人(大部分为库做者)很是理解我提的问题,他们也提出了一些方案。
方案1: 在底层实现一个全局模块缓存,而 CJS 就自带这种东西。
CommonJS 就暴露了缓存 … 也许 ESM 也能够作到,缓存失效也容易作,代码覆盖率测试也不难 … 不过个人测试代码全是 CJS 的,不太想改