برنامه نویسی

ایجاد یک برنامه SSR در Next.js 14

مقدمه

سلام! در این مقاله، نحوه ایجاد یک برنامه وب ساده با رندر سمت سرور (SSR) و چرایی مفید بودن آن را بررسی خواهیم کرد.

رندر سمت سرور (SSR) به ما این امکان را می دهد که صفحات را سریعتر به کاربران تحویل دهیم و معیارهای عملکرد و تجربه کاربر را بهبود ببخشیم. این امر به ویژه زمانی مفید است که کاربران گوشی های هوشمند ارزان قیمت داشته باشند یا اتصالات اینترنت کندی داشته باشند، زیرا تمام درخواست ها و محاسبات سنگین روی سرور انجام می شود.

برای SSR، ما از Next.js 14 استفاده خواهیم کرد. Next.js یک فریم ورک محبوب است که بسیاری از ویژگی‌های مفید را ارائه می‌کند:

  • رندر سمت سرور و استاتیک
  • بهینه سازی کد خودکار
  • پشتیبانی از TypeScript داخلی
  • مسیریابی هوشمند
  • مسیرهای API برای ساخت یک باطن
  • نسخه 14+ قابلیت‌های جدیدی از جمله عملکرد بهبودیافته، روتر جدید مبتنی بر سیستم فایل و پشتیبانی بهبود یافته از اجزای سرور را معرفی می‌کند.

ایده پروژه
به عنوان مثال، ما یک MVP یک صرافی مالی با عملکرد اولیه ایجاد خواهیم کرد. در اینجا برنامه ما قادر به انجام آن است:

  • نمایش کاتالوگ سهام با صفحه بندی
  • سهام اضافی را از طریق دکمه “نمایش بیشتر” بارگیری کنید
  • اطلاعات دقیق در مورد هر سهم را در یک صفحه جداگانه ارائه دهید
  • نمایش آخرین قیمت و درصد تغییر برای هر سهم
  • نمایش نمودار تغییر قیمت برای هر سهم
  • از نظر سئو بهینه باشید
  • برای عملکرد بهتر از سرور ارائه شود

https://www.youtube.com/watch?v=Ic7q3ySAt78

پیاده سازی

در Next.js مفهومی از اجزای سرور و کلاینت وجود دارد. بیایید تفاوت های آنها را بررسی کنیم:

اجزای سرور در سرور پردازش می شوند. آنها درخواست‌های API را انجام می‌دهند، داده‌ها را واکشی می‌کنند و نشانه‌گذاری HTML را تولید می‌کنند که مرورگر آن را ارائه می‌کند. مزایا عبارتند از:

  • کاهش جاوا اسکریپت ارسال شده به مشتری
  • بارگذاری صفحه اولیه سریعتر
  • دسترسی مستقیم به منابع سرور (به عنوان مثال، پایگاه داده)
  • بهبود بهینه سازی سئو

اجزای مشتری مانند اجزای معمولی React در مرورگر عمل می کند. آنها می توانند:

  • تعامل با API های مرورگر
  • رویدادهای کاربر را مدیریت کنید
  • ایالت محلی را مدیریت کنید
  • از قلاب های چرخه حیات React استفاده کنید

در اینجا نحوه تقسیم اجزای برنامه خود آمده است:

طرح بندی

اجزای سرور (قرمز):

  1. سربرگ: ایستا، نیازی به تعامل ندارد
  2. لیست سهام: داده ها روی سرور بارگذاری می شوند
  3. صفحه بندی: منطق برای سئو روی سرور انجام می شود
  4. جزئیات موجودی: داده ها از سرور واکشی شده است

اجزای مشتری (سبز):

  1. دکمه “بارگیری بیشتر”: به کنترل کلیک و حفظ موقعیت اسکرول نیاز دارد
  2. نمودار تغییر قیمت: تعاملی، از APIهای مرورگر برای رندر استفاده می کند
  3. این جداسازی به ما اجازه می دهد تا از نقاط قوت هر دو مؤلفه سرور و مشتری به طور مؤثر استفاده کنیم.

کار با API

برای مدیریت داده های بازار سهام، از یک API عمومی استفاده می کنیم، اما یک BFF (Backend for Frontend) برای آن ایجاد می کنیم. BFF به ما این امکان را می دهد که:

  • فقط داده های لازم برای برنامه ما را بازیابی کنید
  • داده ها را به فرمتی مناسب برای قسمت جلویی تبدیل کنید
  • داده ها را از چندین منبع ترکیب کنید
  • اطلاعات مکرر درخواستی را در حافظه پنهان نگه دارید

