纵向

朱利安·贡格里普

智能解决方案
共同利益
开源

引入模块化下芯

强调十年来一直是JavaScript的非官方标准函数编程库(及其主要分支,洛达什). 其最新的重大发展是ECMAScript 6模块(ESM)。Undercore 1.11是第一个完全模块化的版本。

有了新的模块化,您现在可以用更小的占地面积创建Undercore的自定义构建。同时,我们仍然提供标准的UMD构建,如果您想快速入门,这是完美的。UMD捆绑包很容易使用,如果您从CDN加载它,它具有很好的缓存保留率。

底芯最初由创建杰里米·阿什克纳斯他仍在帮助发布。除了Undercore,他还创建了其他几个很棒的JavaScript库和工具,尤其是骨干咖啡脚本。如果你还没有听说过,你应该去看看。

不是杰里米·阿什克纳斯,而是我模块化了Undercore。在本文中,我将讨论模块化Undercore的输入和输出,并尝试回答您可能有的所有问题。这篇文章是问答式的,所以你可以读一次,然后再回来作为参考。

目录

概述

什么是_?

除了一些基本功能外,Undercore还导出一个名为_,这是其功能的核心。因此得名“Undercore”。_同时满足以下所有条件:

名称空间句柄的角色可能是最广为人知的。在ES6模块出现之前,有这样一个名称空间句柄是必要的。这个时代还有许多其他的库也利用名称空间句柄作为一个有用的函数。jQuery的$这是一个著名的例子。虽然不再强烈要求使用多用途名称空间句柄,但它仍然很有用,所以我们将其保留在Undercore中。在ES6模块上下文中,它仍然是违约出口。

可以说,特殊值包装函数的作用要有趣得多。由于此功能,您可以_.map(x,f),_(x) .map(f)_.chain(x).map(f).value()。这些表达式是等效的,它们共享地图功能。这同样适用于所有其他Undercore函数。通过对每个函数进行独立定义,然后将所有函数添加到_只需拨打一个电话_.混合蛋白这是一个非常优雅的设计;请看一下(底部)带注释的源如果你想欣赏细节。

什么是单片接口?

这个_这个带有神奇铃铛和哨声的物体是由Undercore库的所有部分组成的。像这样的,_一次表示整个库;这就是我所说的单片接口。历史上,这曾经是只有库的接口。

提供以下功能的任何模块_取决于所有Undercore。如果您的项目从这样的模块中导入某些内容,即使您没有导入_它本身,然后您的项目有效地导入所有Undercore。我将其称为“使用单片接口”。

不过别担心!使用模块化Undercore,您可以掌握_提供了链接等功能,并且仍然可以选择要导入的部分。基本上,您可以创建自己的单片接口变体。我将介绍以下细节。

我应该补充一点,Undercore既轻便又被广泛采用。在许多情况下,使用标准单片接口是明智的选择。下面也将对此进行详细介绍。

什么是模块化接口?

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

每个Undercore函数都有一个单独的模块。其中一些功能,特别是混合蛋白,可以用来创建自己的自定义单片接口。

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

我将详细介绍以下选项和陷阱。

为什么是微型模块?

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

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

爬树怎么样?

理论上,我不需要为了获得完美的选择性而一直深入到各个功能模块。工具,如汇总可以将单个函数与较大的模块隔离开来,这是一种称为树绘制的“技巧”。然而,在实践中,JavaScript的静态分析是很困难的。由于所有内容都是可变的且类型不安全的,因此很难判断一段代码是否会影响另一段代码,尤其是在没有人类智能的情况下,因此这些工具远远不够完美。

在一个早期在我的模块化工作中,所有导出的函数仍然集中在一个ES模块中。在这个阶段,我用_.map(地图)它有点管用,因为结果只有整个Undercore库的一半左右。然而,当我进一步将此模块分解为各个功能时,_.map(地图)又缩水了3倍。事实证明_.map(地图)和它所依赖的其他Undercore函数一起只占库的六分之一。

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

我可以从哪些构建变体中选择?

源代码由ESM模块组成。这些模块位于模块/package子目录,您可以直接使用它们。从源头上看,我们构建了几个变体,以满足广泛的用例:

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

当然,只有模块化变体支持模块化使用。所有变体都支持单片使用,但为了提高效率,最好使用单片束。

为什么有单片构建变体?

模块很有用,但单片束也有一些引人注目的优点(除了向后兼容之外):

CDN的好处是什么?

CDN令人惊叹的特别是对于开放源码库。简而言之,网站通常使用相同的资源,例如Underscore,通过从公共URL引用这些资源,而不是托管自己的副本,他们可以享受协作缓存。对于Undercore 1.11 UMD捆绑包,您可以使用以下CDN URL之一:

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

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

