ДокументацияFullstack-фреймворки
От Next.js до Некси
В Nexy проект Next.js App Router сохраняет свои файловые маршруты, макеты и React компоненты, в то время как серверный код, обработчики маршрутов и действия сервера перемещаются в Python файлы рядом с ними.
В каждом разделе ниже показана привычка Next.js, затем то же самое в Nexy.
Сначала выберите режим
Приложение Next.js объединяет серверные и клиентские компоненты в одном дереве. Проект Nexy выбирает одну из двух форм в nexyconfig.py:
| Ваше Next.js приложение | Некси-режим | Страницы |
|---|---|---|
| в основном интерактивная, клиентская навигация | клиентский режим, useCSR = True |
.tsx файлы, например app/ |
| в основном контент, несколько интерактивных частей | режим сервера, по умолчанию | .nexy страниц с React островами |
from nexy.core.models import NexyConfigModelfrom nexy.frontend import react class NexyConfig(NexyConfigModel): useFF = react() useCSR = TrueВ разделах ниже используется клиентский режим, если не указано иное. См. Режим сервера и режим клиента.
page.tsx
export default function About() { return <h1>About</h1>;}Страница - это сам файл, а не page.tsx внутри папки:
export default function About() { return <h1>About</h1>;}app/page.tsx - это src/routes/index.tsx, а app/blog/page.tsx - это src/routes/blog/index.tsx.
Маршрутизация файлов, маршрутизатор приложений и маршрутизатор страниц
Оба маршрутизатора Next.js отображаются на src/routes/. Файл Pages Router сохраняет свой путь; page.tsx App Router становится index.tsx или файлом, названным в честь его папки:
| Next.js | Некси |
|---|---|
pages/about.tsx |
src/routes/about.tsx |
pages/blog/[slug].tsx |
src/routes/blog/[slug].tsx |
app/about/page.tsx |
src/routes/about/index.tsx или src/routes/about.tsx |
app/blog/[slug]/page.tsx |
src/routes/blog/[slug]/index.tsx или src/routes/blog/[slug].tsx |
app/docs/[...slug]/page.tsx |
src/routes/docs/[...slug].tsx |
app/(marketing)/pricing/page.tsx |
src/routes/(marketing)/pricing.tsx |
pages/api/items/[id].ts |
src/routes/api/items/[id].py |
app/api/items/[id]/route.ts |
src/routes/api/items/[id].py или src/routes/api/items/[id]/index.py |
Файл .tsx - это страница, а файл .py - это обработчик: расширение указывает, что это такое, поэтому обработчик маршрута теряет свою папку /route.
Динамические сегменты и params
export default async function Post({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; return <h1>{slug}</h1>;}import { useParams } from "@nexy/client"; export default function Post() { const { slug } = useParams<{ slug: string }>(); return <h1>{slug}</h1>;}[...slug] и группы маршрутов, такие как (marketing), работают так же, как в Next.js.
generateStaticParams
export async function generateStaticParams() { return [{ slug: "hello" }, { slug: "world" }];}Тот же экспорт под этим именем или getStaticParams дает один предварительно обработанный документ для каждого значения. Nexy вызывает его без await, поэтому верните сам массив:
import { useParams } from "@nexy/client"; export function getStaticParams() { return [{ slug: "hello" }, { slug: "world" }];} export default function Post() { const { slug } = useParams<{ slug: string }>(); return <h1>{slug}</h1>;}layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body>{children}</body> </html> );}Тот же файл, на одну папку выше: корневой макет содержит <html>, а layout.tsx в подпапке находится внутри него.
import type { ReactNode } from "react"; export default function RootLayout({ children }: { children: ReactNode }) { return ( <html lang="en"> <head> <title>My app</title> </head> <body>{children}</body> </html> );}metadata
export const metadata = { title: "Blog", description: "Posts about Nexy" };Страница .tsx не имеет экспорта metadata: запишите теги в корневом макете. В режиме сервера страница .nexy устанавливает их для каждой страницы:
metadata = {"title": "Blog", "description": "Posts about Nexy"}<h1>Blog</h1>Корень layout.nexy записывает теги вместо Next.js в <head>:
from nexy import useMetadata children: prop[NexyElement]<!DOCTYPE html><html> <head>{{ useMetadata().html() }}</head> <body>{{ children | safe }}</body></html>next/link и next/navigation
"use client";import Link from "next/link";import { usePathname, useRouter } from "next/navigation";Те же имена из @nexy/client:
import { Link, usePathname, useRouter } from "@nexy/client"; export function Nav() { const pathname = usePathname(); const router = useRouter(); return ( <nav> <Link href="/">Home</Link> <Link href="/about" activeClassName="active">About</Link> <span>{pathname}</span> <button onClick={() => router.push("/dashboard")}>Dashboard</button> </nav> );}Обработчики маршрутов
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) { const { id } = await params; return Response.json({ id: Number(id) });}Тот же экспорт GET в формате Python без /route:
async def GET(id: int): return {"id": id}Тело - это модель Pydantic, а /docs перечисляет все обработчики:
from pydantic import BaseModel from nexy.http import JSONResponse class NewItem(BaseModel): name: str async def POST(item: NewItem): return JSONResponse({"created": item.name}, status_code=201)Действия сервера и revalidatePath
"use server";import { revalidatePath } from "next/cache"; export async function addTodo(title: string) { await db.todo.create({ data: { title } }); revalidatePath("/todos");}Действие - это функция Python. Nexy генерирует типизированного клиента в @nexy/actions:
from pydantic import BaseModel from nexy.decorators import Action, revalidate class Todo(BaseModel): title: str done: bool = False @Actionasync def add_todo(title: str) -> Todo: revalidate("/todos") return Todo(title=title)import { useAction } from "@nexy/client";import { add_todo } from "@nexy/actions"; export default function Todos() { const { run, pending, error, data } = useAction(add_todo); return ( <button disabled={pending} onClick={() => run({ title: "Write the docs" })}> {error ? error.message : data ? `Added ${data.title}` : "Add"} </button> );}useActionState и useFormStatus отображаются на pending, error и data из useAction.
Компоненты сервера - это .nexy страниц.
export default async function Users() { const users = await db.user.findMany(); return <ul>{users.map((user) => <li key={user.id}>{user.name}</li>)}</ul>;}Серверный компонент Next.js запускается на сервере, считывает данные и отправляет HTML. Страница .nexy - это то же самое в режиме сервера: ее фронтальная часть - это тело асинхронного компонента в Python, а шаблон - это то, что она возвращает.
users = [{"id": 1, "name": "Ada"}, {"id": 2, "name": "Alan"}]<ul> {% for user in users %} <li>{{ user.name }}</li> {% endfor %}</ul>| Next.js серверный компонент | .nexy страница |
|---|---|
app/users/page.tsx, нет "use client" |
src/routes/users.nexy |
await db.user.findMany() в компоненте |
запрос в заголовке |
{params} и {searchParams} |
slug: prop[str] и useSearchParams() |
cookies() и headers() из next/headers |
useCookies() и useHeaders() |
export const revalidate = 60 |
revalidate = 60 |
компонент "use client" внутри него |
остров: <Counter directive="client:load" /> |
В клиентском режиме страница .tsx запускается в браузере, поэтому она считывает данные из обработчика:
async def GET(): return [{"id": 1, "name": "Ada"}, {"id": 2, "name": "Alan"}]import { useEffect, useState } from "react"; type User = { id: number; name: string }; export default function Users() { const [users, setUsers] = useState<User[]>([]); useEffect(() => { fetch("/api/users").then((response) => response.json()).then(setUsers); }, []); return <ul>{users.map((user) => <li key={user.id}>{user.name}</li>)}</ul>;}"use client" компоненты внутри страниц сервера
import Counter from "./counter"; // "use client" export default function Home() { return ( <> <h1>Static heading</h1> <Counter /> </> );}В режиме сервера клиентский компонент представляет собой остров, и его директива гласит, когда он гидратируется:
from "@/components/Counter.tsx" import Counter<h1>Static heading</h1><Counter start=3 directive="client:visible" />import { useState } from "react"; export default function Counter({ start = 0 }: { start?: number }) { const [count, setCount] = useState(start); return <button onClick={() => setCount(count + 1)}>{count}</button>;}См. Острова для client:load, client:idle, client:visible и client:media.
ISR с revalidate
export const revalidate = 60;Та же строка в начале страницы серверного режима:
revalidate = 60<h1>News</h1>middleware.ts
import { NextResponse, type NextRequest } from "next/server"; export function middleware(request: NextRequest) { if (!request.cookies.get("session")) return NextResponse.redirect(new URL("/login", request.url));} export const config = { matcher: "/dashboard/:path*" };matcher - это папка: middleware.py в dashboard/ оборачивает каждый маршрут ниже нее.
from nexy.http import RedirectResponse, Request async def middleware(request: Request, call_next): if "session" not in request.cookies: return RedirectResponse("/login", status_code=307) return await call_next(request)async def GET(): return {"visits": 12}notFound() и not-found.tsx
import { notFound } from "next/navigation"; if (!post) notFound();В обработчике поднимите; на странице режима сервера вызовите notFound():
from nexy.http import HTTPException POSTS = {"hello": "Hello, world"} async def GET(slug: str): if slug not in POSTS: raise HTTPException(status_code=404, detail="No such post") return {"title": POSTS[slug]}app/not-found.tsx - это src/routes/notfound.tsx в режиме клиента и notfound.nexy в режиме сервера.
next/image
import Image from "next/image"; <Image src="/logo.png" alt="Logo" width={128} height={32} />
На странице .nexy компонент Image из nexy:
from nexy import Image<Image src="/logo.png" alt="Logo" width=128 height=32 />Бег
| Next.js | Некси |
|---|---|
npx create-next-app |
nx new, шаблон web-fbr-react |
next dev |
nx dev |
next build && next start |
nx build, затем nx start |
public/ |
public/, служил в / |
| Узел во время выполнения | Python во время выполнения; Node собирает только клиентские пакеты |