Pradžia / Programavimas / Next.js – pilnas vadovas

Next.js – pilnas vadovas

Kas yra Next.js ir kodėl verta jį naudoti

Jei kada nors bandei sukurti React aplikaciją nuo nulio, tikriausiai žinai tą jausmą, kai reikia sukonfigūruoti webpack, babel, routing, server-side rendering ir dar dešimtis kitų dalykų prieš parašant bent vieną reikšmingą kodo eilutę. Next.js šią problemą išsprendžia elegantiškai – tai React framework’as, kuris ateina su viskuo, ko reikia, jau sukonfigūruotu.

Next.js sukūrė Vercel komanda, ir per pastaruosius kelerius metus jis tapo de facto standartu kuriant produkcinės kokybės React aplikacijas. Versija po versijos jis prideda naujų galimybių, tačiau išlieka pakankamai paprastas, kad pradedantieji galėtų greitai susiorientuoti.

Pagrindiniai dalykai, dėl kurių Next.js išsiskiria:

  • File-based routing – failų struktūra tampa maršrutų struktūra, nereikia konfigūruoti React Router
  • Server-side rendering (SSR) – puslapiai generuojami serveryje, kas pagerina SEO ir pradinį įkrovimo greitį
  • Static site generation (SSG) – puslapiai generuojami build metu, idealiai tinka turiniui, kuris nesikeičia dažnai
  • API routes – galima kurti backend endpoint’us tame pačiame projekte
  • Automatinis code splitting – kiekvienas puslapis įkelia tik tai, ko jam reikia
  • Image optimization – integruotas paveikslėlių optimizavimas be papildomų bibliotekų

Trumpai tariant, Next.js leidžia susifokusuoti į tai, kas svarbu – aplikacijos logiką ir vartotojo patirtį – o ne į infrastruktūros konfigūraciją.

Projekto sukūrimas ir aplinkos paruošimas

Pradėti su Next.js yra stebėtinai paprasta. Viskas, ko reikia – Node.js (rekomenduojama versija 18 ar naujesnė) ir npm arba yarn. Projekto sukūrimas atliekamas viena komanda:

npx create-next-app@latest mano-projektas

Paleidus šią komandą, terminale pasirodys keli klausimai – ar nori naudoti TypeScript, ESLint, Tailwind CSS, App Router ar Pages Router. Jei esi naujokas, rekomenduoju atsakyti taip į TypeScript ir App Router klausimus – tai šiuo metu yra geriausia praktika ir ateities kryptis.

Sukurto projekto struktūra atrodys maždaug taip:

mano-projektas/
├── app/
│   ├── layout.tsx
│   ├── page.tsx
│   └── globals.css
├── public/
├── next.config.js
├── package.json
└── tsconfig.json

Projekto paleidimas vyksta su npm run dev komanda, kuri paleidžia development serverį adresu localhost:3000. Bet kuris failų pakeitimas automatiškai atnaujina naršyklę – tai vadinama Hot Module Replacement.

Vienas praktinis patarimas: iš karto sukonfigūruok .env.local failą aplinkos kintamiesiems. Jame laikyk API raktus, duomenų bazės prisijungimo duomenis ir kitus jautrius duomenis. Šis failas automatiškai ignoruojamas git’o, tad neatsitiktinai nepateks į repozitoriją.

# .env.local
DATABASE_URL=postgresql://localhost:5432/mano_db
NEXT_PUBLIC_API_URL=https://api.example.com
SECRET_KEY=labai_slaptas_raktas

Svarbu žinoti: kintamieji su NEXT_PUBLIC_ prefiksu bus prieinami kliento pusėje (naršyklėje), o be šio prefikso – tik serveryje. Tai svarbi saugumo detalė, kurią dažnai pamiršta pradedantieji.

App Router – nauja maršrutizavimo era

Next.js 13 versijoje buvo pristatytas App Router, kuris iš esmės pakeitė tai, kaip kuriamos Next.js aplikacijos. Jei anksčiau naudojai Pages Router (pages/ direktorija), App Router (app/ direktorija) gali atrodyti keistai, tačiau kai supranti jo logiką, grįžti nenori.

