文章

全局装的 lodash,为什么项目里 require 不到

一个新手几乎必踩的坑。你听说 lodash 是个好用的工具库,于是:

1
npm install -g lodash

装完了,终端还友好地打印了版本号。回到项目里写 require('lodash'),运行:

1
Error: Cannot find module 'lodash'

明明刚装的,”全局”的,装给整台电脑的,怎么就找不到?更气人的是反方向的委屈:教程让你 npm install express,每个项目都得装一遍,磁盘上几十份拷贝–“我不是全局有一份吗,为什么不让用?”

同一个动作”把包装到电脑上”,npm 生态给了你至少三个入口:本地安装、全局安装(-g)、npx,外加 pnpm 这个想把 npm 连锅端走的搅局者。入口选错,代价不是报错就是磁盘白烧。这篇把它们一次理清。

L1:先会用–三种装法,三种场合

你在第一层,目标是拿到”什么时候敲哪条命令”的直觉。先给结论,为什么留到后面:

1
2
3
npm install express        # 本地安装:给这个项目用,代码要 require 它
npm install -g typescript  # 全局安装:给你自己在终端里敲的(装完敲 tsc --version 试试)
npx cowsay 喵              # 不装:临时下载跑一次,完事不留痕

三种场合一句话:给项目买的进厨房,给自己买的进工具间,尝一口的用一次性筷子。

本地和全局都直观,npx 值得多看一眼。它是 npm 自带的(npm 5.2 之后),专门解决”我只想跑一次这个工具”:

1
npx cowsay 喵

第一次会先下载再运行,输出一头 ASCII 牛:

1
2
3
4
5
6
7
8
 ____
< 喵 >
 ----
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

跑完检查 package.json–没多出任何一行。

到这里”怎么选”你会了。但开头的坑还是解释不了:npm install -g lodash 明明装上了,require('lodash') 为什么就是找不到?”全局”这两个字,到底全局在哪?往下爬一层。

L2:懂原理–“全局”是命令行的全局,不是 require 的全局

你在第二层,目标是搞清楚每种装法到底把东西放到了哪、谁找得到它。

先看本地安装。包装进项目的 node_modules,包里附带的命令行工具放进 node_modules/.bin(你在 package.json 里写 "scripts": { "test": "jest" } 时,npm 就是在这个目录找到 jest 的)。关键是:这份菜单记了账–package.json 里多出一条依赖,lock 文件里多出一串精确版本。

再看全局安装。跑一下 npm root -g,nvm 用户多半得到:

1
~/.nvm/versions/node/v22.11.0/lib/node_modules

一个独立于所有项目的仓库,命令行工具放进同级的 bin 目录–它在 PATH 上,所以在任何目录都敲得动 tsc。这就是”全局”的真实含义:命令行的全局,仅此而已。

至于 require 为什么找不到,看它的查找规则就明白了:

从当前文件所在目录的 node_modules 找起,一级一级向上–父目录的 node_modules、再上一级……直到文件系统根。全局目录不在搜索路径上。这是刻意设计,不是疏忽。

动手验证(接着开头的坑)。先预测再看答案:下面两条命令跑在同一个项目目录里,第二条能成吗?

1
2
npm install -g lodash
node -e "console.log(require('lodash').VERSION)"

报错,和开头一模一样。接着:

1
2
npm install lodash
node -e "console.log(require('lodash').VERSION)"

这次输出 4.17.21(或你安装时的最新版)。同一条命令,去掉 -g,世界就通了。全局那份 lodash 还好端端躺在硬盘上,但对你的项目来说,它不存在。

npx 的原理也顺手收掉。它的查找顺序是:先看当前项目的 node_modules/.bin,再看全局的 bin,两处都没有就临时下载到缓存里跑一次,不留安装记录。本地优先这个顺序给了 npx 一个反直觉的正确用法:npx jest 跑的一定是本项目锁定的 jest,而不是你全局装的不知道哪个版本。

全局安装还有第二个坑,藏在 npm root -g 的输出里:注意那个 v22.11.0–全局包跟着 Node 版本走。nvm 用户明天装了新版本一切换,昨天 -g 装的工具集体”失踪”(其实还在旧版本的目录里躺着,切回去就回来了)。npm ls -g --depth=0 可以清点当前版本下的全局家当。类比:nvm 给每个 Node 版本发了一副眼镜,全局包住在某一副眼镜的口袋里;换了眼镜,口袋里的东西自然不在身上。

到这里,每种装法”放哪、谁找得到”都清楚了。但最根本的问题还悬着:require 故意不看全局目录,这不是刁难人吗?如果 Node 让 require 找不到时去全局兜个底,开头的坑不就不存在了?往上爬最后一层。

