为什么NPM的重复依赖政策起作用?

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

现在,假设corelib定义了一些在foo,bar和主应用程序之间传递的数据结构(例如URL对象)。 现在,我所期望的是,如果这个对象发生了一个向后不兼容的变化(例如,一个字段名称发生了变化),并且foo依赖于corelib-1.0,bar依赖于corelib-2.0,悲伤的pandas:酒吧版本的corelib-2.0可能会看到由旧版本的corelib-1.0创build的数据结构,事情不会奏效。

我真的很惊讶地发现,这种情况基本上从来没有发生过 (我search了谷歌,堆栈溢出等,寻找其应用程序停止工作的人的例子,但谁可以通过运行重复数据删除来修复它)。所以我的问题是, 为什么这是怎么回事? 是否因为node.js库永远不会定义在程序员之外共享的数据结构? 是不是因为node.js开发者永远不会破坏其数据结构的向后兼容性? 我真的很想知道!

       

网上收集的解决方案 "为什么NPM的重复依赖政策起作用?"

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

  • 模块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并创builddummy packages.json,我想我创build了你正在谈论的案例。

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

简短的回答:

它呢。 所以节点不处理冲突的版本。

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

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

在一个大的JS程序中,我对这种情况没有亲身经历,但是我猜想它与将数据捆绑在一起的OO风格和将这些数据作用于单个对象的function捆绑在一起。 有效地,对象的“ABI”是从字典中拉出公共方法,然后通过传递对象作为第一个参数来调用它们。 (或者也许字典包含已经部分应用于对象本身的闭包;这并不重要。)

在Haskell中,我们在模块级别进行封装。 例如,取一个定义typesT和一堆函数的模块,并输出types构造函数T (但不是它的定义)和一些函数。 使用这种模块的正常方法(以及types系统将允许的唯一方式)是使用一个导出的函数create来创build一个typesT的值,而另一个导出的函数consumetypesT的值: consume (create abc) xyz

如果我有两个不同版本的模块,其中有不同的T定义,我可以使用版本1中的create和版本2中的consume ,然后我可能会崩溃或错误的答案。 请注意,即使两个版本的公共API和外部可观察行为是相同的,这也是可能的。 也许版本2具有不同的T表示,允许更有效地实现consume 。 当然,GHC的types系统阻止你这样做,但是在dynamic语言中没有这样的保护措施。

您可以将这种编程风格直接转换为JavaScript或Python之类的语言:

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

它会和你谈论的完全一样的问题。

但是,使用OO风格则更为常见:

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

请注意, 只有create从模块导入consume在某种意义上是从我们从create得到的对象导入的。 在你的foo / bar / corelib例子中,假设foo(依赖于corelib-1.0)调用create并将结果传递给bar(取决于corelib-2.0),这将调用consume 。 实际上,当foo需要依赖于corelib来调用create ,bar并不需要依赖corelib来调用consume 。 它只使用基础语言的概念来调用consume (我们可以在Python中描述getattr )。 在这种情况下,无论Corelib的“依赖”版本是什么版本, bar都会调用corelib-1.0的consume版本。

当然,为了这个工作,Corelib的公共API在corelib-1.0和corelib-2.0之间一定不能改变太多。 如果bar想要使用corelib-2.0中新增的方法fancyconsume ,那么它将不会出现在由corelib-1.0创build的对象上。 不过,这种情况比我们在原有的Haskell版本中要好得多,即使是那些不会影响公共API的变化都会导致破坏。 也许bar依赖于它自己创build和使用的对象的corelib-2.0特性,但是只使用corelib-1.0的API来使用它从外部接收的对象。

为了在Haskell中实现类似的function,你可以使用这个翻译。 而不是直接使用底层的实现

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

我们用一个API包corelib-api中的存在实体来封装消费者接口:

 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) 

然后在一个单独的包corelib中执行:

 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 abc = T { impl = create_ abc, _consume = consume_ } 

现在foo使用corelib-1.0来调用create ,但bar只需要corelib-api来调用consume 。 typesT位于corelib-api中,所以如果公共API版本没有改变,那么即使bar与不同版本的corelib链接,foo和bar也可以互操作。

(我知道Backpack对这种东西有很多话要说,我把这个翻译作为解释面向对象程序中发生的事情的一种方式,而不是作为一种应该认真采用的风格)。

这种情况基本上不会发生

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

以下是我的观点,为什么和如何。

原始人,早期

