الانتقال من Vite

سيساعدك هذا الدليل في نقل تطبيق Vite موجود إلى Next.js.

لماذا التبديل؟

هناك عدة أسباب قد تدفعك للتبديل من Vite إلى Next.js:

بطء وقت تحميل الصفحة الأولي

إذا قمت ببناء تطبيقك باستخدام إضافة Vite الافتراضية لـ React، فإن تطبيقك هو تطبيق يعمل فقط على جانب العميل. التطبيقات التي تعمل فقط على جانب العميل، والمعروفة أيضًا باسم تطبيقات الصفحة الواحدة (SPAs)، غالبًا ما تعاني من بطء في وقت تحميل الصفحة الأولي. يحدث هذا بسبب عدة أسباب:

  1. يحتاج المتصفح إلى انتظار تحميل وتشغيل كود React وحزمة التطبيق بالكامل قبل أن يتمكن الكود من إرسال طلبات لتحميل بعض البيانات.
  2. ينمو كود التطبيق مع كل ميزة جديدة وإضافة تبعية إضافية.

عدم وجود تقسيم تلقائي للكود

يمكن إدارة مشكلة بطء أوقات التحميل السابقة إلى حد ما باستخدام تقسيم الكود. ومع ذلك، إذا حاولت تقسيم الكود يدويًا، فغالبًا ما ستجعل الأداء أسوأ. من السهل إدخال شلالات شبكة عن غير قصد عند تقسيم الكود يدويًا. يوفر Next.js تقسيمًا تلقائيًا للكود مدمجًا في جهاز التوجيه الخاص به.

شلالات الشبكة

يحدث سبب شائع لضعف الأداء عندما تقوم التطبيقات بعمل طلبات متتابعة بين العميل والخادم لجلب البيانات. أحد الأنماط الشائعة لجلب البيانات في تطبيق SPA هو عرض عنصر نائب في البداية، ثم جلب البيانات بعد تحميل المكون. لسوء الحظ، هذا يعني أن المكون الفرعي الذي يجلب البيانات لا يمكنه البدء في الجلب حتى ينتهي المكون الأصلي من تحميل بياناته الخاصة.

بينما يتم دعم جلب البيانات على العميل باستخدام Next.js، فإنه يمنحك أيضًا خيار نقل جلب البيانات إلى الخادم، مما يمكنه القضاء على شلالات العميل-الخادم.

حالات تحميل سريعة ومقصودة

مع الدعم المدمج للبث عبر React Suspense، يمكنك أن تكون أكثر تحديدًا بشأن الأجزاء التي تريد تحميلها أولاً وبأي ترتيب دون إدخال شلالات شبكة.

هذا يتيح لك بناء صفحات يتم تحميلها بشكل أسرع والقضاء على تحولات التخطيط.

اختر استراتيجية جلب البيانات

اعتمادًا على احتياجاتك، يسمح لك Next.js باختيار استراتيجية جلب البيانات على أساس كل صفحة ومكون. يمكنك أن تقرر الجلب في وقت البناء، أو في وقت الطلب على الخادم، أو على العميل. على سبيل المثال، يمكنك جلب البيانات من نظام إدارة المحتوى الخاص بك وعرض مشاركات المدونة الخاصة بك في وقت البناء، والتي يمكن بعد ذلك تخزينها مؤقتًا بكفاءة على CDN.

Middleware

Next.js Middleware يسمح لك بتشغيل الكود على الخادم قبل اكتمال الطلب. هذا مفيد بشكل خاص لتجنب ظهور محتوى غير مصادق عليه عندما يزور المستخدم صفحة تتطلب مصادقة عن طريق توجيه المستخدم إلى صفحة تسجيل الدخول. يكون Middleware مفيدًا أيضًا للتجربة والتدويل.

تحسينات مدمجة

غالبًا ما يكون لـ الصور، والخطوط، ونصوص الطرف الثالث تأثير كبير على أداء التطبيق. يأتي Next.js مع مكونات مدمجة تقوم بتحسينها تلقائيًا لك.

خطوات النقل

هدفنا من هذا النقل هو الحصول على تطبيق Next.js يعمل في أسرع وقت ممكن، بحيث يمكنك بعد ذلك تبني ميزات Next.js تدريجيًا. في البداية، سنحافظ عليه كتطبيق يعمل فقط على جانب العميل (SPA) دون نقل جهاز التوجيه الحالي الخاص بك. هذا يساعد في تقليل فرص مواجهة مشكلات أثناء عملية النقل ويقلل من تعارضات الدمج.

الخطوة 1: تثبيت تبعية Next.js

أول شيء تحتاج إلى فعله هو تثبيت next كتبعية:

Terminal
npm install next@latest

الخطوة 2: إنشاء ملف تكوين Next.js

