一、为什么我们该认真对待依赖管理

很多前端开发者都有过这样的经历:项目第一天特别清爽,两个月后打开 node_modules,好家伙,比冰箱里放了一个月的剩菜种类还多。依赖这东西,装的时候一时爽,维护起来火葬场。Yarn 作为一款成熟的包管理器,它不只是帮你把包下载下来,更是在帮你管理项目的“饮食结构”。我们用生活化的方式理解,Yarn 就是你家厨房的管家:他清楚每一味调料放在哪、什么时候过期、哪些能混着用、哪些会导致吃了拉肚子。今天我们就从项目初始化开始,把依赖管理的全流程捋一遍。

二、项目初始化时的依赖规范

2.1 别再用 npm init 一把梭了

新建一个项目,很多人习惯敲 npm init -y,然后一路回车。但 Yarn 给出的方式更讲究。Yarn 在初始化时就会为后续的依赖管理打好地基。我们用 Yarn 独有的 yarn init,它同样会生成 package.json,但更推荐结合 Yarn 的 workspace 能力来规划项目结构。

假设我们要做一个电商网站的前端,包含用户端、管理后台、共享组件库三个部分。一开始就得按 monorepo 的思路来,不要等代码写了一半再拆分。

# 初始化项目根目录
yarn init

# 在根目录的 package.json 中添加上 workspace 配置

上面的命令执行完毕后,你会得到一个最基础的 package.json。接下来我们需要手动编辑它,加入 workspace 字段。注意,所有配置都必须写在代码块里,别裸奔。

{
  "name": "shop-frontend",
  "private": true,
  "workspaces": [
    "apps/*",
    "packages/*"
  ],
  "scripts": {
    "dev": "yarn workspace @shop/web dev",
    "build": "yarn workspaces run build"
  }
}

这样配置完之后,我们创建对应的子目录结构:

# 创建用户端目录
mkdir -p apps/web

# 创建管理后台目录
mkdir -p apps/admin

# 创建共享组件目录
mkdir -p packages/components

每个子项目里要有自己的 package.json。用户端和管理后台的 package.json 可以简单一点,但共享组件库的必须指定 package.json 中的 name 和 main,因为会被其他项目引用。

{
  "name": "@shop/components",
  "version": "1.0.0",
  "main": "src/index.js",
  "license": "MIT"
}

到这里,项目的地基就打好了。很多新手容易忽略一件事:根目录下的 node_modules 是共享的,所有子项目公用的依赖都会提升到根目录。如果你装了某个包,发现它出现在了根目录的 node_modules 里,别奇怪,这是 Yarn workspace 的默认行为。

2.2 lockfile 要从第一天就锁死

不管你是用 yarn add 还是 yarn install,只要操作的是同一个项目,就一定会生成 yarn.lock 文件。这个文件就像是菜谱的精确计量表:每种调料放几克、什么时候放、用什么火候,全部记下来。没有 lockfile 的项目,就像没有菜谱的厨师,今天咸了明天淡了。

强烈建议把 yarn.lock 提交到代码仓库,并且不要手动去改他。我们来看一个典型的 yarn.lock 片段:

# 这是 yarn.lock 的片段,注意不要手动编辑
"@babel/code-frame@^7.0.0 - ^7.10.4":
  version "7.12.11"
  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#..."
  integrity sha512-0qXcZYKp3VH0vvRqN9ZzF8tEC1Q==
  dependencies:
    "@babel/highlight" "^7.10.4"

平时我们根本不需要读懂这个文件的每一行,只需要知道:只要 lockfile 存在,任何人在任何时间执行 yarn install,得到的依赖树完全一致。这就是可复现环境的前提。

三、安装依赖的日常操作

3.1 添加依赖时的分类思维

使用 Yarn 两年以上的老手都知道,yarn add 命令很强大,但也要用对场合。依赖分为两大类:生产依赖(dependencies)和开发依赖(devDependencies)。生产依赖是程序运行时要用的,比如 Vue、React、lodash;开发依赖是编译、测试、格式化时用的,比如 webpack、babel、eslint。

生活化理解:你开了一家奶茶店。生产依赖就是珍珠、牛奶、茶包,客人来了必须要用;开发依赖就是封口机、记账本、培训手册,开店之前准备好,营业之后这些工具别再手忙脚乱找。区分不清的话,很容易把一大堆工具包装进生产依赖,导致线上部署时下载了一大堆根本用不到的东西。

# 安装一个运行时依赖
yarn add axios

# 安装一个开发依赖
yarn add jest --dev

上面的命令执行完毕后,我们看一下 package.json 的变化:

{
  "dependencies": {
    "axios": "^0.21.1"
  },
  "devDependencies": {
    "jest": "^26.6.3"
  }
}

注意那个 ^ 符号,它表示允许安装 0.21.x 范围内的最新版本。这其实是双刃剑:一方面能自动获取补丁,另一方面也可能带进不兼容的更新。如果你希望严格锁定补丁版本,可以在 yarn add 时去掉 ^,或者在 package.json 里改成精确版本。

# 安装精确版本,不保留 ^ 符号
yarn add axios --exact

3.2 依赖版本的哲学:不推车,不赌马

版本范围是个大学问。很多项目出问题,都是因为 package.json 写的是 "vue": "^2.6.0",而某个人的 yarn.lock 里因为各种原因解析到了 2.6.14,另一台机器却解析到了 2.6.15。虽然都是小版本,但一个小修复可能改了 API 的内部行为,导致部分功能崩溃。

因此,在团队协作中,yarn.lock 必须入库。如果有人改了 package.json 中的依赖,他必须同时更新 yarn.lock。否则在代码审查时,这应该被当成一个硬伤拦下来。

我们还可以利用 yarn upgrade-interactive 来有选择地升级依赖:

# 交互式升级依赖
yarn upgrade-interactive

这个命令会罗列出当前所有可以升级的依赖,并显示它们的新旧版本,你可以用快捷键手动勾选要升级哪些。这比 yarn upgrade --latest 一次性全升要安全得多。全量升级就像是把所有菜都倒在锅里重新炒一遍,味道大概率不对。

四、迭代中的依赖增删改

4.1 删除依赖时别怕,但要验尸

项目做久了,总有一些功能被砍掉,依赖却还留在 package.json 里。一个健康的项目,依赖数量应该像定期整理的衣柜:该扔的扔,该叠的叠。

删除依赖用 yarn remove:

# 移除一个生产依赖
yarn remove moment

# 移除一个开发依赖
yarn remove webpack --dev

有时候你以为删干净了,其实因为 workspace 或者传递依赖的关系,包可能还存在于 node_modules 中。此时可以执行:

# 清理一下缓存并重新安装
yarn install --force

不过 --force 是最重的手感,没事别老用。更温和的方法是 yarn install 本身就会根据 lockfile 来调整已安装的包。

4.2 升级依赖的节奏感

依赖升级不能像手机系统推送一样,一看到红点就慌,也没必要来一次升一次。遇到安全补丁,必须及时升;遇到大版本升级,要评估兼容性。看一个典型例子:

比如你想把 lodash 从 4.17.20 升级到 4.17.21,因为后者修复了安全漏洞。

# 安全升级还是非常有必要的
yarn upgrade lodash --scope @babel

上面这个例子有点特殊,--scope 是用于限定某个 scope 的所有包。我们平时直接升级单个包即可:

# 升级 lodash 到最新补丁版本
yarn upgrade lodash@^4.17.21

升级完以后,一定要跑一遍测试。这就是买菜回来先闻一闻,别直接下锅。

4.3 批量检查过时依赖

想知道目前哪些依赖有新版本,可以使用:

# 列出所有过时的依赖
yarn outdated

输出结果会显示当前的版本、期望的版本、最新的版本。这样你就能制定更新计划。比如,当前版本是 1.0.0,期望版本是 1.2.3(根据 package.json 范围),最新版是 2.0.0,那你就要权衡:跳一个主版本可能有不兼容改动。

五、缓存与离线策略

5.1 Yarn 的缓存机制到底有多香?

Yarn 会把下载过的包缓存到本地,第二次安装就不需要联网下载了。这个缓存目录在 Mac 上通常是 ~/.yarn/cache,Windows 上是 %LocalAppData%\Yarn\Cache。我们可以用命令查看:

# 查看缓存目录
yarn cache dir

既然有缓存,我们完全可以做到“断网也能装依赖”。只要 lockfile 存在,并且本地缓存里有对应的包,就可以用离线模式安装:

# 离线安装依赖
yarn install --offline

这个命令特别适合在飞机上写代码,或者在网络状况很差的咖啡馆调试项目。

5.2 清理缓存,释放空间

缓存也不是越大越好。时间久了,缓存目录里会堆满各种旧版本的包。可以定期清理:

# 清理全部缓存
yarn cache clean

不过要注意,清理全部缓存之后,下次安装就得重新下载了。如果你想精确清理某个包:

# 清理指定包的缓存
yarn cache clean axios

值得注意的是,Yarn 的缓存策略在过去和现在略有不同。老版本 Yarn 使用全局缓存目录,而 Yarn 2 以后引入了 .yarn/cache 零安装模式。这里我们不展开,但你要知道这不是一个完全不变的概念。

六、安全与团队协作

6.1 检查已知漏洞

依赖的版本库就像一个开放社区,任何人都可能提交恶意代码。有些漏洞被发现后,Yarn 会通过 yarn audit 提醒你。这个命令会检查当前依赖树中是否存在已知的安全漏洞。

# 检查安全漏洞
yarn audit

输出的报告中会列出漏洞等级:critical、high、moderate、low。对于 critical 级别的漏洞,就相当于家里煤气泄漏,必须马上处理。处理方法一般是升级到修复后的版本。

6.2 使用 .yarnrc 统一配置

如果你是团队的技术负责人,可以创建一个 .yarnrc 文件,统一团队的 Yarn 行为。比如,禁止使用 --ignore-engines 或者设置镜像源。

# 统一使用淘宝镜像,并关闭进度条
registry "https://registry.npm.taobao.org"
ignore-optional true

这样团队成员在安装依赖时,就不需要每个人都自己配置镜像了。当然,现在企业级项目可能会自己搭建私有 npm 仓库,那就把 registry 改成公司内部地址。

6.3 锁定 engines 字段

不同的包可能要求不同的 Node 版本。为了不让团队成员用错误的 Node 版本跑项目,我们可以在 package.json 中加入 engines 字段:

{
  "engines": {
    "node": ">=14.0.0",
    "yarn": ">=1.22.0"
  }
}

然后在 .yarnrc 中开启引擎检查:

--ignore-engines false

这样如果有人用了 Node 12,Yarn 会给出警告。这就像厨房门口贴了一张告示:身高不足一米四禁止进入,避免乱碰厨具伤到手。

七、进阶技巧:事件钩子与脚本

7.1 使用 pre 和 post 钩子

Yarn 或 npm 在执行任意 yarn run 脚本时,都会自动执行带有 pre 或 post 前缀的同名脚本。比如我们定义了一个 deploy 脚本,再定义一个 predeploy 脚本,那么执行 yarn deploy 时,predeploy 会自动先跑。

{
  "scripts": {
    "predeploy": "yarn build",
    "deploy": "node deploy.js",
    "postdeploy": "curl -X POST https://api.example.com/notify"
  }
}

这个机制非常实用,可以避免你在部署时忘记构建。当然要注意,如果 predeploy 失败了,deploy 不会执行。这就好比做菜前一定要洗手,手不干净就不让你碰锅铲。

7.2 自定义 Yarn 插件

如果你用的是 Yarn 2 及以上版本,还可以通过 API 扩展 Yarn 的能力。不过大多数团队不需要折腾插件。这里先略过,保持简单。

八、工作流程中的依赖管理规范

8.1 新功能从依赖评审开始

每当你准备引入一个新依赖,先问自己三个问题:第一,这个功能我能用 Web API 或已有代码实现吗?第二,这个库的维护频率如何?第三,它会不会拖累打包体积?如果三问过后还是决定要用,那就指定一个负责人去安装并提交 lockfile。

8.2 编写清晰的 README 文档

依赖管理不是一个人默默做的事。在项目 README 中要写清楚怎么安装、怎么更新、怎么升级依赖。比如:

# 开发指南

- 安装依赖:yarn install
- 添加依赖:yarn add <package> --dev 或 yarn add <package>
- 升级依赖:yarn upgrade-interactive
- 检查漏洞:yarn audit

8.3 定期维护依赖的健康状态

建议每个月做一次依赖治理,查看 yarn outdated,利用 yarn upgrade-interactive 更新补丁版本。对于超过半年不更新的依赖,要评估是否替换。一个项目如果长期不关注依赖,就像一个几个月不清扫的厨房,最终会是蟑螂的乐园。

九、简要分析配套工具与生态

在 Yarn 的工作流中,我们常常会搭配一些辅助工具。比如 npx 可以用来执行一次性命令,但在 monorepo 场景下,yarn exec 也很顺手。

# 运行 workspace 中的某个命令
yarn workspace @shop/admin build

又比如,我们希望某个依赖只安装在根目录,而子项目不用手动安装。可以使用 yarn add -W -D。

# 将 TypeScript 安装为根目录的开发依赖
yarn add -W -D typescript

这里的 -W 表示允许 workspace 根目录安装依赖。

此外,yarn why 可以帮助我们排查为什么某个包被安装到了项目里。这个命令特别有用,当你想删除一个包却担心其他依赖依赖它时。

# 查看为什么安装了 chalk 这个包
yarn why chalk

它会输出依赖路径,让你看清楚到底是哪个包引入了 chalk。生活化类比:你发现厨房里多了包花椒,不知道谁买的,查一下购物记录,发现是朋友上周来做水煮鱼时顺手买的。

十、注意事项与踩坑记录

10.1 路径问题

不要在文件名中使用中文和空格,否则 Yarn 在处理 workspace 时可能遇到奇怪的报错。别问我是怎么知道的,换一个干净的目录名,心情都会变好。

10.2 不要混合使用 npm 与 Yarn

同一个项目里,今天用 npm install,明天用 yarn install,是很多混乱的根源。因为 npm 会生成 package-lock.json,Yarn 使用 yarn.lock,两者锁定的版本可能不一致。如果一个项目同时出现了这两个文件,团队又没共识,那问题会像滚雪球一样越来越大。

10.3 CI 环境中的安装策略

在持续集成环境中,我们应该使用 yarn install --frozen-lockfile,这个命令会严格依据 yarn.lock 安装,如果 package.json 与 yarn.lock 不一致就会报错。这是保证 CI 构建可复现的关键。

# CI 环境下安装依赖
yarn install --frozen-lockfile

此命令就像一个规则的厨师,只要菜谱上写了几克盐,就绝不改变。

10.4 全局安装要克制

尽量不要 yarn global add 那些只在某个项目里用的命令行工具。全局安装的版本冲突很难排查。不如把工具放进项目开发依赖里,通过 yarn run 调用。

十一、总结

Yarn 依赖管理不是一项高不可攀的技能,而是一套日常的、和项目共生的习惯。从初始化时搭建 workspace,到每一条 yarn add 命令,再到 lockfile 的提交、漏洞的检查,每一步都在为项目的可持续维护性添砖加瓦。我们不需要背下所有命令,但必须理解依赖管理背后的原则:可复现、可审查、可分治、可回退。

生活总是充满变化,依赖管理也是如此。今天完善的 lockfile,明天可能就要因为新需求而调整。保持敬畏,保持记录,让 yarn.lock 像一本账,把每一次变动都写清楚。这样就算哪天项目出了诡异的问题,我们也能顺着账目一步步找出元凶。希望这篇文章能帮你建立起对 Yarn 依赖管理的整体认识,在后续的开发中少踩一点坑,多安稳合几次代码。

请记住,依赖管理不是目的,只是工具。真正重要的是我们如何让项目在快速迭代中依然保持健康,像一棵不断生长的树,根系扎实,枝叶清晰。