Skip to content
This page has been auto-translated and may contain errors.View in English

设置 React 项目

React 应用需要一些工具配置才能运行。需要有工具把 JSX 转换成浏览器能执行的 JavaScript,还需要有工具在你开发时提供本地服务。Vite 两件事都能做,而且可以在约三十秒内搭建一个完整的项目。本章会从空文件夹带你到一个运行中的应用,然后追踪最重要的一条路径:组件是如何最终出现在屏幕上的。

创建项目

Vite 是一个内置项目生成器的构建工具。用 npm create 运行它,回答两个提示问题,你就有了一个项目:

bash
npm create vite@latest my-react-app
cd my-react-app
npm install
npm run dev

第一条命令会问你想用哪个框架(选 React)以及想用哪个变体(选 JavaScript,或者如果你用 TypeScript 就选 TypeScript)。它会创建一个名为 my-react-app 的文件夹,里面有 React 项目需要的所有东西。npm install 会下载 package.json 里列出的依赖,React 本身就来自这里。npm run dev 启动开发服务器并输出一个本地地址,通常是 http://localhost:5173。在浏览器里打开这个地址,启动应用就运行起来了。

生成的文件夹里有几个早期重要的文件:

  • index.html:浏览器加载的单一 HTML 页面。
  • src/main.jsx:入口文件,React 在这里附加到该页面。
  • src/App.jsx:你的顶层组件,也是你最先编辑的文件。
  • src/assets/:你在组件里引入的图片和其他静态文件。
  • package.json:依赖和 devbuildpreview 脚本。
  • vite.config.js:构建配置,你可以长时间不用理它。

各部分如何关联

三个文件按顺序相互传递。从 index.html 开始,它故意几乎是空的:

html
<body>
  <div id="root"></div>
  <!-- 后面跟一个 <script type="module" src="/src/main.jsx"> 标签 -->
</body>

这个空的 <div id="root"> 是页面上 React 被允许填充的地方。你的应用渲染的一切都会放在里面。下面的 module <script> 标签加载 main.jsx,这是 React 接管的地方:

jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import './index.css'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>
)

从右往左读这个调用。document.getElementById('root') 找到那个空的 div。createRoot 把它包装在一个 React root 里,意思是 React 现在拥有那个元素的内容,并将使其与你的组件保持同步。.render(...) 告诉它要放什么进去:App 组件。

<StrictMode> 是一个仅在开发时有效的包装器,它会对里面的所有内容运行额外检查,并对可能导致 bug 的模式发出警告。它不会给页面添加任何东西,在生产构建中会自动关闭。它最显著的作用出现在 Effects 里,它故意在开发期间挂载每个组件两次。

App.jsx 是一个普通的组件,它导出自己:

jsx
export default function App() {
  return <h1>来自 React 的问候</h1>
}

这就是整个链条。浏览器加载 index.html,脚本加载 main.jsxmain.jsx 在 root div 上调用 createRoot 并渲染 <App />App 返回的 JSX 变成你看到的标题。从这里开始你写的每个组件都坐在 App 里的某个地方,所以它通过这同一条路径到达页面。main.jsx 通常写一次就很少再改动;你几乎所有的工作都发生在 App.jsx 和它引入的组件里。

版本说明

React 17 及更早版本使用 ReactDOM.render(<App />, document.getElementById('root')) 作为入口点。React 18 引入了 createRoot,它启用了并发渲染,旧的调用方式仍然可用但有弃用警告。React 19 移除了它,所以 ReactDOM.render 现在会抛出错误。在仍然使用它的教程上,createRoot 是要写的替代行。参见 历史和版本 了解什么改变了。

开发服务器

npm run dev 启动 Vite 的开发服务器,它会在你工作时保持在终端运行。它的工作是提供你的应用服务,并对你的编辑做出反应。

保存对 App.jsx 的更改,浏览器会几乎立即更新,不需要手动刷新。这叫热模块替换:Vite 把改变的模块推送到页面并进行替换,让应用的其余部分保持运行。一个你点过的计数器如果显示七,编辑后通常还会显示七,所以你在调整周围的标记时保持了你设置的任何状态。某些改动仍然会强制完全重新加载,Vite 会为你判断。

项目自带两个其他脚本。npm run builddist 文件夹里产生一个优化的包,这是你要部署的东西。npm run preview 在本地提供那个构建后的输出,这样你可以在发布前检查它。在开发期间,npm run dev 是你唯一需要的。

