包管理与前端工程化:从 Maven 到 pnpm
为什么这节课对 Java 开发者特别重要
前端工程化是后端开发者最容易卡壳的地方。同一份依赖,npm、yarn、pnpm 装出三种目录结构;package.json 里的字段多得让人心慌;Vite、Webpack、esbuild 各自扮演什么角色也不清楚。
这篇笔记用 Maven 做锚点,把前端工具链梳清楚。
1. package.json:前端的 pom.xml
每个前端项目根目录都有一个 package.json,它相当于 Maven 的 pom.xml + Spring 的 application.yml + 启动脚本的合体。
看项目根目录的 package.json:
{
"name": "in4vue",
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview",
"lint": "eslint . --fix"
},
"dependencies": {
"vue": "^3.5.32",
"vue-router": "^5.0.6",
"pinia": "^3.0.4"
},
"devDependencies": {
"vite": "^8.0.10",
"typescript": "~6.0.2",
"eslint": "^10.3.0"
}
}
Java 对照:
| 字段 | Maven 对照 |
|---|---|
name / version |
<artifactId> / <version> |
type: "module" |
无,声明用 ES Module 还是 CommonJS |
scripts |
有点像 Maven 的 <build> 插件 goal,但更像 npm 自己的任务运行器 |
dependencies |
<dependencies> 里 <scope>compile</scope> |
devDependencies |
<scope>test</scope> 或 provided,只开发时用 |
2. 版本号前缀 ^ 和 ~:语义化版本
"vue": "^3.5.32" // 允许 3.x.x,但主版本号锁死
"typescript": "~6.0.2" // 允许 6.0.x,主+次都锁死
"axios": "1.16.0" // 完全锁死
规则:
^X.Y.Z:兼容主版本。^3.5.32允许装3.5.33 / 3.6.0 / 3.9.9,但不装 4.x~X.Y.Z:兼容补丁。~6.0.2允许装6.0.3 / 6.0.9,但不装6.1.xX.Y.Z:完全锁定*或latest:永远装最新(几乎没人这么写,不可控)
Java 对照:Maven 3.x 才支持 [1.0, 2.0) 这样的版本范围。前端几乎所有项目都用 ^ 范围 + lockfile 配合。
3. dependencies vs devDependencies vs peerDependencies
"dependencies": {
"vue": "^3.5.32" // 运行时需要,部署产物里会用到
},
"devDependencies": {
"vite": "^8.0.10", // 只开发时用的工具,构建完就不需要了
"typescript": "~6.0.2" // TS 编译成 JS 后就没它事了
},
"peerDependencies": {
"vue": "^3.0.0" // "我依赖宿主项目里的 Vue,请你自己装"
}
Java 对照:
| 前端 | Maven |
|---|---|
dependencies |
<scope>compile</scope>(默认) |
devDependencies |
<scope>test</scope> 或 <scope>provided</scope> |
peerDependencies |
<scope>provided</scope> 且让使用者自己提供(类似 Servlet 依赖 Tomcat 自带) |
什么时候放哪里:
- 组件库、工具库、框架 →
dependencies - 构建工具(Vite/Webpack)、类型(@types/*)、测试(Vitest)、linter →
devDependencies - 如果你在写库(不是应用),需要宿主提供 Vue 本身 →
peerDependencies
4. node_modules 和三种包管理器
前端有三个主流包管理器:npm / yarn / pnpm。它们做的事一样(下载依赖到 node_modules/),但实现差异巨大。
npm / yarn:扁平化 node_modules
默认会把所有依赖的依赖都提升到顶层 node_modules/,结构大致如下:
node_modules/
├── vue/ # 直接依赖
├── vue-router/ # 直接依赖
├── @vue/reactivity/ # vue 的依赖也被提升到这里
├── @vue/shared/ # 同上
└── ...一堆未声明的依赖
问题:
- 幻影依赖:你代码里
import { something } from '@vue/shared'能跑起来,但你没在package.json里声明它,哪天 Vue 换了依赖你就炸 - 占空间:100 个项目各自装一份 Vue,硬盘 100 份
- 装得慢:大项目
node_modules轻松上 GB
pnpm:硬链接 + 符号链接(项目在用)
pnpm 做法:
- 全局存放一份包:
~/.pnpm-store/ - 项目
node_modules/里放符号链接指向全局 store - 只有你声明的直接依赖才在
node_modules/顶层可见
node_modules/
├── vue → ~/.pnpm-store/vue@3.5.32 # 直接依赖才可见
├── vue-router → ...
└── .pnpm/ # 传递依赖都藏在这里
├── @vue+reactivity@3.5.34/node_modules/@vue/reactivity
└── ...
好处:
- 严格:你没声明的包,import 时直接报错
- 省空间:全局 store 共享,10 个项目共享一份 Vue
- 快:硬链接创建几乎零成本
Java 对照:
- npm/yarn 扁平 ≈ Maven 直接把所有 jar 塞到
WEB-INF/lib(没这问题,但类比而言) - pnpm 硬链接 ≈ Maven 的
~/.m2/repository本地仓库 + 项目只引用 - 本项目就用 pnpm,
CLAUDE.md里规定的
5. lockfile:锁死完整依赖树
^3.5.32 允许装任何 3.x.x,那不同时间/不同机器装的版本可能不一样。lockfile 就是锁死"本次实际装的版本号完整列表"的文件。
| 包管理器 | lockfile |
|---|---|
| npm | package-lock.json |
| yarn | yarn.lock |
| pnpm | pnpm-lock.yaml(本项目用这个) |
规则:
- 必须提交到 git(和 Maven 的
pom.xml一起提交) - 不要手动改它,让包管理器自己维护
- CI 构建时用
pnpm install --frozen-lockfile严格按 lockfile 装,一旦不一致就报错
Java 对照:Maven 默认没有 lockfile 机制(Maven 的 <version> 通常就是精确版本)。Gradle 有 gradle.lockfile 做类似的事。
6. npm scripts:类似 Maven 的 goal
package.json 里的 scripts 字段定义命令别名:
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview",
"lint": "eslint . --fix"
}
运行:
pnpm dev # 等于运行 vite
pnpm build # 等于运行 vue-tsc -b && vite build
pnpm lint # 等于运行 eslint . --fix
Java 对照:
| npm scripts | Maven lifecycle |
|---|---|
pnpm dev |
mvn spring-boot:run |
pnpm build |
mvn package |
pnpm test |
mvn test |
pnpm lint |
mvn checkstyle:check |
差别:
- Maven 的 goal 是插件定义好的,有固定生命周期(validate → compile → test → package → install)
- npm scripts 是你自己起的名,运行任意 shell 命令。更灵活但也更散
约定俗成的脚本名(大家都这么叫):
dev:启动开发服务器build:构建生产版本test:运行测试lint:代码检查preview:预览 build 产物
7. 构建工具:Vite / Webpack / esbuild
前端代码写完不能直接给浏览器用,要先打包:合并文件、转译 TS→JS、压缩、替换环境变量……
为什么要打包
- 浏览器不支持 TS,要转成 JS
- Vue 的
.vue文件是特殊格式,浏览器不认 - 几百个模块全加载太慢,要合并
- 生产环境需要代码压缩
Java 对照:类似把源码编译打包成 jar/war,但前端打包产物是 JS + CSS + HTML 的静态文件集合。
三个主流方案
| 工具 | 特点 | 场景 |
|---|---|---|
| Webpack | 老牌王者,生态最全,配置复杂,慢 | 老项目、复杂定制 |
| Vite(本项目用) | 开发模式用 ES Module 原生加载,秒启动;生产用 Rollup 打包 | 新项目首选 |
| esbuild | Go 写的,极快,但功能相对基础 | 工具库打包、Vite 底层就用它做 TS 转译 |
Vite 的魔法:开发模式下,你修改一个 .vue 文件,浏览器毫秒级更新,而且不需要重启 dev server。秘诀是利用浏览器原生的 ES Module,按需加载每个文件。
Vite 配置看一眼
项目里 vite.config.ts 的核心:
export default defineConfig({
plugins: [
vue(), // 支持 .vue 文件
tailwindcss(), // 支持 Tailwind v4
AutoImport({ imports: ['vue', 'vue-router'] }), // 自动 import
],
resolve: {
alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) }, // @ 指向 src
},
})
类比:相当于 Maven 的 <build> + 各种 plugin 配置,但写起来短得多。
8. import.meta.glob:Vite 特有的魔法
项目的 utils/notes.ts 里用了这个:
const rawNotes = import.meta.glob('@/notes/**/*.md', {
eager: true,
query: '?raw',
import: 'default',
})
它在构建时被 Vite 编译成一堆静态 import。效果:所有笔记 .md 文件在构建时被扫描、读取、打包进产物。
Java 对照:类似 Spring 的 @ComponentScan 或用 ClassLoader.getResources() 扫 classpath 下的资源文件。
9. 环境变量:多环境配置
Vite 约定:VITE_ 开头的环境变量会被注入到前端代码。
# .env.development
VITE_API_BASE_URL=http://localhost:8080
# .env.production
VITE_API_BASE_URL=https://api.example.com
代码里:
const url = import.meta.env.VITE_API_BASE_URL
Java 对照:
.env.development≈application-dev.yml.env.production≈application-prod.ymlVITE_前缀 ≈ Spring 的@Value("${VITE_xxx}")白名单
注意:不带 VITE_ 前缀的变量不会暴露给前端(防止误把 SECRET 打进前端产物)。
10. 常用命令清单
# 初始化新项目
pnpm create vite@latest my-app --template vue-ts
# 安装项目依赖(按 package.json + lockfile)
pnpm install
# 别名
pnpm i
# 加一个运行时依赖
pnpm add axios
# 加一个开发依赖
pnpm add -D eslint
# 加一个全局工具
pnpm add -g typescript
# 升级所有依赖(谨慎)
pnpm update
# 查看过时的依赖
pnpm outdated
# 运行 scripts
pnpm dev
pnpm build
pnpm run xxx # xxx 是自定义 script 名
# 清理
rm -rf node_modules
pnpm install
Java 对照:pnpm install ≈ mvn dependency:resolve,pnpm add X ≈ 在 pom.xml 里加 <dependency> 然后执行 resolve。
11. 踩过的坑
node_modules别进 git:它太大了,而且完全可重建。.gitignore必备- lockfile 必须进 git:不然每次装的版本都可能不同
- 不同包管理器 lockfile 不兼容:选定 pnpm 就全队用 pnpm,不要混用
type: "module"很重要:决定项目用 ESM (import) 还是 CommonJS (require)。现代项目全选modulepnpm add和pnpm install不一样:add装新包并更新package.json,install按package.json装- 镜像加速:国内直接
pnpm install有时候很慢,可以配淘宝镜像:pnpm config set registry https://registry.npmmirror.com
小结:心智模型
Java 世界 前端世界
────────────────── ──────────────────
pom.xml ↔ package.json
~/.m2/repository ↔ ~/.pnpm-store
target/ ↔ dist/
mvn install ↔ pnpm install
mvn package ↔ pnpm build
mvn spring-boot:run ↔ pnpm dev
application-{env}.yml ↔ .env.{mode}
Maven Central ↔ npm registry
<dependency> ↔ dependencies 字段
记住这张对照表,前端工程化的 80% 心智负担就解决了。剩下的 20% 是 Vite/Webpack 的具体配置,遇到了再查文档。
延伸阅读
- pnpm 官方文档 — 为什么选 pnpm 讲得最清楚
- Vite 官方指南 — 读完"功能"和"配置"两章就够日常用
- npm Docs: package.json — 字段说明大全
- Node.js 模块系统 — 想搞懂 ESM vs CJS 看这里
下一篇:进入第二阶段 —— Vue 3 模板语法与指令(v-if / v-for / v-bind / v-on)