App Router grindžiamas React Server Components koncepcija. Pagal nutylėjimą visi komponentai app/ direktorijoje yra serverio komponentai – jie renderinami serveryje ir siunčiami klientui jau kaip paruoštas HTML. Tai reiškia, kad galima tiesiogiai kreiptis į duomenų bazę komponente be jokio API sluoksnio:

// app/blog/page.tsx
import { db } from '@/lib/db'

export default async function BlogPage() {
  const posts = await db.post.findMany()
  
  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  )
}

Maršrutai kuriami pagal failų struktūrą. Kiekviena direktorija app/ viduje tampa URL segmentu, o page.tsx failas toje direktorijoje – to URL turiniu:

app/
├── page.tsx          → /
├── about/
│   └── page.tsx      → /about
├── blog/
│   ├── page.tsx      → /blog
│   └── [slug]/
│       └── page.tsx  → /blog/koks-nors-straipsnis

Dinaminiai maršrutai žymimi laužtiniais skliaustais – [slug]. Parametras tada prieinamas per params prop’ą:

// app/blog/[slug]/page.tsx
export default async function BlogPost({ 
  params 
}: { 
  params: { slug: string } 
}) {
  const post = await getPostBySlug(params.slug)
  return <article>{post.content}</article>
}

Specialūs failai App Router sistemoje:

  • layout.tsx – išdėstymas, kuris apgaubia visus to lygio ir žemesnio lygio puslapius
  • loading.tsx – rodomas kol kraunasi puslapis (automatiškai naudoja React Suspense)
  • error.tsx – rodomas kai įvyksta klaida
  • not-found.tsx – 404 puslapis
  • template.tsx – panašus į layout, bet iš naujo inicializuojamas kiekvienam puslapiui

Duomenų gavimas – nuo SSR iki ISR

Viena stipriausių Next.js savybių – lankstumas renkantis, kaip ir kada gauti duomenis. Tai ne tik techninis sprendimas, bet ir strateginis – teisingas pasirinkimas gali ženkliai paveikti aplikacijos greitį ir SEO.

Server-side Rendering (SSR) – duomenys gaunami kiekvieno užklausos metu. Tai tinka, kai turinys dažnai keičiasi arba priklauso nuo vartotojo:

// Kiekviena užklausa generuoja naują puslapį
export const dynamic = 'force-dynamic'

export default async function Page() {
  const data = await fetch('https://api.example.com/data', {
    cache: 'no-store' // arba { next: { revalidate: 0 } }
  })
  const json = await data.json()
  return <div>{json.content}</div>
}

Static Site Generation (SSG) – duomenys gaunami build metu, puslapis generuojamas vieną kartą ir tiekiamas iš CDN. Idealiai tinka blogams, dokumentacijai, rinkodaros puslapiams:

// Pagal nutylėjimą Next.js bando statiškai generuoti puslapius
export default async function Page() {
  const data = await fetch('https://api.example.com/data')
  // fetch automatiškai cacheinama build metu
  const json = await data.json()
  return <div>{json.content}</div>
}

Incremental Static Regeneration (ISR) – turbūt labiausiai įdomus variantas. Puslapis generuojamas statiškai, tačiau po nurodyto laiko automatiškai atnaujinamas fone:

export default async function Page() {
  const data = await fetch('https://api.example.com/data', {
    next: { revalidate: 3600 } // atnaujinti kas valandą
  })
  const json = await data.json()
  return <div>{json.content}</div>
}

Praktinis patarimas: naudok ISR su pagrįstu revalidation laiku daugumai puslapių. Blogas straipsnis gali turėti revalidate: 86400 (parą), o produktų katalogas – revalidate: 300 (5 minutes). Tai suteikia statinių puslapių greitį su dinaminių puslapių aktualumu.

Taip pat galima naudoti revalidatePath arba revalidateTag funkcijas, kad priverstinai atnaujintum konkrečius puslapius ar duomenų grupes, pvz., kai vartotojas pakeičia turinį CMS sistemoje:

// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache'
import { NextRequest } from 'next/server'

export async function POST(request: NextRequest) {
  const { path } = await request.json()
  revalidatePath(path)
  return Response.json({ revalidated: true })
}

API Routes ir Server Actions

Next.js leidžia kurti backend funkcionalumą tame pačiame projekte. Tai labai patogu mažesniems projektams ar prototipams, kur nereikia atskiro backend serverio.

