DokumentasiKerangka kerja fullstack

Dari Next.js hingga Nexy

Di Nexy, proyek Next.js App Router menyimpan rute file, tata letak, dan komponen React, sementara kode server, penangan rute, dan Tindakan Server dipindahkan ke file Python di sebelahnya.

Setiap bagian di bawah menunjukkan kebiasaan Next.js, lalu hal yang sama di Nexy.

Pilih modenya terlebih dahulu

Aplikasi Next.js menggabungkan komponen server dan klien dalam satu pohon. Proyek Nexy memilih salah satu dari dua bentuk di nexyconfig.py:

Aplikasi Next.js Anda adalah Modus Nexy Halaman adalah
sebagian besar interaktif, navigasi klien mode klien, useCSR = True .tsx file, seperti pada app/
sebagian besar konten, beberapa bagian interaktif mode server, default .nexy halaman dengan React pulau
nexyconfig.py
from nexy.core.models import NexyConfigModelfrom nexy.frontend import react  class NexyConfig(NexyConfigModel):    useFF = react()    useCSR = True

Bagian di bawah ini menggunakan mode klien kecuali dinyatakan lain. Lihat Mode server dan mode klien.

page.tsx

aplikasi/tentang/halaman.tsx
export default function About() {  return <h1>About</h1>;}

Halaman adalah file itu sendiri, bukan page.tsx di dalam folder:

src/rute/tentang.tsx
export default function About() {  return <h1>About</h1>;}

app/page.tsx adalah src/routes/index.tsx, dan app/blog/page.tsx adalah src/routes/blog/index.tsx.

File rute, Router Aplikasi, dan Router Halaman

Kedua router Next.js dipetakan ke src/routes/. File dari Pages Router menyimpan jalurnya; a page.tsx dari App Router menjadi index.tsx, atau file yang diberi nama sesuai foldernya:

Next.js Nexy
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, atau src/routes/about.tsx
app/blog/[slug]/page.tsx src/routes/blog/[slug]/index.tsx, atau 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, atau src/routes/api/items/[id]/index.py

File .tsx adalah halaman dan file .py adalah penangannya: ekstensi menyatakan yang mana, sehingga penangan rute kehilangan folder /route miliknya.

Segmen dinamis dan params

aplikasi/blog/[siput]/halaman.tsx
export default async function Post({ params }: { params: Promise<{ slug: string }> }) {  const { slug } = await params;  return <h1>{slug}</h1>;}
src/rute/blog/[siput].tsx
import { useParams } from "@nexy/client"; export default function Post() {  const { slug } = useParams<{ slug: string }>();  return <h1>{slug}</h1>;}

[...slug] dan grup rute seperti (marketing) berfungsi seperti pada Next.js.

generateStaticParams

aplikasi/blog/[siput]/halaman.tsx
export async function generateStaticParams() {  return [{ slug: "hello" }, { slug: "world" }];}

Ekspor yang sama, dengan nama ini atau getStaticParams, memberikan satu dokumen pra-render per nilai. Nexy memanggilnya tanpa await, jadi kembalikan array itu sendiri:

src/rute/blog/[siput].tsx
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

aplikasi/tata letak.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {  return (    <html lang="en">      <body>{children}</body>    </html>  );}

File yang sama, satu folder ke atas: tata letak root memiliki <html>, dan layout.tsx dalam subfolder bersarang di dalamnya.

src/route/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

aplikasi/blog/halaman.tsx
export const metadata = { title: "Blog", description: "Posts about Nexy" };

Halaman .tsx tidak memiliki ekspor metadata: tulis tag di tata letak root. Dalam mode server, halaman .nexy mengaturnya per halaman:

src/rute/blog.nexy
---metadata = {"title": "Blog", "description": "Posts about Nexy"}---<h1>Blog</h1>

Root layout.nexy menulis tag di mana Next.js akan, di <head>:

src/routes/layout.nexy
---from nexy import useMetadata children: prop[NexyElement]---<!DOCTYPE html><html>  <head>{{ useMetadata().html() }}</head>  <body>{{ children | safe }}</body></html>
app/nav.tsx
"use client";import Link from "next/link";import { usePathname, useRouter } from "next/navigation";

Nama yang sama, dari @nexy/client:

src/komponen/Nav.tsx
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>  );}

Pengendali rute