我认为首要的原因是语言为原始types(数字,string,布尔,空,未定义)和一些基本化合物types(对象,数组,RegExp等)提供了一个共同的基础。

所以,如果我从我使用的一个库的API接收到一个string,并将它传递给另一个,它不会出错,因为只有一个stringtypes。

这是以前发生的事情,至今仍有一定程度的发生:图书馆作者试图尽可能地依靠内在的东西,只有在有足够的理由和足够的关心和思考的情况下才会有所不同。

在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 

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

作为一个例子,它可能是一个小错误修正intersperse函数,保证1.2.2.1的版本。 如果我所关心的,在这个假设的例子中,连接一些Text并比较它们的length ,这与我完全无关。

化合物types,对象

有时,从内置的数据types中有足够的理由使JS发生分歧:以Promise s为例。 与许多API开始使用它们的callback相比,它是asynchronous计算的一个非常有用的抽象。 现在怎么办? 当这些{then(), fail(), ...}对象的不同版本在依赖关系树上向下,向下传递时,我们如何不碰到许多不兼容的情况?

我认为这要归功于健壮性原则 。

在你所发的内容上要保守,在你所接受的内容上要宽容。

所以,如果我正在编写一个我知道返回承诺并承诺作为API的一部分的JS库,我将非常小心如何与接收的对象进行交互。 例如,我不会调用花哨的.success() .finally()['catch']()方法,因为我想尽可能与不同的用户兼容,不同的Promise实现。 所以,非常保守地说,我可能只是使用.then(done, fail)等等。 在这一点上,如果用户使用我的lib返回的承诺,或者Bluebirds ,或者即使他们自己手写,只要那些坚持最基本的Promise '定律' – 最基本的API合同。

这仍然会导致运行时崩溃吗? 是的,它可以。 如果即使是最基本的API合同没有履行,你可能会得到一个exception说:“Uncaught TypeError:promise.then不是一个函数”。 我认为这里的诀窍是库作者明确了他们的API需求:例如提供对象的.then方法。 然后,它build立在API之上,以确保该方法可用于传入的对象。

我还想在这里指出Haskell也是这样,不是吗? 如果我写这样一个types类的实例是愚蠢的,它仍然没有遵循它的规律进行types检查,我会得到运行时错误,不是吗?

我们从哪里出发?

考虑到这一切,我认为即使在Haskell中,与JavaScript相比,运行时exception/错误的风险要小得多(甚至没有(?)),我们或许能够获得健壮性原则的好处:我们只需要types系统要足够精细,以便能够区分我们想要对我们操作的数据做什么,并确定它是否仍然安全。 例如上面假设的Text例子,我打赌仍然是安全的。 编译器只应该抱怨,如果我试图使用intersperse ,并要求我有资格。 例如与T.intersperse所以它可以肯定哪一个我想用。

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

就在最近我发现了簿记员 ,这是一个编译时检查types的匿名logging的实现。

请注意 :以下是我的猜想,我没有花太多的时间去尝试使用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)] 

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

 Prelude Bookkeeper > let fo = (o ?: #foo) "a" "b" in f $ emptyBook & #foo =: (++) "ab" 

结论

也许簿记员或类似的东西在我的实验中会变得有用。 也许背包会急着用其通用的界面定义来拯救。 或者有其他的解决scheme。 但无论如何,我希望我们能够走向能够利用稳健性原则。 而Haskell的依赖pipe理也可以在大多数情况下“正常工作”,只有在真正的保证时才会出现types错误。

以上是否有意义? 有什么不清楚的地方? 它回答你的问题吗? 我会好奇的听到。


在这个/ r / haskell reddit线程中可能会find进一步的相关讨论,这个话题刚刚发生,不久前,我想把这个答案发布到两个地方。

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

Node.js模块不会污染全局范围,所以在需要的时候,它们对于需要它们的模块来说是私有的 – 这是一个很好的function。

当两个或两个以上的软件包需要同一个库的不同版本时,NPM会为每个软件包安装它们,所以不会发生冲突。
当他们不这样做时,NPM将只安装一次那个库。

另一方面, Bower是浏览器的包pipe理器,它只安装平面依赖项,因为libs将会进入全局范围,所以你不能安装jQuery 1.xx和2.xx它们只会导出相同的jQuery$ vars。

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

考虑到大多数软件包还没有达到v2.0.0,我相信他们在从v0.xx到v1.0.0的转换中保持了相同的API。