学习基础知识

本指南将帮助您了解 TanStack Start 工作原理背后的基础知识,无论您如何设置项目。

依赖

TanStack Start 当前* 由 VinxiNitroTanStack Router 驱动。

  • TanStack Router: 用于构建 Web 应用程序的路由器。
  • Nitro: 用于构建服务器应用程序的框架。
  • Vinxi: 用于构建 Web 应用程序的服务器框架。

注意

Vinxi 将在 1.0.0 版本发布之前被移除,TanStack 将仅依赖于 Vite 和 Nitro。使用 Vinxi 的命令和 API 很可能被替换为 Vite 插件。

一切都从 "Router" 开始

此文件将决定 Start 中使用的 TanStack Router 的行为。在这里,您可以配置从默认 预加载功能缓存过期时间 的一切。

tsx
// app/router.tsx
import { createRouter as createTanStackRouter } from '@tanstack/solid-router'
import { routeTree } from './routeTree.gen'

export function createRouter() {
  const router = createTanStackRouter({
    routeTree,
    scrollRestoration: true,
  })

  return router
}

declare module '@tanstack/solid-router' {
  interface Register {
    router: ReturnType<typeof createRouter>
  }
}
// app/router.tsx
import { createRouter as createTanStackRouter } from '@tanstack/solid-router'
import { routeTree } from './routeTree.gen'

export function createRouter() {
  const router = createTanStackRouter({
    routeTree,
    scrollRestoration: true,
  })

  return router
}

declare module '@tanstack/solid-router' {
  interface Register {
    router: ReturnType<typeof createRouter>
  }
}
  • 注意 scrollRestoration 属性。这用于在路由之间导航时恢复页面的滚动位置。

路由生成

routeTree.gen.ts 文件在您首次运行 TanStack Start(通过 npm run devnpm run start)时生成。此文件包含生成的路由树和一些使 TanStack Start 完全类型安全的 TS 实用程序。

服务端入口点

尽管 TanStack Start 采用客户端优先 API 设计,但总体而言,它是一个全栈框架。这意味着所有用例,包括动态和静态,都依赖于服务器或构建时入口来渲染我们应用程序的初始 HTML 有效负载。

这是通过 app/ssr.tsx 文件完成的

tsx
// app/ssr.tsx
import {
  createStartHandler,
  defaultStreamHandler,
} from '@tanstack/solid-start/server'
import { getRouterManifest } from '@tanstack/solid-start/router-manifest'

import { createRouter } from './router'

export default createStartHandler({
  createRouter,
  getRouterManifest,
})(defaultStreamHandler)
// app/ssr.tsx
import {
  createStartHandler,
  defaultStreamHandler,
} from '@tanstack/solid-start/server'
import { getRouterManifest } from '@tanstack/solid-start/router-manifest'

import { createRouter } from './router'

export default createStartHandler({
  createRouter,
  getRouterManifest,
})(defaultStreamHandler)

无论我们是静态生成应用还是动态提供应用,ssr.tsx 文件都是执行所有 SSR 相关工作的入口点。

  • 重要的是为每个请求创建一个新的路由器。这确保路由器处理的任何数据对于请求都是唯一的。
  • getRouterManifest 函数用于生成路由器清单,该清单用于确定我们应用程序的资源管理和预加载的许多方面。
  • defaultStreamHandler 函数用于将我们的应用程序渲染到流,从而使我们能够利用流式 HTML 到客户端。(这是默认处理程序,但您也可以使用其他处理程序,例如 defaultRenderHandler,甚至构建自己的处理程序)

客户端入口点

将 HTML 发送到客户端只是战斗的一半。到达客户端后,一旦路由解析到客户端,我们需要水合客户端 JavaScript。我们通过使用 StartClient 组件水合我们应用程序的根来实现这一点

tsx
// app/client.tsx
/// <reference types="vinxi/types/client" />
import { hydrate } from 'solid-js/web'
import { StartClient } from '@tanstack/solid-start'
import { createRouter } from './router'

const router = createRouter()

hydrate(() => <StartClient router={router} />, document)
// app/client.tsx
/// <reference types="vinxi/types/client" />
import { hydrate } from 'solid-js/web'
import { StartClient } from '@tanstack/solid-start'
import { createRouter } from './router'

