一个模块化的Underscore.js

2020-08-29 05:39:07

十年来,下划线一直是JavaScript的非官方标准函数编程库(以及它的主要分支Lodash)。它最新的重大发展是转向ECMAScript 6模块(ESM)。下划线1.11是第一个完全模块化的版本。

有了新的模块性,您现在可以创建具有更小占用空间的自定义下划线版本。同时,我们还提供标准的UMD版本,如果您想要快速入门,这是非常完美的。UMD捆绑包很容易使用,如果您从CDN加载它,它会有很好的缓存保留。

下划线最初是由Jeremy Ashkenas创建的,他仍然在帮助发布。除了下划线,他还创建了其他几个非常棒的JavaScript库和工具,特别是Backbone和CoffeeScript。如果你还没有听说过它们,你应该去看看。

我不是杰里米·阿什凯纳斯,但我模块化了下划线。在本文中,我将讨论模块下划线的细节,并尝试回答您可能有的所有问题。这篇文章是问答风格的,所以你可以先看一遍,然后再回来作为参考。

除了一些基本函数外,下划线还导出一个名为_的对象,该对象是其功能的核心。因此得名“下划线”。_是同时执行以下所有操作(_S):

一个命名空间句柄,您可以通过它访问版本字符串以及所有函数:_.each、_.VERSION。

用于自定义下划线函数行为的入口点,当前通过_.iteratee和_.templateSettings。

名称空间句柄的角色可能是最广为人知的。在ES6模块出现之前,拥有这样的名称空间句柄是必要的。这个时代的许多其他库也利用名称空间句柄作为有用的函数。JQuery的$就是一个著名的例子。虽然不再存在拥有多用途名称空间句柄的强烈动机,但它仍然很有用,所以我们将其保留在下划线中。在ES6模块上下文中,它仍然是默认导出。

可以说,特定值包装函数的作用要有趣得多。多亏了这个函数,您可以执行_.map(x,f)、_(X).map(F)或_.chain(X).map(F).value()。这些表达式是等效的,并且它们共享映射函数的单个底层实现。这同样适用于所有其他下划线函数。这是通过对每个函数有一个独立的定义,然后通过一个对_.Mixin的调用将所有函数添加到_来实现的。这是一个非常优雅的设计,如果您想欣赏它的细节,请看一下带注释的源代码(底部)。

_对象及其神奇的铃铛和口哨是由下划线库的所有部分组成的。因此,_一次表示整个库;这就是我所说的单块接口的意思。从历史上看,这曾经是通向图书馆的唯一接口。

任何提供_的模块都依赖于所有下划线。如果您的项目从这样的模块导入某些内容,即使您没有导入_本身,您的项目也会有效地导入所有下划线。我把这称为广义上的“使用单一界面”。

不过别担心!使用模块下划线,您可以拥有_提供的所有技巧,例如链接,并且仍然可以选择要导入的零件。基本上,您可以创建您自己的单一界面变体。我将在下面介绍详细内容。

我应该补充说,下划线既是轻量级的,也是被广泛采用的。在许多情况下,使用标准单片接口是明智的选择。下面也有更多关于这方面的信息。

从版本1.11开始,下划线的每个单独部分都可以从单独的模块导入。有选择地直接导入这些部件,而不是依赖于提供单一接口的模块,相当于使用模块化接口。

每个下划线函数都有一个单独的模块。其中一些函数,特别是Mixin和Chain,可以用来创建您自己的自定义整体界面。

请记住,模块化接口带来了强大的灵活性,但也带来了责任感。标准的单片接口设计精良,连贯一致。如果你选择省略它的一部分,你最终可能会得到一些你想要的东西。

通常,将项目的源代码分散到多个模块可以促进代码重用。如果我可以从您的库中选择我感兴趣的代码,而不必导入我不感兴趣的代码,那么我更有可能使用您已经编写的代码,而不是重新发明轮子。每个人都节省了时间,而且我们更快地得到了会飞的汽车。

