تم كتابة كتابة هذه المقالة بسبب عدم وجود أي دليل أكثر أو أقل اكتمالًا حول كيفية جعل "تقديم جانب الخادم" لتطبيق React.
عندما واجهت هذه المشكلة ، كان لدي خياران للقيام بذلك ، إما باستخدام إطار
Next.js أو باستخدام
Express.js .
تم إنفاق حوالي 100 ساعة على Next.js للتحقيق للحصول على منصة OTT الكبيرة الجاهزة لدينا ، ولكن كانت هناك العديد من المشكلات التي رفضناها (سأكتب مقالًا حول هذا مرة أخرى) ، كان هناك خيار لمقالة صغيرة ،
Express.js ، الذي أريد أن أقول.
الرمز الكامل للعرض التجريبي في هذه المقالة
هنا .
لنبدأ بالمهمة الأولية وما كان لدينا.
كان لدينا في ذلك الوقت:
الأهداف:
- يجب أن يقدم الحل تقديم HTML بالكامل مع البيانات
- يجب أن يؤثر هذا الحل على الكود الموجود إلى الحد الأدنى ، أي يجب أن تظل البنية مع الحد الأدنى من التغييرات.
- متابعة استخدام المكتبات الموجودة بالفعل في المشروع
- يجب أن تأتي بيانات تعريف مُحسنات محركات البحث من الخادم ، إلى جانب الصفحة
- يجب تحميل الصور والأنماط والخطوط على الخادم وإرسالها إلى العميل ، دون تنزيل لاحق
- جانب العميل دعم الاستيراد الديناميكي
- تجنب تكرار التعليمات البرمجية للتحميل المسبق على الخادم وعند التنقل من جانب العميل
لقد قررنا المهام ، دعنا نتعرف على كيفية القيام بذلك.
من وثائق رد الفعل ، يمكننا معرفة أنه بالنسبة لـ SSR ، يمكنك استخدام
طريقتي renderToString () و
hydrate () ، لكن ماذا تفعل بعد ذلك؟
renderToString - يستخدم لإنشاء HTML على خادم تطبيقنا.
هيدرات - يستخدم لتقديم عالمي على العميل وعلى الخادم.
تحميل البيانات
لتنزيل البيانات على جانب الخادم ، نستخدم مكتبة
redux-connect ، التي تتيح لك تنزيل البيانات الضرورية قبل الاتصال بالعرض الأول ، وهو ما نحتاج إليه. للقيام بذلك ، استخدم hoc asyncConnect. على جانب الخادم ، يقوم بتحميل البيانات ، وعند التوجيه ، يعمل كـ componentDidMount.
@asyncConnect([ { key: 'usersFromServer', promise: async ({ store: { dispatch } }) => { await dispatch(getUsersData()); return Promise.resolve(); }, }, ])
نحتاج إلى إنشاء متجر مسترجع على جانب الخادم. كل شيء كالمعتاد ، فقط قم بإنشائه في ملف server.js.
على جانب الخادم أيضًا ، باستخدام طريقة loadOnServer من
redux-connect ، ننتظر التحميل المسبق للبيانات.
باستخدام renderToString ، نحصل على Html لتطبيق بياناتنا.
يمكن استرداد البيانات التي زرناها باستخدام getState () وإضافتها عبر العلامة <script /> إلى كائن النافذة العالمية. من ثم سنحصل على البيانات من العميل ونضعها في البوابة.
كل شيء يبدو مثل هذا
app.get('*', (req, res) => { const url = req.originalUrl || req.url; const history = createMemoryHistory({ initialEntries: [url], }); const store = configureStore(initialState, history); const location = parseUrl(url); const helpers = {}; const indexFile = path.resolve('./build/main.html'); store.runSaga(sagas).toPromise().then(() => { return loadOnServer({ store, location, routes, helpers }) .then(() => { const context = {}; if (context.url) { req.header('Location', context.url); return res.send(302) } const css = new Set();
يتم تمرير الدعائم 2 إلى مكون ReduxAsyncConnect:
الأول هو طرقنا ، والمساعدين الثانيين (الوظائف المساعدة) ، والتي نريد أن نكون في متناول الجميع من خلال التطبيق ، وهو نوع من التماثلية للسياق.
لتوجيه الخادم ، استخدم StaticRouter.
يتم استخدام مكتبة
خوذة لإضافة علامات التعريف سيو. كل مكون الصفحة يحتوي على وصف مع العلامات.
لكي تأتي العلامات من الخادم فورًا ، يتم استخدامها
const helmet = Helmet.renderStatic(); helmet.title.toString() helmet.meta.toString()
كان لا بد من إعادة كتابة التوجيه إلى مجموعة من الكائنات ، يبدو هذا.
export const StaticRoutesConfig = [ { key: 'usersGender', component: UsersGender, exact: true, path: '/users-gender/:gender', }, { key: 'USERS', component: Users, exact: true, path: '/users', }, { key: 'main', component: Users, exact: true, path: '/', }, { key: 'not-found', component: NotFound, }, ];
اعتمادًا على عنوان url الذي يأتي إلى الخادم ، قام جهاز رد فعل رد بإرجاع صفحة البيانات المطلوبة.
كيف يبدو العميل؟
هنا هو ملف العميل الرئيسي. يمكنك إضافة تحليلات لمحركات البحث والرمز الذي يجب تنفيذه لكل صفحة على جانب العميل.
المتصفح / index.js import 'babel-polyfill'; import { browserRender } from '../app/app'; browserRender();
ملف
App.js const initialState = !process.env.IS_SERVER ? window.__INITIAL_DATA__ : {}; const history = process.env.IS_SERVER ? createMemoryHistory({ initialEntries: ['/'], }) : createBrowserHistory(); const store = configureStore(initialState, history); if (!process.env.IS_SERVER) { window.store = store; } const insertCss = (...styles) => {
للتوجيه ، يتم استخدام ConnectedRouter من
جهاز التوجيه المتصل / غير القابل للتغيير .
بالنسبة
للتقديم من جانب الخادم ، لا يمكننا استخدام r
eact-router-dom ونوصف بشكل صحيح التوجيه الخاص بنا عبر Switch:
<Switch> <Route path="/about"> <About /> </Route> <Route path="/users"> <Users /> </Route> <Route path="/"> <Home /> </Route> </Switch>
بدلاً من ذلك ، كما ذكرنا سابقًا ، لدينا مجموعة من المسارات الموصوفة ، ولكي نضيفها إلى التطبيق ، نحتاج إلى استخدام
react-router-config :
التطبيق / index.js import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { Helmet } from 'react-helmet'; import { renderRoutes } from 'react-router-config'; import { getRouterLocation } from './selectors/router'; @connect(state => ({ location: getRouterLocation(state), }), null) export default class App extends Component { static propTypes = { location: PropTypes.shape().isRequired, route: PropTypes.shape().isRequired, }; render() { const { route } = this.props; return ( <div> {renderRoutes(route.routes)} </div> ); } }
قم بتنزيل الحروف والأساليب والخطوط على الخادم
بالنسبة للأنماط ،
تم استخدام اللودر المتماثل الشكل ، نظرًا لأن
اللودر العادي لا يعمل في حزمة الويب مع الهدف: "node"؛
يضيف جميع الأنماط إلى DOM ، وبالتالي تأتي الصفحة الجميلة الجاهزة من الخادم. يمكنك حفظ الأنماط في ملف منفصل ، بحيث يمكن للمتصفح تخزينها مؤقتًا.
لعرض الصور وتنزيل الخطوط على الخادم ، تم استخدام webpack loader
base64-inline-loader .
{ test: /\.(jpe?g|png|ttf|eot|otf|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/, use: 'base64-inline-loader?limit=1000&name=[name].[ext]', },
تم تضمينه للصور وجميع أنواع الخطوط التي استخدمناها. نتيجة لذلك ، تلقينا رمزًا من خادم base64 يعرض صفحة بها خطوط وصور بدون تحميل لاحق.
لبناء العميل ، تم استخدام
url-loader و
loader-loader المعتاد.
{ test: /\.(eot|svg|otf|ttf|woff|woff2)$/, use: 'file-loader', }, { test: /\.(mp4|webm|png|gif)$/, use: { loader: 'url-loader', options: { limit: 10000, }, }, },
نعم ، زاد هذا حجم الصفحة المحملة ، لكن لم يكن ملحوظًا جدًا للمستخدم من حيث سرعة التنزيل.
استيراد ديناميكي على الخادم
يستخدم
React.js React.lazy و React.suspense لاستيراد المحمل وعرضه بشكل حيوي ، لكنهما لا يعملان مع SSR.
استخدمنا
رد الفعل القابل للتحميل ، والذي يفعل نفس الشيء.
import Loadable from 'react-loadable'; import Loader from './Loader'; const LoadableComponent = Loadable({ loader: () => import('./my-component'), loading: Loader, }); export default class App extends React.Component { render() { return <LoadableComponent/>; } }
على العميل ، يعرض هذا الرمز المحمل ، على الخادم ، لكي يتم تحميل الوحدات ، تحتاج إلى إضافة الكود التالي:
Loadable.preloadAll().then(() => { app.listen(PORT, () => { console.log(` Server is listening on port ${PORT}`); }); });
Loadable.preloadAll () - بإرجاع وعد يقول يتم تحميل الوحدات النمطية.
يتم حل جميع الضوء.
قدمت عرضًا مصغرًا باستخدام كل ما تم
وصفه في المقالة .