本月初的柏林JSConf EU 2019会议上,npm CLI 首席维护架构师 Kat Marchan 介绍了名为 Tink 的下一代包管理器技术概况。本文整理如下。
迄今为止,npm 生态系统已经拥有了近 1,000,000 个软件包,是规模最大的包管理平台。但这个生态系统及软件包管理器在诞生时并没有考虑如此复杂的局面,它们原本是适用于以 Node.js 生态系统为中心的小型项目和软件包的平台。
现在是时候重新定义适合现代 Web 开发的包管理技术了,而答案就是 Tink:它同样来自于 npm 团队,是下一代的 JS 包管理方案。Tink 将带来前所未有的性能表现、与 Node.js 等已有技术的深度兼容性,以及非常适合现代 Web 开发者的用户体验。
首先我们来看一个问题,那就是 npm 怎样才能使 JavaScript 应用正常工作?一直以来,npm 都有一个终极目标,就是在本地 Node 模块里塞很多内容,让所有工作都从本地 Node 获取数据,并且尽量加快这个过程。这个目标看起来很简单,但实现它需要哪些条件呢?
JS 安装程序流程
主流包管理器的架构都是类似的。他们会做很多优化,加入不少特色功能,但为了和 Node 兼容都需要做同一件事情。
上图是 JavaScript 安装程序的操作示意图。
读取本地依赖项(如果有的话)。这个操作一般得花点时间,也许长达几秒钟,或者起码能让你感觉得到延迟。Yarn 对这个问题做了一个挺有意思的优化——他们根据 Node 模块做一个元数据文件来做匹配,假定模块里的数据和元数据是对应的,这样就不需要传输那么多数据了。在这方面 npm 已经有点落伍了,将来我们也可能会学习 Yarn 做一些类似的优化,加快这个过程。不过不管怎样,这一步消耗的时间还是没那么久的。
接下来这个步骤就比较耗时了。从存储库中获取元数据非常占用网络资源,单单这个操作就可能需要数以千计的请求。所以我们搞了一个日志文件,其实它只是一个文件缓存,缓存包管理器的树计算步骤。这样只要你构建好项目后,你的同事们就不用再重复这个复杂步骤了——除非你们再添加或删除什么依赖项。所以我推荐大家使用日志文件,它能简化很多工作。
第三个步骤消耗的时间没那么长,不过这里还是值得一提的。我们在npm 3.0版本之后优化了这个步骤的操作,将树的计算与相关动作的延迟又缩减了一些。
到了第四步,下载并解压缺少的包是最最费时间的一步了。在这一步中,我们不仅要下载几百兆的数据,而且还要耗费大量的 CPU 资源来解压压缩包,同时当然也会占用很多磁盘 IO 能力。网络、CPU 和磁盘都有可能成为瓶颈,消耗大量的时间,也是最值得优化的一步操作。所有的包管理器都会在这方面下很大功夫,当然对我们也不例外。
那么我们能对此做些什么呢?
首先,我们可以做一堆缓存——不同的缓存策略有不同的权衡。这里 Yarn 的策略是解压后做缓存,所以占用的空间多一些,但是复制的需求就少一些;npm 的策略很有意思,它也有像 Yarn 一样的缓存,但用的是硬链接来减少复制的需求并节省空间。Yarn 还有一种叫 PMP 的工具也可以优化这个步骤的表现,但是 PMP 的兼容性是个问题。
数据都下载完毕,压缩包也都解压好之后,我们就要扫描准备安装的软件包并运行脚本了。这一步一般来说没那么费劲,速度都可以接受,但在一些项目中这一步还是会浪费很长时间。有些软件包管理器为了加快这个步骤会引入并行操作,代价就是风险和复杂度显著提升。不过 npm 没有采用这种策略。我认为我们最好使用 Node Preget 之类的解决方案。
总结下来,Node 模块变得非常庞大,这是最大的问题。很多麻烦都因此而生,诸如可用性和效率损失等等。很多人都在抱怨这件事,把责任推到包管理系统架构本身上。
但我还是觉得包管理的架构是很不错的。一方面,依赖项独立出来是很棒的事情,我们不用再为依赖关系而头疼。以前我们需要自己研究各种依赖关系,自己手动下载各种依赖项,现在这些都可以自动化嵌套,变得非常方便。每个独立项目都可以管理自己的依赖项,这就是很大的好处。
另一方面,现在 npm 的生态系统中已经有了 90 万个包,虽然其中也有很多垃圾,但多数包还是很有用的。这是一笔巨大的财富,我们这些开发者,我们整个开发社区应该继续想办法充分利用它们,而不是一股脑把这么好的资源抛弃掉。那么我们现在应该做什么来继续改进呢?我认为答案就是直接在运行时中实现包管理,不再把包管理当作是一项外部工具。
Tink 特性
基于上面的思路,我们开发出来了 Tink。Tink 能做什么呢?
那就是你不用再自己调用 Node 了,调用 Tink 就行,Tink 有一个名为 shell 的子命令,负责打包 Node,加些补丁,然后就可以生成一个叫 Virtual node_modules 的神奇产物了。
Virtual node_modules 有几大特性:
因为 Tink 是一个运行时包管理器,所以它基本上不再需要物理 Node 模块了,同时并不需要改动模块加载器或者包中的 API。包可以像往常一样访问文件系统,所以就能和之前的配置等细节完全兼容。
既然 Node 模块不存在了,那么文件都去哪儿了呢?Tink 把文件都保存在了类似 npm 那样的单一全局缓存里,然后通过哈希算法来清除所有重复数据。因为使用了哈希算法,所以 Tink 不用再存储重复的内容。就算你的一个项目有 5 个历史版本,Tink 也只会存储原始版本和每次更新的内容,大大节约了存储空间,同时优化了数据读写的性能表现。
既然现在包管理集成到了运行时中,那么当你需要依赖项的时候就不用再手动获取了,Tink 会自动根据需要下载所需的依赖项,完全不用你再操心了。你不需要的依赖项它也不会下载,你需要的依赖项它都能预先下载好。Tink 还设置了一种机制,防止管理器随机下载一些其实你用不到的内容。
这也就意味着你不用再操心 npm 包安装流程了。你只需要 Tink Add、Tink Move 就能安装、卸载包,后面有什么过程都会自动完成。如果你的同事推上来什么内容,你只需要拉下来就可以直接运行,具体的细节 Tink 都为你打点好了。这可是一项巨大的改进,值得掌声鼓励。
有人可能会担心,把 fs 也打包起来不是很危险吗?其实我们不觉得这有什么,因为 Electron 就是这么干的!他们在这方面很有一套,我们也会学习他们的经验,所以这并不是什么大问题。其实 Tink 的一些相关代码就是直接从 Electron 过来的,我还做了一些调整和改进。
其他特性
除了上面提到的这些,Tink 还带来了其他一些很棒的特性,包括:
作为运行时包管理器,Tink 直接提供了对TypeScript、ESM、Wasm 和 JX 的支持!
Tink 从全局缓存中加载依赖文件时会实时做校验,大大提升了安全性。
运行 Tink 时,如果你缺少某些依赖项,或者缓存中的某个文件已经损坏,那么 Tink 会自动为你下载并安装这些文件,自动修复你的依赖项。在生产流程中可以关闭这个选项,但在项目开发过程中这个功能非常方便,我也很喜欢它。
最后也是我最喜欢的特性:所有这些功能都是开箱即用的,无需专门配置,也用不着额外安装任何内容。这里要强调一下,所有这些特性都不需要任何类型的加载器,不需要额外的调整选项;Webpack 之类的内容都能直接使用。你需要做的只不过是安装并运行 Tink,然后一切就自动部署妥当了。
Tink 使用简介
首先来看 Tink 的核心功能,也就是 tink shell。如图所示,简单的一个命令就能获得上面提到的所有能力。你不需要再安装什么 npm 包,只要运行这个 Tink shell 即可。这个 shell 还是交互式的,可以屏蔽或等待各种内容。需要的时候它能为你自动获取依赖项,自动在运行时完成下载、解压等等操作,不需要你操心多余的事情。
之前我提过很多次效率这个话题。我们开发 Tink 的一个主要目的就是简化开发者的工作,让大家只需记住一点点内容,下载安装很少的数据就可以开始工作。之前人们用 npm 时这是个很大的问题,就好像要拖下来半个 npm 库,JavaScript 项目的开发工作才能准备就绪似的。
工作流程应该尽可能变得简单方便。Tink 要做的一件很重要的事情就是替开发者把准备工作都做好,在运行时尽量帮开发者节省时间。包管理器应该做到“大象无形”,让大家感觉不到它的存在,这才是坠好的。
大家都很熟悉 NPX。要是你还不知道 NPX 是什么的话,其实它就是一个 npm 内置的工具。你可以用 NPX 执行本地二进制文件,或者临时安装什么内容。比如说你想把 Jest 安装为一个依赖项,那么就可以用 NPX 临时安装它一下,无需全局安装。这样你就不用为它配置运行脚本了。Tink 就很像 NPX,可以提供和 NPX 类似的能力。通过 tink exec 这个命令来运行本地二进制文件,我们就可以在预装二进制文件时使用和 Tink shell 对脚本使用的相同逻辑了。
你可能会觉得包管理的那些操作会拖累你的工作流程,那么 tink prepare 这个命令就很有用了。这个命令可以提前预热缓存数据,搞定安装、二进制文件之类的必要工作。之后当你运行 tink shell 的时候,你会感觉好像已经提前安装好了所有 npm 包一样。
有人会说这个命令和 npm 安装流程有什么区别啊?不是一回事吗?其实也不一样。Tink 里这个命令是可选的,而且用了这个命令也不见得会加速之后的流程,甚至可能会拖慢也说不定,比如命令行模式下 Tink 可能会在运行时获取依赖项的。
接下来是 tink unwind 命令。这个命令是做什么的呢?比如说你还是想要完整的 Node 模块,因为你想用其他什么工具,或者你的编辑器还不支持 Tink 等等,总之就是需要 Node 模块。那么 tink unwind 命令就会把数据都解压到 Node 模块里,基本上相当于做了 npm 安装过程。这样一来你就可以以最大兼容性使用各种编辑器、构建工具之类,一切都和以往一样了。
不过 tink unwind 还是和 npm 安装略有差异,比如说,如果这时我只是要调试特定依赖项呢?那么就可以用 tink unwind <dep>
这个命令了。它可以只解压特定的依赖项到 Node 模块里,然后你就可以随心所欲地修补它、调试它,对它做各种各样的事情,等等。
这里要强调一点,那就是 tink 的工作机制是 Node 模块内部的任何内容的优先级都比 Virtual node_modules 更高。如果你在使用 FS.right 文件,tink 将在 Node 模块中为你创建一个物理文件,保证完全兼容性。类似的,你需要什么文件,Tink 都会给你准备好物理版本,不用担心出现兼容问题。对于使用安装脚本的依赖项来说,这个命令也会在包层级上自动执行,以保持最大兼容能力。
下面的问题是如何添加和构建依赖项呢?如上图所示,经典的三大命令,添加、删除和更新。这里要提一下,如果你不给这些命令附加任何参数,它们就会是完全可互动的。你可以用交互方式搜索新的依赖项,然后从菜单中选择依赖项来删除和更新它们。这个功能太棒了,毕竟是完全可互动的菜单!值得掌声鼓励!
我要介绍的最后一个命令是 tink check 实用程序。这里最棒的一点在于它是一站式的,可用于所有的验证和测试任务。值得注意的是它是开箱即用的,无需安装 TypeScript 就可以运行你需要的类型检查。你也可以把它安装为依赖项来用。
Tink 的下一步计划?
首先我们要把 Tink 的原型发展成一个可用的版本,毕竟现在还是项目的初期阶段嘛。
接下来的事情很重要,我们希望建立一个自己的基于 RFC 的团队,引入更多的外部贡献者。我们要比以前做 npm 时更加开放,让大家一起参与到 Tink 的发展历程,早早培养起一个开放社区,让人们都可以贡献自己的力量。
等我们准备就绪后,我们就会把 Tink 集成到 npm 的 8.0 版本中。将来你升级后,这个 Tink 包管理器就会作为 npm 的一部分开箱即用。你既可以在新版 npm 中继续使用以往的工作流程,也可以选择这个全新的工具和流程体验不一样的感觉。它还会和 NPX 集成,加速所有的 NPX 流程。
最后,等 npm 第 8 版正式发布后,我们就要开始将 Tink 与 Node 的核心集成在一起。这个工作的确很难,需要搞定很多复杂的问题,比如说撬开 fs 来做集成什么的。我们有很多人在做这件事,虽说前路漫漫,但是我们终将达成目标,放心吧。
其他事情
还有一件事要谈一下:我们计划在不久的将来发布一个新的包管理 API。这件事很值得期待,因为新的 API 在 Tink 等新技术的帮助下会带来很多优势。具体来看一下:
首先,新 API 可以获取独立的文件取代压缩包,这样就更加灵活,更节约资源。
其次,新 API 会减少多达 40%的数据传输需求,大大加快你的安装流程。毕竟很多人用的还不是超高带宽的光纤网络,这个改进当然还是很实用的。你用不着下载什么自述文件、更改日志、测试代码之类无关紧要的内容,只下载实际要用到的文件就行了,这样也能显著减少存储空间的需求。
上述功能主要是通过 tink shell 的懒加载获取功能实现的。一方面我们懒加载文件,另一方面使用很积极的缓存策略。
最后我们还有一项计划,就是改进单个存储库的发布流程。现在人们在发布存储库时可能需要同时同步很多小包,很容易遇到超时、某个包失效之类的问题。所有包的版本都要同步,实际作业时很容易搞到一团糟。而 Tink 将会改变这种状况,它会把 babel 作为单独的包发布,而且只会使用你下载的这些子包而不是整个大包,这样就简化了发布的内容,消除了很多问题。不过这个功能目前还在开发中,还需要一段时间才能出来。
总结
Tink 是很值得期待的项目,但现在还没做好准备与大家见面。谁要是下载了它开始用搞不好会惹出大事的,所以还是要多等一等。Tink 使用 Virtual node_modules 作为新的运行时,具备开箱即用的 TypeScript、ESM、JSX 和 Wasm 支持。它将随 npm 的第 8 版一同发布,将来会是 npm 的一个可选组件。此外,我们将来会加入一个新的包管理 API。
原视频链接:https://youtu.be/SHIci8-6_gs
评论 3 条评论