
فهرست
مقدمه
صفحه ۱دانلود و نمایش دمو
نصب و اجرا
ویرایش و توسعه
جزییات نصب و اجرا ـ ۱
صفحه ۲عملیات Build
Start پروژه
جزییات نصب و اجرا ـ ۲
صفحه ۳استفاده از Node Stream
آمادهسازی Router
ارتباط با API
صفحه ۴تابع FetchData و پارامترهای آن
تابع API و پارامترهای آن
ارتباط کامپوننتهای React با دادههای API
صفحه ۵پیادهسازی Loading
صفحه ۶پیادهسازی تگهای سرصفحه
صفحه ۷پیادهسازی نقشهی سایت XML
صفحه ۸
در صفحههای قبلی، نحوهی دریافت، نصب و اجرا ، توسعهی پروژه، جزییات نصب و اجرای پروژه و ارتباط با API، توضیح داده شد. در این صفحه به سراغ نحوهی ارتباط کامپوننتهای React با دادههای دریافتی از API میرویم.
ارتباط کامپوننتهای React با دادههای دریافتی از API
این پروژه به صورت Single Page Application(SPA) (برنامهی تک صفحهیی) است؛ پس Source هر صفحه، یکبار توسط درخواست مستقیم کاربر با اجرای آدرس URL، بارگذاری میشود، این Source یک HTML رندر شده است و دادههای دریافت شده از API و محتویات کامپوننتها را در خود دارد. (توسط فایل server.mjs در چارچوب SSR)
اما درصورتی که کاربر با انتخاب لینکهای داخلی، درخواست اجرای دیگر صفحهها، در همان Tab مرورگر را داشته باشد، بدون بارگذاری مجدد Source صفحه، React به کمک React Router، فقط منطقهی تعیین شده را تغییر خواهد داد و کامپوننت مورد نظر و دادههای تازه را در آن منطقه، به نمایش درخواهد آورد. (توسط فایل جاوااسکریپت فراخوانی شده در همان HTML رندر شده)
اگر کاربر هر لینکی را در Tab جدید باز کند، درخواست مستقیم خواهد بود ولی اگر بین Route ها، فقط در یک Tab مرورگر، پیمایش کند، درخواست کاربر، یک درخواست ثانویه خواهد بود.
پس نمایش صفحهها در این پروژه به دو طریق انجام خواهد شد، یکی بهوسیلهی اجرای آدرس URL صفحهها در مرورگر که درخواست مستقیم کاربر است و دیگری بهوسیلهی پیمایش از طریق لینکهای داخلی که درخواستهای بعدی کاربر برای دیدنِ دیگر صفحههای وبسایت است.
هنگام درخواست مستقیم
هنگامی که درخواست کاربر، درخواست مستقیم برای اجرای صفحه باشد، دادههای موردنیازی که از طریق API مایل به دریافت آنها هستیم را پیشاپیش باید از طریق تابع FetchData در فایل server.mjs
دریافت کرده باشیم تا در چارچوب SSR تهیهشده، آن دادهها را به کامپوننتهای React انتقال بدهیم و در Source صفحه بگنجانیم. حالا Source نهایی صفحه، شامل دادههای دریافت شده از API و محتویات کامپوننتهای React همان صفحه (در سمت سرور) است و برای نمایش به مرورگر (سمت کلاینت) ارسال شده است.