最佳实践

如何减少代码大小?

大多数项目仅使用Undercore函数的子集。原则上,这意味着您的用户下载的代码比运行您的软件所需的代码多一点。Undercore非常小,并且有协作缓存效果,所以这并不总是重要的,但有时确实如此。你可以做两件事来减肥。

你的第一个选择很简单:不要运送你不使用的Undercore部分。换言之,使用选择性的自定义下划线。虽然简单明了,但并非每个人都可以选择。下一节将对此进行详细介绍。

有点自相矛盾的是,您的另一个选择是使用更多下划线。无论如何,如果您正在加载标准的单片接口,那么您不妨接受它,并尽可能多地使用Undercore的功能优势。使用Undercore的100多个函数,并采用更具功能性的总体风格,可以帮助您的代码更简洁、更易于维护。这是实现信任的一个重要因素骨干在这么小的空间里打包这么多功能。

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

标准下划线还是定制下划线?

一般来说,标准的Undercore可以省力,而且是离线的,而自定义的UnderCore可以减少代码的大小。对于服务器端、移动和桌面应用程序,这将是通知您选择的主要权衡。

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

当您有一个客户端应用程序,其中有许多经常返回的用户,并且您可以使用CDN时,这是一个使用自定义Undercore的令人信服的案例。否则,我建议您使用标准下划线。卡梅隆·贝卡里奥地球是一个非常好的使用定制Undercore的网站的早期展示。

如果要创建依赖Undercore的库,则可以将选择权留给库的用户。有关这方面的更多信息,请参阅后面的部分。

整体还是模块化进口?

正如我之前提到的,通过创建自己的自定义Undercore,或者重用其他人已经创建的自定义Undercore,您可以进行选择性操作,并且仍然拥有一个单片接口(具有链接和其他技巧)。基本上,你可以做出两个独立的选择。

第一个选择是关于从中提取的函数池。这可能是Undercore提供的标准池,也可能是选择性的自定义库。除了这两个扩展库之外,您还可以从任意数量的扩展库中进行绘制,例如下核控制下芯管柱。扩展库只会为您选择的Undercore添加更多函数。

第二个选择是关于从该池导入函数的方式。在单片使用中,您可以导入所有函数以及_对象。在模块化使用中,您可以从各自独立的模块中导入每个函数。在构建自己的自定义Undercore时,模块化是一种方式。在所有其他情况下,您应该使用单片导入。

为什么要为定制保留模块化导入?

当应用程序及其依赖项都使用单片导入(无论是从标准Undercore还是自定义Undercores)时,很容易确保您只需要加载一个UnderCore即可同时为所有Undercore's提供服务。您只需配置构建工具,使所有从任何单一的Undercore类接口别名导入到同一个底层库中(只要函数名不冲突—我在看您,洛达什!).

如果任何一方直接使用模块化接口,几乎不可能避免多次加载同一代码。这意味着代码更大,网络传输更大,内存消耗更大,最终能耗更大。这对环境有害。

我应该提到的是,扩展库有点灰色。一方面,您可能想让其他开发人员将您的扩展功能合并到他们定制的Undercore构建中。这要求您将每个函数放在一个单独的模块中,每个这样的模块都应该使用模块化接口。另一方面,如果您创建一个bundle,这个bundle应该使用单片接口,就像其他依赖Undercore的库一样。虽然我自己还没有尝试过,但我认为应该可以使用捆绑工具将前者转换为后者。在以后的文章中会对此进行更多介绍。

如何给你的图书馆用户一个选择?

如果您正在维护一个依赖于Undercore的库,那么您的一些用户可能也会直接或通过其他库独立使用UnderCore。通常,此类用户只想包含Undercore的一个副本,尤其是在客户端。他们可能并不总是想使用库所依赖的相同风格的Undercore(标准或自定义),所以给他们自由在库中注入不同的UnderCore是很好的。

最重要的是使用单片导入:从单个入口点导入所有Undercore函数。扩展库也是如此。这使得用户很容易将该入口点别名为不同风格的Undercore。只要另一种风格具有库所需的所有功能,这对他们来说是可行的。

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

为了容易混淆,如果您记录导入Undercore所使用的模块标识符,尤其是当它不是默认值时,这也很有用'下划线'。例如,如果您的库从内部./lib/自定义undercore.js,您的用户可以使用别名your-package/lib/custom-underscore.js根据需要连接到不同的底芯。

建议摘要

下表总结了我对每个用例的建议。