این رویکرد مدیریت داده ها را بهینه می کند و عملکرد را بهبود می بخشد.

اجزای سرور

در اینجا مثالی از یک جزء سرور آورده شده است: صفحه اطلاعات سهام.

import React from 'react';
import { Metadata } from 'next';
import { BASE_URL } from '@/config';
import { serviceStocks } from '@/services';
import { StockIntro } from '@/components/StockIntro';
import { BuyStock } from '@/components/BuyStock';
import Candles from '@/components/Candles/Candles';
import styles from './styles.module.scss';

type Props = {
  params: { slug: string };
};

export async function generateMetadata({ params }: Props): PromiseMetadata> {
  const { data: dataIntro } =
    (await serviceStocks.getByTicker(params.slug)) || {};

  if (!dataIntro) {
    return {
      title: 'Stock not found',
    };
  }

  const metaDescription = `Information about stock ${dataIntro.name} (${dataIntro.ticker}). Sector: ${dataIntro.sector}, Country: ${dataIntro.countryOfRisk}`;
  const keywords = `${dataIntro.name}, ${dataIntro.ticker}, ${dataIntro.sector}, ${dataIntro.countryOfRiskName}, stock, invest`;
  const canonicalUrl = `${BASE_URL}/${params.slug}`;

  return {
    title: `${dataIntro.name} (${dataIntro.ticker}) - Information about stock`,
    description: metaDescription,
    keywords: keywords,
    openGraph: {
      title: `${dataIntro.name} (${dataIntro.ticker}) - Information about stock`,
      description: metaDescription,
      type: 'website',
    },
    alternates: {
      canonical: canonicalUrl,
    },
  };
}

const PageStock = async ({ params }: Props) => {
  try {
    const [stockData, lastPriceData, candlesData] = await Promise.all([
      serviceStocks.getByTicker(params.slug),
      serviceStocks.getLastPriceByTicker(params.slug),
      serviceStocks.getCandlesByTicker(params.slug),
    ]);

    return (
      div className={styles.page}>
        div className={styles.main}>
          div className={styles.intro}>
            {stockData ? (
              StockIntro data={stockData.data} />
            ) : (
              span>Not data/span>
            )}
          /div>
          {candlesData ? (
            Candles
              currency={stockData?.data?.currency}
              data={candlesData.data}
            />
          ) : (
            span>Not data/span>
          )}
        /div>
        div className={styles.side}>
          {lastPriceData && candlesData ? (
            BuyStock
              candlesData={candlesData.data}
              currency={stockData?.data?.currency}
              data={lastPriceData.data}
            />
          ) : (
            span>Not data/span>
          )}
        /div>
      /div>
    );
  } catch (error) {
    console.error('Error loading stock data:', error);
    return div>Error loading stock data/div>;
  }
};

export default PageStock;
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اجزای مشتری

در اینجا یک مثال از یک جزء مشتری برای نمودار تغییر قیمت آورده شده است:

'use client';

import React from 'react';
import dynamic from 'next/dynamic';
import styles from './styles.module.scss';
const Chart = dynamic(() => import('./Chart/Chart'), { ssr: false });

const Candles = ({ data, currency }: { data: any; currency?: string }) => {
  return (
    div className={styles.chart}>
      Chart data={data} currency={currency} />
    /div>
  );
};

export default Candles;
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

import React, { FC } from 'react';
// @ts-ignore
import CanvasJSReact from '@canvasjs/react-charts';
import { ICandle } from '@/typing';
const CanvasJSChart = CanvasJSReact.CanvasJSChart;

interface ChartProps {
  data: {
    candles: ICandle[];
  };
  currency?: string;
}

const Chart: FCChartProps> = ({ data, currency }) => {
  const dataPoints = data.candles.map((candle) => ({
    x: new Date(candle.time),
    y: [
      parseFloat(`${candle.open.units}.${candle.open.nano}`),
      parseFloat(`${candle.high.units}.${candle.high.nano}`),
      parseFloat(`${candle.low.units}.${candle.low.nano}`),
      parseFloat(`${candle.close.units}.${candle.close.nano}`),
    ],
  }));

  const options = {
    theme: 'light2',
    axisX: {
      valueFormatString: 'DD MMM',
    },
    axisY: {
      prefix: currency,
    },
    data: [
      {
        type: 'candlestick',
        yValueFormatString: `${currency} ###0.00`,
        xValueFormatString: 'DD MMM YYYY',
        dataPoints: dataPoints,
      },
    ],
  };

  return CanvasJSChart options={options} />;
};