L3:想得透–require 只认一本账

你在第三层,最后一层。不谈命令也不谈机制,谈”为什么非这样不可”。

先认真对待刚才那个”改进方案”:require 找不到时,去全局目录兜底。听起来体贴,失败场景却排着队来:

  • 账本出现两套:代码用着 lodash,package.json 里却没有它。你把代码发给同事、推上 CI–Cannot find module。经典的”在我电脑上是好的”;
  • 版本随机器漂移:你的全局是 lodash 4.17,同事的全局是某个老版本,同一段代码两台机器行为不同,排查时连怀疑对象都没有;
  • 全局升级连坐全场:你为了新项目把全局的某个包升到最新,所有悄悄依赖它的老项目同时被改了配方,而且没有任何记录。

这三条加起来有个你熟悉的名字:依赖地狱。全局兜底省下的那点磁盘和安装时间,会在某个深夜加倍讨回去。所以 Node 的设计是刻意的:一个包对项目来说是否存在,只以 package.json 的显式声明为准;全局目录从一开始就只服务命令行工具,不服务 require。开头的报错不是 bug,是系统在替你把关:”想依赖它?先记账。”

本质段来了。去掉所有术语,本地、全局、npx 的分工到底是什么?

把”项目的家当”和”我的家当”分成两本账:项目的记在 package.json 里,跟着仓库走,谁拿到都能原样复现;我的记在机器上,跟着人走,丢了重装就行。require 只认第一本账。全局装 lodash 之所以失败,不是功能缺陷,是你把伙食费记到了别人的账本上。

检验一下这个本质成不成立:你能不借用任何术语,跟同事讲清”为什么项目要用的包不能装 -g“吗?

最后轮到那个搅局者出场。pnpm 冲着 npm 的另一桩罪状而来:磁盘爆炸。上一篇讲过,npm 把整棵依赖树平铺进每个项目,同一个包在磁盘上被复制几十上百份。pnpm(名字是 “performant npm”)的做法:所有包在磁盘上只存一份(一个中央仓库,硬链接保证文件不占第二遍空间),每个项目的 node_modules 用符号链接指过去。隔离照旧,重复归零。

想换,动手成本极低:

1
2
npm install -g pnpm   # 安装它用的正是本文讲的全局安装--跨项目的命令行工具
pnpm --version        # 能出版本号就成了

装进项目时命令几乎无缝替换:npm install 换成 pnpm install,npm install express 换成 pnpm add express,npm ci 换成 pnpm install --frozen-lockfile,npx cowsay 换成 pnpm dlx cowsay;lock 文件换成 pnpm-lock.yaml(记得提交)。npm 老项目迁移:进目录跑一遍 pnpm import(把 package-lock.json 转成 pnpm-lock.yaml),删掉 node_modules 重装即可。

还有一个意外的红利:pnpm 的符号链接结构让 require 只认你亲手声明过的依赖。npm 项目里那种”没装过 cookie 却能 require 成功”的幽灵依赖(见上一篇的实验),在 pnpm 里会当场报错。雷响在开发机上,总好过埋到线上。

一句话总结这层权衡:require 只认 package.json 这本账,用”多敲一次 npm install”的小麻烦,换来”任何机器上都能原样复现”的大确定;pnpm 则在这份确定之上,把磁盘的账也一并算清了。

收尾:一张决策表

你想要什么敲什么记在哪本账上
项目代码要 require 的包npm install <包名>package.json(连 lock 文件一起提交)
每天在终端敲的跨项目工具npm install -g <包名>机器(换电脑、换 Node 版本记得重装)
临时跑一次的工具npx <命令>不记账,用完即走
跑本项目锁定的命令npx <命令>(如 npx jest)本地优先,版本钉死
磁盘被 node_modules 吃穿换 pnpm:npm install -g pnpm,老项目 pnpm import 迁移同 npm,账本换成 pnpm-lock.yaml

两个自测问题,能答上来说明真懂了:

  1. npm install -g lodash 之后项目里 require('lodash') 报错,根本原因是什么?(答案在 L2:require 的搜索路径里根本没有全局目录。)
  2. 全局和项目里各装了一份 jest,npx jest 会跑哪个?为什么这个行为是特性而不是巧合?(答案在 L2 末段:本地优先。)

下一站:如果还没读环境搭建那篇–package.json、lock 文件、node_modules 的来龙去脉–建议先补上:那篇讲”这些文件是怎么来的”,这篇讲”这些命令该怎么选”。都齐了,就装个 Express 写第一个 API 吧。

本文由作者按照 CC BY 4.0 进行授权