قم بإنشاء ملف next.config.mjs في جذر مشروعك. سيحتوي هذا الملف على خيارات تكوين Next.js.

next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export', // يخرج تطبيق صفحة واحدة (SPA).
  distDir: './dist', // يغير دليل إخراج البناء إلى `./dist/`.
}

export default nextConfig

جيد أن تعرف: يمكنك استخدام إما .js أو .mjs لملف تكوين Next.js الخاص بك.

الخطوة 3: تحديث تكوين TypeScript

إذا كنت تستخدم TypeScript، فأنت بحاجة إلى تحديث ملف tsconfig.json الخاص بك بالتغييرات التالية لجعله متوافقًا مع Next.js. إذا كنت لا تستخدم TypeScript، يمكنك تخطي هذه الخطوة.

  1. أزل مرجع المشروع إلى tsconfig.node.json
  2. أضف ./dist/types/**/*.ts و ./next-env.d.ts إلى مصفوفة include
  3. أضف ./node_modules إلى مصفوفة exclude
  4. أضف { "name": "next" } إلى مصفوفة plugins في compilerOptions: "plugins": [{ "name": "next" }]
  5. اضبط esModuleInterop على true: "esModuleInterop": true
  6. اضبط jsx على preserve: "jsx": "preserve"
  7. اضبط allowJs على true: "allowJs": true
  8. اضبط forceConsistentCasingInFileNames على true: "forceConsistentCasingInFileNames": true
  9. اضبط incremental على true: "incremental": true

إليك مثال على ملف tsconfig.json يعمل مع هذه التغييرات:

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "allowJs": true,
    "forceConsistentCasingInFileNames": true,
    "incremental": true,
    "plugins": [{ "name": "next" }]
  },
  "include": ["./src", "./dist/types/**/*.ts", "./next-env.d.ts"],
  "exclude": ["./node_modules"]
}

يمكنك العثور على مزيد من المعلومات حول تكوين TypeScript في وثائق Next.js.

الخطوة 4: إنشاء تخطيط الجذر

يجب أن يتضمن تطبيق جهاز توجيه التطبيق في Next.js ملف تخطيط جذر، وهو مكون خادم React سيلف كل الصفحات في تطبيقك. يتم تعريف هذا الملف في المستوى الأعلى من دليل app.

أقرب ما يعادل ملف تخطيط الجذر في تطبيق Vite هو ملف index.html، الذي يحتوي على علامات <html> و <head> و <body> الخاصة بك.

في هذه الخطوة، ستحول ملف index.html إلى ملف تخطيط جذر:

  1. قم بإنشاء دليل app جديد في دليل src الخاص بك.
  2. قم بإنشاء ملف layout.tsx جديد داخل دليل app:
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return null
}
export default function RootLayout({ children }) {
  return null
}

جيد أن تعرف: يمكن استخدام امتدادات .js أو .jsx أو .tsx لملفات التخطيط.

  1. انسخ محتوى ملف index.html الخاص بك إلى مكون <RootLayout> الذي تم إنشاؤه مسبقًا مع استبدال علامات body.div#root و body.script بـ <div id="root">{children}</div>:
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <link rel="icon" type="image/svg+xml" href="/icon.svg" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <link rel="icon" type="image/svg+xml" href="/icon.svg" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
  1. يتضمن Next.js بالفعل بشكل افتراضي علامات meta charset و meta viewport، لذا يمكنك إزالتها بأمان من <head> الخاص بك:
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <link rel="icon" type="image/svg+xml" href="/icon.svg" />
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <link rel="icon" type="image/svg+xml" href="/icon.svg" />
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
  1. يتم إضافة أي ملفات بيانات وصفية مثل favicon.ico أو icon.png أو robots.txt تلقائيًا إلى علامة <head> للتطبيق طالما قمت بوضعها في المستوى الأعلى من دليل app. بعد نقل جميع الملفات المدعومة إلى دليل app، يمكنك حذف علامات <link> الخاصة بها بأمان:
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
  1. أخيرًا، يمكن لـ Next.js إدارة علامات <head> الأخيرة الخاصة بك باستخدام واجهة برمجة تطبيقات البيانات الوصفية. انقل معلومات البيانات الوصفية النهائية الخاصة بك إلى كائن metadata مُصدَّر:
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'My App',
  description: 'My App is a...',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
export const metadata = {
  title: 'My App',
  description: 'My App is a...',
}

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}

مع التغييرات المذكورة أعلاه، انتقلت من الإعلان عن كل شيء في index.html الخاص بك إلى استخدام النهج القائم على الاتفاقيات في Next.js المدمج في الإطار (واجهة برمجة تطبيقات البيانات الوصفية). يتيح لك هذا النهج تحسين تحسين محركات البحث وقابلية مشاركة صفحاتك على الويب بسهولة أكبر.

