为什么NPM的重复依赖政策有效?

Edw*_*ang 40 node.js npm

默认情况下,当我使用NPM管理包依赖于foo和bar时,两者都依赖于corelib,默认情况下,NPM将安装corelib两次(一次用于foo,一次用于bar).它们甚至可能是不同的版本.

现在,让我们假设corelib定义了一些在foo,bar和主应用程序之间传递的数据结构(例如URL对象).现在,我期望的是,如果这个对象有一个向后不兼容的变化(例如,其中一个字段名称发生了变化),而foo依赖于corelib-1.0而bar依赖于corelib-2.0,我会非常悲伤的熊猫:吧的corelib-2.0版本可能会看到旧版本的corelib-1.0创建的数据结构,但事情不会很好.

我真的很惊讶地发现这种情况基本上没有发生过(我搜索了谷歌,Stack Overflow等,寻找那些应用已经停止工作但可以通过运行重复数据删除修复它的人的例子.)所以我的问题是,为什么是这样的吗?是因为node.js库从不定义在程序员之外共享的数据结构?是因为node.js开发人员从不破坏其数据结构的向后兼容性?我真的很想知道!

Wiz*_*zek 7

这种情况基本上不会发生

是的,我的经验确实是在Node / JS生态系统中这不是问题。我认为部分原因在于健壮性原则

以下是我对原因和方式的看法。

原始,早期

我认为首要的原因是该语言为原始类型(数字,字符串,布尔,空值,未定义)和一些基本复合类型(对象,数组,RegExp等)提供了通用基础。

因此,如果我从我使用的一个库的API中接收到一个String并将其传递给另一个,则不会出错,因为只有一个String类型。

这是过去发生的事情,直到今天仍然在某种程度上仍在发生:图书馆作者试图尽可能地依赖内置函数,并且只有在有足够的理由,足够的谨慎和思想的情况下才有所不同。

在Haskell中并非如此。在开始使用之前stack,使用Text和ByteString遇到了以下情况很多次:

Couldn't match type ‘T.Text’
               with ‘Text’
NB: ‘T.Text’
      is defined in ‘Data.Text.Internal’ in package ‘text-1.2.2.1’
    ‘Text’ is defined in ‘Data.Text.Internal’ in package ‘text-1.2.2.0’
Expected type: String -> Text
  Actual type: String -> T.Text
Run Code Online (Sandbox Code Playgroud)

这非常令人沮丧,因为在上面的示例中,仅补丁程序版本不同。两种数据类型名义上可能仅不同,并且ADT定义和基础内存表示可能完全相同。

举例来说,它可能是该intersperse功能的次要错误修正,保证了的发布1.2.2.1。如果在这个假设的示例中,我关心的只是连接一些Texts并比较它们length的s,那与我完全无关。

复合类型,对象

有时,有足够的理由使JS与内置数据类型有所不同:以Promises为例。与许多API开始使用回调的回调相比,它是对异步计算的有用抽象。现在怎么办?当这些{then(), fail(), ...}对象的不同版本在依赖关系树的上,下和周围传递时,怎么会不会遇到许多不兼容性?

我认为这要归功于鲁棒性原则

在发送的内容上要保持保守,在接受的内容上要保持开放。

因此,如果我正在编写一个我知道返回承诺并接受承诺作为其API一部分的JS库,我将非常小心如何与接收到的对象进行交互。例如,我不会打电话花哨.success().finally()['catch']()它的方法,因为我想尽可能用不同的用户,不同的实现兼容Promise秒。因此,非常保守地讲,我可能只使用.then(done, fail),仅此而已。此时,只要用户Bluebirds遵循最基本的Promise“法律”(即最基本的API),用户是否使用我的lib返回的诺言,或者“或者甚至是他们自己手写的” 都无所谓合同。

这样还会在运行时导致损坏吗?是的,它可以。如果没有满足最基本的API约定,您可能会收到一个异常消息,说明“未捕获的TypeError:promise.then不是函数”。我认为这里的技巧是库作者明确说明其API的需求:例如.then,提供的对象上的方法。然后由在该API之上构建的人员来决定是否要确定传入的对象是否可以使用该方法。

我还要在这里指出,Haskell也是如此,不是吗?我应该这么愚蠢以至于为一个类型类编写一个实例,而该实例仍不遵循其规律进行类型检查,我会遇到运行时错误,不是吗?

我们从这里去哪里?

刚刚考虑了所有这些问题之后,我认为即使在Haskell中,我们也可以享受鲁棒性原则的好处,与JavaScript相比,运行时异常/错误的风险要少得多(甚至没有(?)):我们只需要typesystem足够精细,因此可以区分我们要处理的数据的用途,并确定该数据是否仍然安全。例如,Text上面的假设示例,我仍然打赌还是安全的。而且编译器仅应在我尝试使用时抱怨intersperse,并要求我对其进行限定。例如,T.intersperse这样可以确定我要使用哪一个。

我们如何在实践中做到这一点?我们是否需要额外的支持,例如来自GHC的语言扩展标志?我们可能不会。

