知

模块二 · 工程化与网络

第 5 / 14 节19 分钟阅读型

包管理与构建:npm、语义化版本与 Vite

现代前端的第一道门槛不是语法,而是依赖与构建。这一节讲清 package.json、锁文件与语义化版本,以及 Vite 为什么快、配置项各自管什么。

读完这一节,你能做到

  1. 读懂 package.json 的每个关键字段,不再照抄别人的配置。
  2. 解释 ^ 与 ~ 的差异,知道锁文件为什么必须提交。
  3. 用 pnpm/npm 装依赖、跑脚本、排查"本地能跑 CI 不能跑"。
  4. 配出别名、代理与环境变量,理解构建产物的结构。

一、package.json:项目契约

{
  "name": "frontend-demo",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "engines": { "node": ">=18" },
  "packageManager": "pnpm@9.12.0",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "preview": "vite preview"
  },
  "dependencies": { "react": "^19.0.0" },
  "devDependencies": { "vite": "^5.4.0", "typescript": "^5.6.0" }
}
字段作用常见错误
private禁止误发布到 npm内部项目忘了加,被 npm publish 发出去
type: "module"让 .js 按 ESM 解析与 CommonJS 混用,require 报错
engines声明 Node 版本只在提示层生效,要配合 CI 检查
packageManager锁定包管理器版本团队 npm/pnpm 混用,锁文件打架
dependencies运行时依赖把构建工具装进生产依赖,包体变大
devDependencies开发/构建期依赖把只在本地用的工具装进生产依赖

判断一个包该放哪:运行时代码里会 import 它吗?会,放 dependencies;只在构建、测试、lint 时用,放 devDependencies。

二、语义化版本:^ 与 ~ 的差别

版本号 主.次.补丁(1.4.2):

写法允许升级范围风险
1.4.2只装这个版本收不到安全修复
~1.4.21.4.x较小
^1.4.21.x.x(不跨大版本)可能引入行为变化
latest任意最新完全不可复现

主版本变更意味着不兼容修改,所以 ^ 不会跨主版本。但"次版本一定兼容"只是约定,现实中仍有破坏性变更,这就是锁文件存在的理由。

锁文件必须提交:package-lock.json、pnpm-lock.yaml、yarn.lock。它记录了整棵依赖树的精确版本,保证"我这里能跑"等于"CI 也能跑"。删除锁文件重装,是排查依赖问题的最后手段,不是日常操作。

三、npm、pnpm 与 node_modules

维度npmpnpm
安装速度中快(硬链接 + 内容寻址)
磁盘占用每个项目一份全局一份,硬链接复用
幽灵依赖容易出现默认禁止
锁文件package-lock.jsonpnpm-lock.yaml

幽灵依赖指代码里 import 了一个没有写在 package.json 里的包,只是因为它恰好被别的包装在顶层 node_modules。它在本机能跑,换一个包管理器就报"模块找不到"。

pnpm 用符号链接 + 严格的 node_modules 结构从机制上杜绝这件事。团队统一用 pnpm,能省掉一类玄学问题。

常用命令:

pnpm install            # 按锁文件安装
pnpm add axios          # 生产依赖
pnpm add -D vitest      # 开发依赖
pnpm remove axios
pnpm run dev            # 跑脚本
pnpm dlx create-vite    # 临时执行,不全局安装

CI 里要用 npm ci 或 pnpm install --frozen-lockfile:严格按锁文件安装,锁文件与 package.json 不一致就失败,而不是悄悄升级。

四、Vite 为什么快

传统打包器(webpack)启动时要先把你所有模块打成 bundle,模块越多启动越慢。Vite 换了两条路:

阶段做法收益
开发不打包,浏览器原生 ESM 按需请求冷启动与项目规模基本无关
依赖预构建用 esbuild 把 node_modules 的依赖打成少量 ESM减少请求数,兼容 CJS
生产Rollup 打包、压缩、分包产物可控,浏览器兼容

所以"Vite 快"只在开发阶段成立。生产构建依然要打包,项目大了照样要几十秒。这也是为什么它和 webpack 不是"谁取代谁",而是开发体验上的取舍。

五、vite.config:四个必配项

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { fileURLToPath, URL } from "node:url";

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) },
  },
  server: {
    port: 5173,
    proxy: {
      "/api": {
        target: "http://localhost:8080",
        changeOrigin: true,
      },
    },
  },
  build: {
    outDir: "dist",
    sourcemap: false,
  },
});
配置解决什么注意
alias深目录引入难读要在 tsconfig 里同步 paths
server.proxy开发期跨域只对 dev 生效,生产要靠 nginx
build.outDir产物目录部署脚本要与此一致
build.sourcemap线上错误定位公开 sourcemap 等于泄露源码

代理只解决开发跨域,不要把"生产也配代理"当成方案。生产环境的跨域要么同域部署,要么后端配 CORS。

