在今天的文章中,我们将解密当我们在一个相对简单的项目中工作时,JavaScript依赖项的数量是如何增长的。你应该担心依赖关系的数量吗?
请记住,这篇博文与前不久发布的《驰骋在JavaScript依赖性地狱》博文有关。我们将展示一个 "真实世界 "的例子,说明一个项目的依赖性如何从零增长到13K。
在一个项目中拥有大量的依赖关系并不一定是坏事,但我们稍后会讨论这个问题。首先,让我们看看一个项目是如何从几个依赖关系发展到大量依赖关系的。
在HTML和JavaScript中建立一个TODO
为了最好地说明成长的痛苦,我们将创建一个简单的应用程序,记录我们需要做的事情。这就是它将会是什么样子。
在这一点上相当简单--我们此刻不想变得疯狂和花哨。你可以看一下just-do-it GitHub repo上的代码。
现在,这个应用程序需要的唯一依赖是一个可以打开HTML页面的浏览器。我们可以用几乎所有的浏览器来完成这个任务。
让我们看看我们在这一点上的依赖性数量。我们将创建一个有四列的表格:
- 直接依赖项 - 我们安装的依赖项的数量(连同
devDependencies) - 继承的依赖项 - 与直接依赖项一起安装的依赖项的数量
- 总数 - 上述两项的总数
- 新的依赖关系 - 在之前的安装中,有多少依赖关系得到了安装
| 直接依赖关系 | 继承的依赖性 | 总数 | 新的依赖关系 |
|---|---|---|---|
| 0 | 0 | 0(不计入浏览器) | 0 |
该项目仍然没有一个package.json ,所以我们仍然不是那个可爱的NPM包生态系统的一部分。如果你不想让事情复杂化,这很有用。但是现在,我们在这里不是为了简单的解决方案。我们想模拟一下,你是如何从零到一堆依赖关系中快速发展的!这就是我们的目标。
因此,正如任何有前端的项目一样,我们不想要普通的JS。我们想变得很酷,使用React!
将React添加到我们的项目中
React的热潮现在正如火如荼地进行着。它甚至可能在2020年都不会达到它的高峰。现在是抛弃我们项目中一直使用的普通(Vanilla)JavaScript,转而使用现在的酷儿们正在使用的东西--万能的React的最佳时机。
我们将遵循React官方网站的"将React添加到网站 "的说明。这意味着我们将在我们的HTML中添加一个脚本标签元素:
<head>
<!-- ... other HTML ... -->
<script
src="https://unpkg.com/react@16/umd/react.development.js"
crossorigin\
></script>
<script
src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"
crossorigin\
></script>
<!-- ... other HTML ... -->
</head>
但是,由于我们想使用JSX,我们要亲身体验一下NPM的生态系统。为了使用JJSX,我们必须安装Babel。Babel将把我们的JSX文件编译成JS,这样我们的浏览器就可以毫不费力地渲染它。
首先,我们需要package.json 。我们可以用下面的命令添加它:
$ npm init -y
前面的命令将设置package.json ,使我们的项目为安装Babel做好准备:
$ npm install --save-dev babel-cli@7.1.0 babel-preset-react-app@3
...
+ babel-cli@6.26.0
+ babel-preset-react-app@3.1.2
added 305 packages from 128 contributors and audited 3622 packages in 15.904s
哇,所以我们明确地安装了两个包--babel-cli 和babel-preset-react-app ,但这些包带来了更多自己的依赖。如果你是第一次接触这个想法,你可能会觉得很奇怪。我建议你阅读之前的博文,了解JavaScript的依赖性是如何工作的。
安装这两个包使我们的NPM依赖性从0增加到2个直接依赖性和3620个继承依赖性:
| 直接依赖 | 继承的依赖关系 | 总数 | 新的依赖关系 |
|---|---|---|---|
| 2 | 3620 | 3622 | +3622 |
很好,现在我们可以使用JSX,同时在后台运行Babel来观察我们的变化:
$ npx babel --watch src --out-dir . --presets react-app/prod
看看修改后的repo 是什么样子。
Babel会把我们的JSX文件预处理成JS文件。但这听起来有点简略。我不想让React在一个脚本标签中加载。我想要一个完整的开发者体验,类似于create-react-app。
为此,我们必须多加一点Babel和一个捆绑器--Webpack。
添加一个类似于Create-React-App的设置
为了做到这一点,我们将按照推荐的方式,在一个项目中从头开始设置React。
让我们删除我们使用的旧的Babel依赖项:
$ npm uninstall babel-cli babel-preset-react-app --save-dev
很好,我们又回到了零NPM依赖的状态。
接下来,让我们添加新版本的Babel依赖项:
$ npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/preset-react
...
+ @babel/cli@7.8.4
+ @babel/preset-react@7.9.4
+ @babel/core@7.9.6
+ @babel/preset-env@7.9.6
added 25 packages from 4 contributors, removed 4 packages, updated 7 packages and audited 4216 packages in 13.394s
babel-core babel-cli 允许你从命令行编译文件。 和 都是预设,用于转换特定类型的代码--在本例中,env预设允许我们将ES6+转换为更传统的JS,React预设也是如此,但用JSX代替。preset-react preset-env
现在我们得到了这个方法,根据npm的审计报告,我们总共有4216个依赖项。
现在,我们的依赖关系表看起来像这样:
| 直接依赖关系 | 继承的依赖关系 | 总数 | 新的依赖关系 |
|---|---|---|---|
| 4 | 4212 | 4216 | +594 |
在这之后,我们需要Webpack为我们很好地捆绑一切。另外,我们将设置一个服务器,它将观察我们在开发中对应用程序所做的任何改变。
我们可以用安装必要的依赖项:
$ npm install --save-dev webpack webpack-cli webpack-dev-server style-loader css-loader babel-loader
...
+ css-loader@3.5.3
+ babel-loader@8.1.0
+ style-loader@1.2.1
+ webpack-cli@3.3.11
+ webpack-dev-server@3.10.3
+ webpack@4.43.0
added 448 packages from 308 contributors and audited 13484 packages in 23.105s
这又增加了一些包。现在我们总共有13484个:
| 直接依赖性 | 继承的依赖关系 | 总数 | 新的依赖关系 |
|---|---|---|---|
| 10 | 13474 | 13484 | +9268 |
好吧,这很快就升级了!这里发生的事情是,我们安装的每个依赖项都有自己的依赖项。然后,这些依赖关系有它们的依赖关系。在总数(13484)中,所有的依赖性加上依赖性的依赖性都包括在内。甚至包括devDependencies!这就是为什么我们通过安装webpack和其他需要的依赖来使其工作,得到了额外的9268个依赖。
这个总数可能看起来很吓人,让你想知道发生了什么,但这就是NPM生态系统的工作方式,嘿嘿,这就是很多包管理器的工作方式。大多数的依赖关系都是单行的,不应该成为让我们担心的东西。有时,它们可能是,但我们将在后面讨论这个问题。
例如,如果我们在这时运行npm audit ,我们会得到这个结果:
$ npm audit
=== npm audit security report ===
┌──────────────────────────────────────────────────────────────────────────────┐
│ Manual Review │
│ Some vulnerabilities require your attention to resolve │
│ │
│ Visit https://go.npm.me/audit-guide for additional guidance │
└──────────────────────────────────────────────────────────────────────────────┘
┌───────────────┬──────────────────────────────────────────────────────────────┐
│ Low │ Prototype Pollution │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Package │ yargs-parser │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Patched in │ >=13.1.2 <14.0.0 || >=15.0.1 <16.0.0 || >=18.1.2 │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Dependency of │ webpack-dev-server [dev] │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Path │ webpack-dev-server > yargs > yargs-parser │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ More info │ https://npmjs.com/advisories/1500 │
└───────────────┴──────────────────────────────────────────────────────────────┘
found 1 low severity vulnerability in 13484 scanned packages
1 vulnerability requires manual review. See the full report for details.
我们看到,一个webpack-dev-server 的依赖关系有一个yargs-parser ,它的严重性很低。这是webpack-dev-server 的一个开发依赖项,它可能不应该让我们担心。作为一个开发依赖,意味着webpack-dev-server 在开发中使用这个yargs-parser ,而且这个代码很可能不会出现在我们的生产包中。
记住,到目前为止,我们只添加了devDependencies。所有这些都不会出现在我们的生产包中,所以没有什么可担心的,对吗?让我们继续看看吧。
我们现在需要添加React,因为我们不希望在头部有脚本标签来获取它。
添加React
让我们把React从头部直接移到package.json :
$ npm install react react-dom
...
+ react@16.13.1
+ react-dom@16.13.1
added 5 packages and audited 13506 packages in 6.148s
| 直接依赖关系 | 继承的依赖关系 | 总计 | 新的依赖关系 |
|---|---|---|---|
| 12 | 13494 | 13506 | +22 |
现在,这是不一样的。我们在安装react 和react-dom 时只得到了 22 个额外的依赖项。 如果我们为生产运行 npm audit,我们会得到这个:
npm audit --production
=== npm audit security report ===
found 0 vulnerabilities
in 22 scanned packages
审计输出意味着总共有22个包会出现在我们的生产包中--我们将为访问我们的TODO列表应用程序的人提供代码。与我们在开发环境中的13506个依赖项相比,这听起来并不坏。
在我们安装了所有这些依赖项,并重新编写了我们的代码以使用Webpack和Babel之后,我们的应用程序仍然在运行。

BTW,你可以在GitHub的repo中查看我们添加的所有代码。我们没有深入研究我们如何创建React组件和设置Webpack的细节。我们更多关注的是依赖关系和它们的数字意味着什么。如果你对TODO列表如何工作的技术细节感兴趣,请访问上面提到的 repo。
你应该为这么多的依赖关系感到担心吗?
是的,也不是。外面总是有风险的。让我们回顾一下社区最近遇到的2个依赖性问题的案例。
将恶意代码放入库中
不久前,有一个广泛使用的NPM包--event-stream的情况。有人在这个流行的软件包中注入恶意代码,目的是为了从用户那里获得加密货币信息。该软件包并没有被黑掉。恰恰相反,攻击者是一个拥有软件包创建者给予的写入权限的维护者。如果你对这一对话感兴趣,可以查看GitHub上的相关问题。还有NPM的博客文章也是如此。
好的方面是,这个黑客在到达用户之前就被发现并阻止了。糟糕的是,这是一个官方发布的软件包。
破坏大家的构建过程
最近,另一个大量使用的is-promise包破坏了整个JS生态系统。该软件包本身由两行源代码组成。它的作用是检查一个JS对象是否是一个Promise。根据GitHub的报告,该库被340万个项目使用。是的,你没看错--340万个项目。
最近的2.2.0更新打破了所有人的构建过程,因为该项目没有遵守ES模块标准。诸如此类的事情时有发生。这不是第一次发生,也肯定不会是最后一次。如果你想了解更多关于这个问题的信息,有一篇关于它的事后总结。
"相信,但要验证 "你所安装的东西
你应该知道你在你的项目里放了什么。你应该采取 "信任,但核实 "的心态。不能保证发生在event-stream 或is-promise 上的情况将来不会发生在其他依赖关系上。不过,你不应该对使用或贡献于开源项目感到气馁。这一事件之所以被发现,就是因为团体的努力。只要注意你在package.json ,这样你就可以在艰难时期到来时正确行事。
另一方面,你不可能知道每一个依赖关系。想象一下,如果你知道我们安装的所有13000个依赖项会发生什么,你可能会疯掉。幸运的是,现在有一些工具,比如GitHub安全警报和GitHub的整个安全计划。
最后,除了注意你添加到项目中的内容,你能做的并不多,请确保:
- 总是小心翼翼地升级软件包,看看它们在使用/添加什么内容
- 尽量避免嵌套的依赖关系
- 将一个软件包固定在一个特定的版本上,不要自动更新。
我希望这篇博文能让你更深入地了解在你的JavaScript项目中添加依赖项时发生了什么。我也希望它能提高一些对开源安全的认识,并希望它能使你在运行npm install 命令时更加小心。