前端工程化基础

包管理、打包构建、环境配置——让项目跑起来

🎯 学习目标

学完本章,你将能够:

  • 能够使用 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

三大包管理器对比

一张表看清该选哪个

特性npmyarnpnpm
安装速度慢快最快
磁盘占用高高极低(硬链接)
幽灵依赖有有(v1)无(严格隔离)
monorepoworkspacesworkspacesworkspace(内置)
锁文件package-lock.jsonyarn.lockpnpm-lock.yaml
幽灵依赖是指你在代码里用了某个包,但它并不在你的 package.json 里。npm 和 yarn 的扁平 node_modules 结构会导致这个问题。pnpm 通过严格的依赖隔离彻底解决了它。

语义化版本

版本号里的数学题,读懂就不会翻车

打开 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
新手常见问题:团队协作时各人装到不同版本,导致"你电脑能跑我的不行"。解决办法:把锁文件(package-lock.json)提交到 Git,保证所有人装到完全一样的版本。

依赖的三种类型

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),不同时间安装可能装到不同版本。锁文件记录了精确的版本号,保证团队所有人装到一模一样的依赖。

包管理器锁文件名特点
npmpackage-lock.json自动生成,必须提交到 Git
yarnyarn.lock同上
pnpmpnpm-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() 是运行时调用的,打包器无法静态分析。

关键前提:你的代码必须用 ES Module(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 要保持一致
最常见的坑:tsconfig 里配了 "@/*": ["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",
        },
    }
);
eslint-config-prettier 的作用:它不做任何格式化,只是关闭 ESLint 中和 Prettier 冲突的规则。这是两者共存的关键。不要试图用 ESLint 做格式化——那是 Prettier 的事。

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:修 Bug
  • docs:文档修改
  • chore:构建/工具变更

为什么重要

规范化的提交信息可以被工具自动解析,生成 changelog(更新日志),甚至自动判断版本号该升 Major 还是 Patch。

常见排错

遇到红色报错先别慌。 90% 的新手问题都是安装步骤漏了或配置文件写错了。仔细读报错信息,一般都会告诉你哪里出了问题。

问题一: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——在组件中用类型系统写出更安全的代码。

1 / X