模块一 · 起步与渲染
起步:用 Vite 跑通第一个 React 应用
从装环境到跑起第一个组件,讲清 Vite 与 Next.js 的选择、入口文件做了什么、四个脚本的差异,以及 Fast Refresh 与 StrictMode 的实际影响。
读完这一节,你能做到
- 判断项目该用 Vite 还是 Next.js,而不是跟风选。
- 说清
main.tsx里createRoot与StrictMode各自做了什么。 - 读懂目录结构与脚本,知道生产产物从哪来。
- 定位"改了没生效"与"开发环境请求两次"这两类常见现象。
一、环境与工具
| 工具 | 要求 | 检查 | 不满足的后果 |
|---|---|---|---|
| Node | 18 LTS 及以上 | node -v | Vite / Next 启动失败 |
| 包管理器 | npm 9+ / pnpm 8+ | npm -v | 依赖树漂移 |
| 编辑器 | VS Code + ESLint 插件 | 扩展面板 | 语法错误只能运行时发现 |
| 浏览器 | React Developer Tools | 开发者工具多两个面板 | 看不到组件树与 props |
React DevTools 的两个面板分工:
- Components:组件树、props、hooks 状态,可直接改值试探。
- Profiler:录制一次交互,看哪些组件渲染、各花多久。这是"重渲染"问题的第一现场。
二、Vite 还是 Next.js
| 维度 | Vite + React | Next.js |
|---|---|---|
| 定位 | 纯客户端 SPA | 全栈框架,含 SSR/RSC |
| 路由 | 自己装 React Router | 文件路由 |
| 首屏 SEO | 需要额外方案 | 内置 |
| 数据获取 | 客户端请求 | 服务端/客户端均可 |
| 学习成本 | 低,只学 React | 中,要学框架约定 |
| 适用 | 后台系统、内部工具、需要强交互的 SPA | 面向搜索的站点、需要服务端能力的应用 |
判断标准只有一条:首屏是否需要被搜索引擎抓取,或是否需要在服务端拿数据。后台管理 99% 用不上 SSR,先跑 Vite 更省心。
三、起项目与脚本
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev
{
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
}
}
dev:不打包,按需编译,浏览器原生 ESM 加载。build:先做类型检查(tsc -b),再产出dist/。preview:本地静态托管产物,用来验证生产行为。
注意:tsc -b 只检查 .ts/.tsx,不检查运行时的 props 类型。TypeScript 在 React 里是编译期约束,不是校验器。
四、入口文件在做什么
// src/main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import "./index.css";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>,
);
四件事:
document.getElementById("root")!找到index.html里的挂载点。!是告诉 TS"我确定它存在"。createRoot创建 React 18+ 的根,取代了ReactDOM.render(React 19 已移除旧 API)。<StrictMode>在开发环境故意双调用组件函数与 effect,用来暴露不纯的渲染与缺失的清理。.render(<App />)触发首次渲染;此时只是算出元素树,DOM 在 commit 阶段才更新。
StrictMode 引起的"请求两次"不是 bug。它在开发环境模拟"组件挂载→卸载→再挂载",目的就是让你发现没写清理的 effect。生产环境只执行一次。处理方式是补清理、把副作用写纯,而不是删掉 StrictMode。
五、目录结构与组件
my-app/
├─ index.html # 挂载点 <div id="root">
├─ vite.config.ts
├─ tsconfig.json
└─ src/
├─ main.tsx # 入口
├─ App.tsx # 根组件
├─ components/ # 可复用组件
├─ hooks/ # 自定义 Hook
├─ pages/ # 页面
├─ api/ # 请求层
└─ assets/
第一个组件:
// src/components/Counter.tsx
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<button className="counter" onClick={() => setCount((c) => c + 1)}>
{count}
</button>
);
}
三个约束:
- 组件名必须大写,否则 JSX 会当成原生标签。
- 返回单一根节点(Fragment
<>...</>可以包多个)。 - 组件函数必须纯:同样的 props/state 必须得到同样的输出,不能在里面发请求或改全局。
六、JSX 编译与 Fast Refresh
JSX 不是字符串模板,编译后是一次函数调用:
const el = <button className="btn">点击</button>;
// 编译后(自动运行时)
const el = jsx("button", { className: "btn", children: "点击" });
React 17 起使用自动运行时,文件里不需要再 import React;只有用到 React.xxx 时才显式引入。
Fast Refresh 的行为要记清:
| 改动 | 行为 |
|---|---|
| 只改组件函数体 | 保留 state,热替换 |
| 改 hooks 顺序或数量 | 整组件重新挂载,state 丢失 |
| 改文件导出结构 | 可能整页刷新 |
| 改非组件文件(如工具函数) | 依赖它的模块全部刷新 |
所以"改了一行代码输入框内容清空"通常不是 bug,而是 Hook 结构变了导致重挂载。
七、常见坑
- 还在用
ReactDOM.render,React 19 直接报错。 - 忘了
import { useState } from "react",或引入整个React只为用useState。 - 组件名小写,JSX 把它当原生标签,页面显示空标签。
- 在
index.html里重复引入脚本,导致挂载两次。 - 删除 StrictMode 来"解决"双请求,掩盖了真正的清理缺失。
- 用
as any消掉 props 类型错误,类型检查形同虚设。 - 生产直接打开
dist/index.html,file://下资源 404。
八、动手任务:自写 → AI 写 → 对比 → 取舍
四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。
① 自己写
- 起一个 Vite + TS 项目,删掉示例,只保留
App.tsx与Counter.tsx。 - 在
StrictMode下给组件加一个useEffect请求,观察请求次数;再补上清理函数,解释差异。 - 改一次 hooks 顺序,观察 Fast Refresh 后 state 是否还在。
- 跑
build+preview,确认产物可用。
② 让 AI 写同一题
把第 2 题交给 AI,补一句 React 19 + TS + Vite,不新增依赖。
③ 对比要盯的点
- 它写了清理函数或
AbortController吗?没写的话,StrictMode 双调用会打两次请求,卸载后还会setState。 loading/data/error它拆成几个useState?有没有漏掉error分支?- 它给的目录结构,能不能直接当项目起点用?
④ 一句话取舍
它的版本“跑起来”了,但哪一处会让 StrictMode 打出两次请求?说明为什么这是特性不是 bug,而你仍然要写清理。
九、自测
createRoot取代了什么,为什么取代?- StrictMode 在生产环境会双调用吗?
dev与build的本质区别是什么?- Fast Refresh 什么情况下会丢 state?
- 什么时候该选 Next.js 而不是 Vite?
参考答案要点:ReactDOM.render,支持并发特性与多次渲染;不会;按需编译 vs 预打包;Hook 结构变化或导出变化;需要 SEO 或服务端能力时。
附:常见启动报错对照
新手期 80% 的报错集中在下面九条。看到报错先查表,比改代码快:
| 报错 | 根因 | 修法 |
|---|---|---|
Cannot find module 'react' | 依赖没装或装错目录 | 在项目根执行 npm install |
'React' must be in scope | JSX 运行时配置成了 classic | 改 jsx: "react-jsx" 或补 import |
Objects are not valid as a React child | 直接渲染了对象 | 渲染对象的字段,或 JSON.stringify |
Each child in a list should have a unique key | 列表缺 key 或 key 重复 | 用业务 id,不要用下标 |
Too many re-renders | 渲染期间直接调了 setState | 改成事件回调或 effect |
Maximum update depth exceeded | effect 依赖每次新建,且内部 setState | 依赖改稳定值,或加条件判断 |
Rendered more/fewer hooks than expected | 条件分支里调用了 Hook | Hook 提到组件顶层 |
Hydration failed | 服务端与客户端首次渲染不一致 | 随机值/环境判断移到 effect |
Cannot update a component while rendering a different component | 渲染期触发了别的组件更新 | 移到 effect 或事件 |
两条排查习惯:
- 先看第一条错误。后面的报错往往是它的连锁反应,尤其是
Objects are not valid as a React child之后的一串堆栈。 - 看报错里的组件栈。React 会给出
at Component (file.tsx:12),直接点进去,比从入口逐层找快得多。
还有一个不属于报错但同样常见的问题:页面空白且控制台没有任何错误。这时先检查 index.html 里有没有 <div id="root">,再检查 main.tsx 是否真的被执行(Network 里有没有加载到入口模块)。挂载点写错时,React 不会报错,只会安静地什么都不做。
排查顺序建议固定为:依赖是否安装 → 入口是否被加载 → 挂载点是否存在 → 首个组件是否抛错。四步走完,绝大多数“页面空白”都能定位到具体一层,而不是靠猜。另外,开发服务器控制台与浏览器控制台要同时看:编译错误只出现在终端,运行时错误只出现在浏览器,只看一边很容易得出错误结论。
配套题目
★ 越多越该先背;点题目可看完整答案与深入版。
提示:读完可以顺手做几道对应题目,检验是否真的理解