您创建的内容 条件 底部下芯 进口
自定义下划线 标准 模块化的
底核扩展库 标准 源中模块化,束中单片
其他库 标准(由用户选择) 单片的
客户端应用程序 一般的 标准(CDN) 单片的
客户端应用程序 出色的缓存和节省 自定义(CDN) 单片的
其他应用程序 尽量减少工作量 标准 单片的
其他应用程序 最小化大小 习俗 单片的

如何操作

如何使用单片接口?

在Undercore 1.11之前,您一直在使用单片接口。导入它的旧方法仍然有效,但有一些新选项。

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

通常,我建议在静态ESM新项目的语法,然后在需要时使用构建工具将其转换为其他语法之一。

进口_,{地图,过滤器} '下划线';
//您还可以执行_.map、_.filter等操作。

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

进口_ '下划线';      //默认导出
进口 * 作为_ '下划线'; //模块别名

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

动态ESM在浏览器中,如果您可以只支持新浏览器:

常数{
    “默认”:_,
地图,
滤波器,
}= 等待 进口(
    'https://your.cdn.com/underline@1.11.0/下划线esm.js'
);

请注意,如果您使用ESM构建,而您也有一个使用UMD构建的依赖项,那么您的应用程序运行时将以Undercore的两个独立副本结束。自定义到_.迭代,_.templateSettings(.template设置)_.部分.占位符在一个实例中,使用另一个实例的代码将看不到。

下面剩下的所有选项都使用UMD构建。

AMD公司语法:

定义(['下划线'], 功能(_) {
    //像往常一样使用_.map等。
});

在require.js配置中,设置路径对于强调https://your.cdn.com/underline@1.11.0/underscore.js.

通用JS语法:

无功功率,无功功率_= 要求('下划线');
//像往常一样使用_.map等。
//或者你也可以选择ES6:
常数{地图,过滤器}= 要求('下划线');

如果您正在使用Browserify,我建议使用阐明或者类似的插件,以便通过全局浏览器来替换这样的导入。

浏览器全局(嵌入):

<脚本
型钢混凝土="https://your.cdn.com/underline@1.11.0/下划线.js“
></script>
<脚本>
    //_是全局变量
</script>

扩展脚本:

#包括“路径/to/node_modules/underline/hunderline.js”
//_是全局变量

如何扩展单片接口?

“整体式”并不意味着界面是石头做的!您仍然可以添加或覆盖函数。如果您使用自定义Undercore而不是标准界面,这也适用。

添加函数非常简单_.混合蛋白。您添加的任何函数都会自动支持链接,只要它至少需要一个参数并返回结果。例如,这就是如何在链中间启用大写字符串的方法:

进口_,{链条,混合} '下划线';

功能 到上部(字符串){
    返回一串.将字符串转换成大写();
}

//可以在多个别名下添加相同的函数。
//这几乎是免费的。
混合蛋白({
    到上部:到上部,
    上面的:到上部,
    资本化:到上部
});

//仅此而已,请像使用任何其他Undercore函数一样使用它。
_.上面的(“大”);
//“大”
链条([“一个”, “两个”, “三个”]).参加('! ').到上部().价值();
//一个!两个!三个!

覆盖现有函数与添加新函数完全相同,只是混合了接口中已有的名称。

如何导入单个函数?

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

ESM公司语法:

进口地图 'underline/modules/map.js';

AMD公司语法:

定义(['下划线/amd/map'], 功能(地图){/*...*/})

通用JS语法:

无功功率,无功功率地图= 要求('下划线/cjs/map.js');

对于具有别名的函数,显示在文档始终用作模块名称。例如,减少/注入/折叠:

//不管你喜欢什么别名,
//模块名为reduce。
进口减少 '下划线/modules/reduce.js';
进口注入 '下划线/modules/reduce.js';
进口折叠 '下划线/modules/reduce.js';

//你也可以自己编别名。
进口总结 '下划线/modules/reduce.js';

//以下操作无效!
进口注入 'underline/modules/inject.js';

您可以将ESM导入转换为其他语法。与ESM、AMD或CommonJS语法一起使用的任何构建工具都允许您对模块路径前缀进行别名,例如,您可以编写下划线/modules/map.js,与模块约定无关。

如何导入bare_?

如果您选择模块化路线,您可能仍然偶尔需要导入_对象以覆盖_.迭代_.templateSettings(.template设置),或使用OO风格连锁具有一组受限的函数。您可以从模块/underscore.js:

进口_ '下划线/modules/underline.js';

无功功率,无功功率x个=[“a”];

//这些线路工作:
无功功率,无功功率包装器=_(x);
包装器.价值(); //x
包装器+ “b”; //“ab”
JSON格式.纤细的(包装); //'[“a”]'

//这些行不会,因为方法还没有
//混合在:
包装器.大小();
包装器.分类();

下面是关于覆盖、混合和链接的更多信息。

如何覆盖_.interate?

其中的模块_.迭代也在上设置属性_作为副作用。您只需要导入这两个_迭代然后将覆盖分配给_.迭代:

//这些进口的顺序无关紧要。
进口_ '下划线/modules/underline.js';
进口builtinIterate '下划线/modules/iterate.js';
进口地图 'underline/modules/map.js';
进口isRegExp公司 '下划线/modules/isRegExp.js';

//经典示例:支持正则表达式匹配的iterate。
功能 迭代Override(值,上下文){
    如果(isRegExp(值))返回 功能(对象){
        返回价值.测试(对象);
};
    返回builtinIterate(值,上下文);
}

_.迭代 =迭代Override;

地图([“苹果”, “香蕉”, “樱桃色”], /e(电子)/);
//[真,假,真]

如何覆盖_.templateSettings?

其中的模块_.templateSettings(.template设置)也在上设置属性_作为副作用。您只需要导入这两个_模板设置,然后覆盖_.templateSettings(.template设置):

//这些进口的顺序无关紧要。
进口_ '下划线/modules/underline.js';
进口 '下划线/modules/templateSettings.js';

//仅覆盖关键点。
_.模板设置.插入 = /\{\{\{(.+?)\}\}\}/克;

//或者整个物体。
_.模板设置 ={
    插入: /\{\{\{(.+?)\}\}\}/克,
    逃跑: /\{\{(.+?)\}\}/克,
    评价: /\{\{#(.+?)\}\}/克
};

如何使用_.template?

您只需导入_.模板函数并调用它,但存在一些问题。模板评估分为两步:

  1. 模板一串编译为模板功能.
  2. 使用数据调用模板函数以生成最终字符串。

可以使用读取模板函数的源代码_.template(templateString).source然后存储在JavaScript文件中。这意味着步骤1和2可以在时间上广泛分开;它们可能在不同的机器上,在不同的JavaScript运行时,使用Underscore库的不同实例执行。

您有责任确保步骤2执行的任何环境都与模板功能兼容。如果该运行时使用模块化Undercore或定制的单片UnderCore,则需要注意两件事。

我建议始终使用_作为模板代码内的命名空间句柄:调用_.每个而不仅仅是每个。这确保保存模板函数的模块只需导入_然而,这需要将模板中使用的所有函数混合到_。您最安全的选择是使用标准单片接口,尤其是当您不控制模板字符串的内容时。

如何启用链接?

只需混合要链接到的功能_。确保还包括_.链条函数本身:

进口混合蛋白 '下划线/modules/mixin.js';
进口 '下划线/modules/chain.js';
进口地图 'underline/modules/map.js';
进口滤波器 '下划线/modules/filter.js';

//mixin将修改_对象作为副作用。
混合蛋白({
    :,
    地图:地图,
    滤波器:滤波器,
    //让我们也添加一些别名。
    项目:地图,
    选择:滤波器
});

链条([1, 2, ])
    .地图(x)=>x个*x个*x)
    .滤波器(x)=>x个> 5)
    .价值();
    // [8, 27]

如何添加Array原型方法?

这个核下阵列方法模块将这些添加到_作为副作用。此模块还重新导出_为了您的方便。

进口_ '下划线/modules/underscore-array-methods.js';

_([1, 2, ]).颠倒(); // [3, 2, 1]

请注意,为了这个阵列方法,您需要混合_.链条也。

我如何制作我自己的自定义底纹?

如果您想创建一个与标准Undercore类似但具有不同功能集的接口,可以模拟Undercores本身的内部结构。这需要两个或三个模块,具体取决于您的需要。

第一个模块只是收集并重新导出要包含的所有公共函数。这也是创建别名的地方。让我们称之为这个模块./索引js.

//要在未修改的情况下重用的任何Undercore函数。
出口{违约 作为地图} 'underline/modules/map.js';
出口{违约 作为滤波器,
         违约 作为选择} '下划线/modules/filter.js';
//如果要链接,则需要此选项。
出口{违约 作为链} '下划线/modules/chain.js';

//要添加的任何函数。
出口{违约 作为食品} './foo.js’;
出口{违约 作为条形} './bar.js'条;
出口{违约 作为巴兹} './baz.js';

第二个模块从./索引js并将其混合成_。它还出口_作为默认值。让我们称之为./index-default.js.

进口 * 作为所有导出 './索引.js';
进口混合蛋白 '下划线/modules/mixin.js';
//如果需要Array原型方法,请添加下一个。
进口 '下划线/modules/underscore-array-methods.js';

//mixin返回,所以我们不需要显式导入。
出口 违约mixin(所有导出);

基本上,您自己的定制单片接口现在已经完成。您可以在内部使用它,并且可以使用Rollup从中创建一个漂亮的UMD包。然而,如果您还想以一种ESM友好的方式从单个入口模块中公开整个接口,那么还需要做一件事./index-all.js.

//_仍然是默认导出。
出口{违约} './index-default.js’;
//还可以按名称分别导出每个函数。
出口 *  './索引.js';

最后一个模块是我们在Undercore中创建的单片ESM包。

兼容性

Undercore还会是我喜欢的小图书馆吗?

对!模块化为缩小和压缩的UMD包添加了110个字节。然而,Undercore的明确目标仍然是拥有较小的占地面积。这永远不会改变。虽然由于新功能的出现,总的趋势是规模不断扩大,就像任何库一样,但我们努力将其限制在一定范围内。我们对添加新功能非常挑剔,我们总是寻找方法使库再次变小。

ECMAScript 3和ExtendScript怎么样?

别担心,这些仍然有效!而本地人进口出口语句是在ECMAScript 6中引入的,我们只在源代码中使用它们。当我们创建UMD、AMD和CommonJS变体时,它们就会消失。代码的其余部分仍然与ES3兼容。您可以直接使用UMD、AMD或CommonJS变体,无需编译。

那.mjs呢?

Node.js有实验性ESM支持。要使用它,您必须设置类型的字段package.json包“模块”,或使用.mjs码使用ESM表示法的模块的扩展。在Undercore,我们目前没有使用这些设施。

坦率地说,实验性的Node.js约定很难采用,尤其是如果您想同时支持CommonJS和ESM,尤其地如果您正在维护像Undercore这样的复杂库,并且不想破坏向后兼容性。我甚至不确定它是否能做到。我暂时选择了不参加这个会议,而依赖于esm包,的package.json包 模块领域,并构建Rollup等工具以提供ESM支持。

当Node.js约定通过实验阶段并得到广泛采用时,我们将重新考虑它。希望Node.js团队能找到一种方法,使其更易于管理。

WebPack和包裹怎么样?

在撰写本文时,WebPack和Parcel都存在漏洞(WebPack(网络包),包裹)在某些情况下会中断从程序包入口点进行的非ESM导入。如果您这样做:

无功功率,无功功率_= 要求('下划线');

如果您告诉WebPack或Parcel创建一个包含Undercore和您自己的应用程序代码的捆绑包,则可能会看到以下错误:

_不是函数

简而言之,这是由于这些工具不分青红皂白地对模块领域结束主要的,即使在解析CommonJS时要求,其中模块田地本来就不适合。

要解决此问题,您有许多选项:

Undercore 2.0怎么样?

(是的,将来会有Undercore 2.0版!)

虽然显然会有一些突破性的变化,但我预计在下一个主要版本的Undercore中,总体架构将保持不变。我们可能会提供相同的构建变体。你仍然可以用同样的方式制作你自己的自定义底纹。目前的模块化设计已经考虑到Undercore 2.0。

此时,我可以想到两件可能会改变的事情:

总结

你对Undercore还有什么其他计划?

很高兴你这么问!这是我当前的愿望清单:

我如何才能学到更多并随时了解情况?

我能帮忙吗?

通过使用Undercore,您已经做得够多了。谢谢您!也就是说,如果你想做更多,你可以。

首先,你可以帮助在网上传播信息。在推特和其他社交媒体上发布关于模块化Undercore的帖子书写器,告诉你的朋友和同事。

其次,如果你能胜任,你可以帮助完成工作。回答堆栈溢出问题,查看新拉入请求,或者甚至可以贡献自己的代码。

最后,如果你有一点多余的钱,你可以在Patreon上亲自支持我。捐款帮助我花更多的时间来做开源贡献。在近期内,这意味着我将能够花更多的时间研究Undercore和相关库。

致谢

卡梅隆·贝卡里奥,杰里米·阿什克纳斯丹尼尔·沙曼尼对本文的草稿给出了很好的反馈。我还要感谢Inge Hoogendam、Nadine Gonggrijp、Baloe Gonggryjp、Arie de Bruin和Diedel Kornet给予我无尽的鼓励,以及我所有的赞助人感谢他们的持续支持。你们都很摇滚。