دادههای دریافتی از API را ابتدا باید به دو فایل ورودی React یعنی entry-server.jsx
و entry-client.jsx
، منتقل کنیم. سپس با تعریف یک props در کامپوننت اولیه (App.jsx) و قراردادن آبجکت دریافت شده از API در آن props، به دیگر کامپوننتها برسانیم.
همانطور که قبلا توضیح دادهشد فایلهای
entry-server.jsx
وentry-client.jsx
هنگام اجرای پروژه، فراخوانی نخواهند شد. در واقع ما در حال تعیین چارچوبهای مورد نظرمان در این دو فایل هستیم. این دو فایل با اجرای دستور build به فایل(های) دیگری تبدیل خواهند شد، پس چارچوبهای تعیین شدهی ما نیز در فایل(های) تازه تبدیلشده، موجود خواهد بود. آن فایل(های) تازه تبدیلشده قرار است در سمت سرور و کلاینت فراخوانی شوند.
۱. برای فایل entry-server.jsx
همانطور که توضیح داده شد، در فایل server.mjs، دادهها به صورت یک آبجکت از API دریافت شد و در ثابتی به نام dataFromServer
قرار گرفت.
حالا آبجکت دریافت شده از API به پارامتری به نام dataFromServer
در تابع render که از فایل entry-server.js
وارد یا import شده است، اختصاص مییابد تا در یک props به همان نام به کامپوننت اولیه React، منتقل شود.
// server.mjs
import FetchData from './core/FetchData.mjs';
.
.
.
route.get('*', async (req, res) => {
const dataFromServer = await FetchData(...); // Fetch data from API
const render = (await import(`./dist/client/server/entry-server.js`)).render;
const { pipe, abort } = render(dataFromServer, {
.
.
.
همانطور که در کد بالا میبینید، دادههای دریافتی از API در ثابتی به نام
dataFromServer
مقداردهی شده است. به پارامتری به نامdataFromServer
در تابع render منتقل شده است. و همانطور که در کدر زیر میبینید، در انتها به یک props به همان نام در کامپوننت App منتقل شده است.
// src/entry-server.jsx
import { renderToPipeableStream } from 'react-dom/server';
import { StaticRouter } from "react-router-dom/server";
import App from './App';
export function render(dataFromServer, options) {
return renderToPipeableStream(
<StaticRouter>
<App dataFromServer={dataFromServer} />
</StaticRouter>,
options
)
}
۲. برای فایل entry-client.jsx
ابتدا باید در فایل server.mjs یک ثابت با تایپ String، به شکل یک خط جاوااسکریپت اضافه کنیم و دادههای دریافتی را که یک Object است به صورت stringify به متغیری در آن اختصاص دهیم. همانطور که میدانید، تابع render در فایل server.mjs سمت سرور اجرا میشود و Source اولیه را با مقادیر موردنظرمان بازتولید میکند. پس متغیری که تعریف کردهایم را از قبل بهوسیلهی تابع render در Source اضافه میکنیم.
// server.mjs
import FetchData from './core/FetchData.mjs';
.
.
.
route.get('*', async (req, res) => {
const dataFromServer = await FetchData(...); // Fetch data from API
const apiDataInScript =
`<script>window.__data__=${JSON.stringify(dataFromServer)}</script>`;
.
.
.
template = template.replace('<!--client-script-->', apiDataInScript);
.
.
.
همانطور که در کد بالا میبینید، ۱. دادههای دریافتی از API در ثابتی به نام
dataFromServer
مقداردهی شده است. ۲. بهwindow.__data__
در ثابتapiDataInScript
منتقل شده است. ۳. ثابتapiDataInScript
بهوسیلهی تابع render در فایل server.mjs به شکل یک خط<script>
به جای خط کامنت<!--client-script-->
در HTML ما قرار میگیرد.
حالا در فایل entry-client.jsx همان window.__data__
که حالا در Source ما وجود دارد را فراخوانی میکنیم و به یک props در کامپوننت اولیه (dataFromServer در کامپوننت App) اختصاص میدهیم.
// src/entry-client.jsx
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
var dataFromServer;
if (typeof window !== 'undefined') {
var dataFromServer = window.__data__;
};
ReactDOM.hydrateRoot(
document.getElementById('root'),
<BrowserRouter>
<App dataFromServer={dataFromServer} />
</BrowserRouter>
)
توجه
دادههای API که با متد GET جهت نمایش در هر صفحه دریافت میشوند(مثال بالا)، در خط جاوااسکریپتی در انتهای Source صفحه به شکل JSON.stringify
قرار خواهند گرفت، پس دقت کنید که دادهی محرمانهیی در بین آنها نباشد.
نکته
به دلیل اینکه کدهای جاوااسکریپت مربوط به DOM، در سمت سرور شناخته نمیشوند و Source این پروژه در سمت سرور رندر میشود، باید تمام اسکریپتهای جاوااسکریپت مربوط به DOM را در شرطی قرار بدهیم تا خطایی دریافت نکنیم. بنابراین مثلا در کد بالا خط const dataFromServer = window.__data__;
را درون شرط زیر قرا دادیم تا فقط اگر تایپ window یک تایپ معتبر بود و تعریف نشده، شناخته نشد، متغیر dataFromServer
مقداردهی شود.
var dataFromServer;
if (typeof window !== 'undefined') {
dataFromServer = window.__data__;
}
۳. انتقال به کامپوننتهای نهایی
بعد از انتقال دادهها به dataFromServer
در کامپوننت App باید آن دادهها را به کامپوننتهای نهایی موردنظر منتقل کنیم. (خط ۸ کد زیر):
// src/App.jsx
import Layouts from './Layouts/Layouts';
import Home from './Pages/Home';
function App({ dataFromServer }) {
return (
<Routes>
<Route path="/" element={<Layouts />}>
<Route index element={<Home dataFromServer={dataFromServer} />} />
.
.
.
</Route>
</Routes>
)
}
export default App;
همانطور که در فایل core/FetchData.mjs مشاهده میشود، دادههای دریافتی از API در یک آرایهیی به نام firstData
قرار خواهد گرفت و همانطور که در کد زیر میبینید، پس از ورود از طریق dataFromServer
به کامپوننت نهایی، آن آرایه به ثابتی به نام data
اختصاص داده میشود.
// src/Pages/Home.jsx
import {useState} from "react";
const Home = ({ dataFromServer }) => {
const [data,setData] = useState(dataFromServer?.firstData || []);
return (
<>
{data?.map((_v, _i) => {return ({<h1>_v?.title</h1>})})}
</>
);
};
export default Home;
ثابت data
که حالا دارای محتویات دریافت شده از API است، در کامپوننت ما در دسترس است و میتوان مثلاً مقدار title را از آن خارج نماییم و به نمایش دربیاوریم (کد بالا). دلیل اینکه چرا مستقیماً از خود dataFromServer
استفاده نکردیم و آن را در ثابت data
به شکل useState
قرار دادیم این است که چون میخواهیم در صورت تغییر Route، دوباره از تابع FetchData استفاده کنیم و دادههای تازهیی را از API دریافت کنیم باید این تابع را درون شرط خالی یا پر بودن آبجکت [dataFromServer[firstData
قرار دهیم تا فقط در صورت خالی بودن و تغییر Route توسط کاربر، دادههای تازهیی را از API دریافت کرده و دوباره در ثابت data
قرار دهیم. اگر این شرط را قائل نشویم، در هر بار ورود مستقیم کاربر به هر URL، بدون آنکه نیاز باشد تابع FetchData را دوباره اجرا کردهایم.
نکته مهم: دادههایی که توسط تابع render (فایل server.mjs) به کامپوننتها منتقل شدهاند و در آبجکت dataFromServer[firstData] قرار گرفتهاند، باید بدون ایجاد عمدی تاخیر، در کامپوننتها وارد شده باشند. مثلا اگر در هوک useEffect که بعد از رندر اولیه فراخوانی خواهد شد، دادههای آبجکت dataFromServer[firstData] را set کنیم یا به هر طریقی، تاخیر در انتقال دادههای آبجکت dataFromServer[firstData] به کامپوننتها ایجاد کنیم، دادههای منبع API در خروجی HTML سمت سرور، وجود نخواهند داشت.
هنگام پیمایش بین صفحهها
همانطور که در بخش قبلی توضیح داده شد Source اصلی یکبار در نخستین اجرای صفحه به همراه دادههای مربوطه از API دریافت شده است. حالا وقتی کاربر بین بخشهای مختلف وبسایت که ما قبلا آن بخشها را در Route ها تعیین کردهایم (فایل App.jsx)، پیمایش میکند، فقط بخش <Outlet />
که قبلا در فایل Layouts/Layouts.jsx
تعیین شده تغییر خواهد کرد. در این حالت با تغییر صفحه (Route) توسط کاربر (در همان Tab مرورگر)، دادههای دریافتی از API که در اجرای نخست، دریافت شده بود، احتمالا مورد نیاز نخواهد بود. پس به دادههایی مربوط به صحفهی «تازه درخواست شده» نیاز خواهیم داشت.
مثلا کاربر برای بار نخست، صفحهی معرفی یکی از محصولات را اجرا کرده و دیده است و دادههای دریافتی از API فقط مربوط به همان محصول بوده که از طریق فایل server.mjs به کامپوننتهای React منتقل شده است. حالا کاربر بر روی لینکی کلیک میکند که در همان Tab مرورگر به صفحهی لیست محصولات برود. بنابراین دادههای دریافت شدهی قبلی دیگر نیاز نخواهد بود و باید دادههایی مربوط به لیست محصولات از API دریافت شود.
در اینصورت ابتدا لازم است دادههای دریافتی از API که از server.mjs منتقل شدهاند، بعد از قرارگیری در ثابت data
و قرارگیری در Source صفحه، از قبل خالی شوند؛ بنابراین از useEffect
استفاده میکنیم و دادههای موجود در آرایهی firstData
را خالی میکنیم. (خط ۱۱ در کدر زیر)
// src/Pages/Home.jsx
import { useState, useEffect } from "react";
const Home = ({ dataFromServer }) => {
const [data,setData] = useState(dataFromServer?.firstData || []);
useEffect(() => {
.
.
.
dataFromServer['firstData'] = {}
}, []);
return (
<>
{data?.map((_v, _i) => { return (<h1>_v?.title</h1>) })}
</>
);
};
export default Home;
نکتهی اصلی در اینجا، این است که به دلیل اینکه میخواهیم «اگر وقتی که کاربر در صورت پیمایش تک صفحهیی در همان Tab مرورگر، با انتخاب لینکی دیگر به Route دیگیری منتقل شد، دادههای تازهیی از API دریافت شود» ، پس باید دادههای قبلی را بعد از استفادهشدن، خالی کرده باشیم تا فقط در صورت خالی بودن آن بتوانیم دادهی تازهیی را دریافت کنیم. اگر این شرط را قائل نشویم، در هر بار ورود مستقیم کاربر به هر URL، بدون آنکه نیاز باشد دوباره تابع FetchData را اجرا کردهایم. در واقع ما میخواهیم فقط در صورت تغییر Route دادههای تازهیی را دریافت کنیم.
// src/Pages/Home.jsx
import { useState, useEffect } from "react";
import FetchData from "../../core/FetchData.mjs";
const Home = ({ dataFromServer }) => {
const [data,setData] = useState(dataFromServer?.firstData || []);
var response;
useEffect(() => {
if (Object.keys(dataFromServer?.['firstData'])?.length === 0)
(async () => {
response = await FetchData('get', 'https://fakeapi.platzi.com/products');
setData(response);
})()
dataFromServer['firstData'] = {};
}, []);
return (
<>
{data?.map((_v, _i) => { return (<h1>_v?.title</h1>) })}
</>
);
};
export default Home;
با ورود مستقیم و اولیهی کاربر به هر Route، دادههای API از server.mjs درون آرایهی
firstData
قرار میگیرند و بعد از قرارگیری در HTML, خالی میشوند. حالا در صورت پیمایش کاربر بین Route ها که درخواستهای ثانویهی کاربر است، خالی بودن یا نبودنfirstData
چک میشود(خط ۹ کد بالا) و در صورت خالی بودن (که حالا حتما خالی است) دوباره تابع FetchData را اجرا می کنیم(خط ۱۱ کد بالا) و دادههای تازهیی از API دریافت میکنیم و درون ثابتdata
قرار میدهیم(خط ۱۳ کد بالا) تا در کامپوننت ما مورد استفاده قرار گیرند. این فرمول باید برای همهی کامپوننتهایی که دادههایی از API دریافت میکنند، مورد استفاده قرار گیرد.
صفحهی قبل: ارتباط با API
صفحهی بعد: پیادهسازی Loading
اطلاعات برنامه
نام: express-react-ssr
پلتفرم: Node.js
زبان: JavaScript
لایسنس: MIT
تاریخ انتشار اولیه: ۹ مرداد ۱۴۰۳
تاریخ آخرین بهروزرسانی: ۱۱ شهریور ۱۴۰۳
مخزن: GitHub