当我模块化下划线时,我选择了最极端的选择:我将每个函数放在单独的模块中。我这样做是为了最大限度地发挥代码重用的潜力。我希望用户能够准确地选择他们需要的代码;不多也不少。我已经在为一家图书馆工作,它将自己利用这一点。

从理论上讲,我不需要一直到各个功能模块才能获得完美的选择性。像Rollup这样的工具可以将单个函数从较大的模块中隔离出来,这是一种称为树共享的“技巧”。然而,在实践中,静态分析JavaScript是很困难的。由于所有内容都是可变的和不安全的类型,很难判断一段代码是否会影响另一段代码,特别是在没有人类智能的情况下,因此工具远非完美。

在我模块化工作的早期阶段,所有导出的函数仍然集中在单个ES模块中。在这个阶段,我测试了使用_.map共享树木。它在某种程度上起作用了,从某种意义上说,结果只有整个下划线库大小的一半左右。然而,当我将此模块进一步拆分为各个函数时,_.map又缩小了3倍。结果表明,_.map及其依赖的其他下划线函数加起来只占库的六分之一。

\(\frac{1}{2}-\frac{1}{6}=\frac{1}{3}\),因此有整整三分之一的下划线代码无法树共享。避免不必要地导入该代码的唯一方法是手动将其隔离在单独的模块中,这正是我所做的。

源代码由ESM模块组成。这些模块位于module/Package子目录中,您可以直接使用它们。从源代码开始,我们构建了几个变体,以迎合广泛的用例:

下划线.js(在包根目录中),这是单一的UMD捆绑包。此文件与旧版本的下划线具有相同的路径和格式。它仍然是包的主要条目,也是您最有可能在短期内使用的变体。

下划线-esm.js,单块ESM捆绑包。虽然完全支持,但这个变体在短期内很可能主要是试验性使用。

AMD/,模块化的AMD版本。此目录包含每个源模块的AMD版本。

前两个构建变体中的“整体式捆绑包”意味着该变体提供了整体式接口,并且它完全包含在单个文件中。

当然,只有模块化变体支持模块化使用。所有变体都支持整体式使用,但为了提高效率,最好使用整体式捆绑包。

模块很有用,但单片捆绑包也有一些令人信服的优势(除了向后兼容):

代码更容易阅读,也更容易看到全局。这就是为什么我们从整体构建呈现单读注释源的原因。不过,通过单击带注释的模块来发现代码可能会很有趣,而且有些启发性。

从文件系统读取单个大文件比读取许多小文件要快得多。

在Web上,发送具有大响应的单个请求比发送具有小响应的多个请求的效率要高得多。

由于下划线是如此重要的库,如果您嵌入来自CDN的标准整体捆绑包,特别是UMD捆绑包,缓存保留可能会非常好。有关这一点的更多信息,请参阅下一节。

CDN非常棒,尤其是对于开源库。简而言之,网站通常使用相同的资源(如下划线),通过从公共URL引用这些资源,而不是托管自己的副本,它们可以享受协作缓存。对于下划线1.11 UMD捆绑包,您可以使用以下CDN URL之一:

下划线被广泛使用(有些数字),因此如果您使用上述URL之一,几乎可以肯定您站点的访问者的浏览器缓存中已经有该下划线副本。即使情况并非如此,CDN仍然减少了所需的网络流量,这意味着更短的延迟和更低的碳排放。

更令人敬畏的是,访问您的站点还有助于在其他站点上保留缓存。您不仅可以从缓存效果中获益,还可以帮助放大其他站点的效果。双赢!

大多数项目只使用下划线函数的子集。原则上,这意味着您的用户下载的代码比运行您的软件所需的代码多一点。下划线非常小,并且存在协作缓存效果,因此这并不总是重要的,但有时会重要。你可以做两件事来动摇体重。

您的第一个选择很简单:不要附带您不使用的下划线部分。换句话说,使用选择性的自定义下划线。虽然很简单,但并不是每个人都适合这个选项。有关这一点的更多信息,请参阅下一节。

