包管理与前端工程化:从 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"      // 完全锁死

规则

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 自带)

什么时候放哪里


4. node_modules 和三种包管理器

前端有三个主流包管理器:npm / yarn / pnpm。它们做的事一样(下载依赖到 node_modules/),但实现差异巨大。

npm / yarn:扁平化 node_modules

默认会把所有依赖的依赖都提升到顶层 node_modules/,结构大致如下:

node_modules/
├── vue/              # 直接依赖
├── vue-router/       # 直接依赖
├── @vue/reactivity/  # vue 的依赖也被提升到这里
├── @vue/shared/      # 同上
└── ...一堆未声明的依赖

问题

pnpm:硬链接 + 符号链接(项目在用)

pnpm 做法:

  1. 全局存放一份包:~/.pnpm-store/
  2. 项目 node_modules/ 里放符号链接指向全局 store
  3. 只有你声明的直接依赖才在 node_modules/ 顶层可见
node_modules/
├── vue → ~/.pnpm-store/vue@3.5.32       # 直接依赖才可见
├── vue-router → ...
└── .pnpm/                                # 传递依赖都藏在这里
    ├── @vue+reactivity@3.5.34/node_modules/@vue/reactivity
    └── ...

好处

Java 对照


5. lockfile:锁死完整依赖树

^3.5.32 允许装任何 3.x.x,那不同时间/不同机器装的版本可能不一样。lockfile 就是锁死"本次实际装的版本号完整列表"的文件。

包管理器 lockfile
npm package-lock.json
yarn yarn.lock
pnpm pnpm-lock.yaml(本项目用这个)

规则

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

差别

约定俗成的脚本名(大家都这么叫):


7. 构建工具:Vite / Webpack / esbuild

前端代码写完不能直接给浏览器用,要先打包:合并文件、转译 TS→JS、压缩、替换环境变量……

为什么要打包

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 对照

注意:不带 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 installmvn dependency:resolvepnpm add X ≈ 在 pom.xml 里加 <dependency> 然后执行 resolve。


11. 踩过的坑

  1. node_modules 别进 git:它太大了,而且完全可重建。.gitignore 必备
  2. lockfile 必须进 git:不然每次装的版本都可能不同
  3. 不同包管理器 lockfile 不兼容:选定 pnpm 就全队用 pnpm,不要混用
  4. type: "module" 很重要:决定项目用 ESM (import) 还是 CommonJS (require)。现代项目全选 module
  5. pnpm addpnpm install 不一样add 装新包并更新 package.jsoninstallpackage.json
  6. 镜像加速:国内直接 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 的具体配置,遇到了再查文档。


延伸阅读


下一篇:进入第二阶段 —— Vue 3 模板语法与指令(v-if / v-for / v-bind / v-on)