六、环境变量:三套环境不能靠手改

.env                # 所有环境共享
.env.development    # dev
.env.production     # build
# .env.production
VITE_API_BASE=https://api.example.com
const base = import.meta.env.VITE_API_BASE;

三条规则:

  1. 只有 VITE_ 前缀的变量会被注入客户端。没有前缀的变量不会暴露,这是安全设计,不要试图绕过。
  2. 变量在构建时被静态替换,不是运行时读取。改 .env 必须重新构建。
  3. .env.local 放个人配置并加入 .gitignore;.env.example 提交到仓库,列出必填项。

七、构建产物与部署

dist/
├─ index.html
└─ assets/
   ├─ index-a1b2c3.js
   ├─ vendor-d4e5f6.js
   └─ index-g7h8i9.css

三条部署规则:

  • 静态资源带 hash,可设长缓存(immutable)。
  • index.html 不长缓存,否则发版后用户仍是旧版本。
  • history 路由需要服务器 fallback 到 index.html,否则刷新 404。

八、常见坑

  1. 把 vite、typescript 装进 dependencies,生产镜像多出几十 MB。
  2. 删掉锁文件"重新装一下",导致依赖被静默升级。
  3. 团队一半 npm 一半 pnpm,node_modules 结构不一致。
  4. 以为 ^ 一定安全,次版本升级引入 breaking change。
  5. 环境变量没加 VITE_ 前缀,客户端读到 undefined。
  6. 改了 .env 没重启 dev server,以为配置没生效。
  7. 生产环境配 server.proxy,上线后发现代理根本不存在。
  8. 把 sourcemap 一起发到 CDN,源码可被直接下载。

九、动手任务:自写 → AI 写 → 对比 → 取舍

四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。

① 自己写

  1. 新建一个项目,把 package.json 的每个字段用自己的话注释一遍。
  2. 分别用 npm 与 pnpm 安装同一个项目,对比 node_modules 结构与磁盘占用。
  3. 配一个 /api 代理,验证开发环境请求能打到本地后端。
  4. 建三套 .env,在页面里打印 import.meta.env,确认不同命令下取值不同。

② 让 AI 写同一题

把第 2 题(依赖排查)交给 AI,补一句 不许改 package-lock.json,不许全局安装。

③ 对比要盯的点

  • 它是不是给了 npm install -g?全局安装会让同事复现不了你的环境。
  • 它改了 lock 文件吗?改了就说明装的东西和声明不一致。
  • ^ 与 ~ 的解释对吗?前者允许次版本浮动,后者只允许补丁。

④ 一句话取舍

按它给的命令做完,同事拉下代码能一次装好吗?

十、自测

  1. dependencies 与 devDependencies 的判断标准是什么?
  2. ^1.4.2 允许升级到哪些版本?
  3. 为什么锁文件必须提交?
  4. Vite 开发为什么快,生产是否同样快?
  5. 为什么环境变量必须有 VITE_ 前缀?

参考答案要点:运行时代码是否 import;1.x.x;保证依赖树可复现;原生 ESM 按需编译,生产仍要打包;防止服务端密钥被打进客户端。

附:常见依赖与构建报错对照

报错根因修法
Cannot find module 'xxx'依赖未装 / 装了但没写进 package.jsonpnpm add xxx 后确认 lockfile 变化
ERR_MODULE_NOT_FOUNDESM 路径缺扩展名或 type 不对补 .js 扩展名或调整 type: module
Cannot use import statement outside a moduleNode 按 CommonJS 解析了 ESM 文件改 type: "module" 或改用 .mjs
EACCES: permission denied全局目录权限问题改用项目内安装,不用 -g
Unsupported engineNode 版本不满足用 nvm 切换到 engines 要求的版本
Port 5173 is already in use端口被占换端口或杀掉旧进程
Failed to resolve import "@/xxx"别名只在 tsconfig 配了在 vite.config 同步 alias
process is not defined浏览器里用了 Node 全局变量改用 import.meta.env 或注入 define
Out of memory构建内存不足提高 NODE_OPTIONS=--max-old-space-size 或排查循环依赖

两条排查经验:

  1. 删 node_modules 重装前先看 lockfile 有没有变化。很多时候问题在版本冲突,删了重装会把现场也删掉。
  2. 报错先看第一条。构建工具的报错会级联,第一条往往才是根因,后面的只是“因为前面失败了所以我也不干了”。

还有一个隐蔽问题:本地装了多个 Node 版本,终端与 IDE 用的不是同一个,导致“命令行能跑、编辑器报错”。统一用 .nvmrc 或 package.json 的 engines 声明版本,能省掉大量无效排查。

配套题目

★ 越多越该先背;点题目可看完整答案与深入版。

提示:读完可以顺手做几道对应题目,检验是否真的理解