有点自相矛盾的是,你的另一种选择是更多地使用下划线。如果您无论如何都要加载标准的单片接口,那么您不妨接受它,并尽可能多地利用下划线的功能优势。更多地使用下划线的100多个函数,并总体上采用更具功能性的风格,有助于使您的代码更简洁,同时更易于维护。这是一个重要因素,使得可信赖的主干能够在如此小的空间中封装如此多的功能。

这些选项可以组合在一起。大多数下划线函数在内部依赖于其他下划线函数,因此即使您是选择性的,一些不使用的函数也可能会漏掉。您不妨尝试一下是否可以找到从这些功能中获益的方法。

通常,标准下划线是现成的,可以省力,而自定义下划线往往会减小代码大小。对于服务器端、移动和桌面应用程序,这将是通知您选择的主要权衡。

在客户端,每个会话都是从互联网下载应用程序开始的,缓存效果将是决定性因素。如果自定义下划线的缓存保留不佳,那么无论您减少多少代码大小,最终都会生成更多而不是更少的网络流量。但是,如果缓存保留非常好,您可能会节省大量成本。

如果您的客户端应用程序有很多频繁返回的用户,并且您可以使用CDN,这就是使用自定义下划线的令人信服的案例。否则,我建议您使用标准下划线。卡梅隆·贝卡里奥(Cameron Beccario)的“地球”(Earth)是一个非常好的早期展示网站,它使用了定制的下划线。

如果要创建依赖于下划线的库,则可以将选择权留给库的用户。关于这一点的更多信息将在后面的部分中介绍。

正如我前面提到的,通过创建您自己的自定义下划线,或者通过重用其他人已经创建的自定义下划线,您可以有选择性地仍然拥有一个整体的界面(带有链接和其他技巧)。从本质上讲,您可以做出两个独立的选择。

第一个选择是关于从中提取的函数池。这可能是下划线提供的标准库,也可能是选择性的定制库。除了这两个库之外,您还可以从任意数量的扩展库中提取内容,比如下划线-contrib或underscore.string。扩展库只是将更多函数添加到您选择作为基础的任何下划线上。

第二个选择是关于从该池导入函数的方式。在整体使用中,您可以从单个入口点导入所有函数以及_object。在模块化使用中,您可以从各自单独的模块导入每个函数。在编写您自己的自定义下划线时,模块化是可行的。在所有其他情况下,您应该使用整体导入。

当应用程序及其依赖项都使用整体导入(无论是从标准下划线还是自定义下划线导入)时,很容易确保您只需要加载一个下划线即可同时为所有应用程序提供服务。您只需配置您的构建工具,以便将所有从任何类似下划线的接口别名导入到同一底层库中(只要函数名不冲突-我正在看着您,Lodash!)。

如果涉及的任何一方直接使用模块化接口,几乎不可能避免多次加载相同的代码。这意味着更大的代码大小、更大的网络传输、更大的内存消耗,并最终导致更大的能源消耗。这对环境不好。

我应该提一下,扩展库有点灰色地带。一方面,您可能希望使其他开发人员能够将您的扩展功能合并到他们的自定义下划线构建中。这要求您将每个函数放在单独的模块中,并且每个这样的模块都应该使用模块化接口。另一方面,如果您创建了一个包,这个包应该使用整体接口,就像任何其他依赖下划线的库一样。虽然我自己还没有尝试过,但我认为应该可以使用捆绑工具将前者转换为后者。有关这一点的更多信息,请在以后的文章中介绍。

如果您维护的是依赖于下划线的库,则某些用户可能还会直接或通过其他库独立使用下划线。通常,这样的用户会希望只包含一个下划线副本,尤其是在客户端。他们可能并不总是想要使用您的库所依赖的相同风格的下划线(标准或自定义),所以给他们在您的库中插入不同的下划线的自由是很好的。

要做的最重要的事情是使用整体导入:从单个入口点导入所有下划线函数。扩展库也是如此。这使得用户很容易将该入口点别名为不同风格的下划线。只要另一种口味具有您的库所需的所有功能,这将对他们起作用。

作为一项附加服务,请考虑在文档中列出您的库需要的所有函数。这有助于您的用户评估哪些功能是必需的,哪些可以省略(如果他们决定使用自定义下划线)。

