Amazing Enyichi Agu 2025-04-09
TanStack Start 是一个全新的全栈 React 框架,基于 Vite、Nitro 和 TanStack Router 构建。该框架集成了诸多功能,如服务端渲染(SSR)、服务端函数(Server functions)、API 路由等。
尽管 TanStack Start 仍处于早期开发阶段,但其文档表明它已经可以安全地用于生产环境。
凭借其所提供的优势,TanStack Start 可能是你下一个项目的理想选择。然而,选择它也存在一些缺点,包括社区规模相对较小、文档尚不够全面等。
本文将解释 TanStack Start 的工作原理,并通过一个教程演示如何使用该框架构建一个简单的开发者作品集网站(使用 Tailwind CSS 进行样式设计)。我们还将总结选择 TanStack Start 用于项目时的优缺点。要顺利跟随本教程,你需要具备 React 和 TypeScript 的知识。
TanStack Start 的特性
以下是 TanStack Start 的一些显著特性:
全栈类型安全
由于 TanStack Start 是一个全栈框架,使用它可以保证应用程序客户端和服务器端的类型安全。这也使得两端可以轻松共享类型(例如表单验证类型)。
服务端函数(Server Functions)
这些函数可以从客户端或服务器调用,但始终在服务器上运行。在 TanStack Start 中,你可以使用服务端函数为路由获取数据、查询数据库,或执行任何应在服务器上运行的操作。值得注意的是,TanStack Start 允许通过中间件进一步扩展服务端函数。
API 路由
通过 API 路由,开发者可以为应用程序创建后端 API。这意味着在使用 TanStack Start 时,通常不再需要单独的后端服务器。API 路由采用基于文件的约定,并保存在 TanStack Start 项目的 /app/routes/api 目录中。
服务端渲染(SSR)
TanStack Start 支持使用 SSR 和静态预渲染(static prerendering)来渲染应用程序。SSR 和预渲染可加快页面加载速度,从而提升用户体验和 SEO 效果。
基于 TanStack Router 构建
TanStack Start 使用 TanStack Router(React Router 的一个流行替代方案)处理路由。该路由库具备类型安全导航、带 SWR 缓存的数据加载、路由中间件等重要特性。使用 TanStack Start 也就意味着同时采纳了这些优势。
快速的开发服务器
由于 TanStack Start 由 Vite 驱动,其开发服务器启动迅速。同样,热模块替换(HMR)也能即时生效。使用 TanStack Start,开发者可以最大化地将时间投入到项目构建中。
如何安装 TanStack Start
截至本文撰写时,安装 TanStack Start 有两种方式:你可以使用 degit 克隆一个模板,也可以从零开始搭建。本节将展示如何使用其各个依赖项和文件从头开始设置 TanStack Start。此外,还会解释该框架如何与每个依赖项/文件协同工作。
首先,为 TanStack Start 项目创建一个文件夹,然后进入该文件夹:
mkdir tanstack-app
cd tanstack-app
接着,为项目初始化 npm:
npm init -y
TanStack Start 文档强烈推荐使用 TypeScript 进行开发,因此请将其作为开发依赖项安装:
npm install --save-dev typescript
在项目根目录下创建一个 TypeScript 编译器配置文件(tsconfig.json)。TanStack 文档建议该配置文件至少包含以下选项:
// tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
"moduleResolution": "Bundler",
"module": "ESNext",
"target": "ES2022",
"skipLibCheck": true,
"strictNullChecks": true
}
}
接下来,安装 React 和 React DOM 的 npm 包。它们作为用户界面的渲染引擎。同时,也安装它们的类型定义以确保使用这些包时的类型安全:
npm install react react-dom
npm install --save-dev @types/react @types/react-dom
然后,安装 React 的 Vite 插件以及 vite-tsconfig-paths。后者是一个 Vite 插件,可即时解析路径别名:
npm install --save-dev @vitejs/plugin-react vite-tsconfig-paths
最后,安装 TanStack Start、TanStack Router 和 Vinxi 的 npm 包。Vinxi 是一个用于使用 Vite 构建全栈 Web 应用程序(甚至是有主见的全栈框架)的工具。它是 TanStack Start 框架的重要基础:
TanStack Start 团队承诺未来将开发一个自定义的 Vite 插件来取代 Vinxi。然而,目前 TanStack Start 仍然严重依赖它。
安装完成后,打开 package.json 文件并添加以下脚本:
// package.json
{
// ...
"type": "module",
"scripts": {
"dev": "vinxi dev",
"build": "vinxi build",
"start": "vinxi start"
}
}
这些脚本指示框架使用 Vinxi 来启动开发服务器、打包生产构建以及提供生产构建服务。同时,请确保将 "type" 设置为 "module",以便 Vite 能正确地将 React 代码转译为浏览器可识别的格式。
在根目录下创建一个 app.config.ts 文件。此配置文件用于初始化已安装的 Vite 插件。使用该文件初始化 vite-tsconfig-paths 插件:
// app.config.ts
import { defineConfig } from '@tanstack/react-start/config';
import tsConfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
vite: {
plugins: [
tsConfigPaths({
projects: ['./tsconfig.json'],
}),
],
},
})
完成应用配置后,设置框架的文件结构。使用下面的文件树作为地图,在适当的位置创建文件:
.
├── app/
│ ├── routes/
│ │ └── __root.tsx
│ ├── client.tsx
│ ├── router.tsx
│ ├── routeTree.gen.ts
│ └── ssr.tsx
├── .gitignore
├── app.config.ts
├── package.json
└── tsconfig.json
app/router.tsx 文件用于配置 TanStack Router。它导入一个自动生成的路由树,并允许控制诸如滚动恢复等选项。向该文件添加以下内容并保存:
// app/router.tsx
import { createRouter as createTanStackRouter } from '@tanstack/react-router';
import { routeTree } from './routeTree.gen';
export function createRouter() {
const router = createTanStackRouter({
routeTree,
scrollRestoration: true,
});
return router;
}
declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof createRouter>;
}
}
在 app/ssr.tsx 文件中,导入新创建的 createRouter 函数,以及来自 @tanstack/react-start 的其他服务器工具。该文件使框架能够处理 SSR,并将用户请求的任何路由正确地提供给客户端:
// app/ssr.tsx
import {
createStartHandler,
defaultStreamHandler,
} from '@tanstack/react-start/server';
import { getRouterManifest } from '@tanstack/react-start/router-manifest';
import { createRouter } from './router';
export default createStartHandler({
createRouter,
getRouterManifest,
})(defaultStreamHandler);
同样,在 app/client.tsx 中导入 createRouter 函数。该文件是客户端入口点,处理与客户端路由相关的任何功能。它负责在用户从服务器获得已解析的路由后,对客户端进行水合(hydrate):
// app/client.tsx
/// <reference types="vinxi/types/client" />
import { hydrateRoot } from 'react-dom/client';
import { StartClient } from '@tanstack/react-start';
import { createRouter } from './router';
const router = createRouter();
hydrateRoot(document, <StartClient router={router} />);
完成上述所有步骤后,打开 __root.tsx 文件以设置应用程序的根路由。这是一个始终会被渲染的路由,因此是设置全局客户端配置(如默认 HTML meta 标签)和导入编译后的 Tailwind CSS 文件的好地方。向该文件添加以下内容:
// app/routes/__root.tsx
import {
Outlet,
createRootRoute,
HeadContent,
Scripts,
} from '@tanstack/react-router';
import type { ReactNode } from 'react';
export const Route = createRootRoute({
head: () => ({
meta: [
{
charSet: 'utf-8',
},
{
name: 'viewport',
content: 'width=device-width, initial-scale=1',
},
{
title: 'TanStack Start Starter',
},
],
}),
component: RootComponent,
});
function RootComponent() {
return (
<RootDocument>
<Outlet />
</RootDocument>
);
}
function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
return (
<html>
<head>
<HeadContent />
</head>
<body>
{children}
<Scripts />
</body>
</html>
);
}
接下来,使用 createFileRoute 函数创建一个索引路由。由于 TanStack Router 采用基于文件的路由,每个路由都需要一个新文件(位于 app/routes 目录内)。
创建文件 app/routes/index.tsx 作为你的第一个路由。为了演示目的,该路由将显示一个写着“Hello, World!”的 h1 标题:
// app/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/')({
component: Home,
});
function Home() {
return <h1>Hello, World!</h1>;
}
最后,从 CLI 启动开发服务器:
npm run dev
这将启动一个本地服务器,地址为:http://localhost:3000。在浏览器中打开该 URL。如果你准确地遵循了上述步骤,你应该会看到如下输出:
Hello, World!
至此,你已经从头创建了一个 TanStack Start 项目。你导入了必要的库,现在对框架如何使用它们有了更清晰的认识。
请确保将 node_modules 和任何 .env 文件排除在 Git 跟踪之外:
# .gitignore
node_modules/
.env
使用 TanStack Start 进行开发
本节是一个教程,展示了如何使用 TanStack Start 构建一个项目。该项目是一个简单的开发者作品集网站。它展示了 TanStack Start 的一些特性用例,如路由、SSR、使用路由加载器(route loaders)进行数据加载、客户端导航和服务端函数。项目的最终源代码可以在 GitHub 上找到。
如何在 TanStack Start 中安装 Tailwind CSS
要使用 Tailwind CSS 为 TanStack Start 项目设置样式,请安装 Tailwind 及其 Vite 插件:
npm install tailwindcss @tailwindcss/vite
在应用配置文件 app.config.ts 中初始化该插件:
// ...
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
vite: {
// ...
tailwindcss(),
],
},
});
接下来,创建一个文件 app/styles/app.css。该文件将成为应用程序的主样式表。Vite 会将项目编译后的 Tailwind 样式发送到此文件中。在文件内添加以下内容:
/* app/styles/app.css */
@import 'tailwindcss';
最后,将 CSS 文件导入到应用程序的根路由中。在应用的 <head> 标签中链接到该样式表。这样,应用程序的每个部分都能访问编译后的 Tailwind 样式:
//...
import appCSS from '../styles/app.css?url';
export const Route = createRootRoute({
head: () => ({
// ...
links: [{ rel: 'stylesheet', href: appCSS }],
})
// ...
})
// ...
现在,你就可以在项目中使用 Tailwind 类了。
创建主页
该项目使用了 React Icons 库中的几个图标,因此请安装 react-icons npm 包:
npm install react-icons
创建一个新文件夹 app/components 用于存储应用程序的组件。要创建的第一个组件是一个在应用程序每个路由中都会出现的页眉。创建文件 Header.tsx 并添加以下内容:
// app/components/Header.tsx
import { Link } from '@tanstack/react-router';
const Header = () => {
return (
<header className='bg-gray-100 flex justify-between px-8 py-3 items-center'>
<h1 className='text-2xl'>
<Link to='/'>Portfolio</Link>
</h1>
<ul className='flex gap-5 text-blue-900'>
<li>
<Link to='/'>Home</Link>
</li>
<li>
<Link to='/projects'>Projects</Link>
</li>
<li>
<Link to='/contact'>Contact</Link>
</li>
</ul>
</header>
);
};
export default Header;
请注意,上面的代码片段使用了来自 TanStack Router 的 <Link> 组件。<Link> 用于在 TanStack Start 项目中创建内部链接。由于 Header 组件应出现在每个路由中,因此将其导入到根路由 app/routes/__root.tsx 中。注意为根组件添加了 <main> 标签和一些 Tailwind 样式,以实现样式和语义目的:
// app/routes/__root.tsx
// ...
import Header from '../components/Header';
// ...
function RootComponent() {
return (
<RootDocument>
<Header />
<main className='max-w-4xl mx-auto pt-10'>
<Outlet />
</main>
</RootDocument>
);
}
// ...
在组件文件夹中创建一个名为 Hero.tsx 的新文件。该文件将包含作品集网站主页的 Hero 组件:
// app/components/Hero.tsx
import { FaXTwitter, FaLinkedinIn, FaGithub } from 'react-icons/fa6';
const Hero = () => {
return (
<div className='bg-blue-900 text-gray-50 py-10 px-15 flex gap-15 rounded-2xl items-center'>
<div>
<img
src='https://avatars.githubusercontent.com/u/58449038?v=4'
alt='Profile Picture'
className='h-50 rounded-full'
/>
</div>
<div>
<span className='text-3xl mb-3 block'>Amazing Enyichi Agu</span>
<p className='mb-5'>generic and easily forgettable developer bio</p>
<div className='text-2xl flex gap-3'>
<a href='#'>
<FaGithub />
</a>
<a href='#'>
<FaXTwitter />
</a>
<a href='#'>
<FaLinkedinIn />
</a>
</div>
</div>
</div>
);
};
export default Hero;
再创建一个组件文件 SkillBox.tsx。在文件内添加以下内容:
// app/components/SkillBox.tsx
interface SkillBoxProps {
children?: React.ReactNode;
}
const SkillBox = ({ children }: SkillBoxProps) => {
return (
<span className='px-4 py-2 text-blue-800 bg-blue-50 rounded'>
{children}
</span>
);
};
export default SkillBox;
创建完这些组件后,导航到索引路由(index.tsx)并添加以下标记:
// app/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router';
import Hero from '../components/Hero';
import SkillBox from '../components/SkillBox';
export const Route = createFileRoute('/')({
component: Home,
});
function Home() {
return (
<>
<Hero />
<div className='mt-10'>
<h2 className='text-2xl'>Languages</h2>
<div className='mt-2.5 flex gap-3'>
<SkillBox>HTML</SkillBox>
<SkillBox>CSS</SkillBox>
<SkillBox>JavaScript</SkillBox>
<SkillBox>TypeScript</SkillBox>
</div>
</div>
<div className='mt-10'>
<h2 className='text-2xl'>Tools</h2>
<div className='mt-2.5 flex gap-3'>
<SkillBox>React</SkillBox>
<SkillBox>GraphQL</SkillBox>
<SkillBox>Node.js</SkillBox>
<SkillBox>Socket.io</SkillBox>
<SkillBox>Next.js/Remix</SkillBox>
</div>
</div>
</>
);
}
这将创建一个带有 Hero 区域和开发者技能列表的简单介绍页面。保存所有文件后,浏览器中的输出应如下所示:

创建项目页面
项目页面是一个简单的页面,用于展示作品集所有者的一些项目。它将利用 TanStack Start 的服务端函数和 TanStack Router 的加载器(loader)。该加载器会显示作者的公开 GitHub 仓库。
但首先,创建一个名为 ProjectCard.tsx 的组件。该组件是作品集中每个列出项目的卡片:
// app/components/ProjectCard.tsx
import { FaCodeFork, FaRegStar } from 'react-icons/fa6';
interface ProjectCardProps {
url: string;
projectName: string;
language: string;
stars: number;
forks: number;
}
const ProjectCard = (props: ProjectCardProps) => {
return (
<a
className='px-6 py-4 rounded-md bg-green-50 shadow mb-5 block'
href={props.url}
>
<div className='flex justify-between mb-2'>
<span>{props.projectName}</span>
<div className='flex gap-3'>
<span>
{props.stars} <FaRegStar className='inline' />
</span>
<span>
{props.forks} <FaCodeFork className='inline' />
</span>
</div>
</div>
<div>
<span className='text-sm bg-blue-800 text-gray-50 px-1 py-0.5'>
{props.language}
</span>
</div>
</a>
);
};
export default ProjectCard;
接下来,使用服务端函数创建项目路由。该路由将加载服务端函数返回的任何数据:
// app/routes/projects.tsx
import { createServerFn } from '@tanstack/react-start';
import { createFileRoute } from '@tanstack/react-router';
import ProjectCard from '../components/ProjectCard';
interface Project {
full_name: string;
html_url: string;
language: string;
stargazers_count: number;
forks: number;
}
const getProjects = createServerFn({
method: 'GET',
}).handler(async () => {
const res = await fetch(
'https://api.github.com/users/enyichiaagu/repos?sort=updated&per_page=5',
{
headers: {
'X-GitHub-Api-Version': '2022-11-28',
accept: 'application/vnd.github+json',
},
}
);
return res.json();
});
export const Route = createFileRoute('/projects')({
component: Projects,
loader: () => getProjects(),
});
function Projects() {
const projects: Project[] = Route.useLoaderData();
return (
<>
<h2 className='text-2xl'>Projects</h2>
<div className='mt-2.5'>
{projects.map(
(
{ full_name, html_url, language, stargazers_count, forks },
index
) => (
<ProjectCard
projectName={full_name}
url={html_url}
language={language}
stars={stargazers_count}
forks={forks}
key={index}
/>
)
)}
</div>
</>
);
}
现在,如果客户端导航到 /projects,结果应如下所示:

使用服务端函数意味着 fetch() 操作发生在服务器上,这可能会带来更快的加载时间。
创建联系页面
项目的最后一个路由是联系页面。该页面包含一个表单,作品集网站的访客可以用它向所有者发送电子邮件。此路由使用 TanStack Start 的服务端函数,借助 Nodemailer 发送电子邮件。
为了正确实现此功能,首先需要安装 Nodemailer 库。Nodemailer 允许 Node.js 程序发送电子邮件:
npm install nodemailer
npm install --save-dev @types/nodemailer
安装 nodemailer 后,设置用于通过 Nodemailer 发送电子邮件的凭据。这里 是一个快速设置的有用资源。
接下来,创建一个 .env 文件,将这些凭据作为环境变量存储。通常只需要一个电子邮件地址和密码。示例如下:
# .env
EMAIL_ADDRESS=XXXXXX
EMAIL_PASSWORD=XXXXXX
最后,设置联系页面:
// app/routes/contact.tsx
// Imports
import { useState } from 'react';
import { createServerFn } from '@tanstack/react-start';
import { createFileRoute } from '@tanstack/react-router';
import nodemailer from 'nodemailer';
import { FaCheck } from 'react-icons/fa';
// Defining the contact route
export const Route = createFileRoute('/contact')({
component: Contact,
});
// Creating the transporter object for nodemailer
const transporter = nodemailer.createTransport({
host: 'smtp.gmail.com',
secure: true,
port: 465,
auth: {
user: process.env.EMAIL_ADDRESS,
pass: process.env.EMAIL_PASSWORD,
},
});
// Function that uses the nodemailer transporter to send the email
const sendEmailMessage = async ({ email, message }) => {
const res = await transporter.sendMail({
from: process.env.EMAIL_ADDRESS,
to: process.env.EMAIL_ADDRESS,
subject: `Message from ${email}, sent from Portfolio Website`,
text: message,
replyTo: email,
});
return res;
};
// Server function that calls that validates the input and calls the `sendEmailMessage` function
const submitForm = createServerFn({ method: 'POST' })
.validator((data: FormData) => {
const email = data.get('email');
const message = data.get('message');
if (!email || !message) {
throw new Error('Email and Message are required');
}
return { email: email.toString(), message: message.toString() };
})
.handler(async (ctx) => {
return await sendEmailMessage(ctx.data);
});
// JSX for contact page
function Contact() {
const [isSuccess, setIsSuccess] = useState<boolean>(false);
return (
<>
<p className='text-2xl'>Contact Me</p>
{isSuccess && (
<div className='bg-green-50 text-green-900 px-6 py-3 rounded w-md mt-5'>
<FaCheck className='inline' /> Email Sent Successfully
</div>
)}
<form
method='post'
className='mt-5'
onSubmit={async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form: HTMLFormElement = event.currentTarget;
const formData = new FormData(form);
await submitForm({ data: formData });
setIsSuccess(true);
return form.reset();
}}
>
<div className='mb-2'>
<label htmlFor='email'>Email</label>
<br />
<input
type='email'
name='email'
id='email'
required
className='border border-gray-400 w-md px-3 py-1.5'
/>
</div>
<div className='mb-2'>
<label htmlFor='message'>Message</label>
<br />
<textarea
name='message'
id='message'
placeholder='Write me a message'
required
className='border border-gray-400 w-md px-3 py-1.5 h-50'
></textarea>
</div>
<button className='bg-blue-900 text-gray-50 px-4 py-2 rounded'>
Send
</button>
</form>
</>
);
}
现在,就可以从作品集的联系页面发送电子邮件了:

TanStack Start 的优缺点
毫无疑问,TanStack Start 是一个很有前景的全新框架——也是构建全栈 React 应用的新范式。它拥有众多特性,并且背后有一个曾开发过其他强大库的优秀团队。
然而,在当前状态下使用该框架也存在一些缺点。以下是 TanStack Start 框架的优缺点列表。
TanStack Start 的优点
- 由 TanStack Router 驱动 — TanStack Start 让开发者有机会利用 TanStack Router 的优势(类型安全导航、带 SWR 缓存的数据加载、路由中间件等)。
- 支持 SSR 和静态预渲染 — 该框架足以满足任何 SSR 或 SSG 需求。它甚至包含静态服务端函数(Static Server Functions),这些函数在构建时运行并缓存结果。有了静态服务端函数,在生产环境中调用函数时就无需服务器再次运行该函数。
- 额外的功能 — TanStack Start 确保包含构建快速、健壮的全栈应用程序所需的尽可能多的功能。
TanStack Start 的缺点
- 需要熟悉 TanStack Router — 为了充分利用该框架,你需要熟悉 TanStack Router 的 API(使用加载器、处理导航等)。
- 学习曲线并非 trivial(非平凡) — 虽然 TanStack Start 的学习曲线并不太陡峭,但仍需付出努力去理解和实践。
- 不支持单页应用程序(SPAs) — 目前,无法使用该框架构建 SPAs。这一点未来可能会改变。
- 文档有待改进 — 虽然框架的文档足以构建一个项目,但仍需改进。有些 TanStack Start 的特性,文档尚未充分演示。
- 仍处于重度开发阶段 — 这意味着某些工具可能还不完善(例如,它没有用于脚手架新项目的 CLI),或者某些特性可能会随时间而改变。
- 社区规模较小 — 由于 TanStack Start 仍是新事物,其社区规模相对较小。这意味着初学者遇到问题时,目前能提供帮助的资源还不多。
尽管如此,TanStack Start 仍然是一个出色的框架,而大多数缺点的存在仅仅是因为该框架在生态中还很新。
总结
本文只是对 TanStack Start 框架的一个简介。官方文档更详细地解释了该框架的众多能力。但通过本文,你应该已经对框架的工作原理以及如何使用它有了很好的理解。
如前所述,TanStack Start 将在未来推出新特性,并具有巨大的潜力。凭借团队迄今为止所取得的成就,它无疑已成为 Next.js 和 Remix/React Router 的一个强有力替代品。