الخطوة 5: إنشاء صفحة نقطة الدخول

في Next.js، يمكنك تحديد نقطة دخول لتطبيقك عن طريق إنشاء ملف page.tsx. أقرب ما يعادله في Vite هو ملف main.tsx. في هذه الخطوة، ستقوم بإعداد نقطة الدخول لتطبيقك.

  1. قم بإنشاء دليل [[...slug]] داخل دليل app الخاص بك.

نظرًا لأننا في هذا الدليل نهدف أولاً إلى إعداد Next.js كتطبيق صفحة واحدة (SPA)، تحتاج إلى أن تكون نقطة دخول الصفحة قادرة على التقاط جميع المسارات المحتملة لتطبيقك. لهذا، قم بإنشاء دليل جديد [[...slug]] داخل دليل app.

هذا الدليل هو ما يسمى بقطاع مسار اختياري شامل. يستخدم Next.js جهاز توجيه يعتمد على نظام الملفات حيث تستخدم الأدلة لتحديد المسارات. سيضمن هذا الدليل الخاص أن جميع مسارات تطبيقك سيتم توجيهها إلى ملف page.tsx الموجود بداخله.

  1. قم بإنشاء ملف جديد page.tsx داخل دليل app/[[...slug]] بالمحتوى التالي:
import '../../index.css'

export function generateStaticParams() {
  return [{ slug: [''] }]
}

export default function Page() {
  return '...' // سنقوم بتحديث هذا لاحقًا
}
import '../../index.css'

export function generateStaticParams() {
  return [{ slug: [''] }]
}

export default function Page() {
  return '...' // سنقوم بتحديث هذا لاحقًا
}

معلومة مفيدة: يمكن استخدام امتدادات .js أو .jsx أو .tsx لملفات الصفحات.

هذا الملف هو مكون خادم (Server Component). عند تشغيل next build، يتم تقديم الملف مسبقًا إلى أصل ثابت. لا يتطلب أي كود ديناميكي.

يستورد هذا الملف CSS العام الخاص بنا ويخبر generateStaticParams أننا سننشئ مسارًا واحدًا فقط، وهو مسار الفهرس /.

الآن، لننقل بقية تطبيق Vite الذي سيعمل على جانب العميل فقط.

'use client'

import React from 'react'
import dynamic from 'next/dynamic'

const App = dynamic(() => import('../../App'), { ssr: false })

export function ClientOnly() {
  return <App />
}
'use client'

import React from 'react'
import dynamic from 'next/dynamic'

const App = dynamic(() => import('../../App'), { ssr: false })

export function ClientOnly() {
  return <App />
}

هذا الملف هو مكون عميل (Client Component)، يتم تعريفه باستخدام التوجيه 'use client'. لا يزال يتم تقديم مكونات العميل مسبقًا إلى HTML على الخادم قبل إرسالها إلى العميل.

نظرًا لأننا نريد بدء تطبيق يعمل على جانب العميل فقط، يمكننا تكوين Next.js لتعطيل التقديم المسبق من مكون App فما دون.

const App = dynamic(() => import('../../App'), { ssr: false })

الآن، قم بتحديث صفحة نقطة الدخول لاستخدام المكون الجديد:

import '../../index.css'
import { ClientOnly } from './client'

export function generateStaticParams() {
  return [{ slug: [''] }]
}

export default function Page() {
  return <ClientOnly />
}
import '../../index.css'
import { ClientOnly } from './client'

export function generateStaticParams() {
  return [{ slug: [''] }]
}

export default function Page() {
  return <ClientOnly />
}

الخطوة 6: تحديث استيراد الصور الثابتة

يتعامل Next.js مع استيراد الصور الثابتة بشكل مختلف قليلاً عن Vite. مع Vite، يؤدي استيراد ملف صورة إلى إرجاع عنوان URL العام كسلسلة نصية:

App.tsx
import image from './img.png' // `image` ستكون '/assets/img.2d8efhg.png' في الإنتاج

export default function App() {
  return <img src={image} />
}

مع Next.js، يُرجع استيراد الصور الثابتة كائنًا. يمكن بعد ذلك استخدام هذا الكائن مباشرة مع مكون Next.js <Image>، أو يمكنك استخدام خاصية src للكائن مع وسم <img> الحالي.

يتمتع مكون <Image> بمزايا إضافية مثل تحسين الصور تلقائيًا. يضبط مكون <Image> تلقائيًا سمات width و height لوسم <img> الناتج بناءً على أبعاد الصورة. هذا يمنع حدوث تغييرات في التخطيط عند تحميل الصورة. ومع ذلك، يمكن أن يسبب هذا مشاكل إذا كان تطبيقك يحتوي على صور بأبعاد غير متساوية دون ضبط الآخر على auto. عندما لا يتم ضبطه على auto، سيعتمد البعد على قيمة سمة البعد لوسم <img>، مما قد يجعل الصورة تظهر مشوهة.

