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

时间:2014-08-12 15:40:31

标签: 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开发人员从不破坏其数据结构的向后兼容性?我真的很想知道!

4 个答案:

答案 0 :(得分:6)

  

这种情况基本上永远不会发生

是的,我的经验确实是Node / JS生态系统中的问题。我认为这部分归功于robustness principle

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

基元,早期

我认为首要的原因是该语言为原始类型(Number,String,Bool,Null,Undefined)和一些基本复合类型(Object,Array,RegExp等)提供了一个共同的基础。

因此,如果我从其中一个库中收到一个字符串'我使用的API,并将其传递给另一个,它不会出错,因为只有一个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

这非常令人沮丧,因为在上面的例子中只有补丁版本不同。这两种数据类型名义上可能不同,ADT定义和底层内存表示可能完全相同。

作为一个例子,它可能是intersperse函数的一个小错误修正,保证了1.2.2.1的发布。在我假设的例子中,如果我只关心某些Text并比较他们的length,那么这与我完全无关。

复合类型,对象

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

我认为这要归功于robustness principle

  

在发送的内容中要保守,在接受的内容中保持自由。

因此,如果我正在创作一个JS库,我知道它返回promises并将promises作为其API的一部分,我将非常小心地如何与接收到的对象进行交互。例如。我不会在其上调用花哨的.success().finally()['catch']()方法,因为我希望尽可能与不同的用户兼容,具有{{1的不同实现}}秒。所以,非常保守地说,我可能只使用Promise,仅此而已。此时,如果用户使用我的lib返回的promises,或.then(done, fail)'或者即使他们手写自己的,只要那些人遵守最基本的Bluebirds法律' - 最基本的API合同。

这仍然会导致运行时破损吗?是的,它可以。如果即使是最基本的API合同也未得到满足,你可能会得到一个例外,说明" Uncaught TypeError:promise.then不是函数"。我认为这里的技巧是图书馆作者明确了解他们的API需求:例如提供的对象上的Promise方法。然后由那些建立在该API之上的人来确保该方法可用于他们传入的对象。

我还想在此指出,Haskell的情况也是如此,不是吗?我是否应该如此愚蠢地为类型类编写一个实例而仍然不遵守其定律进行类型检查,我会得到运行时错误,不会赢得我吗?

我们从哪里开始?

刚才考虑过这一切,我认为即使在Haskell中我们也可以获得健壮性原则的好处,与JavaScript相比,运行时异常/错误的风险要小得多(甚至没有(?)):我们只需要足够粒度的类型系统,以便它可以区分我们想要操作的数据,并确定它是否仍然安全。例如。上面假设的.then例子,我打赌仍然是安全的。如果我尝试使用Text,编译器应该只是抱怨,并要求我对其进行限定。例如。使用intersperse因此可以确定我要使用哪一个。

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

就在最近,我找到了bookkeeper,这是一个编译时类型检查的匿名记录实现。

请注意:以下是我的猜想,我没有花太多时间尝试使用Bookkeeper。但我打算在我的Haskell项目中看看我下面写的内容是否可以用这样的方法实现。

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

T.intersperse

因为函数也是一等值。无论哪个API采用此emptyBook & #then =: id & #fail =: const :: Bookkeeper.Internal.Book' '["fail" 'Data.Type.Map.:-> (a -> b -> a), "then" 'Data.Type.Map.:-> (a1 -> a1)] 作为参数都可以非常具体地说明它需要什么:即Book函数,它必须匹配某个类型的签名。并且它不关心任何其他可能存在或不存在任何签名的功能。所有这些都在编译时检查过。

#then

结论

也许Bookkeeper或类似的东西在我的实验中会变得有用。也许Backpack会用其常见的界面定义来抢救。或者其他一些解决方案。但不管怎样,我希望我们能够利用稳健性原则。并且Haskell的依赖管理也可以“正常工作”。大部分时间只有在真正有保证的情况下才会出现类型错误。

以上是否有意义?有什么不清楚的吗?它能回答你的问题吗?我很想听到。

进一步可能的相关讨论可以在this /r/haskell reddit thread中找到,这个主题不久前出现了,我想把这个答案发布到这两个地方。

答案 1 :(得分:4)

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

  • 模块A

    exports = require("c") //v0.1
    
  • 模块B

    console.log(require("a"))
    console.log(require("c")) //v0.2
    
  • 模块C

    • V0.1

      exports = "hello";
      
    • V0.2

      exports = "world";
      

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

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

简答:

确实如此。因此节点无法处理冲突的版本。

你没有在互联网上看到它的原因是gustavohenke解释说节点自然不鼓励你污染模块之间的全局范围或链传递结构。

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

答案 2 :(得分:2)

我没有在大型JS程序中掌握这种情况的第一手经验,但我猜想它与OO风格的数据捆绑以及作用于该数据的函数有关。成一个对象。实际上" ABI"对象的一个​​方法是通过名称从字典中提取公共方法,然后通过将对象作为第一个参数传递来调用它们。 (或者字典可能包含已经部分应用于对象本身的闭包;它并不重要。)

在Haskell中,我们在模块级别进行封装。例如,使用一个定义类型T的模块和一堆函数,并导出类型构造函数T(但不是它的定义)和一些函数。使用这种模块的常规方法(以及类型系统允许的唯一方法)是使用一个导出函数create来创建类型T的值,以及另一个导出函数{{1使用consume类型的值:T

如果我有两个不同版本的模块,其中consume (create a b c) x y z的定义不同,我可以使用版本1中的T和版本2中的create,那么我&# 39; d可能会发生崩溃或错误的答案。请注意,即使两个版本的公共API和外部可观察行为相同,也可以这样做;或许版本2具有consume的不同表示,可以更有效地实现T。当然,GHC的类型系统阻止你这样做,但动态语言中没有这样的安全措施。

您可以将这种编程风格直接翻译成JavaScript或Python等语言:

consume

它与你所说的问题完全相同。

然而,使用OO风格更为常见:

import M
result = M.consume(M.create(a, b, c), x, y, z)

请注意,仅从模块中导入import M result = M.create(a, b, c).consume(x, y, z) create在某种意义上是从我们从consume返回的对象导​​入的。在你的foo / bar / corelib示例中,让我们说foo(取决于corelib-1.0)调用create并将结果传递给bar(取决于corelib-2.0),它将调用{{ 1}}就可以了。实际上,虽然foo需要依赖于corelib来调用create,但是bar根本不需要依赖于linib来调用consume。它只使用基本语言概念来调用create(我们可以在Python中拼写consume)。在这种情况下,consume将最终从corelib-1.0调用getattr的版本,而不管任何版本的corelib bar"取决于"。

当然,为了实现这一点,corelib-1.0和corelib-2.0之间的corelIB的公共API必须没有太大的改变。如果bar想要使用在corelib-2.0中新增的方法bar,那么它将不会出现在由corelib-1.0创建的对象上。尽管如此,这种情况比我们在原始Haskell版本中要好得多,即使是那些根本不影响公共API的更改也会导致破坏。也许bar依赖于它创建和使用的对象的corelib-2.0功能,但只使用了corelib-1.0的API来使用它在外部接收的对象。

要在Haskell中实现类似的功能,您可以使用此翻译。而不是直接使用底层实现

consume

我们使用API​​软件包corelib-api:

中的存在主义来结束使用者界面
fancyconsume

然后在单独的包corelib中实现:

data TImpl = TImpl ...     -- private
create_ :: A -> B -> C -> TImpl
consume_ :: TImpl -> X -> Y -> Z -> R
...

现在foo使用corelib-1.0来调用module TInterface where data T = forall a. T { impl :: a, _consume :: a -> X -> Y -> Z -> R, ... } -- Or use a type class if preferred. consume :: T -> X -> Y -> Z -> R consume t = (_consume t) (impl t) ,但是bar只需要使用corelib-api来调用module T where import TInterface data TImpl = TImpl ... -- private create_ :: A -> B -> C -> TImpl consume_ :: TImpl -> X -> Y -> Z -> R ... create :: A -> B -> C -> T create a b c = T { impl = create_ a b c, _consume = consume_ } 。类型create存在于corelib-api中,因此如果公共API版本没有更改,那么即使consume链接到不同版本的corelib,foo和bar也可以互操作。

(我知道背包对这类事情有很多话要说;我提供这种翻译是为了解释OO程序中发生的事情,而不是作为一种应该认真采用的风格。)< / p>

答案 3 :(得分:-1)

这是一个主要回答相同问题的问题:https://stackoverflow.com/a/15948590/2083599

Node.js模块不会污染全局范围,因此当他们需要时,他们将对需要它们的模块保密 - 这是一个很棒的功能。

当2个或更多包需要同一个库的不同版本时,NPM会为每个包安装它们,因此不会发生任何冲突。
当他们不在时,NPM将只安装一次lib。

另一方面,Bower是浏览器的包管理器,它只安装平面依赖项,因为libs将转到全局范围,因此你无法安装jquery 1.xx和2.xx他们只会导出相同的jQuery$ vars。

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

考虑到那里的大多数软件包尚未达到v2.0.0,我相信他们在从v0.x.x到v1.0.0的交换机中保留了相同的API。