رندر سمت سرور (SSR)
نکته
رندر سمت سرور یا SSR، به قابلیتی در فریمورکهای فرانتاند مانند React ، Preact ، Vue و Svelte گفته میشود که اجازه میدهد همان اپلیکیشن در محیط Node.js اجرا شود، خروجی HTML تولید کند و سپس در مرورگر کاربر فرآیند "hydration" (فعالسازی تعاملی) را انجام دهد. اگر به دنبال یکپارچهسازی با فریمورکهای سنتی سمت سرور هستید، پیشنهاد میشود به راهنمای اتصال به بکاند مراجعه کنید.
این راهنما فرض را بر آن دارد که شما پیشتر با مفاهیم SSR در فریمورک انتخابیتان آشنا هستید، و تمرکز آن صرفاً بر نکات خاص و کاربردی SSR در Vite است.
API سطح پایین
این رابط برنامهنویسی (API) مخصوص توسعهدهندگان کتابخانهها و سازندگان فریمورکهاست، و جزو ابزارهای سطح پایین Vite محسوب میشود. اگر هدف شما ساخت یک اپلیکیشن است، توصیه میکنیم ابتدا ابزارها و افزونههای سطح بالاتری که در بخش SSR پروژه Awesome Vite معرفی شدهاند را بررسی کنید. با این حال، اپلیکیشنهای بسیاری تاکنون با تکیه بر همین API بومی و سطح پایین Vite بهخوبی توسعه یافتهاند.
در حال حاضر، Vite در حال توسعه یک API پیشرفتهتر برای SSR مبتنی بر Environment API است. برای آگاهی بیشتر میتوانید به لینک مورد نظر مراجعه کنید.
پروژههای نمونه
Vite بهصورت داخلی از رندر سمت سرور (SSR) پشتیبانی میکند. پروژه create-vite-extra
مجموعهای از تنظیمات آمادهی SSR را فراهم کرده که میتوانید بهعنوان مرجع در این راهنما از آنها استفاده کنید:
همچنین میتوانید این پروژهها را بهصورت محلی با اجرای دستور create-vite
راهاندازی کنید. در بخش انتخاب فریمورک، گزینهی Others > create-vite-extra
را انتخاب نمایید.
ساختار پروژه
یک اپلیکیشن معمولی با پشتیبانی از SSR (رندر سمت سرور) معمولاً دارای ساختاری مشابه زیر است:
- index.html
- server.js # سرور اصلی اپلیکیشن
- src/
- main.js # کد عمومی برنامه که مستقل از محیط اجراست
- entry-client.js # در مرورگر DOM مسئول نصب برنامه روی
- entry-server.js # در فریمورک SSR مربوط به API مسئول رندر کردن برنامه در سمت سرور با استفاده از
در فایل index.html
باید به entry-client.js
اشاره شود و همچنین محلی برای جایگذاری خروجی رندر شده توسط سرور در نظر گرفته شود:
<div id="app"><!--ssr-outlet--></div>
<script type="module" src="/src/entry-client.js"></script>
میتوانید بهجای <!--ssr-outlet-->
از هر نشانهگذاری دلخواه دیگری استفاده کنید، فقط کافی است که قابل شناسایی و جایگزینی دقیق باشد.
منطق شرطی
اگه نیاز دارید کد شرطی بر اساس SSR یا کلاینت بنویسید، میتونید از این کد استفاده کنید:
if (import.meta.env.SSR) {
// ... فقط کد مخصوص سرور
}
این شرط در زمان build بهصورت استاتیک جایگزین میشود، بنابراین کدهایی که استفاده نمیشوند حذف میشوند (tree-shaking)، که باعث سبکتر شدن باندل نهایی میشود.
راهاندازی سرور توسعه (Dev Server)
وقتی دارید یک برنامه SSR میسازید، احتمالاً میخواهید کنترل کامل روی سرور اصلیتان داشته باشید و Vite را از محیط پروداکشن (production) جدا کنید. به همین دلیل، توصیه میشود از Vite در حالت middleware استفاده کنید. در ادامه یک نمونه با Express (نسخه ۴) آورده شده است:
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import express from 'express'
import { createServer as createViteServer } from 'vite'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
async function createServer() {
const app = express()
// ایجاد کنید و نوع اپلیکیشن را به middleware را در حالت Vite سرور
// غیرفعال شود و سرور والد Vite داخلی HTML تنظیم کنید تا سرو 'custom'
// بتواند کنترل را به دست بگیرد
const vite = await createViteServer({
server: { middlewareMode: true },
appType: 'custom'
})
// خودتان express استفاده کنید. اگر از روتر middleware به عنوان vite مربوط connect از نمونه
// .استفاده کنید router.use استفاده میکنید، باید از (express.Router())
// زمانی که سرور مجدداً راهاندازی میشود )برای مثال بعد از اینکه کاربر فایل
// همچنان همان ارجاع `vite.middlewares` (را تغییر میدهد، vite.config.js
// .(های تزریقشده توسط پلاگینها middleware و Vite با یک پشته داخلی جدید از) خواهد بود
// مورد زیر حتی پس از راهاندازی مجدد معتبر است
app.use(vite.middlewares)
app.use('*', async (req, res) => {
// را سرو میکنیم - در ادامه به این موضوع خواهیم پرداخت index.html فایل
})
app.listen(5173)
}
createServer()
در اینجا vite
یک نمونه از ViteDevServer است. vite.middlewares
یک نمونه از Connect است که میتوان از آن به عنوان یک middleware در هر فریمورک Node.js که با connect سازگار است استفاده کرد.
گام بعدی پیادهسازی یک handler کلی (*
) برای سرو HTML رندر شده در سمت سرور است:
app.use('*', async (req, res, next) => {
const url = req.originalUrl
try {
// 1. index.html خواندن فایل
let template = fs.readFileSync(
path.resolve(__dirname, 'index.html'),
'utf-8',
)
// 2. مربوط به HMR شامل تزریق کلاینت .Vite توسط HTML اعمال تغییرات
// است Vite توسط پلاگینهای HTML و همچنین انجام ترنسفورمهای Vite
// @vitejs/plugin-react های سراسری از preamble مثل
template = await vite.transformIndexHtml(url, template)
// 3. را طوری تبدیل میکند ESM بهطور خودکار کد ssrLoadModule .بارگذاری ورودی سمت سرور
// مشابه، کارایی در HMR قابل اجرا باشد! نیازی به باندل نیست و با Node.js که در
// کردن ماژولها دارد invalidate
const { render } = await vite.ssrLoadModule('/src/entry-server.js')
// 4. entry-server.js خروجی `render` اپلیکیشن. فرض شده که تابع HTML رندر کردن
// فریمورک استفاده میکند، SSR های API از
// ReactDOMServer.renderToString() مثلاً
const appHtml = await render(url)
// 5. HTML رندرشده در قالب HTML قرار دادن
const html = template.replace(`<!--ssr-outlet-->`, () => appHtml)
// 6. نهایی به مرورگر HTML ارسال
res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
} catch (e) {
// استکترِیس را اصلاح میکند Vite ،در صورت بروز خطا،
// تا به کد اصلی شما ارجاع دهد
vite.ssrFixStacktrace(e)
next(e)
}
})
اسکریپت dev
در فایل package.json
نیز باید طوری تغییر داده شود که بهجای آن از اسکریپت سرور استفاده کند.
"scripts": {
- "dev": "vite"
+ "dev": "node server"
}
ساخت(build) برای محیط پروداکشن
برای انتشار یک پروژه SSR در محیط پروداکشن، باید مراحل زیر را انجام دهید:
- یک بیلد (build) برای کلاینت مثل همیشه تولید شود؛
- یک بیلد SSR نیز تولید شود که مستقیماً با
import()
بارگذاری شود، تا نیازی به استفاده ازssrLoadModule
در Vite نباشد.
اسکریپتهای ما در فایل package.json
به شکل زیر خواهند بود:
{
"scripts": {
"dev": "node server",
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --outDir dist/server --ssr src/entry-server.js"
}
}
به فلگ --ssr
توجه کنید که نشان میدهد این بیلد مخصوص SSR است. همچنین باید ورودی (entry) مربوط به SSR را مشخص کند.
سپس در فایل server.js
باید منطق مخصوص پروداکشن را اضافه کنیم با بررسی مقدار process.env.NODE_ENV
:
به جای خواندن فایل
index.html
از ریشه پروژه، از فایلdist/client/index.html
به عنوان قالب استفاده کنید، چون این فایل شامل لینکهای درست برای فایلهای خروجی کلاینت است.به جای استفاده از
await vite.ssrLoadModule('/src/entry-server.js')
، ازimport('./dist/server/entry-server.js')
استفاده کنید (این فایل خروجی بیلد SSR است).ساخت و استفاده از سرور توسعه
vite
را فقط در حالت توسعه (dev) انجام دهید و در حالت پروداکشن، از میان افزارهای سرو فایلهای استاتیک برای ارائه فایلها از مسیرdist/client
استفاده کنید.
برای مشاهده نمونه پیادهسازی، به پروژههای نمونه مراجعه کنید.
تولید دستورهای پیش بارگذاری
دستور vite build
از فلگ --ssrManifest
پشتیبانی میکند که یک فایل به نام .vite/ssr-manifest.json
در دایرکتوری خروجی بیلد ایجاد میکند:
- "build:client": "vite build --outDir dist/client",
+ "build:client": "vite build --outDir dist/client --ssrManifest",
اسکریپت بالا حالا فایل dist/client/.vite/ssr-manifest.json
را برای بیلد کلاینت تولید میکند (بله، مانیفست SSR از بیلد کلاینت ایجاد میشود چون هدف این است که ID های ماژولها را به فایلهای مربوط به کلاینت متصل کنیم). این مانیفست شامل اطلاعاتی است که به ما میگوید هر ID ماژول به کدام بخشها (chunks) و فایلهای مرتبط با آن ماژول در بیلد کلاینت اشاره دارد.
برای استفاده از مانیفست، فریمورکها باید روشی فراهم کنند تا ID ماژولهای کامپوننتهایی که در هنگام رندر سرور استفاده شدهاند، جمعآوری شوند.
@vitejs/plugin-vue
این قابلیت را به طور پیشفرض ارائه میدهد و به طور خودکار ID های ماژول کامپوننتهایی که در هنگام رندر سرور استفاده شدهاند را در SSR context مربوط به Vue ثبت میکند:
const ctx = {}
const html = await vueServerRenderer.renderToString(app, ctx)
// ماژولهایی است که در طول رندر شدن استفاده شدهاند ID یک مجموعه از ctx.modules حالا
در بخش پروداکشن فایل server.js
، باید فایل manifest را بخوانیم و آن را به تابع render
که از src/entry-server.js
اکسپورت شده، بدهیم. این کار اطلاعات کافی برای تولید دستورهای preload مربوط به فایلهایی که در مسیرهای async استفاده شدهاند را فراهم میکند. برای نمونهی کامل، کد دمو را ببینید. همچنین میتوان از این اطلاعات برای ارسال 103 Early Hints استفاده کرد.
پیشرندرینگ / تولید سایت ایستا (SSG)
اگر مسیرها (routes) و دادههای مورد نیاز آنها از قبل مشخص باشند، میتوان آنها را با استفاده از همان منطق SSR در حالت production، به صورت HTML ایستا (static) پیشپردازش کرد. این روش را میتوان نوعی از تولید سایت ایستا یا SSG نیز در نظر گرفت. برای نمونهی عملی، به اسکریپت pre-render دمو مراجعه کنید.
وابستگیهای خارجی در SSR
در حالت SSR، وابستگیها بهصورت پیشفرض از سیستم تبدیل ماژول Vite خارج (externalize) میشوند. این کار باعث افزایش سرعت در هر دو زمان توسعه (dev) و ساخت (build) پروژه میشود.
اگر یک وابستگی نیاز داشته باشد که توسط زنجیرهی تبدیل Vite پردازش شود (مثلاً چون از قابلیتهای Vite بهصورت مستقیم و بدون تبدیل در آن استفاده شده)، میتوانید آن را به گزینهی ssr.noExternal
اضافه کنید.
وابستگیهایی که لینک شدهاند (مثل آنهایی که با npm link
یا در محیطهای monorepo استفاده شدهاند)، بهصورت پیشفرض external نمیشوند تا Vite بتواند از HMR استفاده کند. اگر نمیخواهید این رفتار اتفاق بیفتد (مثلاً برای شبیهسازی شرایطی که انگار وابستگیها لینک نشدهاند) میتوانید آنها را به گزینهی ssr.external
اضافه کنید.
کار با aliasها
اگر aliasهایی تعریف کردهاید که یک پکیج را به پکیج دیگری هدایت میکنند، ممکن است بهتر باشد بهجای آن، خود پکیجهای داخل node_modules
را alias کنید تا قابلیت external شدن آنها در SSR به درستی عمل کند. هر دو ابزار Yarn و pnpm از alias کردن با پیشوند npm:
پشتیبانی میکنند.
منطق افزونه مخصوص SSR
برخی فریمورکها مثل Vue یا Svelte، کامپوننتها را بسته به اینکه برای کلاینت باشند یا SSR، به شکلهای مختلفی کامپایل میکنند. با استفاده از این ویژگی، افزونهها میتوانند رفتار متفاوتی در حالت SSR نسبت به کلاینت داشته باشند. برای پشتیبانی از این تبدیلهای شرطی، Vite یک ویژگی اضافی به نام ssr
را در آبجکت options
به هوکهای زیر از افزونهها منتقل میکند:
resolveId
load
transform
مثال:
export function mySSRPlugin() {
return {
name: 'my-ssr',
transform(code, id, options) {
if (options?.ssr) {
// ... SSR انجام تبدیل مخصوص
}
},
}
}
آبجکت options
که در توابع load
و transform
استفاده میشود، اختیاری است. Rollup در حال حاضر از این آبجکت استفاده نمیکند، اما ممکن است در آینده برای افزودن اطلاعات بیشتر به این hookها از آن استفاده شود.
نکته
قبل از نسخه 2.7 از Vite، اطلاعات مربوط به SSR به جای اینکه در آبجکت options
قرار بگیرد، به صورت یک پارامتر جداگانه به توابع پلاگین داده میشد. الان بیشتر فریمورکها و پلاگینها بهروز شدهاند، ولی ممکن است هنوز در برخی منابع قدیمی نسخه قبلی این روش را ببینید.
هدف SSR
هدف پیشفرض برای ساخت SSR، محیط Node است، اما شما همچنین میتوانید سرور را در یک Web Worker اجرا کنید. نحوه حل ارجاعات پکیجها برای هر پلتفرم متفاوت است. شما میتوانید هدف را به Web Worker تغییر دهید با تنظیم ssr.target
به 'webworker'
.
بستهبندی SSR
در برخی موارد مانند رانتایم webworker
، ممکن است بخواهید بیلد SSR خود را به در یک فایل جاوا اسکریپت واحد بستهبندی (bundle) کنید. شما میتوانید این رفتار را با تنظیم ssr.noExternal
به true
فعال کنید. این کار دو چیز انجام میدهد:
- تمام وابستگیها را به عنوان
noExternal
در نظر میگیرد. - اگر هرکدام از کتابخانههای داخلی Node.js ایمپورت شوند، یک خطا ایجاد میکند.
شرایط حل بسته برای SSR
به طور پیشفرض، برای ساخت SSR، Vite از شرایطی که در resolve.conditions
تنظیم شده است، برای حل ورودی بستهها استفاده میکند. شما میتوانید با استفاده از گزینههای ssr.resolve.conditions
و ssr.resolve.externalConditions
، این رفتار را تغییر داده و سفارشیسازی کنید.
Vite CLI
دستورات CLI مانند $ vite dev
و $ vite preview
میتوانند برای برنامههای SSR نیز استفاده شوند. شما میتوانید میدلورهای SSR خود را با استفاده از configureServer
به سرور توسعه و با استفاده از configurePreviewServer
به سرور پیشنمایش اضافه کنید.
نکته
از هوک پس از عملیات استفاده کنید تا میانهافزار SSR شما پس از میدلورهای Vite اجرا شود.