const router = createRouter()

hydrate(() => <StartClient router={router} />, document)

这使我们能够在用户的初始服务器请求完成后启动客户端路由。

你的应用根目录

除了客户端入口点之外,应用程序的 __root 路由也是您应用程序的入口点。此文件中的代码将包裹应用程序中的所有其他路由,包括您的首页。它的行为类似于整个应用程序的无路径布局路由。

因为它始终渲染,所以它是构建应用程序 shell 和处理任何全局逻辑的理想场所。

tsx
// app/routes/__root.tsx
import {
  Outlet,
  createRootRoute,
  HeadContent,
  Scripts,
} from '@tanstack/solid-router'

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 <Outlet />
}
// app/routes/__root.tsx
import {
  Outlet,
  createRootRoute,
  HeadContent,
  Scripts,
} from '@tanstack/solid-router'

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 <Outlet />
}
  • 随着我们推出 SPA 模式,此布局将来可能会更改,SPA 模式允许根路由渲染 SPA shell,而无需任何页面特定内容。
  • 注意 Scripts 组件。这用于加载应用程序的所有客户端 JavaScript。

路由

路由是 TanStack Router 的一个广泛功能,在 路由指南 中进行了详尽的介绍。总结一下

  • 路由是使用 createFileRoute 函数定义的。
  • 路由是自动代码拆分和延迟加载的。
  • 关键数据获取从路由的加载器协调
  • 还有更多!
tsx
// app/routes/index.tsx
import * as fs from 'node:fs'
import { createFileRoute, useRouter } from '@tanstack/solid-router'
import { createServerFn } from '@tanstack/solid-start'

const filePath = 'count.txt'

async function readCount() {
  return parseInt(
    await fs.promises.readFile(filePath, 'utf-8').catch(() => '0'),
  )
}

const getCount = createServerFn({
  method: 'GET',
}).handler(() => {
  return readCount()
})

const updateCount = createServerFn({ method: 'POST' })
  .validator((d: number) => d)
  .handler(async ({ data }) => {
    const count = await readCount()
    await fs.promises.writeFile(filePath, `${count + data}`)
  })

export const Route = createFileRoute('/')({
  component: Home,
  loader: async () => await getCount(),
})

function Home() {
  const router = useRouter()
  const state = Route.useLoaderData()

  return (
    <button
      type="button"
      onClick={() => {
        updateCount({ data: 1 }).then(() => {
          router.invalidate()
        })
      }}
    >
      Add 1 to {state}?
    </button>
  )
}
// app/routes/index.tsx
import * as fs from 'node:fs'
import { createFileRoute, useRouter } from '@tanstack/solid-router'
import { createServerFn } from '@tanstack/solid-start'

const filePath = 'count.txt'

async function readCount() {
  return parseInt(
    await fs.promises.readFile(filePath, 'utf-8').catch(() => '0'),
  )
}

const getCount = createServerFn({
  method: 'GET',
}).handler(() => {
  return readCount()
})

const updateCount = createServerFn({ method: 'POST' })
  .validator((d: number) => d)
  .handler(async ({ data }) => {
    const count = await readCount()
    await fs.promises.writeFile(filePath, `${count + data}`)
  })

export const Route = createFileRoute('/')({
  component: Home,
  loader: async () => await getCount(),
})

function Home() {
  const router = useRouter()
  const state = Route.useLoaderData()

  return (
    <button
      type="button"
      onClick={() => {
        updateCount({ data: 1 }).then(() => {
          router.invalidate()
        })
      }}
    >
      Add 1 to {state}?
    </button>
  )
}

TanStack Start 100% 构建于 TanStack Router 之上,因此 TanStack Router 的所有导航功能都可供您使用。总结一下

  • 使用 Link 组件导航到新路由。
  • 使用 useNavigate hook 命令式地导航。
  • 在应用程序中的任何位置使用 useRouter hook 来访问路由器实例并执行失效。
  • 每个返回状态的路由器 hook 都是响应式的,意味着当适当的状态更改时,它将自动重新运行。