export default Chart;
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

برای مشاهده دقیق تر کد، می توانید به مخزن GitHub مراجعه کنید.

بهینه سازی سئو

import { Open_Sans } from 'next/font/google';
import { BASE_URL } from '@/config';
import { Footer } from '@/components/Footer/Footer';
import { Header } from '@/components/Header';
import { Metadata } from 'next';
import type { Viewport } from 'next';
import styles from './layout.module.scss';
import './globals.css';

const roboto = Open_Sans({
  weight: '400',
  subsets: ['latin'],
});

const RootLayout: React.FC{ children: React.ReactNode }> = ({ children }) => {
  return (
    html lang="en" className={roboto.className}>
      body className={styles.layout}>
        header className={styles.header}>
          Header />
        /header>
        main className={styles.main}>
          div className={styles.content}>{children}/div>
        /main>
        footer className={styles.footer}>
          Footer />
        /footer>
      /body>
    /html>
  );
};

export default RootLayout;

export const viewport: Viewport = {
  colorScheme: 'light dark',
  themeColor: '#2196f3',
};

export const metadata: Metadata = {
  title: {
    default: 'Financial Exchange Platform',
    template: '%s | Financial Exchange Platform',
  },
  description:
    'A leading financial exchange platform for trading and investment.',
  applicationName: 'Financial Exchange Platform',
  authors: [
    {
      name: 'Financial Exchange Team',
      url: BASE_URL,
    },
  ],
  metadataBase: new URL(BASE_URL),
  generator: 'Next.js',
  keywords: ['financial', 'exchange', 'trading', 'investment', 'platform'],
  referrer: 'origin',
  creator: 'Financial Exchange Team',
  publisher: 'Financial Exchange Inc.',
  robots: { index: true, follow: true },
  alternates: {
    canonical: BASE_URL,
  },
  icons: {
    icon: '/favicon.ico',
    apple: '/apple-touch-icon.png',
  },
  manifest: '/manifest.webmanifest',
  openGraph: {
    title: 'Financial Exchange Platform',
    description:
      'A leading financial exchange platform for trading and investment.',
    type: 'website',
    url: BASE_URL,
    siteName: 'Financial Exchange Platform',
    images: [
      {
        url: '/images/og-image.jpg',
        width: 800,
        height: 600,
        alt: 'Financial Exchange Platform',
      },
    ],
  },
  appleWebApp: {
    capable: true,
    title: 'Financial Exchange Platform',
    statusBarStyle: 'black-translucent',
  },
  formatDetection: {
    telephone: false,
  },
  abstract: 'A leading financial exchange platform for trading and investment.',
  category: 'Finance',
  classification: 'Financial Services',
  other: {
    'msapplication-TileColor': '#0a74da',
  },
};

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

استقرار برنامه

برای خودکار کردن استقرار برنامه ما، از GitHub Actions و Docker Hub استفاده می کنیم. این تنظیمات به ما این امکان را می دهد که با هر بار فشار دادن به شاخه اصلی مخزن، تصاویر Docker را به صورت خودکار بسازیم و منتشر کنیم.

name: Docker

on:
  push:
    branches: [ main ]

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1

      - name: Login to Docker Hub
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push Docker image
        uses: docker/build-push-action@v2
        with:
          push: true
          tags: u4aew/next:latest
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

FROM node:18-alpine

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

RUN npm run build

EXPOSE 6060

CMD ["npm", "start"]
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

حداقل عملکرد اولیه برنامه ما آماده است.

محیط تست

در آینده، برای افزایش عملکرد، ما اضافه خواهیم کرد:

  • WebSocket برای به روز رسانی قیمت در زمان واقعی
  • فیلترها و مرتب سازی برای کاتالوگ سهام
  • احراز هویت کاربر
  • داشبورد شخصی برای ردیابی سهام انتخاب شده

از توجه شما متشکرم و امیدوارم این مقاله مفید بوده باشد!

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا