zzxworld

2021 JavaScript 包管理工具入门指南

现如今,任何一个看起来能跟上时代的编程语言,都要有一个包管理工具。否则就容易让人感觉缺少点什么。JavaScript 作为前端开发的必备编程语言,自然也是少不了这一标配。今天就来总结下 JavaScript 目前的包管理方式和工具。

什么是包

在介绍包管理之前,先对编程新人解释下什么是「包」。包的英语单词:Package,针对编程来说,更具象的名字叫「软件包」。代表了一组特定功能的源码集合。软件包可以是一个单独的源码文件,也可以是一个包含很多文件和目录的文件夹。具体如何呈现,就要看这个软件包提供了什么样的功能。

对于编程来说,包的使用不是必须的。但选择这么做的代价就是完全要靠自己来编写所有对应的非业务功能。拿 JavaScript 的网络请求功能来举例,可以仅靠浏览器提供的接口 XMLHttpRequest 就能很轻松的实现该功能,但要让这个功能在所有浏览器上都稳定运行却没那么容易。

而软件包就是有人对一些具有重复性的功能问题做出了针对性的解决方案。我们在软件开发的过程中有了相应的功能需求时,除了自己写,也可以选择直接使用这些有功能针对性的软件包。这除了可以节省自己大量的精力和时间,也能让自己少踩一些坑。

以我的编程体验来看,在软件开发过程中使用包不是必须的,但却是一项最佳的工程化实践

作为今天的主题,接下来就看看 JavaScript 目前在使用包方面有哪些方式。

源码复制

这是最原始,也是最容易理解的一种包管理方式。

对于任何需要的 JavaScript 软件包来说,通过这种方式,都只需要以下三步:

  1. 获得源码。
  2. 复制到项目。
  3. 使用。

相信即便是不懂编程的人,对这个流程也不会感到费解。事实上我从接触编程开始,很长一段时期都是采用这种方式来管理要用到的 JavaScript 软件包。

我当时的电脑里有一个目录,分门别类的保存着这些软件包的源码。根据使用项目需求,会碰到以下使用场景:

  • 需要某个功能的软件包。从专门保存软件包的目录复制到项目里面。然后通过 linkscript 标签引用这些资源。
  • 当软件包有了新版本,同样复制到项目,然后删除旧的引用,添加新的引用。
  • 如果某个软件包不需要了,就直接删除引用标签,然后从项目中删除软件包的源码。

通过这些使用场景可以看出,这是一种纯手工的管理和使用方式。需要自己关注几方面的问题:

  1. 公共软件包库的管理。也就是我那个专门存放各种软件包的目录,需要做好归类和储存工作。
  2. 处理压缩和合并。这一点主要针对大量使用 JavaScript 功能的项目,对上线时的一些访问优化处理。

这些问题的影响以项目对 JavaScript 需求的复杂度而定。但不论影响大小,都无法否认这是一项很原始的包管理方式。

NPM

NPM,全称 Node Package Manager。直译过来就是「Node 软件包管理器」。从这个名字可以看出,它是 Node.js 的好基友没跑了。

Node.js 可以说是 JavaScript 历史上里程碑式的产物。也可以说是纯正 JavaScript 包管理工具诞生的基石。想想在此之前,用来压缩 JavaScript 代码的热门工具 YUI Compressor,还需要 Java 的运行环境。这是不是有一种居然自己事情都做不好,还需要另一种语言来擦屁股的感觉?

所以在 Node.js 出现之前,JavaScript 看起来像是一门寄生于浏览器端的「玩具脚本语言」。

Node.js 让 JavaScript 改头换面,而 NPM 让 JavaScript 看起来更加时尚。让我们看看有了 NPM,JavaScript 的包管理方式有了那些改变。

package.json

如果在任何项目根目录下看到这样一个文件,那十有八九就是采用 NPM 来管理 JavaScript 的软件包了。

一个极简的 package.json 文件内容看起来类似于下面这样:

{
  "name": "app",
  "version": "1.0.0",
  "description": "my test appp.",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "zzxworld",
  "license": "ISC"
}

看起来充斥了一些对业务项目来说无关紧要的信息。上面说了,这是极简状态。以上信息就是通过 npm init 命令生成的,它代表了当前项目下前端软件包的初始状态。

让我们从实际业务需求出发,接着看看它会发生什么变化。

npm install

根据业务场景,首先当然是要引入需要的软件包。比如 jQuery 吧。这可以通过以下命令来实现:

npm install jquery

等待命令执行完成后,再看看 package.json 文件,会发现最后多了些内容:

// ...
  "dependencies": {
    "jquery": "^3.6.0"
  }
}

这应该很一目了然了,因为我们通过 npm install 命令引入了 jQuery 软件包到当前项目。NPM 把这一举动记录到了 package.json 文件中,并标明了当前引入的版本。

这样做的意义,或者说作用是什么?很简单,当别人看到这份文件时,就能很直观的了解到这个项目使用了那些软件包,是什么版本的,一清二楚。

从功能开发的角度来说,仅仅是这个作用也意义不大。所以我们继续看看当前目录下还有些什么。

node_modules 和 package-lock.json

不出意外的话,当前的目录下应该会多出这样一个文件和目录:

  • package-lock.json 文件
  • node_modules 目录

package-lock.json 文件内容如下:

{
  "name": "app",
  "version": "1.0.0",
  "lockfileVersion": 1,
  "requires": true,
  "dependencies": {
    "jquery": {
      "version": "3.6.0",
      "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
      "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw=="
    }
  }
}

和 package.json 文件相比,它的版本号更加明确,并且多了软件包的 URL 地址。根据文件名,结合它的内容,基本也可以猜到它的两大用途:

  1. 明确当前依赖软件包的具体版本。
  2. 加速软件包二次安装时下载。

而 node_modules 目录,打开一看就知道,通过 npm 安装的软件包源码,都在这里。

实践指南

目前为止,通过 NPM 做到的也仅是完成了软件包的引入。通过 npm install 命令,很方便的获得了项目的源码。接下来要面对的,是如何把软件包使用到项目中的问题。

最原始的方式,就是把 node_modules 中软件包的源码复制到项目中来。这有点类似于把 node_modules 当成了一个软件包库。这样做最直观的好处就是不用再搜索各种软件包的官方网站,去下载最新的软件包源码。通过 npm 命令,可以很轻松的引入自己想要使用的任何软件包。

更加优雅的方式,是通过 npm 安装 webpack 这类模块化打包工具。这样可以在项目中通过 import 或者 require 方法无缝的引用安装的软件包。这样才能享受到流程化和工具化带来的便捷。

另外 node_modules 目录作为 npm 命令的自动化产物,非特殊情况下,没必要加入到项目的源码跟踪系统。只要有 package.json 文件,并确保里面定义了项目所需的软件包。之后通过 npm install 这一条命令就能简单的重建 node_modules 目录。

而 package-lock.json 作为 npm 命令自动化生成的另外一个产物,是推荐要保留了。主要原因是 package.json 的版本号定义很宽泛。而 package-lock.json 作为补充,类似于 package.json 的快照,能确保任何时候安装的软件包都跟首次安装的版本一致。

CNPM

CNPM 的官方解释是 Company npm,不过对于国内使用它的 JavaScript 开发者来说,有另外一个解释。

简单来说就是 npm 的软件包库在国外,国内访问速度不是很理想。通过 npm install 命令安装软件包时,要等待比较长的时间,这十分影响工作效率,以及心情。CNPM 应运而生。

所以 CNPM 的主要作用就是给国内的 NPM 使用者加速。也可以看成是 CNPM 就是在 NPM 外套了层壳。这从 CNPM 的安装使用上就能一窥一二。

比如可以这样通过命令映射的方式使用:

alias cnpm="npm --registry=https://registry.npm.taobao.org \
--cache=$HOME/.npm/.cache/cnpm \
--disturl=https://npm.taobao.org/mirrors/node \
--userconfig=$HOME/.cnpmrc"

也可以使用 npm 命令:

npm install -g cnpm --registry=https://registry.npm.taobao.org

之后就可以使用 cnpm 命令来执行软件包的安装流程,操作上和使用 npm 别无二致。唯一的区别就是下载软件包的速度变快了。

YARN

NPM 给 JavaScript 语言的软件包管理带来了一些便利。不过由于一些设计上的缺陷,也带来了一些应用上的问题。一些很有幽默感的程序员们就通过一些漫画来调侃了这些 NPM 的问题。

比如这张形容 NPM 安装依赖时的海量软件包的「NPM 快递」漫画:

npm delivery

还有形容 node_modules 超越黑洞的夸张比喻:

heaviest objects in the universe

Yarn 可以说是针对 NPM 现有问题,全新推出的一个 JavaScript 包管理工具。不过它完全兼容 NPM 的 package.json 文件。同样也使用 node_modules 目录来作为软件包的源码存储目录。不过它有自己的版本锁定文件:yarn.lock。

和 NPM 比较,它主要有以下优势:

  • 使用扁平模式来处理软件包之间的依赖。
  • 采用并发处理模式。
  • 支持离线模式,在有缓存的前提下可以不依赖网络连接就能正常安装软件包。

这一系列特性带来的直观体验就是安装软件包比 NPM 快得多。所以在碰到一些 NPM 死活都安装不成功的情况时,我通常会换成 Yarn 试试。因为在软件包的管理流程上,Yarn 和 NPM 也是一致的。

PNPM

PNPM 是一个更加「新」的 JavaScript 包管理工具。也更加激进。相比 NPM,Yarn,它主要有两个十分显眼的特性:

  1. 速度快。
  2. 节省空间。

速度有多快呢?官方宣称比同类工具快近 2 倍。至于节省空间,则是因为它采用文件链接而不是复制的方式来处理软件包之间的安装和依赖。所以它能以最小的存储代价实现相应的软件包使用。

对于 PNPM 采用的这种方式,目前还有争议。不过我个人对此很有兴趣,毕竟拒绝不了时间和空间两方面的改善和提升。

总结

以上介绍的各种 JavaScript 包管理方式,依然都还在不断发展和改善当中。几年后回头看,也许又会有不一样的工具诞生。不过万变不离其宗,不论怎么发展,作为解决如何管理软件包这一需求的本质不会改变。所以对于刚接触 JavaScript 的新人,我建议先了解并尝试下「源码复制」这种原始的软件包管理方式。这样你才能体验到后面各种工具所带来的好处和便捷。

而现阶段(2021年)JavaScript 包管理工具的选择,我依然觉得使用 NPM/CNPM 还是主流,Yarn 是辅助。至于 PNPM,先保持关注吧。