这是一个如何使用 Link 组件导航到新路由的快速示例

tsx
import { Link } from '@tanstack/solid-router'

function Home() {
  return <Link to="/about">About</Link>
}
import { Link } from '@tanstack/solid-router'

function Home() {
  return <Link to="/about">About</Link>
}

有关导航的更深入信息,请查看 导航指南

服务端函数 (RPC)

您可能已经注意到上面使用 createServerFn 创建的服务端函数。这是 TanStack 最强大的功能之一,允许您创建可以从 SSR 期间的服务器和客户端调用的服务端函数!

这是服务端函数工作原理的快速概述

  • 服务端函数是使用 createServerFn 函数创建的。
  • 它们可以从 SSR 期间的服务器和客户端调用。
  • 它们可以用于从服务器获取数据,或执行其他服务端操作。

这是一个如何使用服务端函数从服务器获取和返回数据的快速示例

tsx
import { createServerFn } from '@tanstack/solid-start'
import * as fs from 'node:fs'
import { z } from 'zod'

const getUserById = createServerFn({ method: 'GET' })
  // Always validate data sent to the function, here we use Zod
  .validator(z.string())
  // The handler function is where you perform the server-side logic
  .handler(async ({ data }) => {
    return db.query.users.findFirst({ where: eq(users.id, data) })
  })

// Somewhere else in your application
const user = await getUserById({ data: '1' })
import { createServerFn } from '@tanstack/solid-start'
import * as fs from 'node:fs'
import { z } from 'zod'

const getUserById = createServerFn({ method: 'GET' })
  // Always validate data sent to the function, here we use Zod
  .validator(z.string())
  // The handler function is where you perform the server-side logic
  .handler(async ({ data }) => {
    return db.query.users.findFirst({ where: eq(users.id, data) })
  })

// Somewhere else in your application
const user = await getUserById({ data: '1' })

要了解有关服务端函数的更多信息,请查看 服务端函数指南

变更

服务端函数也可以用于在服务器上执行变更。这也是使用相同的 createServerFn 函数完成的,但附加要求是您需要使客户端上受变更影响的任何数据失效。

  • 如果您仅使用 TanStack Router,则可以使用 router.invalidate() 方法使所有路由器数据失效并重新获取它。
  • 如果您正在使用 TanStack Query,则可以使用 queryClient.invalidateQueries() 方法使数据失效,以及其他更具体的方法来定位特定查询。

这是一个如何使用服务端函数在服务器上执行变更并使客户端上的数据失效的快速示例

tsx
import { createServerFn } from '@tanstack/solid-start'

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
})

const updateUser = createServerFn({ method: 'POST' })
  .validator(UserSchema)
  .handler(async ({ data }) => {
    return db
      .update(users)
      .set({ name: data.name })
      .where(eq(users.id, data.id))
  })

// Somewhere else in your application
await updateUser({ data: { id: '1', name: 'John' } })
import { createServerFn } from '@tanstack/solid-start'

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
})

const updateUser = createServerFn({ method: 'POST' })
  .validator(UserSchema)
  .handler(async ({ data }) => {
    return db
      .update(users)
      .set({ name: data.name })
      .where(eq(users.id, data.id))
  })

// Somewhere else in your application
await updateUser({ data: { id: '1', name: 'John' } })

要了解有关变更的更多信息,请查看 变更指南

数据加载

TanStack Router 的另一个强大功能是数据加载。这允许您获取 SSR 的数据并在渲染之前预加载路由数据。这是使用路由的 loader 函数完成的。

这是数据加载工作原理的快速概述

  • 数据加载是使用路由的 loader 函数完成的。
  • 数据加载器是同构的,意味着它们在服务器和客户端上都执行。
  • 为了执行仅服务器逻辑,请从加载器中调用服务端函数。
  • 与 TanStack Query 类似,数据加载器在客户端上缓存,当数据过期时,会被重用,甚至在后台重新获取。

要了解有关数据加载的更多信息,请查看 数据加载指南

订阅 Bytes

您的每周 JavaScript 新闻。每周一免费发送给超过 10 万名开发者。

Bytes

无垃圾邮件。随时取消订阅。