app/api/items/[id]/route.ts
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {  const { id } = await params;  return Response.json({ id: Number(id) });}

Ekspor GET yang sama, di Python, tanpa /route:

src/routes/api/item/[id].py
async def GET(id: int):    return {"id": id}

Tubuh adalah model Pydantic, dan /docs mencantumkan setiap penangan:

src/routes/api/items/index.py
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)

Tindakan Server dan revalidatePath

aplikasi/tindakan.ts
"use server";import { revalidatePath } from "next/cache"; export async function addTodo(title: string) {  await db.todo.create({ data: { title } });  revalidatePath("/todos");}

Suatu tindakan adalah fungsi Python. Nexy menghasilkan klien yang diketiknya di @nexy/actions:

src/aksi/todos.py
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)
src/rute/todos.tsx
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 dan useFormStatus dipetakan ke pending, error dan data dari useAction.

Komponen server adalah .nexy halaman

aplikasi/pengguna/halaman.tsx
export default async function Users() {  const users = await db.user.findMany();  return <ul>{users.map((user) => <li key={user.id}>{user.name}</li>)}</ul>;}

Komponen server Next.js berjalan di server, membaca data dan mengirimkan HTML. Halaman .nexy sama saja dalam mode server: materi depannya adalah isi komponen async, di Python, dan templatnya adalah apa yang dikembalikannya.

src/rute/orang.nexy
---users = [{"id": 1, "name": "Ada"}, {"id": 2, "name": "Alan"}]---<ul>  {% for user in users %}  <li>{{ user.name }}</li>  {% endfor %}</ul>
Next.js komponen server .nexy halaman
app/users/page.tsx, tidak "use client" src/routes/users.nexy
await db.user.findMany() dalam komponen kueri di frontmatter
{params} dan {searchParams} slug: prop[str] dan useSearchParams()
cookies() dan headers() dari next/headers useCookies() dan useHeaders()
export const revalidate = 60 revalidate = 60
komponen "use client" di dalamnya sebuah pulau: <Counter directive="client:load" />

Dalam mode klien, halaman .tsx berjalan di browser, sehingga membaca data dari pengendali:

src/routes/api/users.py
async def GET():    return [{"id": 1, "name": "Ada"}, {"id": 2, "name": "Alan"}]
src/rute/pengguna.tsx
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" komponen di dalam halaman server

aplikasi/halaman.tsx
import Counter from "./counter"; // "use client" export default function Home() {  return (    <>      <h1>Static heading</h1>      <Counter />    </>  );}

Dalam mode server, komponen klien adalah sebuah pulau, dan arahannya menyatakan kapan terhidrasi:

src/routes/home.nexy
---from "@/components/Counter.tsx" import Counter---<h1>Static heading</h1><Counter start=3 directive="client:visible" />
src/components/Counter.tsx
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>;}

Lihat Pulau untuk client:load, client:idle, client:visible dan client:media.

ISR dengan revalidate

aplikasi/berita/halaman.tsx
export const revalidate = 60;

Baris yang sama, di bagian depan halaman mode server:

src/rute/berita.nexy
---revalidate = 60---<h1>News</h1>

middleware.ts

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 adalah foldernya: middleware.py di dashboard/ membungkus setiap rute di bawahnya.

src/routes/dashboard/middleware.py
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)
src/route/dashboard/stats.py
async def GET():    return {"visits": 12}

notFound() dan not-found.tsx

aplikasi/blog/[siput]/halaman.tsx
import { notFound } from "next/navigation"; if (!post) notFound();

Di pawang, angkat; di halaman mode server, panggil notFound():

src/routes/api/posts/[siput].py
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 adalah src/routes/notfound.tsx dalam mode klien, dan notfound.nexy dalam mode server.

next/image

import Image from "next/image"; <Image src="/logo.png" alt="Logo" width={128} height={32} />

Di halaman .nexy, komponen Image dari nexy:

src/rute/merek.nexy
---from nexy import Image---<Image src="/logo.png" alt="Logo" width=128 height=32 />

Berlari

Next.js Nexy
npx create-next-app nx new, templat web-fbr-react
next dev nx dev
next build && next start nx build, lalu nx start
public/ public/, bertugas di /
Node saat runtime Python pada saat runtime; Node hanya membuat bundel klien

Langkah selanjutnya

Sunting di GitHub

Enter membuka hasil, Esc menutup.