模块二 · 工程化与网络
包管理与构建:npm、语义化版本与 Vite
现代前端的第一道门槛不是语法,而是依赖与构建。这一节讲清 package.json、锁文件与语义化版本,以及 Vite 为什么快、配置项各自管什么。
读完这一节,你能做到
- 读懂
package.json的每个关键字段,不再照抄别人的配置。 - 解释
^与~的差异,知道锁文件为什么必须提交。 - 用 pnpm/npm 装依赖、跑脚本、排查"本地能跑 CI 不能跑"。
- 配出别名、代理与环境变量,理解构建产物的结构。
一、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.2 | 1.4.x | 较小 |
^1.4.2 | 1.x.x(不跨大版本) | 可能引入行为变化 |
latest | 任意最新 | 完全不可复现 |
主版本变更意味着不兼容修改,所以 ^ 不会跨主版本。但"次版本一定兼容"只是约定,现实中仍有破坏性变更,这就是锁文件存在的理由。
锁文件必须提交:package-lock.json、pnpm-lock.yaml、yarn.lock。它记录了整棵依赖树的精确版本,保证"我这里能跑"等于"CI 也能跑"。删除锁文件重装,是排查依赖问题的最后手段,不是日常操作。
三、npm、pnpm 与 node_modules
| 维度 | npm | pnpm |
|---|---|---|
| 安装速度 | 中 | 快(硬链接 + 内容寻址) |
| 磁盘占用 | 每个项目一份 | 全局一份,硬链接复用 |
| 幽灵依赖 | 容易出现 | 默认禁止 |
| 锁文件 | package-lock.json | pnpm-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;
三条规则:
- 只有
VITE_前缀的变量会被注入客户端。没有前缀的变量不会暴露,这是安全设计,不要试图绕过。 - 变量在构建时被静态替换,不是运行时读取。改
.env必须重新构建。 .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。
八、常见坑
- 把
vite、typescript装进dependencies,生产镜像多出几十 MB。 - 删掉锁文件"重新装一下",导致依赖被静默升级。
- 团队一半 npm 一半 pnpm,
node_modules结构不一致。 - 以为
^一定安全,次版本升级引入 breaking change。 - 环境变量没加
VITE_前缀,客户端读到undefined。 - 改了
.env没重启 dev server,以为配置没生效。 - 生产环境配
server.proxy,上线后发现代理根本不存在。 - 把 sourcemap 一起发到 CDN,源码可被直接下载。
九、动手任务:自写 → AI 写 → 对比 → 取舍
四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。
① 自己写
- 新建一个项目,把
package.json的每个字段用自己的话注释一遍。 - 分别用 npm 与 pnpm 安装同一个项目,对比
node_modules结构与磁盘占用。 - 配一个
/api代理,验证开发环境请求能打到本地后端。 - 建三套
.env,在页面里打印import.meta.env,确认不同命令下取值不同。
② 让 AI 写同一题
把第 2 题(依赖排查)交给 AI,补一句 不许改 package-lock.json,不许全局安装。
③ 对比要盯的点
- 它是不是给了
npm install -g?全局安装会让同事复现不了你的环境。 - 它改了 lock 文件吗?改了就说明装的东西和声明不一致。
^与~的解释对吗?前者允许次版本浮动,后者只允许补丁。
④ 一句话取舍
按它给的命令做完,同事拉下代码能一次装好吗?
十、自测
dependencies与devDependencies的判断标准是什么?^1.4.2允许升级到哪些版本?- 为什么锁文件必须提交?
- Vite 开发为什么快,生产是否同样快?
- 为什么环境变量必须有
VITE_前缀?
参考答案要点:运行时代码是否 import;1.x.x;保证依赖树可复现;原生 ESM 按需编译,生产仍要打包;防止服务端密钥被打进客户端。
附:常见依赖与构建报错对照
| 报错 | 根因 | 修法 |
|---|---|---|
Cannot find module 'xxx' | 依赖未装 / 装了但没写进 package.json | pnpm add xxx 后确认 lockfile 变化 |
ERR_MODULE_NOT_FOUND | ESM 路径缺扩展名或 type 不对 | 补 .js 扩展名或调整 type: module |
Cannot use import statement outside a module | Node 按 CommonJS 解析了 ESM 文件 | 改 type: "module" 或改用 .mjs |
EACCES: permission denied | 全局目录权限问题 | 改用项目内安装,不用 -g |
Unsupported engine | Node 版本不满足 | 用 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 或排查循环依赖 |
两条排查经验:
- 删
node_modules重装前先看 lockfile 有没有变化。很多时候问题在版本冲突,删了重装会把现场也删掉。 - 报错先看第一条。构建工具的报错会级联,第一条往往才是根因,后面的只是“因为前面失败了所以我也不干了”。
还有一个隐蔽问题:本地装了多个 Node 版本,终端与 IDE 用的不是同一个,导致“命令行能跑、编辑器报错”。统一用 .nvmrc 或 package.json 的 engines 声明版本,能省掉大量无效排查。
配套题目
★ 越多越该先背;点题目可看完整答案与深入版。
提示:读完可以顺手做几道对应题目,检验是否真的理解