从零开始构建项目

注意

如果你选择使用示例或克隆项目快速开始,你可以跳过本指南,直接前往学习基础知识指南。

所以你想从零开始构建一个 TanStack Start 项目?

本指南将帮助你构建一个非常基础的 TanStack Start Web 应用程序。我们将一起使用 TanStack Start 来

  • 提供一个索引页面...
  • 其中显示一个计数器...
  • 带有一个按钮,用于持久地增加计数器。

这就是它的样子

让我们创建一个新的项目目录并初始化它。

shell
mkdir myApp
cd myApp
npm init -y
mkdir myApp
cd myApp
npm init -y

注意

> 在所有这些示例中,我们都使用 npm,但你可以使用你选择的包管理器来代替。

TypeScript 配置

我们强烈建议将 TypeScript 与 TanStack Start 一起使用。创建一个 tsconfig.json 文件,至少包含以下设置

jsonc
{
  "compilerOptions": {
    "jsx": "preserve",
    "jsxImportSource": "solid-js",
    "moduleResolution": "Bundler",
    "module": "ESNext",
    "target": "ES2022",
    "skipLibCheck": true,
    "strictNullChecks": true,
  },
}
{
  "compilerOptions": {
    "jsx": "preserve",
    "jsxImportSource": "solid-js",
    "moduleResolution": "Bundler",
    "module": "ESNext",
    "target": "ES2022",
    "skipLibCheck": true,
    "strictNullChecks": true,
  },
}

注意

> 启用 verbatimModuleSyntax 可能会导致服务器捆绑包泄漏到客户端捆绑包中。建议禁用此选项。

安装依赖

TanStack Start 目前* 由 VinxiTanStack Router 提供支持,并且需要将它们作为依赖项。

注意

> *Vinxi 将在 1.0.0 版本发布之前移除,TanStack 将仅依赖 Vite 和 Nitro。使用 Vinxi 的命令和 API 可能会被 Vite 插件或专用的 TanStack Start CLI 替换。

要安装它们,请运行

shell
npm i @tanstack/solid-start @tanstack/solid-router vinxi
npm i @tanstack/solid-start @tanstack/solid-router vinxi

你还需要 Solid 和 Vite Solid 插件,所以也安装它们

shell
npm i solid-js
npm i -D vite-plugin-solid vite-tsconfig-paths
npm i solid-js
npm i -D vite-plugin-solid vite-tsconfig-paths

以及一些 TypeScript

shell
npm i -D typescript
npm i -D typescript

更新配置文件

然后,我们将更新我们的 package.json 以使用 Vinxi 的 CLI 并设置 "type": "module"

json
{
  // ...
  "type": "module",
  "scripts": {
    "dev": "vinxi dev",
    "build": "vinxi build",
    "start": "vinxi start"
  }
}
{
  // ...
  "type": "module",
  "scripts": {
    "dev": "vinxi dev",
    "build": "vinxi build",
    "start": "vinxi start"
  }
}

然后配置 TanStack Start 的 app.config.ts 文件

typescript
// app.config.ts
import { defineConfig } from '@tanstack/solid-start/config'
import tsConfigPaths from 'vite-tsconfig-paths'

export default defineConfig({
  vite: {
    plugins: [
      tsConfigPaths({
        projects: ['./tsconfig.json'],
      }),
    ],
  },
})
// app.config.ts
import { defineConfig } from '@tanstack/solid-start/config'
import tsConfigPaths from 'vite-tsconfig-paths'

export default defineConfig({
  vite: {
    plugins: [
      tsConfigPaths({
        projects: ['./tsconfig.json'],
      }),
    ],
  },
})

添加基本模板

TanStack Start 使用需要四个文件

  1. 路由器配置
  2. 服务器入口点
  3. 客户端入口点
  4. 你的应用程序的根目录

配置完成后,我们将得到如下所示的文件树

.
├── app/
│   ├── routes/
│   │   └── `__root.tsx`
│   ├── `client.tsx`
│   ├── `router.tsx`
│   ├── `routeTree.gen.ts`
│   └── `ssr.tsx`
├── `.gitignore`
├── `app.config.ts`
├── `package.json`
└── `tsconfig.json`
.
├── app/
│   ├── routes/
│   │   └── `__root.tsx`
│   ├── `client.tsx`
│   ├── `router.tsx`
│   ├── `routeTree.gen.ts`
│   └── `ssr.tsx`
├── `.gitignore`
├── `app.config.ts`
├── `package.json`
└── `tsconfig.json`

路由器配置

这个文件将决定 Start 中使用的 TanStack Router 的行为。在这里,你可以配置从默认的 预加载功能缓存陈旧性 的所有内容。

注意

你现在还没有 routeTree.gen.ts 文件。当你第一次运行 TanStack Start 时,将生成此文件。

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>
  }
}

服务器入口点

由于 TanStack Start 是一个 SSR 框架,我们需要将此路由器信息管道传输到我们的服务器入口点

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)

这使我们能够知道当用户访问给定路由时,我们需要执行哪些路由和加载器。

客户端入口点

现在我们需要一种方法在路由解析到客户端后,水合我们的客户端 JavaScript。我们通过将相同的路由器信息管道传输到我们的客户端入口点来完成此操作

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.body)
// 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.body)

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

你的应用程序的根目录

最后,我们需要创建应用程序的根目录。这是所有其他路由的入口点。此文件中的代码将包装应用程序中的所有其他路由。

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 />
}

编写你的第一个路由

现在我们已经设置了基本模板,我们可以编写我们的第一个路由。这可以通过在 app/routes 目录中创建一个新文件来完成。

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 项目并编写了你的第一个路由。🎉

你现在可以运行 npm run dev 来启动你的服务器并导航到 https://:3000 以查看你的路由运行效果。

你想部署你的应用程序吗?查看托管指南

订阅 Bytes

你的每周 JavaScript 新闻速递。每周一免费发送给超过 100,000 名开发者。

Bytes

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