API Routes App Router versijoje kuriami route.ts failuose:

// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function GET(request: NextRequest) {
  const users = await db.user.findMany()
  return NextResponse.json(users)
}

export async function POST(request: NextRequest) {
  const body = await request.json()
  const user = await db.user.create({ data: body })
  return NextResponse.json(user, { status: 201 })
}

Tačiau Next.js 13.4+ pristatė dar elegantiškesnį sprendimą – Server Actions. Tai serverio funkcijos, kurias galima kviesti tiesiogiai iš kliento komponentų, be jokio API sluoksnio. Ypač naudinga formoms:

// app/contact/page.tsx
async function submitForm(formData: FormData) {
  'use server'
  
  const name = formData.get('name') as string
  const email = formData.get('email') as string
  
  await db.contact.create({
    data: { name, email }
  })
  
  revalidatePath('/contacts')
}

export default function ContactPage() {
  return (
    <form action={submitForm}>
      <input name="name" type="text" />
      <input name="email" type="email" />
      <button type="submit">Siųsti</button>
    </form>
  )
}

Server Actions veikia net be JavaScript kliento pusėje – forma veiks ir su išjungtu JS. Tai progresyvaus pagerinimo principas praktikoje.

Keli svarbūs dalykai apie API Routes saugumą:

  • Visada validuok įvesties duomenis – naudok Zod ar panašią biblioteką
  • Tikrink autentifikaciją kiekviename endpoint’e, net jei atrodo, kad jis „saugus”
  • Negrąžink daugiau duomenų nei reikia – filtruok jautrius laukus
  • Naudok rate limiting, ypač viešiems endpoint’ams

Optimizavimas ir našumas

Next.js turi daug integruotų optimizavimo priemonių, tačiau jas reikia mokėti naudoti teisingai. Pradėkime nuo paveikslėlių, nes tai viena dažniausių našumo problemų.

Vietoj įprasto <img> tago naudok Next.js Image komponentą:

import Image from 'next/image'

export default function Hero() {
  return (
    <Image
      src="/hero.jpg"
      alt="Pagrindinis paveikslėlis"
      width={1200}
      height={600}
      priority // svarbu pirmam ekrane matomam paveikslėliui
      placeholder="blur"
      blurDataURL="data:image/jpeg;base64,..."
    />
  )
}

Image komponentas automatiškai konvertuoja paveikslėlius į WebP formatą, generuoja skirtingus dydžius skirtingiems ekranams ir naudoja lazy loading pagal nutylėjimą. priority prop’as svarbus pirmam ekrane matomam paveikslėliui – jis bus įkeltas iš anksto ir pagerinsi Largest Contentful Paint (LCP) metriką.

Šriftams naudok next/font:

// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
})

export default function RootLayout({ children }) {
  return (
    <html lang="lt" className={inter.className}>
      <body>{children}</body>
    </html>
  )
}

Šriftai bus atsisiųsti build metu ir tiekiami iš to paties domeno – tai eliminuoja išorinius užklausimus ir pagerina privatumą bei greitį.

Klientiniai komponentai ('use client') turėtų būti kuo mažesni ir kuo žemiau komponentų medyje. Dažna klaida – pažymėti visą puslapį kaip kliento komponentą vien dėl to, kad vienas mažas interaktyvus elementas reikalauja useState. Vietoj to, išskirkite tą elementą į atskirą komponentą:

// Blogai:
'use client'
export default function BlogPost({ post }) {
  const [liked, setLiked] = useState(false)
  return (
    <article>
      <h1>{post.title}</h1>
      {/* daug statinio turinio... */}
      <button onClick={() => setLiked(!liked)}>
        {liked ? '❤️' : '🤍'}
      </button>
    </article>
  )
}

// Gerai:
// LikeButton.tsx
'use client'
export function LikeButton() {
  const [liked, setLiked] = useState(false)
  return (
    <button onClick={() => setLiked(!liked)}>
      {liked ? '❤️' : '🤍'}
    </button>
  )
}

// BlogPost.tsx (serverio komponentas)
export default function BlogPost({ post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      {/* statinis turinys */}
      <LikeButton />
    </article>
  )
}

Autentifikacija su NextAuth.js