سيؤدي الاحتفاظ بوسم <img> إلى تقليل كمية التغييرات في تطبيقك ومنع المشاكل المذكورة أعلاه. يمكنك بعد ذلك الانتقال لاحقًا إلى مكون <Image> للاستفادة من تحسين الصور عن طريق تكوين محمل (loader)، أو الانتقال إلى خادم Next.js الافتراضي الذي يحتوي على تحسين تلقائي للصور.

  1. قم بتحويل مسارات الاستيراد المطلقة للصور المستوردة من /public إلى استيراد نسبي:
// قبل
import logo from '/logo.png'

// بعد
import logo from '../public/logo.png'
  1. قم بتمرير خاصية src للصورة بدلاً من كائن الصورة بالكامل إلى وسم <img>:
// قبل
<img src={logo} />

// بعد
<img src={logo.src} />

بدلاً من ذلك، يمكنك الرجوع إلى عنوان URL العام لأصل الصورة بناءً على اسم الملف. على سبيل المثال، سيتم تقديم الصورة public/logo.png على /logo.png لتطبيقك، والذي سيكون قيمة src.

تحذير: إذا كنت تستخدم TypeScript، فقد تواجه أخطاء نوع عند الوصول إلى خاصية src. يمكنك تجاهلها بأمان الآن. سيتم إصلاحها بنهاية هذا الدليل.

الخطوة 7: ترحيل متغيرات البيئة

يدعم Next.js متغيرات البيئة .env المشابهة لـ Vite. الفرق الرئيسي هو البادئة المستخدمة لعرض متغيرات البيئة على جانب العميل.

  • قم بتغيير جميع متغيرات البيئة ذات البادئة VITE_ إلى NEXT_PUBLIC_.

يعرض Vite بعض متغيرات البيئة المدمجة على كائن import.meta.env الخاص الذي لا يدعمه Next.js. تحتاج إلى تحديث استخدامها كما يلي:

  • import.meta.env.MODEprocess.env.NODE_ENV
  • import.meta.env.PRODprocess.env.NODE_ENV === 'production'
  • import.meta.env.DEVprocess.env.NODE_ENV !== 'production'
  • import.meta.env.SSRtypeof window !== 'undefined'

لا يوفر Next.js أيضًا متغير بيئة BASE_URL مدمجًا. ومع ذلك، لا يزال بإمكانك تكوين واحد إذا كنت بحاجة إليه:

  1. أضف ما يلي إلى ملف .env الخاص بك:
.env
# ...
NEXT_PUBLIC_BASE_PATH="/some-base-path"
  1. قم بتعيين basePath إلى process.env.NEXT_PUBLIC_BASE_PATH في ملف next.config.mjs:
next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export', // يُخرج تطبيق صفحة واحدة (SPA).
  distDir: './dist', // يغير دليل إخراج البناء إلى `./dist/`.
  basePath: process.env.NEXT_PUBLIC_BASE_PATH, // يضبط المسار الأساسي على `/some-base-path`.
}

export default nextConfig
  1. قم بتحديث استخدامات import.meta.env.BASE_URL إلى process.env.NEXT_PUBLIC_BASE_PATH

الخطوة 8: تحديث الأوامر في package.json

يجب أن تكون الآن قادرًا على تشغيل تطبيقك لاختبار ما إذا كنت قد نجحت في ترحيله إلى Next.js. ولكن قبل ذلك، تحتاج إلى تحديث scripts في package.json بأوامر Next.js ذات الصلة، وإضافة .next و next-env.d.ts إلى .gitignore:

package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  }
}
.gitignore
# ...
.next
next-env.d.ts
dist

الآن قم بتشغيل npm run dev، وافتح http://localhost:3000. يجب أن ترى تطبيقك يعمل الآن على Next.js.

مثال: تحقق من طلب السحب هذا لمثال عمل لتطبيق Vite تم ترحيله إلى Next.js.

الخطوة 9: التنظيف

يمكنك الآن تنظيف قاعدة التعليمات البرمجية من آثار Vite ذات الصلة:

  • احذف main.tsx
  • احذف index.html
  • احذف vite-env.d.ts
  • احذف tsconfig.node.json
  • احذف vite.config.ts
  • أزل تبعيات Vite

الخطوات التالية

إذا سار كل شيء وفقًا للخطة، فلديك الآن تطبيق Next.js يعمل كتطبيق صفحة واحدة. ومع ذلك، لم تستفد بعد من معظم مزايا Next.js، ولكن يمكنك الآن البدء في إجراء تغييرات تدريجية لجني جميع الفوائد. إليك ما قد ترغب في القيام به بعد ذلك: