ReactJS ، تقديم جانب الخادم وبعض التفاصيل الدقيقة لمعالجة علامات تعريف الصفحة

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

بدءًا من google ، الحل الأول الذي سيتم توجيهك إليه هو على الأرجح React Helmet .

تتمثل إحدى مزايا المكتبة في أنه يمكن اعتبارها غير متجانسة بطريقة ما ويمكن استخدامها بشكل مثالي على جانب العميل وعلى جانب الخادم.

class Page extends Component { render() { return ( <div> <Helmet> <title>Turbo Todo</title> <meta name="theme-color" content="#008f68" /> </Helmet> {/* ... */} </div> ); } } 

على الخادم ، سيبدو جهاز التوجيه هكذا:

 app.get('/*', (req, res) => { const html = renderToString(<App />); const helmet = Helmet.renderStatic(); res.send(` <!doctype html> <html ${helmet.htmlAttributes.toString()}> <head> ${helmet.title.toString()} ${helmet.meta.toString()} </head> <body ${helmet.bodyAttributes.toString()}> <div id="app">${html}</div> </body> </html> `); }); 

كلا هذين المقتطفين صحيحان وفعالان تمامًا ، لكن يوجد BUT واحد ، الكود أعلاه للخادم متزامن تمامًا وبالتالي آمن تمامًا ، ولكن إذا أصبح غير متزامن ، فسيخفي الأخطاء التي يصححها الخطأ في حد ذاته:

 app.get('/*', async (req, res) => { // .... await anyAsyncAction(); //.... const helmet = Helmet.renderStatic(); // ... }); 

تكمن المشكلة هنا بشكل أساسي في مكتبة React Helmet نفسها ، وبصفة خاصة ، حيث تجمع كل العلامات داخل شجرة React وتضعها في الواقع في متغير عمومي ، وبما أن الكود أصبح غير متزامن ، فيمكن للمزج أن يجمع بين الطلبات التي تتم معالجتها في وقت واحد من مستخدمين مختلفين.

والخبر السار هنا هو أن شوكة صنعت على أساس هذه المكتبة والآن من الأفضل إعطاء الأفضلية لمكتبة رد فعل الخوذة المتزامنة . النموذج الرئيسي في ذلك هو أنه في هذه الحالة ، سيتم عزل سياق خوذة التفاعل في إطار طلب واحد عن طريق تغليف تطبيق React Tree في HelmetProvider:

 import { Helmet, HelmetProvider } from 'react-helmet-async'; app.get('/*', async (req, res) => {​ // ... code may content any async actions const helmetContext = {}; const app = ( <HelmetProvider context={helmetContext}> <App/> </HelmetProvider> ); // ...code may content any async actions const html = renderToString(app); const { helmet } = helmetContext; // ...code may content any async actions }); 

قد يتم الانتهاء من ذلك ، ولكن ربما ستذهب إلى أبعد من ذلك في محاولة للضغط على أقصى أداء وتحسين بعض مقاييس SEO. على سبيل المثال ، يمكنك تحسين قياس وقت البايتة الأولى (TTFB) - عندما يمكن للخادم إرسال تخطيط الصفحة مع قطع كما تم حسابها ، بدلاً من الانتظار حتى يتم حسابها بالكامل. للقيام بذلك ، سوف تبدأ في التطلع نحو استخدام renderToNodeStream بدلاً من renderToString .

هنا نواجه مرة أخرى مشكلة صغيرة. من أجل الحصول على جميع العلامات الوصفية التي تحتاجها الصفحة ، يجب علينا الانتقال إلى شجرة رد فعل التطبيق بالكامل ، ولكن المشكلة هي أنه يجب إرسال العلامات الوصفية قبل اللحظة التي نبدأ فيها ببث المحتوى باستخدام renderToNodeStream. في الواقع ، نحتاج بعد ذلك إلى حساب شجرة React Tree مرتين ويبدو مثل هذا:

 app.get('/*', async (req, res) => {​ const helmetContext = {}; let app = ( <HelmetProvider context={helmetContext}> <App/> </HelmetProvider> ); // do a first pass render so that react-helmet-async // can see what meta tags to render ReactDOMServer.renderToString(app); const { helmet } = helmetContext; response.write(` <html> <head> ${helmet.title.toString()}${helmet.meta.toString()} </head> <body> `); const stream = ReactDOMServer.renderToNodeStream(app); stream.pipe(response, { end: false }); stream.on('end', () => response.end('</body></html>')); }); 

مع مثل هذا النهج ، تصبح الحاجة إلى هذا التحسين من حيث المبدأ سؤالًا كبيرًا ومن غير المرجح أن نحسن مقياس TTFB الذي نريد تحقيقه.

هنا يمكننا أن نلعب قليلا التحسين وهناك العديد من الخيارات

  • بدلاً من renderToString ، استخدم renderToStaticMarkup ، مما قد يساعد إلى حد ما على الفوز بعض الوقت
  • بدلاً من استخدام العارضين المقدمين من رد الفعل من المربع ، استنبط النسخة الخفيفة الخاصة بك من المرور عبر شجرة التفاعل ، على سبيل المثال ، استنادًا إلى مكتبة walker رد الفعل ، أو رفض تقديم الشجرة بالكامل والنظر فقط في المستوى الأول من الشجرة ، دون الانتباه إلى المكونات المضمّنة ، لذلك يقول تقديم الضحلة
  • النظر في نظام التخزين المؤقت الذي قد تخطي في بعض الأحيان المشي الأول من خلال شجرة رد الفعل

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

يبدو لي في هذه الحالة ، بالنسبة لأولئك الذين هم على دراية بكيفية استخراج البيانات لعرضها على SSR (وإذا كان شخص ما لا يعرف ، فهذه مقالة ممتازة حول هذا الموضوع) ، فسنساعد في اتباع الطريقة نفسها لاستخراج علامات التعريف للصفحة.

المفهوم العام هو أن لدينا ملف تكوين لأجهزة التوجيه - هذا هيكل JS عادي ، وهو عبارة عن مجموعة من الكائنات ، يحتوي كل منها على عدة حقول لمكون الكتابة ، والمسار . بناءً على عنوان url للطلب ، نجد الموجه والمكون المرتبط به من ملف التكوين. بالنسبة لهذه المكونات ، نقوم بتعريف مجموعة من الطرق الثابتة مثل loadData و ، على سبيل المثال ، createMetatags لعلامات التعريف الخاصة بنا.

وبالتالي ، سيصبح مكون الصفحة نفسه كما يلي:

 class ProductPage extends React.Component { static createMetatags(store, request){ const item = selectItem(store, request.params.product_id); return [] .concat({property: 'og:description', content: item.desc}) .concat({property: 'og:title', content: item.title}) } static loadData(store, request){ // extract external data for SSR and return Promise } // the rest of component } 

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

 app.get('/*', async (req, res) => {​​ const store = createStore(); const matchedRoutes = matchRoutes(routes, request.path); // load app state await Promise.all( matchedRoutes.reduce((promises, { route }) => { return route.component.loadData ? promises.concat(route.component.loadData(store, req)) : promises; }, []) ); // to get metatags const metaTags = matchedRoutes.reduce((tags, {route}) => { return route.component.createMetatags ? tags.concat(route.component.createMetatags(store, req)): tags }); res.write(`​ <html>​ <head>​ ${ReactDOMServer.renderToString(() => metaTags.map(tag => <meta {...tag}/>) )}​​ </head>​ <body>​ `);​ const stream = ReactDOMServer.renderToNodeStream(app);​ stream.pipe(response, { end: false });​ stream.on('end', () => response.end('</body></html>'));​ }); 

أي الآن لا نحتاج إلى تقديم شجرة React مرتين - يمكننا على الفور استخراج كل ما نحتاجه للعمل من تطبيق متماثل ، عن طريق القياس مع استخراج البيانات للمسار.

Source: https://habr.com/ru/post/ar484456/


All Articles