就在最近,我找到了簿记员,这是一种在编译时进行类型检查的匿名记录实现。

请注意:以下是我的推测,我没有花很多时间尝试和尝试Bookkeeper。但是我打算在Haskell项目中,看看用这种方法是否真的可以实现我在下面写的内容。

使用Bookkeeper,我可以这样定义一个API:

emptyBook & #then =: id & #fail =: const
  :: Bookkeeper.Internal.Book'
       '["fail" 'Data.Type.Map.:-> (a -> b -> a),
         "then" 'Data.Type.Map.:-> (a1 -> a1)]
Run Code Online (Sandbox Code Playgroud)

由于函数也是一等值。无论哪个API将此Book参数作为参数,都可以非常具体地要求它:#then函数,即它必须匹配某个类型签名。而且它并不关心任何可能带有或不带有任何签名的功能。所有这些都在编译时检查。

Prelude Bookkeeper
> let f o = (o ?: #foo) "a" "b" in f $ emptyBook & #foo =: (++)
"ab"
Run Code Online (Sandbox Code Playgroud)

结论

也许簿记员或类似的东西对我的实验很有用。也许Backpack会使用其常用的接口定义来抢救。或其他一些解决方案。但是无论哪种方式,我都希望我们能够朝着能够利用健壮性原则的方向发展。而且,Haskell的依赖关系管理在大多数情况下也可以“正常工作”,并且只有在确实需要保证时才因类型错误而失败。

以上有意义吗?有什么不清楚的地方吗?它能回答您的问题吗?我很好奇。


/ r / haskell reddit线程中可以找到更多可能相关的讨论,这个话题不久前就出现了,我想将此答案发布到两个地方。

  • 健壮性原则是一堆废话,过去曾导致无数关键漏洞和数十亿小时的浪费。在你输出的内容上保持保守,在你接受的内容上保持保守,参见 http://langsec.org/。 (2认同)
  • langsec最初是一个提高解析安全性的理论框架。他们的东西在当今世界上更为普遍,“健壮”是关键术语。是的,乍一看,这篇文章似乎对它做了很好的介绍。如果您对更复杂的方面感到好奇,则应该可以在Twitter上与Meredith联系。她是一个令人着迷的人。 (2认同)

小智 5

如果我理解得很好,那么可能的问题可能是:

通过复制node_modules中的C_0.2和node_modules / a / node_modules中的C0.1并创建虚拟的packages.json,我想我创建了您正在谈论的案例。

B会有2个冲突的C_data版本吗?

简短答案:

是的。因此,节点不会处理冲突的版本。

您在Internet上看不到它的原因是因为gustavohenke解释说,节点自然不会鼓励您污染模块之间的全局范围或链式传递结构。

换句话说,您很少会看到一个模块导出另一个模块的结构。

  • 不幸的是 - 它不会:) 这就是政策。我刚刚花了一个小时阅读相关内容 - 似乎他们正在 GitHub 上讨论是否应该将其挂钩以在安装时执行“dedupe”cmd(重复数据删除)。似乎会,但将来...... (2认同)
  • 如果两个软件包都具有其声明表明可以共享的依赖关系,则软件包管理器将把该依赖关系提升到树上,因此仅需要安装一次。我认为npm是从v3开始的,yarn是这样做的。或者更确切地说,我认为所有程序包都应尽可能高地提升而不引起冲突,而不是尽可能地高。 (2认同)

gus*_*nke -3

这是一个主要回答相同问题的问题:/sf/answers/1116401331/

Node.js 模块不会污染全局范围,因此当需要它们时,它们对于需要它们的模块来说是私有的 - 这是一个很棒的功能。

当 2 个或更多包需要同一库的不同版本时,NPM 将为每个包安装它们,因此不会发生冲突。
如果不这样做,NPM 将仅安装该库一次。

另一方面,Bower(浏览器的包管理器)确实只安装平面依赖项,因为库将进入全局范围,因此您无法安装 jquery 1.xx 和 2.xx 它们只会导出相同jQuery$变体。

关于向后兼容性问题:
所有开发人员都会至少破坏一次向后兼容性!Node 开发人员和其他平台的开发人员之间的唯一区别是我们被教导要始终使用semver

考虑到大多数软件包尚未达到 v2.0.0,我相信它们在从 v0.xx 到 v1.0.0 的切换中保留了相同的 API。

  • 我认为这并不能真正回答问题。特别是,语义版本控制是不合逻辑的:即使我具有完全准确的包依赖项,我仍然可以将数据从具有特定包版本的一个本地作用域传递到具有不同包版本的另一个作用域。我很清楚模块的本地作用域可以防止一切“立即”破坏,但是,似乎不太可能永远不会有跨作用域的数据流。 (10认同)
  • 真正的原因是因为在 JavaScript 中创建新的数据类型很糟糕,而 JavaScript 中的多态性也很糟糕,所以大多数开发人员都使用普通的 JS 对象和鸭子类型。所以这意味着大多数时候旧版本的数据将与新版本的数据兼容。 (3认同)