全局装的 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 |
两个自测问题,能答上来说明真懂了:
npm install -g lodash之后项目里require('lodash')报错,根本原因是什么?(答案在 L2:require 的搜索路径里根本没有全局目录。)- 全局和项目里各装了一份 jest,
npx jest会跑哪个?为什么这个行为是特性而不是巧合?(答案在 L2 末段:本地优先。)
下一站:如果还没读环境搭建那篇–package.json、lock 文件、node_modules 的来龙去脉–建议先补上:那篇讲”这些文件是怎么来的”,这篇讲”这些命令该怎么选”。都齐了,就装个 Express 写第一个 API 吧。