导入静态资源

图片存放在 src/assets 下面,通过 import 引入到组件里:

jsx
import logo from './assets/logo.png'

export default function Header() {
  return <img src={logo} alt="公司标志" />
}

import 给你一个持有那个图片最终 URL 的变量,然后你把它传给 src,用花括号包起来。把 src="./assets/logo.png" 写成一个纯字符串往往会破坏,因为你源文件夹里的路径很少会是构建后输出的路径。

改用 import 把构建工具纳入流程。它看到你的代码依赖 logo.png,把它复制到输出里,给你回一个那个文件最终出现的任何 URL。把你的组件移到另一个文件夹,相对 import 仍然会解析。拼错文件名,你会立即发现,因为 import 会失败,而不是在生产环境中发现一个破损的图片。同样的模式对 SVG、字体和任何其他构建工具知道如何处理的文件都有效。

浏览器不知道什么是 .jsx 文件。任何 JavaScript 引擎里都没有 JSX 解析器,<App /> 在纯 JavaScript 里是一个语法错误。浏览器收到的东西已经被转换过了。Vite 把每个 .jsx 文件送给 esbuild,后者把 JSX 改写成函数调用:在 React 19 的自动 runtime 下,<App /> 变成一个类似于 _jsx(App, {}) 的调用。你得到哪个 helper 取决于模式。生产转换从 react/jsx-runtime 导入 jsx,开发转换从 react/jsx-dev-runtime 导入 jsxDEV 并传递额外参数,这些参数携带源文件和行号,这就是 React 控制台里的警告如何能指向你代码里的确切位置的原因。.jsx 扩展名主要是对工具的一个信号,表示这个文件包含需要那个转换的语法。等任何东西到达页面时,它已经是调用普通函数的普通 JavaScript 模块了。

开发服务器和生产构建走不同的路线达到那个结果。在开发中,Vite 提供原生 ES 模块服务,按需转换每个文件,所以启动时间几乎不会随项目大小增长,单次编辑只会使那一个模块失效。npm run build 切换到 Rollup,它打包整个图,树摇掉未使用的导出,压缩,并把代码分割成块。两者之间的行为可能略有不同,这就是 npm run preview 存在的原因。

图片 import 有效是因为打包器也把非 JavaScript 文件视为模块图的一部分。import logo from './assets/logo.png' 不是一个真正的 JavaScript 模块导入。Vite 拦截它,把文件发出到 dist/assets,名字里带有内容哈希,并用那个最终路径的字符串字面量替换 import,比如 /assets/logo-4f2a1c8b.png。哈希是什么让激进的缓存变得安全的:改变图片,文件名也随之改变,所以没有陈旧副本能在 CDN 里存活。低于大小阈值(默认 4KB)的文件完全跳过单独的请求,改为作为 base64 数据 URL 内联。在开发中同样的 import 解析为由开发服务器提供的纯路径,这就是为什么你在 devtools 里看到的 URL 在 npm run dev 和一个真实构建间不同。

Juno从页面到你的组件的一条路径 设置只需要四条命令,之后你就可以忘掉它。

值得记住的是这条链:index.html 有一个 id 为 root 的空 div,main.jsxcreateRoot 抓住那个 div 并把 <App /> 渲染进去,App.jsx 是说明什么会出现的组件。

一旦你能跟踪那条路径,你就确切知道你自己的代码插在哪里。

Juno从页面到你的组件的一条路径npm create vite@latest,选 React,安装,npm run dev

从那以后是 index.htmlmain.jsxApp.jsx:入口文件在 root div 上调用一次 createRoot 并渲染你的顶层组件,你接下来的时间都花在那一行下面。

导入图片而不是硬编码路径,这样构建工具会为你解析并给它们添加版本指纹。

Juno从页面到你的组件的一条路径 设置存在是因为 JSX 没有 runtime:esbuild 把 <App /> 改写成 runtime 调用,在构建里是 react/jsx-runtimejsx,在开发里是 react/jsx-dev-runtimejsxDEV,在任何东西到达浏览器前,资源 import 解析为打包器发出的哈希 URL。

开发运行未打包的 ES 模块通过 esbuild,生产运行 Rollup,所以把 npm run preview 看作两者一致的检查。createRoot 是 React 18 的入口点,它是什么让你的树选择进并发渲染。

接下来:组件,是你会用来填充 App.jsx 的构建块。