前端工程化基础
包管理、打包构建、环境配置——让项目跑起来
🎯 学习目标
学完本章,你将能够:
- 能够使用 npm/yarn/pnpm 管理前端项目依赖与锁文件
- 能够理解语义化版本规则与依赖类型(生产/开发/对等)
- 能够使用 Vite 创建项目并配置常用选项(别名、代理、环境变量)
- 能够配置 ESLint、Prettier 与 tsconfig 实现代码规范
- 能够理解打包器工作原理(Tree-shaking、代码分割)
你有没有想过
为什么别人的项目一行命令就能跑起来,你的却报错满天飞?
当你从 GitHub 拉下一个项目,输入 npm install 后所有依赖自动装好;输入 npm run dev 后浏览器自动打开——这背后是一整套工程化工具链在运转。
前端工程化不是"高级话题",而是每个项目都绕不开的基础。本章就来搞清楚三件事:包管理器怎么选、打包器怎么用、环境变量怎么配。
Part 1: 包管理器
项目从零开始的第一步:管好你的依赖
npm、yarn、pnpm——三个工具,干的都是同一件事:帮你安装和管理第三方库。
做过课后练习的同学可能已经踩过坑:明明装了包,代码里却 import 不了。或者团队协作时,你电脑能跑,同事电脑报错。这些问题的根源都和包管理有关。
接下来我们就来弄清楚:包管理器怎么选、版本号怎么读、依赖怎么分类。
npm 基础命令
Node.js 自带的包管理器,所有前端项目的起点
npm(Node Package Manager)是 Node.js 安装时自带的包管理工具。你用得最多的就是下面这些命令:
$ npm init -y # 初始化 package.json
$ npm install react # 安装生产依赖
$ npm install -D typescript # 安装开发依赖
$ npm install -g pnpm # 全局安装
$ npm run dev # 运行 scripts 中的 dev 命令
$ npm update # 更新所有依赖
$ npm outdated # 查看哪些依赖过期了
$ npm audit # 检查安全漏洞
其中 -D 表示开发依赖(只在开发时需要),-g 表示全局安装(命令行工具用)。
yarn 与 pnpm
两个更快的替代方案,pnpm 尤其推荐
yarn 是 Facebook 推出的替代方案,支持并行安装和离线缓存。
pnpm 是目前最推荐的包管理器。它的核心思路:同一台电脑上所有项目共享同一份包(通过硬链接),磁盘占用极低,安装速度极快。
# yarn 的用法(和 npm 几乎一样)
$ yarn init -y
$ yarn add react
$ yarn add -D typescript
$ yarn dev
# pnpm 的用法(也几乎一样)
$ pnpm init
$ pnpm add react
$ pnpm add -D typescript
$ pnpm dev
三大包管理器对比
一张表看清该选哪个
| 特性 | npm | yarn | pnpm |
|---|---|---|---|
| 安装速度 | 慢 | 快 | 最快 |
| 磁盘占用 | 高 | 高 | 极低(硬链接) |
| 幽灵依赖 | 有 | 有(v1) | 无(严格隔离) |
| monorepo | workspaces | workspaces | workspace(内置) |
| 锁文件 | package-lock.json | yarn.lock | pnpm-lock.yaml |
语义化版本
版本号里的数学题,读懂就不会翻车
打开 package.json,你会看到类似 "react": "^18.2.0" 的写法。这个 ^ 是什么意思?
语义化版本(SemVer)格式:主版本号.次版本号.修订号(Major.Minor.Patch)。
- 主版本号(Major):不兼容的 API 变更
- 次版本号(Minor):向后兼容的功能新增
- 修订号(Patch):向后兼容的问题修复
| 写法 | 含义 | 示例 |
|---|---|---|
^18.2.0 | 兼容更新(锁主版本) | >=18.2.0 <19.0.0 |
~18.2.0 | 小版本更新(锁主+次) | >=18.2.0 <18.3.0 |
18.2.0 | 精确锁定 | 只能是 18.2.0 |
依赖的三种类型
dependencies vs devDependencies,搞混了会怎样?
{
"dependencies": {
"react": "^18.2.0",
"express": "^4.18.0"
},
"devDependencies": {
"typescript": "^5.3.0",
"vite": "^5.0.0",
"@types/react": "^18.2.0"
},
"peerDependencies": {
"react": ">=18.0.0"
}
}
dependencies
生产环境需要的包。框架、UI 库、工具函数等。用 npm install xxx 安装。
devDependencies
只在开发时需要的包。编译器、测试框架、类型声明等。用 npm install -D xxx 安装。
peerDependencies
"我需要你,但请宿主项目安装我"。写插件或组件库时使用。
npm install --production 时会跳过 devDependencies,减小生产包体积。把 Vite、TypeScript 放到 dependencies 里会导致生产环境多装一堆没用的东西。
锁文件的作用
package-lock.json 为什么一定要提交到 Git?
package.json 里写的是版本范围(如 ^18.2.0),不同时间安装可能装到不同版本。锁文件记录了精确的版本号,保证团队所有人装到一模一样的依赖。
| 包管理器 | 锁文件名 | 特点 |
|---|---|---|
| npm | package-lock.json | 自动生成,必须提交到 Git |
| yarn | yarn.lock | 同上 |
| pnpm | pnpm-lock.yaml | 同上 |
# 正确做法:提交锁文件
$ git add package-lock.json
$ git commit -m "chore: update dependencies"
# 错误做法:把锁文件加到 .gitignore(绝对不要这样做!)
Part 2: 模块打包器
浏览器不认识你的代码?打包器帮你翻译
浏览器不认识 node_modules、不认识 TS、不认识 JSX——打包器帮你翻译。
上一节我们学会了安装依赖。但浏览器并不能直接使用 node_modules 里的包。它需要一个"翻译官"把所有模块打包成浏览器能识别的格式。这一节就来认识这个翻译官。
为什么需要打包
四个浏览器做不到的事
你写的源码浏览器直接打开会报错,原因有四个:
裸导入
浏览器不支持 import react 这样的裸模块导入。
编译转换
TS、JSX、SCSS 等文件需要先编译成浏览器能识别的格式。
代码优化
需要 Tree-shaking(剔除未使用的代码)和压缩(减小体积)。
开发体验
开发时需要 HMR(热模块替换),改完代码页面自动刷新。
打包器一览
Vite 是新项目首选,Webpack 是老项目标配
| 工具 | 开发速度 | 配置复杂度 | 适用场景 |
|---|---|---|---|
| Vite | 极快(原生 ESM) | 低 | 新项目首选 |
| Webpack | 较慢 | 高 | 老项目、复杂需求 |
| Turbopack | 极快 | 低(Next.js 内置) | Next.js 项目 |
| esbuild | 极快 | 中 | 库开发、纯编译 |
Vite 的秘密武器:开发时不打包,直接用浏览器原生的 ES Module 加载。只有生产构建时才用 Rollup 打包。所以启动速度极快。
Tree-shaking
摇掉没用的代码,让包更小
想象一棵挂满装饰的圣诞树——你用力一摇,没挂住的装饰就会掉下来。Tree-shaking 就是这个"摇"的过程。
Tree-shaking 是一种基于 ES Module 静态分析的优化技术。打包器在编译时就能确定哪些 export 从未被 import,然后把它从最终产物中剔除。
好的写法(可摇树)
// 只引入需要的
import { debounce } from "lodash-es";
坏的写法(不可摇树)
// 引入整个库
import _ from "lodash-es";
_.debounce(fn, 100);
原理:ES Module 的 import/export 是静态声明的,打包器在编译阶段就能画出依赖图。而 CommonJS 的 require() 是运行时调用的,打包器无法静态分析。
import/export),不能用 CommonJS(require/module.exports),否则 Tree-shaking 无效。
代码分割
不一次全加载,按需加载才快
如果所有代码都打包成一个 JS 文件,首屏加载会很慢。代码分割(Code Splitting)把代码拆成多个小文件,按需加载。
// 动态 import —— 最常用的代码分割方式
// 用户点击"设置"按钮时才加载设置页面
async function openSettings() {
const { Settings } = await import("./pages/Settings");
render(Settings);
}
// React 中配合 Suspense 使用
import { lazy, Suspense } from "react";
const Settings = lazy(() => import("./pages/Settings"));
function App() {
return (
<Suspense fallback={<p>加载中...</p>}>
<Settings />
</Suspense>
);
}
// vite.config.ts 中手动分包
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ["react", "react-dom"], // 框架单独一个包
utils: ["lodash-es", "dayjs"], // 工具库单独一个包
},
},
},
},
});
效果:框架代码(vendor)很少变动,浏览器会缓存它;业务代码变了只需要重新下载业务包。
Part 3: 用 Vite 建项目
从零搭建,跑通完整的前端工程
从零搭建一个 Vite + React + TypeScript 项目,跑通整个流程。
前面两节讲了包管理器和打包器的原理,现在该动手了。我们将用 Vite 创建一个真实项目,配置 TypeScript、处理 CSS,让它成为你开发的基础模板。
创建 Vite 项目
一行命令,完整项目结构到手
$ npm create vite@latest my-app -- --template react-ts
生成的项目结构:
my-app/
├── index.html # 入口 HTML
├── package.json
├── tsconfig.json # TS 配置
├── vite.config.ts # Vite 配置
├── public/ # 静态资源(不经过编译)
└── src/
├── main.tsx # 入口文件
├── App.tsx # 根组件
├── vite-env.d.ts # Vite 类型声明
└── assets/ # 需要编译的资源
$ cd my-app && npm install && npm run dev
浏览器自动打开 http://localhost:5173,项目就跑起来了。
Vite 常用配置
路径别名、代理、代码分割——项目必备
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"), // 路径别名
},
},
server: {
port: 3000, // 开发服务器端口
open: true, // 自动打开浏览器
proxy: {
"/api": {
target: "http://localhost:8080", // API 代理
changeOrigin: true,
},
},
},
build: {
outDir: "dist",
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ["react", "react-dom"], // 手动分包
},
},
},
},
});
tsconfig 核心字段
TypeScript 项目的"宪法",配置错了全场报红
Vite 创建的 TS 项目自带 tsconfig.json,但很多同学一上来就把它的默认配置改坏了。先搞懂几个核心字段:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"types": ["vite/client"]
},
"include": ["src"]
}
| 字段 | 作用 | 常见错误 |
|---|---|---|
strict | 开启所有严格检查 | 设为 false 会隐藏大量类型错误 |
target | 编译后的 JS 版本 | 设太低(ES5)会导致体积膨胀 |
moduleResolution | 模块解析策略 | Vite 项目必须用 bundler |
paths | 路径别名映射 | 和 vite.config.ts 的 alias 要保持一致 |
"@/*": ["src/*"],但 vite.config.ts 里忘了配 alias: { "@": path.resolve(__dirname, "./src") }。结果是 TS 不报错但 Vite 构建报错——两边各管各的。
CSS 处理方案
全局样式、模块化、工具类——选哪个?
Vite 对 CSS 有很好的原生支持。但在团队项目中,"CSS 怎么组织"是个必须提前决定的问题。
原生 CSS Modules
文件名 .module.css,每个类名自动生成唯一 hash。
/* Button.module.css */
.primary { color: blue; }
import styles from "./Button.module.css";
// styles.primary → "Button_primary_1a2b3"
PostCSS
CSS 的" babel "——用插件处理 CSS。最常用的是 autoprefixer 自动加浏览器前缀。
// postcss.config.js
{
"plugins": {
"autoprefixer": {}
}
}
Tailwind CSS
原子化 CSS 框架,用类名直接写样式。下一章(Ch38)会详细介绍。
<div class="flex
items-center
bg-blue-500
text-white">
Tailwind 示例
</div>
| 方案 | 适用场景 | 学习成本 |
|---|---|---|
| CSS Modules | 中大型项目,需要样式隔离 | 低 |
| PostCSS | 所有项目(基础增强) | 低 |
| Tailwind CSS | 快速开发,追求一致性 | 中 |
Part 4: 代码规范
ESLint 找 Bug,Prettier 统一格式
上一节我们搭好了项目骨架。但一个真实项目还需要"规矩"——代码风格不一致,团队协作时就是灾难。
ESLint 负责检查代码质量(潜在的 Bug、不推荐的写法),Prettier 负责统一代码格式(缩进、换行、引号)。两者配合使用,基本告别代码风格争论。
ESLint 配置
扁平化配置是现在的主流方式
ESLint 从 v9 开始推荐使用"扁平化配置"(Flat Config),一个文件搞定所有规则:
// eslint.config.ts
import tseslint from "typescript-eslint";
import reactPlugin from "eslint-plugin-react-hooks";
import prettierConfig from "eslint-config-prettier";
export default tseslint.config(
// 全局忽略
{ ignores: ["dist", "node_modules"] },
// TS + React 推荐规则
...tseslint.configs.recommended,
reactPlugin.configs.recommended,
// Prettier 兼容(关闭和 Prettier 冲突的规则)
prettierConfig,
// 自定义规则
{
rules: {
"no-console": "warn", // 警告 console.log
"no-unused-vars": "off", // TS 已经检查了
"@typescript-eslint/no-explicit-any": "error",
},
}
);
Prettier 配置
团队统一的代码格式,告别"缩进战争"
// .prettierrc
{
"semi": true,
"singleQuote": false,
"tabWidth": 4,
"trailingComma": "all",
"printWidth": 100
}
然后在 package.json 中添加脚本:
{
"scripts": {
"lint": "eslint src --ext .ts,.tsx",
"format": "prettier --write \"src/**/*.{ts,tsx,css,json}\""
}
}
npm run lint
检查代码质量问题。有错误会报错,有警告会提示。配合 --fix 可以自动修复部分问题。
npm run format
直接修改文件,统一格式。推荐在提交前运行,保证仓库里代码格式一致。
最佳实践:用 VS Code 的 Prettier 插件,设置"保存时自动格式化"。这样每次保存文件就自动格式化,不用手动跑命令。
Part 5: 环境变量
不同环境,不同配置
不同环境用不同的配置——开发时连本地 API,生产时连线上 API。
项目搭好了,代码规范也有了。最后一个基础设施问题:怎么让同一份代码在不同环境(开发/测试/生产)下使用不同的配置?答案就是环境变量。
环境变量文件
.env 文件按模式自动加载
Vite 内置了环境变量支持,文件命名规则决定了什么时候加载:
| 文件名 | 加载时机 |
|---|---|
.env | 所有模式都会加载 |
.env.development | 开发模式(vite dev) |
.env.production | 生产模式(vite build) |
.env.staging | 自定义模式(vite build --mode staging) |
# .env.development
VITE_API_URL=http://localhost:8080/api
VITE_APP_TITLE=我的应用(开发)
# .env.production
VITE_API_URL=https://api.example.com
VITE_APP_TITLE=我的应用
VITE_ 开头才会暴露给前端代码。 不以 VITE_ 开头的变量只在 Node 端可用。密钥、数据库连接串等敏感信息绝对不要放在 VITE_ 变量里——它们会被打包进前端代码,任何用户都能看到。
使用环境变量
类型安全地读取配置
// 在代码中使用 Vite 内置变量
const apiUrl = import.meta.env.VITE_API_URL;
const mode = import.meta.env.MODE; // "development" | "production"
const isDev = import.meta.env.DEV; // boolean
const isProd = import.meta.env.PROD; // boolean
为环境变量添加 TypeScript 类型声明:
// src/env.d.ts
interface ImportMetaEnv {
readonly VITE_API_URL: string;
readonly VITE_APP_TITLE: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
// 使用时就有完整的类型提示
function getConfig() {
return {
apiUrl: import.meta.env.VITE_API_URL, // string
title: import.meta.env.VITE_APP_TITLE, // string
};
}
常用脚本命令
四个命令覆盖开发全流程
{
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint src --ext .ts,.tsx"
}
}
| 命令 | 作用 |
|---|---|
npm run dev | 启动开发服务器,支持 HMR |
npm run build | 先类型检查(tsc),再打包构建 |
npm run preview | 本地预览构建产物 |
npm run lint | 代码质量检查 |
Part 6: Git 工作流
哪些文件该提交,哪些不该提交?
前面几节搭好了完整的前端工程。现在要把项目放进 Git。一个新手最常犯的错误就是提交了不该提交的文件——node_modules 几百 MB,.env 里藏着密钥。
这一节很短,但非常重要。搞错一次就可能把密钥泄露到 GitHub 上。
.gitignore 配置
Node.js 项目的"不要提交"清单
# .gitignore —— 前端项目必备
# 依赖目录(可能几百 MB)
node_modules/
# 构建产物(可以随时重新生成)
dist/
build/
# 环境变量文件(可能包含密钥)
.env
.env.local
.env.*.local
# 编辑器配置
.vscode/
.idea/
*.swp
# 系统文件
.DS_Store
Thumbs.db
# 日志
*.log
npm-debug.log*
1.
node_modules/ 永远不要提交(太大了,用 npm install 重新装)。
2.
.env 永远不要提交(密钥会泄露)。只提交 .env.example 作为模板。
3.
dist/ 永远不要提交(构建产物可以随时重新生成)。
规范提交信息
Conventional Commits——让 Git 日志可读
团队协作时,如果提交信息都写"fix"或"update",过几天就没人记得改了什么。Conventional Commits 是一种约定:
feat: 添加用户登录页面
fix: 修复登录按钮无法点击的问题
docs: 更新 README 安装说明
style: 统一缩进为 4 空格
refactor: 重构登录逻辑,提取公共函数
chore: 升级 Vite 到 5.4
test: 添加登录组件的单元测试
格式规则
类型: 简短描述
feat:新功能fix:修 Bugdocs:文档修改chore:构建/工具变更
为什么重要
规范化的提交信息可以被工具自动解析,生成 changelog(更新日志),甚至自动判断版本号该升 Major 还是 Patch。
常见排错
问题一:npm run dev 提示"command not found"
原因:没有安装依赖。
修复:先运行 npm install,再运行 npm run dev。
问题二:修改 .env 文件后不生效
原因:Vite 只在启动时读取 .env 文件,修改后不会自动重新加载。
修复:重启开发服务器(Ctrl+C 再 npm run dev)。
问题三:import.meta.env.VITE_XXX 是 undefined
原因:变量名没有以 VITE_ 开头,或者 .env 文件路径不对。
修复:确认变量名以 VITE_ 开头,确认文件在项目根目录下。
问题四:tsconfig 改了但 Vite 不生效
原因:tsconfig.json 的 paths 只影响 TypeScript 编译器。Vite 用自己的 resolve.alias 做路径解析。
修复:vite.config.ts 和 tsconfig.json 的路径别名要保持一致,两边都要配。
问题五:ESLint 报错和 Prettier 冲突
原因:ESLint 的格式化规则和 Prettier 不一致。比如 ESLint 要求单引号,Prettier 配了双引号。
修复:安装 eslint-config-prettier 并放在 ESLint 配置的最后面,它会关闭所有和 Prettier 冲突的规则。
试一试
练习一
任务:用 Vite 创建一个 React + TypeScript 项目,修改开发服务器端口为 4000,确认可以正常访问。
# 提示:修改 vite.config.ts 中的 server.port
# 你的操作步骤:
参考答案
$ npm create vite@latest my-app -- --template react-ts
$ cd my-app && npm install
// vite.config.ts
export default defineConfig({
plugins: [react()],
server: { port: 4000 },
});
$ npm run dev
练习二
任务:创建 .env.development 文件,定义 VITE_APP_TITLE,然后在组件中用 import.meta.env 读取并显示在页面上。
# 提示:变量必须以 VITE_ 开头
# 你的操作步骤:
参考答案
# .env.development
VITE_APP_TITLE=我的第一个Vite项目
// src/App.tsx
function App() {
return <h1>{import.meta.env.VITE_APP_TITLE}</h1>;
}
练习三
任务:在项目中安装 ESLint 和 Prettier,配置 npm run lint 和 npm run format 脚本,运行后确认没有报错。
# 提示:先安装依赖,再创建配置文件
# 你的操作步骤:
参考答案
$ npm install -D eslint prettier \
typescript-eslint \
eslint-config-prettier
// package.json scripts 中添加
"lint": "eslint src",
"format": "prettier --write \"src/**/*.{ts,tsx}\""
$ npm run lint
$ npm run format
📝 要点总结
本章核心知识点
包管理器
npm/yarn/pnpm 对比,lock 文件锁定版本
语义化版本
MAJOR.MINOR.PATCH,^ ~ 范围符号含义
Vite 配置
alias 路径别名、proxy 代理、env 环境变量
代码规范
ESLint + Prettier + tsconfig 协同配置
打包优化
Tree-shaking 消除死代码、代码分割按需加载
下一章
现在你已经能把项目跑起来了。下一章我们学习 React 与 TypeScript——在组件中用类型系统写出更安全的代码。