为了便于别名,如果您记录用于导入下划线的模块标识符,尤其是当它不是默认的下划线时,这也很有帮助。例如,如果您的库是从内部./lib/Custom-undercore.js导入的,则您的用户可以根据需要将您的-package/lib/Custom-undercore.js别名为不同的下划线。

单片接口是您在下划线1.11之前一直使用的接口。旧的导入方式仍然有效,但有一些新的选择。

可用的语法取决于您的目标环境,尽管工具可以在一定程度上在它们之间进行转换:

通常,我建议在新项目中使用静态ESM语法编写导入,如果需要,然后使用构建工具将其转换为其他语法之一。

从';下划线&//导入_,{map,filter};//您还可以执行_.map,_.filter等操作。

请注意,以下IMPORT语句并不等价,尽管转换工具可能会以相同的方式模拟它们。在新的项目中,你应该避免第二种形式。

IMPORT_FROM';下划线&;//默认导出IMPORT*AS_FROM';下划线';;//模块别名。

在下面的示例中,you.cdn.com是您决定使用的任何CDN的占位符。上面列出了常用选项。

请注意,如果您使用ESM版本,而您还有一个使用UMD版本的依赖项,则您的应用程序运行时将以两个独立的下划线副本结束。一个实例中对_.iteratee、_.templateSettings或_.artial.placeholder的自定义设置将不会被使用另一个实例的代码看到。

Var_=要求(';下划线';);//照常使用_.map等。//或者您可以使用ES6:const{map,filter}=Required(';下划线';);

如果您使用的是Browserify,我建议您使用exposfy或类似的插件,以便将此类导入替换为全局浏览器。

“铁板一块”并不意味着界面是一成不变的!您仍然可以添加或覆盖函数。如果您使用的是自定义下划线而不是标准界面,这也适用。

使用_.Mixin添加函数真的很容易。您添加的任何函数都自动支持链接,只要它至少接受一个参数并返回其结果即可。例如,您可以通过以下方式启用链条中间的上部套管柱:

IMPORT_,{CHAIN,MIXIN}从';下划线';;函数导入到上部(字符串){返回字符串。ToUpperCase();}//您可以在多个别名下添加同一函数。//这几乎是免费的。Mixin({toHigh:toUpper,UPPER:toHigh,Capitalize:ToTop});//这就是全部,请像使用其他下划线函数一样使用它。___。上部(#39;BIG&39;);//';BIG&39;链条([';One&39;,';Two';,';Three';])。加入(';!';)。至上部()。Value();//一!二!。三!。

覆盖现有函数与添加新函数完全相同,不同之处在于您需要混合接口中已有的名称。

从本节开始,我们将讨论模块化接口。如前所述,通常只有在创建自定义下划线时才应执行此操作。注意选择一个接口并坚持使用它;不要在同一项目中混用模块化和整体导入。

对于具有别名的函数,文档中出现的第一个名称始终用作模块名称。例如,Reduce/Inject/Foldl:

//不管您的首选别名是什么,//模块名称为Reduce。从';下划线/MODULES/Reduce.js';;导入注入自';下划线/MODLES/Reduce.js';;导入文件夹';下划线/MODLES/Reduce.js';;//您也可以设置自己的别名。从';下划线/MODULES/Reduce.js';;//以下内容导入汇总将不起作用!导入注入自';下划线/module/inject.js';;

您可以将ESM导入转换为其他语法。与ESM、AMD或CommonJS语法一起使用的任何构建工具还将允许您为模块路径前缀添加别名,因此,例如,您可以编写下划线/module/map.js,而不考虑模块约定。

如果您采用模块化路线,您可能偶尔仍希望导入_Object,以便覆盖_.iteratee或_.templateSettings,或者使用OO样式或具有受限函数集的链接。您可以从module/underscore.js中只导入包装器函数,而不需要混合任何函数:

IMPORT_FROM';下划线/MODULES/underscore.js';;var x=[';a';];//这些行起作用:var wrapper=_(X);wrapper。Value();//x包装器。

.