Autentifikacija yra vienas tų dalykų, kurį galima implementuoti pačiam, bet geriau naudoti patikrintą sprendimą. NextAuth.js (dabar žinomas kaip Auth.js) yra de facto standartas Next.js autentifikacijai.

Instaliavimas ir bazinė konfigūracija:

npm install next-auth
// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth'
import GitHub from 'next-auth/providers/github'
import Google from 'next-auth/providers/google'
import Credentials from 'next-auth/providers/credentials'

const handler = NextAuth({
  providers: [
    GitHub({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!,
    }),
    Google({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    Credentials({
      credentials: {
        email: { label: "El. paštas", type: "email" },
        password: { label: "Slaptažodis", type: "password" }
      },
      async authorize(credentials) {
        const user = await getUserByEmail(credentials?.email)
        if (!user) return null
        
        const isValid = await bcrypt.compare(
          credentials?.password!, 
          user.password
        )
        return isValid ? user : null
      }
    })
  ],
  callbacks: {
    async session({ session, token }) {
      session.user.id = token.sub!
      return session
    }
  }
})

export { handler as GET, handler as POST }

Sesijos tikrinimas serverio komponentuose:

import { getServerSession } from 'next-auth'
import { authOptions } from '@/app/api/auth/[...nextauth]/route'

export default async function ProtectedPage() {
  const session = await getServerSession(authOptions)
  
  if (!session) {
    redirect('/login')
  }
  
  return <div>Sveiki, {session.user.name}!</div>
}

Middleware leidžia apsaugoti visus maršrutus vienu metu, nereikia tikrinti kiekviename puslapyje atskirai:

// middleware.ts (projekto šaknyje)
export { default } from 'next-auth/middleware'

export const config = {
  matcher: ['/dashboard/:path*', '/profile/:path*', '/admin/:path*']
}

Kai viskas susideda į vieną – nuo kodo iki produkcijos

Next.js projekto deploy’inimas yra vienas malonesnių dalykų lyginant su kitais framework’ais. Vercel platforma, kurią sukūrė ta pati komanda, tiesiog veikia – push’ini į GitHub ir per kelias minutes aplikacija yra online su HTTPS, CDN ir automatiniais preview deploy’ais kiekvienai pull request’ui.

Tačiau Vercel nėra vienintelis pasirinkimas. Next.js galima deploy’inti ant bet kurio Node.js serverio:

npm run build  # sukuria optimizuotą build'ą
npm run start  # paleidžia produkcinį serverį

Docker konteinerizavimui rekomenduojamas multi-stage build:

FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

EXPOSE 3000
CMD ["node", "server.js"]

Kad šis Dockerfile veiktų, next.config.js reikia pridėti:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
}

module.exports = nextConfig

Prieš deploy’inant į produkciją, verta patikrinti keletą dalykų. Paleisk npm run build lokaliai ir atkreipk dėmesį į bundle dydžius – jei koks nors puslapis yra per didelis, gali reikėti optimizuoti importus ar naudoti dynamic imports. Naudok Next.js integruotą analytics (@next/bundle-analyzer), kad pamatytum, kas sudaro bundle’ą.

Monitoringui produkcinėje aplinkoje verta integruoti Sentry klaidų sekimui ir OpenTelemetry metrikoms. Next.js turi integruotą OpenTelemetry palaikymą nuo 13.4 versijos – tereikia sukonfigūruoti instrumentation.ts failą.

Galiausiai – testuok. Next.js puikiai veikia su Playwright end-to-end testams ir Vitest unit testams. Bent keletas kritinių vartotojo kelių turėtų būti padengti automatiniais testais, nes Next.js versijų atnaujinimai kartais atneša netikėtų pokyčių.

Next.js nėra sidabrinė kulka – tai įrankis, kuris puikiai tinka daugeliui atvejų, bet ne visiems. Jei kuri paprastą single-page aplikaciją be SEO reikalavimų, galbūt pakaks paprasto Vite + React. Jei kuri labai sudėtingą real-time aplikaciją, gali prireikti papildomų sprendimų. Tačiau e-komercijos svetainėms, blogams, SaaS produktams, korporatyviniams puslapiams – Next.js yra vienas geriausių pasirinkimų šiandien, ir ateityje turbūt tik gerės.