
pnpm 装 React 原型这件事说起来简单但我见过太多人卡在第一步——敲完命令盯着终端里滚动的进度条发呆或者第二天发现 C 盘少了好几个 G。原型项目的特点很明确起得快、扔得也快、数量还多一个人手里同时挂着三五个 Demo 是常态。这种场景下能跑起来只是底线真正影响手感的是装包够不够快、磁盘够不够省、依赖够不够干净。pnpm 恰好在这三件事上都比传统方案更精明但它那套基于硬链接和符号链接的严格目录结构也让不少新手第一次遇到Module not found时一脸茫然。这篇内容就围绕5 分钟用 pnpm 搭一个 React 原型项目这条主线展开从环境自检、脚手架选型、最小可用配置一直讲到踩坑排查和原型沉淀。前端新手能照着抄完整流程有经验的同行也能从依赖管理那一节里找到一些平时容易忽略的细节。1. 原型项目为什么值得专门换用 pnpm1.1 从每个项目一份 node_modules的浪费说起传统 npm 的安装模型很好理解每个项目的node_modules都是独立、完整的一份拷贝。一个带 TypeScript、路由、状态管理、UI 组件库的 React 原型装完之后node_modules目录动辄 300MB 到 500MB文件数量两万到四万个不等。这个数字单看一个项目不吓人但你想想实际情况——验证一个弹窗方案建一个、试一套图表库建一个、给同事演示一个——一个月下来七八个项目躺在硬盘里光依赖就吃掉三四个 G。而且这里面 90% 的内容是完全重复的React 还是那个 ReactTypeScript 还是那个 TypeScript只是被拷贝了七八遍。pnpm 的做法是把包体本身存到全局仓库store里项目里的node_modules只保留硬链接。硬链接你可以理解成给同一份文件起了第二个名字——文件内容只有一份两个名字都指向它删掉其中一个名字文件还在。所以当你建第十个 React 原型时React 那一份代码在磁盘上始终只有一份新项目里增加的只是一个链接条目。我从自己机器的实际观察看第一个 React TS 项目装完大概 280MB后面同样技术栈的新项目增量通常在 30MB 以内主要是一些版本差异和项目自己的构建产物。对于频繁起原型的场景这个差距会随着项目数量线性放大。1.2 安装速度的差距到底来自哪里很多人以为 pnpm 快是因为下载快其实下载环节大家都是从同一个源拉 tarball差距没那么大。真正的差距在文件写入上。npm 要把解压后的几万个文件逐个写到项目目录这是典型的随机小文件 IO在机械硬盘或者被杀的 Windows Defender 面前非常痛苦。pnpm 大部分情况下只需要在 store 里解压一次项目里创建硬链接链接操作几乎是瞬时完成的。我在一台普通办公本上做过非严格对比全新缓存下一个 React TS 路由 axios UI 库的组合npm 大概 45 秒到 1 分钟pnpm 在 20 秒上下如果 store 里已经有这些包的相同版本pnpm 二次安装经常能压到 3 到 5 秒快得有点不真实。这也解释了为什么5 分钟搭原型这个说法成立——时间不是省在敲命令上是省在等进度条上。当然你要是第一次装、网络又一般那 5 分钟确实紧张这时候别硬撑先把源配好再说。1.3 严格的依赖结构是优点也是门槛npm 从 v3 开始搞扁平化提升把所有依赖尽量平铺到顶层node_modules好处是兼容性好坏处是产生幽灵依赖——你的代码里import _ from lodash能跑通但package.json里根本没写 lodash只是某个依赖顺带装了它。这种代码在本地岁月静好换个包管理器或者升级依赖就炸。pnpm 默认不扁平化node_modules顶层只放你package.json里明确声明的包其余全部藏在.pnpm目录里用符号链接串起来。这相当于强制你做依赖诚实的人。好处是项目可移植性强坏处是某些老库、某些插件会找不到自己以为存在的包直接报错。这不是 pnpm 的 bug是那些包本身写得不规范。理解这一点后面遇到报错你就不会慌。1.4 什么情况下我不建议硬上 pnpm说句公道话pnpm 不是万能药。如果你接手的是一套历史项目package.json里一堆包版本互相咬着node_modules里还有手工 patch 过的文件这时候贸然换包管理器大概率要花一整天调依赖得不偿失。另外某些对目录结构极度敏感的构建插件在 pnpm 下需要额外的nodeLinker配置才能正常工作。但对于新建原型这个场景情况完全相反没有历史包袱、没有奇怪的 patch、依赖清单干干净净这正是 pnpm 最舒服的战场。所以我的判断标准很简单——新起的、用完就扔的、拿来验证想法的项目一律 pnpm维护多年的老项目老老实实跟着它原来的走。2. 五分钟倒计时开始前的环境自检2.1 Node 版本这一关必须先过所有现代 React 脚手架都对 Node 版本有硬要求Vite 5 往上基本要求 Node 18 起步用 Node 16 会遇到各种奇奇怪怪的语法报错报错信息还指不到根上非常浪费时间。动手之前先敲两条命令node -v npm -v理想输出是v18.x或v20.x。如果版本太低别想着绕过直接去官网装一个新的。这里有个经验优先装 LTS 版本别追最新的奇数版本。奇数版本是尝鲜版生命周期短某些原生模块的预编译包还没跟上装依赖时容易触发本地编译一编译就是几分钟起步还可能要装构建工具链5 分钟的计划直接崩盘。如果你机器上存在多个项目需要不同 Node 版本可以装一个版本切换工具来管理切换成本很低。我个人的习惯是给原型项目单独留一个 LTS 版本避免和主项目互相干扰。2.2 用 corepack 启用 pnpm别急着全局安装Node 16.13 之后自带了一个叫 corepack 的东西专门用来管理包管理器的版本。相比npm i -g pnpm我更推荐用 corepack原因是它能把 pnpm 版本和项目绑定——你在package.json里写上packageManager: pnpm9.x.x团队成员拉下代码后 corepack 会自动用对应版本不会出现你装的 pnpm 9 我装的 pnpm 8锁文件格式对不上的尴尬。启用方式corepack enable corepack prepare pnpmlatest --activate pnpm -v第一条命令会在 Node 的 bin 目录里创建 shim可以理解成转发器第二条把指定版本的 pnpm 下载并激活。跑完pnpm -v能看到版本号就说明通了。注意Windows 上如果corepack enable报权限错误用管理员权限开一个终端再执行一次。另外某些环境里 corepack 的下载会被拦这时候退回npm i -g pnpm也能用只是少了版本锁定这层保障。2.3 把源和缓存目录一次配好这一步很多人跳过然后在安装阶段卡十分钟。pnpm 读的是.npmrc配置有两件事值得在第一次就设好一是 registry 指向一个访问顺畅的源二是 store 目录的位置。registryhttps://registry.npmmirror.com store-dirD:\.pnpm-store第一行换了源第二行把全局仓库放到空间大的盘符。为什么要把 store 单独拎出来因为它会随着你用过的包版本不断累积时间长了可能有几个 G。如果你的系统盘本来就紧张让它待在默认位置迟早出问题。这个路径一经设定就别随便改改了之后旧项目里的硬链接会全部失效只能重新装。配置文件放全局用户目录下的.npmrc还是放项目里我的建议是全局配源和 store项目里只放和项目相关的配置比如某些包的提升规则。全局一份所有新项目自动继承省得每次重复。2.4 一张自检清单省掉一半返工动手建项目之前花 30 秒过一遍这几项能避免后面 80% 的莫名其妙检查项期望状态不达标的表现Node 版本18.x 或 20.x LTS报语法错误、??和?.无法解析pnpm 可用pnpm -v有输出命令不存在或版本低于 8registry能正常拉包安装卡在resolving阶段不动store 盘空间剩余 5GB 以上安装到一半报ENOSPC终端权限当前目录可写创建文件失败或生成EPERM这套自检看起来啰嗦但我踩过的坑基本都在里面。特别是最后一条Windows 上把项目建在C:\Program Files下面然后一路报权限错误的我见过不止一次。3. 脚手架选型为什么原型项目我几乎只用 Vite3.1 冷启动速度是原型场景的第一生产力原型项目的使用节奏是改一下、看一眼、再改一下热更新反馈的延迟直接影响思路连贯性。基于打包器的老方案在这件事上有天然劣势启动要先把整个依赖图走一遍项目稍微大一点就是十几秒起步改个文件再等一两秒才能在浏览器里看到效果。Vite 的开发模式直接利用浏览器原生模块加载能力启动时间基本和项目规模无关几百毫秒就起来了热更新通常在一两百毫秒内完成。这个差距在原型阶段被放大了。原型的特点是代码量不大但依赖不少——你可能同时装了路由、图表库、UI 组件库、日期处理这些东西在老方案里都要参与预打包启动时间立刻上去了。Vite 的依赖预构建走的是 esbuild速度快一个量级。所以5 分钟出原型这句话能成立Vite 功不可没光靠 pnpm 是撑不起来的。至于另一套曾经很流行的脚手架方案现在新建项目已经不太推荐了它的构建体系对现代工具链的适配比较滞后配置也偏重。原型项目要的是轻不是全。3.2 一条命令把项目骨架拉起来选型定了就动手创建命令很简单pnpm create vite my-proto --template react-ts这条命令会做三件事下载 create-vite 脚手架、按react-ts模板生成文件、在当前目录下建一个叫my-proto的文件夹。用 TypeScript 模板是我的默认选择理由不是TS 更高级这种空话而是原型项目经常改结构、加字段类型提示能在你忘了某个组件要传什么 props 的时候救你一把而且现在编辑器对 TS 的支持已经很顺滑写起来的额外负担很小。如果你连类型都不想管把react-ts换成react就是纯 JS 模板。不过说实话两个模板的文件差异极小多写几个类型注解的成本远低于后期排查这个字段到底是 string 还是 number的时间。命令里的--template参数也可以省略此时会进入交互式选择让你挑框架和变体。交互模式的好处是能看到有哪些可选模板坏处是多按几次回车。赶时间就直接带参数。3.3 进去之后的第一件事不是 npm install创建完成后终端通常会提示你cd进去然后装依赖。这里有个细节一定要用 pnpm 装不要顺手敲 npm install。原因很实在npm 会在项目里生成package-lock.jsonpnpm 生成的是pnpm-lock.yaml两个锁文件同时存在后续谁说了算就很混乱CI 上也容易出问题。正确顺序cd my-proto pnpm install pnpm devpnpm install在 store 已有缓存的情况下通常几秒钟结束然后pnpm dev启动开发服务器终端会打印一个本地地址通常是http://localhost:5173。浏览器打开能看到 Vite 和 React 的默认页面到这一步5 分钟的目标实际上已经达成了。剩下的时间是用来把它改造成你真正想要的原型。3.4 目录结构先摸清楚再动手改脚手架生成的目录不大但每一块的作用值得花一分钟搞清楚不然后面加配置容易放错地方my-proto/ ├── public/ 静态资源原样拷贝到产物目录 ├── src/ │ ├── assets/ 参与构建的图片等资源 │ ├── App.tsx 根组件 │ ├── main.tsx 入口挂载 React 到 DOM │ └── index.css 全局样式 ├── index.html 页面模板Vite 的入口 HTML ├── vite.config.ts 构建与开发服务器配置 ├── tsconfig.json TS 编译配置 └── package.json 依赖与脚本有个地方新手常困惑index.html在项目根目录而不是像老方案那样藏在public里。这是 Vite 的设计它把这个 HTML 当成入口来解析里面的script typemodule src/src/main.tsx就是整个应用的起点。你要改页面标题、加 meta 标签直接改这个文件不用去翻别的地方。提示public目录下的文件不会经过构建处理文件名也不会带 hash。放 favicon、放一些第三方给的静态 JS 是合适的但放需要压缩优化的图片就不合适了那些应该扔到src/assets里让构建流程处理。4. 把一个空壳改成能用的原型4.1 依赖装哪些、装在哪一类空模板只有 react 和 react-dom做原型肯定不够。但我不建议一上来就把前端全家桶全装上那会让package.json变成一坨谁也不清楚用途的清单。我的习惯是按需分批第一批只装跑通核心交互必须的东西pnpm add react-router-dom zustand axios pnpm add -D types/nodereact-router-dom管页面跳转原型里经常要模拟多页面流程zustand是个极轻的状态库几行代码就能建一个 store比引入一整套状态管理方案更适合原型axios负责请求用来对接后端接口或者 mock 数据。types/node放在开发依赖里是为了在vite.config.ts里能正常使用path模块做路径别名这个后面会细说。装包时有两个参数要分清楚pnpm add写进dependenciespnpm add -D写进devDependencies。原型项目里这个区分其实不影响运行构建时都会装但保持规范有个实际好处——将来把这个原型转成正式项目时依赖分类是干净的不用回头整理。4.2 路径别名省掉一堆../../../原型项目目录浅的时候还好一旦开始拆组件import Button from ../../../components/Button这种写法就开始恶心人了。配个别名把src映射成写起来清爽很多。vite.config.ts里加import { defineConfig } from vite import react from vitejs/plugin-react import path from node:path export default defineConfig({ plugins: [react()], resolve: { alias: { : path.resolve(__dirname, src), }, }, })光配 Vite 不够TypeScript 有自己的模块解析逻辑编辑器里会飘红。tsconfig.json里补上对应的 paths{ compilerOptions: { baseUrl: ., paths: { /*: [src/*] } } }两处都要配缺一不可。这是新手最容易漏的地方只配了 Vite运行时正常但编辑器一路红线只配了 tsconfig编辑器不报错但跑起来找不到模块。我一开始也漏过排查了十几分钟才反应过来是两份配置的事。4.3 开发服务器代理绕开跨域这个拦路虎原型阶段对接后端接口十有八九会遇到跨域。这时候别去求后端加响应头自己在前端配个代理最快。vite.config.ts里加server段export default defineConfig({ server: { port: 5173, open: true, proxy: { /api: { target: http://127.0.0.1:8080, changeOrigin: true, rewrite: (p) p.replace(/^\/api/, ), }, }, }, })几个参数值得说清楚target是真实后端地址changeOrigin把请求头里的 Host 改成目标地址很多后端会校验这个rewrite负责把路径前缀去掉——前端写/api/user/list实际请求到后端是/user/list。这三行配置能省掉大量沟通成本。open: true是个小福利启动后自动打开浏览器省一次手动点击。原型阶段一天重启十几次这个设置累积起来能省不少事。4.4 环境变量区分本地和演示环境原型经常要在两个地方跑自己机器上和丢给同事看的临时环境。接口地址不一样硬编码进代码里每次改都麻烦。Vite 的环境变量机制很轻根目录建.env.development和.env.production变量名必须以VITE_开头才会被注入到客户端代码VITE_API_BASE/api VITE_APP_TITLE原型演示代码里用import.meta.env.VITE_API_BASE读取。注意这里有个安全常识带VITE_前缀的变量会被打进产物任何人都能在浏览器里看到所以只放接口地址、标题这类公开信息任何密钥、令牌都不能往这里放。.env.local这个文件名有特殊待遇它会被 git 忽略适合放个人本地的覆盖配置。我通常把团队共用的写进.env.development自己机器上的特殊配置写进.env.local互不干扰。4.5 五分钟实际花在哪了把上面这些拆开看时间大致是这样分布的环节耗时备注环境自检30 秒版本对了基本一次过创建项目10 秒有网络缓存时更快首次装依赖10 到 30 秒冷缓存可能到 1 分钟装常用依赖5 到 15 秒store 命中时几乎瞬时配置别名与代理2 分钟主要是想清楚路径怎么写启动验证5 秒第一次启动稍慢真正敲命令的时间不到一分钟剩下全在配置和理解上。所以我说 5 分钟是熟练之后的稳定值第一次做的话给自己留 15 分钟更现实别被标题带节奏然后怀疑人生。5. 踩过的坑和对应解法5.1Module not found但包明明装着这是 pnpm 新手遇到的第一个经典问题。现象是代码里import xxx from some-lib报找不到模块你去node_modules里翻发现some-lib确实存在。原因几乎总是同一个——这个包是别的依赖带进来的你的package.json里没有声明它。pnpm 的严格结构让这种蹭依赖的行为暴露出来了。解法也很直接需要用什么就显式装什么。pnpm add some-lib这条规矩值得当成习惯来养。它能让你在半年后回头看这个原型时一眼就知道它依赖了什么而不是面对一个能跑但不知道为什么能跑的黑盒。5.2 peerDependencies 警告刷屏装某些 UI 库或者插件时终端会刷出一片黄色警告大意是某某 peer 依赖没有满足。这类警告需要区分对待如果它说的是 react 版本不匹配而你用的是 React 18 而它要 React 17那就要认真看一眼可能有兼容问题如果它说的是某个可选依赖没装通常不影响使用。pnpm 默认不会自动安装 peer 依赖这是有意为之的严格设计。有些团队为了省事会在.npmrc里加auto-install-peerstrue让 pnpm 自动补齐。我的态度是新项目可以开老项目谨慎。新项目开了省心老项目开了可能把原本的依赖版本顶掉引发连锁反应。还有一种情况是某个包死活找不到 react即使你已经装了。这时候可以在.npmrc里针对性地放开提升public-hoist-pattern[]*react*这个配置的意思是把匹配到的包提升到顶层node_modules让那些不会正确解析符号链接的老包也能找到。它是妥协方案不是首选用的时候心里要清楚这是在给不规范的包让路。5.3 锁文件冲突和 CI 上的安装失败多人协作时pnpm-lock.yaml是最容易起冲突的文件之一。冲突本身不可怕可怕的是手工 merge 之后锁文件内容不一致导致本地能跑、CI 挂掉。处理原则很简单永远不要手工编辑锁文件。冲突了就把本地改动放一边重新pnpm install让 pnpm 自己算一遍然后确认依赖版本符合预期再提交。CI 上则应该用严格模式pnpm install --frozen-lockfile这个参数的作用是锁文件和package.json不一致就直接失败不允许 CI 环境自己更新锁文件。听起来很严格但它能保证构建的可重复性——今天构建出来的和三个月后构建出来的依赖完全一致。5.4 store 出问题时的急救手段极少数情况下 store 会损坏表现是安装时报一堆校验失败或者链接错误。这时候不用重装整个环境pnpm 提供了清理命令pnpm store path pnpm store prune第一条告诉你 store 在哪第二条清理掉没有被任何项目引用的包。注意prune清理的是孤儿包不会动正在用的所以相对安全。如果问题依旧可以把 store 目录整个删掉重新装——代价是所有项目的硬链接失效需要各自重新pnpm install这算是最后手段。注意千万不要在项目运行中直接删node_modules下的内容尤其是.pnpm目录。硬链接的删除是即时的删完之后别的项目可能也受影响如果它们共享同一份文件的话虽然实际是删链接不删文件但很容易误操作。要清就整个目录清掉再重装。5.5 常见问题速查表现象大概率原因处理方式Module not found但包存在幽灵依赖未声明pnpm add显式声明装包卡在resolving源不可达检查.npmrc的 registrypeer 依赖警告刷屏严格模式不自动补评估后开auto-install-peers端口被占用上次进程没退干净换端口或结束占用进程别名不生效只配了 Vite 或只配了 TS两处都配环境变量读不到变量名没加VITE_前缀改名并重启开发服务器改.env后无效环境变量在启动时注入重启pnpm dev构建产物白屏部署路径与base不符配base为子路径store 报校验失败缓存损坏pnpm store pruneCI 安装失败锁文件不一致本地重新生成并提交这张表我建议存一份遇到问题时从上往下扫一遍比漫无目的地搜索快得多。特别是环境变量那两行改了.env没重启和忘了加前缀这两个错误我自己犯过不止一次。6. 把一次性原型沉淀成可复用的起手式6.1 模板复用从复制粘贴到一条命令原型做多了你会发现七八个项目的前 80% 配置是一模一样的别名、代理、目录结构、状态管理的基础模板、请求封装。每次重新配一遍是纯浪费。最朴素的沉淀方式是留一个种子项目新建时直接复制目录改个名字就行。缺点是它会带着上一个项目的残留代码容易忘删。稍微进阶一点的做法是把种子项目放到一个内部仓库里用脚手架的方式拉取。Vite 的create命令支持自定义模板源团队内部可以维护自己的模板包。这样一条命令就能拿到带好配置和基础封装的骨架省下每次十几分钟的重复劳动。代价是模板本身需要维护——依赖版本升级、脚手架命令变化都得跟着改。要不要做这件事取决于你起原型的频率一个月两三个的话复制粘贴其实够用了。6.2 原型代码要不要留、留多久这个问题听起来不像技术问题但它直接影响你的磁盘和心态。我的做法是给原型定一个保质期验证完想法、结论已经沉淀成文档或者合并进主项目的两周内归档或删除。留下的只有两种情况——一是还在迭代中二是里面有些工具函数值得抽出来复用。归档的时候有个小技巧把node_modules删掉再打包。因为里面绝大部分是硬链接和软链接压缩工具处理起来又慢又占空间删掉之后一个原型项目的源码压缩包通常只有几百 KB。需要的时候解压出来pnpm install几秒钟就恢复了。这个习惯让我硬盘上永远清清爽爽。6.3 一些让原型更像半成品的小配置原型最终往往要给人看哪怕是演示。花两分钟做这几件事观感会好很多一是把index.html的标题和 favicon 换掉别让人看到一个带着脚手架默认图标的页面二是配个 404 兜底路由演示时手滑输错地址不至于露出空白页三是把开发服务器的host打开方便同一局域网内的同事用手机扫码看效果。server: { host: 0.0.0.0, port: 5173, }这个配置让开发服务器监听所有网卡地址终端会多打印一个局域网 IP同事直接在手机浏览器输入就能访问。做移动端原型演示的时候这招比发截图或者录屏直观得多。当然前提是你们在同一个网络下这个不用多说。6.4 关于5 分钟这个数字的实话最后聊点实在的。5 分钟能搭出来的是一个能跑、能改、结构干净的 React 骨架加上你需要的几个基础依赖。它不包括业务逻辑、不包括像素级的样式、不包括和后端的联调。这个数字的价值在于给你一个心理锚点——让起一个新项目这件事的心理成本降到最低想到就做不因为配环境好麻烦而拖延。我自己现在的实际节奏是环境干净、网络正常的情况下从敲命令到浏览器里看到页面大概两分半剩下两分半用来配别名、代理和装那三个常用依赖。如果碰上首次装包或者网络抖动时间会翻倍这时候我不会硬等先去把别的事情做了回来看进度条已经跑完。工具的价值是省时间不是给人添焦虑卡住了就让它装别盯着看。再分享一个我用了很久的小技巧把常用的那几条命令写成一个文本片段存在笔记里新建项目时直接粘进终端。看起来是很笨的办法但比起每次回忆--template参数是react-ts还是react-tsx翻笔记只要两秒。工具链再先进也替代不了这种土办法带来的确定性。