لذلك ، قررت أن تجعل مشروع جديد. وهذا المشروع هو تطبيق ويب. كم من الوقت سيستغرق إنشاء نموذج أولي أساسي؟ ما مدى صعوبة ذلك؟ ما الذي يجب أن يكون موقع الويب الحديث قادرًا على القيام به من البداية؟
في هذه المقالة ، سنحاول تحديد مخطط تطبيق الويب البسيط مع البنية التالية:
ما سوف نغطي:
- إنشاء بيئة ديف في تأليف عامل ميناء.
- إنشاء الخلفية على قارورة.
- إنشاء الواجهة الأمامية على Express.
- بناء JS باستخدام Webpack.
- الرد ، والاسترجاع ، وتقديم جانب الخادم.
- طوابير المهمة مع RQ.
مقدمة
قبل التطوير ، بالطبع ، عليك أولاً أن تقرر ما الذي نقوم بتطويره! كتطبيق نموذجي لهذه المقالة ، قررت إنشاء محرك ويكي بدائي. سيكون لدينا البطاقات الصادرة في تخفيض السعر. يمكن مشاهدتها و (في وقت ما في المستقبل) تقدم تعديلات. كل هذا سوف نرتب كتطبيق من صفحة واحدة مع تقديم جانب الخادم (وهو أمر ضروري للغاية لفهرسة تيرابايتنا المستقبلية من المحتوى).
دعونا نلقي نظرة أكثر تفصيلا على المكونات التي نحتاجها لهذا:
- العميل. دعونا ننشئ تطبيقًا من صفحة واحدة (على سبيل المثال ، مع انتقالات الصفحة باستخدام AJAX) على حزمة React + Redux ، وهو أمر شائع جدًا في عالم الواجهة الأمامية.
- الواجهة الأمامية . لنقم بإنشاء خادم Express بسيط من شأنه تقديم تطبيق React (طلب جميع البيانات اللازمة في الواجهة الخلفية بشكل غير متزامن) وإصداره للمستخدم.
- الخلفية . ماجستير في منطق الأعمال ، سيكون لدينا الخلفية تطبيق قارورة صغير. سنقوم بتخزين البيانات (البطاقات الخاصة بنا) في مستودع وثائق MongoDB الشهير ، وفي قائمة انتظار المهام ، وربما في المستقبل ، التخزين المؤقت ، سوف نستخدم Redis .
- العامل . سيتم إطلاق حاوية منفصلة للمهام الثقيلة من قبل مكتبة RQ .
البنية التحتية: بوابة
ربما ، لم نتمكن من التحدث عن هذا ، ولكن ، بالطبع ، سنقوم بإجراء التنمية في مستودع بوابة.
git init git remote add origin git@github.com:Saluev/habr-app-demo.git git commit --allow-empty -m "Initial commit" git push
(هنا يجب أن تملأ على الفور.
.gitignore
.)
يمكن الاطلاع
على المسودة النهائية
على جيثب . يتوافق كل قسم من المقالة مع التزام واحد (قمت بتجديد الكثير لتحقيق ذلك!).
البنية التحتية: عامل ميناء يؤلف
لنبدأ بإعداد البيئة. مع وفرة المكونات التي لدينا ، سيكون حل التطوير المنطقي للغاية هو استخدام عامل الإرساء.
أضف ملف
docker-compose.yml
إلى المستودع بالمحتوى التالي:
version: '3' services: mongo: image: "mongo:latest" redis: image: "redis:alpine" backend: build: context: . dockerfile: ./docker/backend/Dockerfile environment: - APP_ENV=dev depends_on: - mongo - redis ports: - "40001:40001" volumes: - .:/code frontend: build: context: . dockerfile: ./docker/frontend/Dockerfile environment: - APP_ENV=dev - APP_BACKEND_URL=backend:40001 - APP_FRONTEND_PORT=40002 depends_on: - backend ports: - "40002:40002" volumes: - ./frontend:/app/src worker: build: context: . dockerfile: ./docker/worker/Dockerfile environment: - APP_ENV=dev depends_on: - mongo - redis volumes: - .:/code
دعنا نلقي نظرة سريعة على ما يحدث هنا.
- يتم إنشاء حاوية MongoDB وحاوية Redis.
- يتم إنشاء حاوية للواجهة الخلفية لدينا (والتي نوضحها أدناه). متغير البيئة APP_ENV = يتم تمرير dev إليه (سننظر إليه لفهم إعدادات Flask المراد تحميلها) ، وسيتم فتح المنفذ 40001 الخاص به في الخارج (من خلاله سينتقل عميل المستعرض الخاص بنا إلى API).
- يتم إنشاء حاوية من الواجهة الأمامية لدينا. يتم أيضًا طرح مجموعة متنوعة من متغيرات البيئة ، والتي ستكون مفيدة لنا في وقت لاحق ، وسيتم فتح المنفذ 4000. هذا هو المنفذ الرئيسي لتطبيق الويب الخاص بنا: في المتصفح سوف نذهب إلى http: // localhost: 40002 .
- يتم إنشاء حاوية عاملنا. إنه لا يحتاج إلى منافذ خارجية ، والوصول فقط مطلوب في MongoDB و Redis.
الآن لنقم بإنشاء أرصفة في الوقت الحالي ، تأتي
سلسلة من الترجمات للمقالات الممتازة حول Docker إلى Habré - يمكنك الذهاب إلى هناك بكل أمان.
لنبدأ مع الخلفية.
# docker/backend/Dockerfile FROM python:stretch COPY requirements.txt /tmp/ RUN pip install -r /tmp/requirements.txt ADD . /code WORKDIR /code CMD gunicorn -w 1 -b 0.0.0.0:40001 --worker-class gevent backend.server:app
من المعلوم أننا ننفذ من خلال تطبيق gunicorn Flask ، مختبئين تحت اسم
app
في وحدة
backend.server
.
لا تقل أهمية
docker/backend/.dockerignore
:
.git .idea .logs .pytest_cache frontend tests venv *.pyc *.pyo
يشبه العامل عمومًا الخلفية ، فقط بدلاً من gunicorn لدينا الإطلاق المعتاد لوحدة الحفرة:
# docker/worker/Dockerfile FROM python:stretch COPY requirements.txt /tmp/ RUN pip install -r /tmp/requirements.txt ADD . /code WORKDIR /code CMD python -m worker
سنفعل كل العمل في
worker/__main__.py
.
.dockerignore
العامل
.dockerignore
تمامًا الواجهة الخلفية
.dockerignore
.
وأخيرا ، الواجهة الأمامية. هناك
مقالة منفصلة كاملة عنه حول حبري ، ولكن إذا نظرنا إلى
النقاش المستفيض حول StackOverflow والتعليقات بروح "الرجال ، هل هو بالفعل 2018 ، هل لا يوجد حل طبيعي بعد؟" كل شيء ليس بهذه البساطة هناك. أنا استقر على هذا الإصدار من ملف عامل ميناء.
# docker/frontend/Dockerfile FROM node:carbon WORKDIR /app # package.json package-lock.json npm install, . COPY frontend/package*.json ./ RUN npm install # , # PATH. ENV PATH /app/node_modules/.bin:$PATH # . ADD frontend /app/src WORKDIR /app/src RUN npm run build CMD npm run start
الايجابيات:
- يتم تخزين كل شيء كما هو متوقع (في الطبقة السفلية - التبعيات ، في الجزء العلوي - بناء تطبيقنا) ؛
docker-compose exec frontend npm install --save newDependency
يعمل كما يجب وتعديل package.json
في docker-compose exec frontend npm install --save newDependency
(والذي لن يكون كذلك إذا استخدمنا COPY ، كما يشير كثير من الناس). لن يكون من المستحسن تشغيل npm install --save newDependency
خارج الحاوية على أي حال ، لأن بعض تبعيات الحزمة الجديدة قد تكون موجودة بالفعل ويتم إنشاؤها تحت نظام أساسي مختلف (تحت النظام الموجود داخل عامل الميناء ، وليس تحت macbook العمل لدينا ، على سبيل المثال ) ، ومع ذلك فنحن عمومًا لا نريد طلب وجود Node على جهاز التطوير. عامل واحد للحكم عليهم جميعا!
حسنا وبالطبع
docker/frontend/.dockerignore
:
.git .idea .logs .pytest_cache backend worker tools node_modules npm-debug tests venv
لذلك ، إطار الحاوية الخاص بنا جاهز ويمكنك تعبئته بالمحتويات!
الخلفية: إطار قارورة
أضف
flask
،
flask-cors
،
gunicorn
و
gunicorn
requirements.txt
gunicorn
وقم بإنشاء تطبيق Flask بسيط في
backend/server.py
.
أخبرنا Flask بسحب الإعدادات من ملف
backend.{env}_settings
، مما يعني أننا سنحتاج أيضًا إلى إنشاء
backend/dev_settings.py
لملف (على الأقل فارغ)
backend/dev_settings.py
كل شيء.
الآن يمكننا أن نزيد رسميا لدينا الخلفية!
habr-app-demo$ docker-compose up backend ... backend_1 | [2019-02-23 10:09:03 +0000] [6] [INFO] Starting gunicorn 19.9.0 backend_1 | [2019-02-23 10:09:03 +0000] [6] [INFO] Listening at: http://0.0.0.0:40001 (6) backend_1 | [2019-02-23 10:09:03 +0000] [6] [INFO] Using worker: gevent backend_1 | [2019-02-23 10:09:03 +0000] [9] [INFO] Booting worker with pid: 9
نحن نمضي قدما.
الواجهة الأمامية: الإطار السريع
لنبدأ بإنشاء حزمة. بعد إنشاء مجلد الواجهة الأمامية وتشغيل
npm init
فيه ، بعد بعض الأسئلة غير
npm init
، نحصل على الحزمة النهائية.
{ "name": "habr-app-demo", "version": "0.0.1", "description": "This is an app demo for Habr article.", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "git+https://github.com/Saluev/habr-app-demo.git" }, "author": "Tigran Saluev <tigran@saluev.com>", "license": "MIT", "bugs": { "url": "https://github.com/Saluev/habr-app-demo/issues" }, "homepage": "https://github.com/Saluev/habr-app-demo#readme" }
في المستقبل ، لسنا بحاجة إلى Node.js على الإطلاق على جهاز المطور (على الرغم من أنه لا يزال بإمكاننا تفادي وبدء تشغيل
npm init
عبر Docker ، ولكن جيدًا).
في
Dockerfile
ذكرنا
npm run build
و
npm run start
- تحتاج إلى إضافة الأوامر المناسبة إلى
package.json
:
أمر
build
لا يفعل شيئًا بعد ، لكنه سيظل مفيدًا لنا.
أضف تبعيات
Express وقم بإنشاء تطبيق بسيط في
index.js
:
الآن
docker-compose up frontend
يثير الواجهة الأمامية لدينا! علاوة على ذلك ، على
http: // localhost: 40002 ، يجب أن يظهر "Hello، world" الكلاسيكي بالفعل.
Frontend: بناء مع تطبيق webpack و React
حان الوقت لتصوير شيء أكثر من نص عادي في طلبنا. في هذا القسم ، سنضيف أبسط مكون React في
App
ونقوم بتكوين التجميع.
عند البرمجة في React ، من المريح جدًا استخدام
JSX ، وهي لهجة من جافا سكريبت ممتدة عبر الإنشاءات النحوية للنموذج
render() { return <MyButton color="blue">{this.props.caption}</MyButton>; }
ومع ذلك ، لا تفهم محركات JavaScript ذلك ، لذلك عادةً ما تتم إضافة مرحلة الإنشاء إلى الواجهة الأمامية. يقوم برنامج التحويل البرمجي لجافا سكريبت الخاص (نعم ، نعم) بتحويل السكر النحوي إلى جافا سكريبت كلاسيكية
قبيحة ، والتعامل مع الواردات ، والتقليل ، وهلم جرا.
2014 سنة. apt-cache search javaلذلك ، يبدو أبسط مكون React بسيطًا جدًا.
وقال انه ببساطة عرض تحية لدينا مع دبوس أكثر إقناعا.
أضف الملف
frontend/src/template.js
يحتوي على الحد الأدنى لإطار HTML لتطبيقنا في المستقبل:
أضف نقطة دخول العميل:
لبناء كل هذا الجمال ، نحتاج إلى:
يُعد
webpack منشئًا شبابيًا عصريًا لـ JS (على الرغم من أنني لم أقرأ مقالات على الواجهة الأمامية لمدة ثلاث ساعات ، لذلك لست متأكدًا من الموضة) ؛
بابل هو مترجم لجميع أنواع المستحضرات مثل JSX ، وفي الوقت نفسه مزود polyfill لجميع حالات IE.
إذا كان التكرار السابق للواجهة الأمامية لا يزال قيد التشغيل ، كل ما عليك فعله هو
docker-compose exec frontend npm install --save \ react \ react-dom docker-compose exec frontend npm install --save-dev \ webpack \ webpack-cli \ babel-loader \ @babel/core \ @babel/polyfill \ @babel/preset-env \ @babel/preset-react
لتثبيت تبعيات جديدة. الآن قم بتكوين webpack:
لجعل بابل العمل ، تحتاج إلى تكوين
frontend/.babelrc
:
{ "presets": ["@babel/env", "@babel/react"] }
أخيرًا ، اجعل الأمر
npm run build
معنى:
// frontend/package.json ... "scripts": { "build": "webpack", "start": "node /app/server.js", "test": "echo \"Error: no test specified\" && exit 1" }, ...
الآن عميلنا ، جنبا إلى جنب مع مجموعة من polyfills وجميع التبعيات ، يمر عبر بابل ، ويجمع ويطوي في ملف مصغّر متجانسة
../dist/client.js
. أضف القدرة على تحميله كملف ثابت إلى تطبيق Express الخاص بنا ، وفي المسار الافتراضي سنبدأ في إرجاع HTML الخاص بنا:
النجاح! الآن ، إذا
docker-compose up --build frontend
،
docker-compose up --build frontend
"مرحبًا أيها العالم!" في غلاف جديد لامعة ، وإذا كان لديك ملحق React Developer Tools مثبتًا (
Chrome ،
Firefox ) ، فهناك أيضًا شجرة مكون React في أدوات المطور:

الخلفية: البيانات في MongoDB
قبل الانتقال وتنفس الحياة في تطبيقنا ، يجب أولاً أن تتنفسه في الواجهة الخلفية. يبدو أننا سنقوم بتخزين البطاقات التي تم ترميزها في تخفيض السعر - حان الوقت للقيام بذلك.
بينما
يوجد ORMs لـ MongoDB في الثعبان ، فأنا أعتبر استخدام ORMs مفروغًا منه وأترك دراسة الحلول المناسبة لك. بدلاً من ذلك ، سنقوم بإنشاء فصل بسيط للبطاقة و
DAO المصاحب:
(إذا كنت لا تزال لا تستخدم التعليقات التوضيحية في Python ، فتأكد من مراجعة
هذه المقالات !)
الآن لنقم بإنشاء تطبيق لواجهة
CardDAO
التي تأخذ كائن
Database
من
pymongo
(نعم ، وقت لإضافة
pymongo
إلى
requirements.txt
):
الوقت لتسجيل التكوين Monga في إعدادات الخلفية. لقد قمنا ببساطة بتسمية
MONGO_HOST = "mongo"
باسم mongo
mongo
، لذلك
MONGO_HOST = "mongo"
:
الآن نحن بحاجة إلى إنشاء
MongoCardDAO
ومنح التطبيق Flask الوصول إليه. على الرغم من أن لدينا الآن تسلسل هرمي بسيط للكائنات (الإعدادات → عميل pymongo → قاعدة بيانات pymongo →
MongoCardDAO
) ، فلنقم على الفور بإنشاء مكون ملك مركزي يعمل
على حقن التبعية (سيأتي مفيدًا مرة أخرى عندما ننفذ العامل والأدوات).
حان الوقت لإضافة مسار جديد إلى تطبيق Flask والاستمتاع بالمنظر!
أعد التشغيل باستخدام
docker-compose up --build backend
:

عفوًا ... أوه ، تمامًا. نحن بحاجة إلى إضافة محتوى! سنفتح مجلد الأدوات ونضيف نصًا إليه يضيف بطاقة اختبار واحدة:
docker-compose exec backend python -m tools.add_test_content
بملء
docker-compose exec backend python -m tools.add_test_content
بمحتوى من داخل حاوية
docker-compose exec backend python -m tools.add_test_content
.

النجاح! الآن هو الوقت المناسب لدعم هذا على الواجهة الأمامية.
الواجهة الأمامية: مسترجع
الآن نريد أن نجعل المسار
/card/:id_or_slug
، والذي سيتم من خلاله فتح تطبيق
/card/:id_or_slug
، تحميل بيانات البطاقة من واجهة برمجة التطبيقات
/card/:id_or_slug
لنا بطريقة أو بأخرى. وهنا ، ربما يبدأ الجزء الأكثر صعوبة ، لأننا نريد من الخادم أن يعطينا HTML على الفور بمحتويات البطاقة ، وهو مناسب للفهرسة ، ولكن في الوقت نفسه ، عندما يتنقل التطبيق بين البطاقات ، يتلقى جميع البيانات في شكل JSON من واجهة برمجة التطبيقات ، ولا يتم تحميل الصفحة بشكل زائد. وهكذا كل هذا - دون نسخ ولصق!
لنبدأ بإضافة Redux. Redux هي مكتبة JavaScript لتخزين الحالة. تكمن الفكرة في أنه بدلاً من الآلاف من الحالات الضمنية التي تتغير مكوناتك أثناء تصرفات المستخدم والأحداث الأخرى المثيرة للاهتمام ، فإن لديهم حالة مركزية واحدة ، ويقومون بإجراء أي تغيير من خلال آلية مركزية للإجراءات. لذلك ، إذا قمنا في وقت سابق من أجل التصفح بتشغيل تطبيق GIF أولاً ، فقدمنا طلبًا من خلال AJAX ، وأخيراً في رد الاتصال الناجح ، قمنا بتحديث الأجزاء الضرورية من الصفحة ، ثم في نموذج Redux ، نحن مدعوون لإرسال الإجراء "تغيير المحتوى إلى gif مع الرسوم المتحركة" ، والذي سيؤدي إلى تغيير الحالة العامة بحيث يطرد أحد مكوناتك المحتوى السابق ويضع الرسوم المتحركة ، ثم يقدم طلبًا ، ويرسل إجراءً آخر في رد الاتصال الناجح ، "قم بتغيير المحتوى ليتم تحميله". بشكل عام ، الآن سوف نرى ذلك بأنفسنا.
لنبدأ بتثبيت تبعيات جديدة في حاوياتنا.
docker-compose exec frontend npm install --save \ redux \ react-redux \ redux-thunk \ redux-devtools-extension
الأول هو ، في الواقع ، Redux ، والثاني هو مكتبة خاصة لعبور React و Redux (كتبها خبراء التزاوج) ، والثالث هو شيء ضروري للغاية ، والحاجة التي تم إثباتها بشكل جيد في
README لها ، وأخيرا ، الرابعة هي المكتبة اللازمة ل
Redux DevTools للعمل
تمديد .
دعنا نبدأ برمز redux المتداول: إنشاء المخفض الذي لا يفعل شيئًا ، وتهيئة الحالة.
يتغير عميلنا قليلاً ، ويستعد عقلياً للعمل مع Redux:
الآن يمكننا تشغيل عامل الإرساء - إنشاء الواجهة الأمامية للتأكد من عدم كسر أي شيء ، وظهرت حالتنا البدائية في Redux DevTools:

الواجهة الأمامية: صفحة البطاقة
قبل أن تتمكن من إنشاء صفحات باستخدام SSR ، تحتاج إلى إنشاء صفحات بدون SSR! دعنا نستخدم أخيرًا واجهة برمجة التطبيقات (API) الخاصة بنا للوصول إلى البطاقات وتشكيل صفحة البطاقة في الواجهة الأمامية.
الوقت للاستفادة من الذكاء وإعادة تصميم هيكل دولتنا. هناك
الكثير من المواد حول هذا الموضوع ، لذلك أقترح عدم إساءة استخدام الاستخبارات وسوف أركز على الموضوع البسيط. على سبيل المثال ، مثل:
{ "page": { "type": "card", // // type=card: "cardSlug": "...", // "isFetching": false, // API "cardData": {...}, // ( ) // ... }, // ... }
دعنا نحصل على مكون "البطاقة" ، الذي يأخذ محتويات cardData كدعائم (إنها في الواقع محتويات بطاقتنا في mongo):
الآن لنحصل على مكون للصفحة بأكملها مع البطاقة. سيكون مسؤولاً عن الحصول على البيانات اللازمة من واجهة برمجة التطبيقات ونقلها إلى البطاقة. وسنفعل جلب البيانات بطريقة React-Redux.
أولاً ، أنشئ ملف
frontend/src/redux/actions.js
وقم بإنشاء إجراء يستخرج محتويات البطاقة من واجهة برمجة التطبيقات ، إن لم يكن بالفعل:
export function fetchCardIfNeeded() { return (dispatch, getState) => { let state = getState().page; if (state.cardData === undefined || state.cardData.slug !== state.cardSlug) { return dispatch(fetchCard()); } }; }
إجراء
fetchCard
، الذي يجعل عملية جلب المعلومات في الواقع أكثر تعقيدًا:
function fetchCard() { return (dispatch, getState) => {
أوه ، لقد حصلنا على شيء يفعله! يجب دعم هذا في المخفض:
(انتبه إلى بناء الجملة العصري لاستنساخ كائن مع تغيير الحقول الفردية.)الآن وبعد تنفيذ كل المنطق في إجراءات Redux ، CardPage
سيبدو المكون نفسه بسيطًا نسبيًا:
أضف معالجة page.type بسيطة إلى مكون التطبيق الجذر:
والآن، واحد النقطة الأخيرة - لا بد من تهيئة page.type
و page.cardSlug
تبعا لURL.ولكن لا يزال هناك العديد من الأقسام في هذه المقالة ، لكن لا يمكننا أن نجعل حلاً عالي الجودة في الوقت الحالي. دعونا نفعل ذلك غبي الآن. هذا مجرد غباء تمامًا. على سبيل المثال ، منتظم عند تهيئة التطبيق!
الآن يمكننا إعادة بناء الواجهة الأمامية للمساعدة docker-compose up --build frontend
في الاستمتاع ببطاقتنا helloworld
...
لذا ، انتظر ثانية ... وأين المحتوى الخاص بنا؟ أوه ، لقد نسينا تحليل Markdown!العامل: RQ
يعد تحليل تخفيض السعر وإنشاء HTML لبطاقة ذات حجم غير محدود مهمة "ثقيلة" نموذجية ، والتي بدلاً من حلها مباشرة على الواجهة الخلفية مع حفظ التغييرات ، يتم وضعها في قائمة الانتظار وتنفيذها على أجهزة عمل منفصلة.هناك العديد من التطبيقات مفتوحة المصدر لقوائم المهام ؛ سنأخذ Redis ومكتبة بسيطة RQ (قائمة انتظار Redis) ، والتي تنقل معلمات المهمة في تنسيق المخلل وتنظم عمليات التفريخ الخاصة بنا لمعالجتها.الوقت لإضافة الفجل اعتمادا على الإعدادات والأسلاك!
وهناك القليل من التعليمات البرمجية للعامل.
للتحليل نفسه ، قم بتوصيل مكتبة الخاطئة وكتابة وظيفة بسيطة:
منطقيا: نحتاج CardDAO
إلى الحصول على الكود المصدري للبطاقة وحفظ النتيجة. ولكن لا يمكن إجراء تسلسل للكائن الذي يحتوي على الاتصال بوحدة التخزين الخارجية عبر المخلل - مما يعني أنه لا يمكن أخذ هذه المهمة على الفور في قائمة انتظار RQ. بطريقة جيدة ، نحتاج إلى إنشاء Wiring
عامل على الجانب ورميها بجميع أنواعها ... دعونا نفعل ذلك:
أعلنا فئة الوظائف لدينا ، ورمي الأسلاك كحجة kwargs إضافية في جميع المشاكل. (لاحظ أنه ينشئ أسلاكًا جديدة في كل مرة ، لأنه لا يمكن إنشاء بعض العملاء قبل أن يحدث الشوكة داخل RQ قبل أن تبدأ المهمة في المعالجة.) حتى لا تعتمد جميع مهامنا على الأسلاك - أي على كل الكائنات - دعنا لنصنع ديكورًا لن يحصل إلا على الأسلاك الضرورية:
أضف ديكورًا إلى مهمتنا واستمتع بالحياة: import mistune from backend.storage.card import CardDAO from backend.tasks.task import task @task def parse_card_markup(card_dao: CardDAO, card_id: str): card = card_dao.get_by_id(card_id) card.html = _parse_markdown(card.markdown) card_dao.update(card) _parse_markdown = mistune.Markdown(escape=True, hard_wrap=False)
استمتع بالحياة؟ آه ، أردت أن أقول ، نبدأ العامل: $ docker-compose up worker ... Creating habr-app-demo_worker_1 ... done Attaching to habr-app-demo_worker_1 worker_1 | 17:21:03 RQ worker 'rq:worker:49a25686acc34cdfa322feb88a780f00' started, version 0.13.0 worker_1 | 17:21:03 *** Listening on tasks... worker_1 | 17:21:03 Cleaning registries for queue: tasks
ثالثا ... لا يفعل شيئا! بالطبع ، لأننا لم نحدد مهمة واحدة!دعنا نعيد كتابة أداتنا ، التي تنشئ بطاقة اختبار ، بحيث: (أ) لا تسقط إذا تم إنشاء البطاقة بالفعل (كما في حالتنا) ؛ ب) وضع مهمة على تحليل marqdown.
يمكن الآن تشغيل الأدوات ليس فقط على الواجهة الخلفية ، ولكن أيضًا على العامل. من حيث المبدأ ، الآن نحن لا نهتم. نطلقها docker-compose exec worker python -m tools.add_test_content
وفي علامة تبويب مجاورة للمحطة نرى معجزة - قام العامل بشيء ما! worker_1 | 17:34:26 tasks: backend.tasks.parse.parse_card_markup(card_id='5c715dd1e201ce000c6a89fa') (613b53b1-726b-47a4-9c7b-97cad26da1a5) worker_1 | 17:34:27 tasks: Job OK (613b53b1-726b-47a4-9c7b-97cad26da1a5) worker_1 | 17:34:27 Result is kept for 500 seconds
بعد إعادة إنشاء الحاوية باستخدام الواجهة الخلفية ، يمكننا في النهاية رؤية محتويات بطاقتنا في المتصفح:
الواجهة الأمامية للملاحة
قبل أن ننتقل إلى SSR ، نحتاج إلى جعل كل ما لدينا من ضجة مع React ذا معنى إلى حد ما على الأقل وجعل تطبيق صفحة واحدة صفحة واحدة حقًا. دعنا نقوم بتحديث أداتنا لإنشاء بطاقتين (ليس أحدهما ، واثنان! أم ، وأنا الآن أضطر إلى DATE DELELOPER!) بطاقات ترتبط ببعضها البعض ، ثم سنتعامل مع التنقل بينهما.الآن يمكننا اتباع الروابط والتفكير في كل مرة يتم فيها إعادة تشغيل تطبيقنا الرائع. توقف عن ذلك!أولاً ، ضع المعالج على نقرات على الروابط. نظرًا لأن HTML مع الروابط يأتي من الواجهة الخلفية ، والتطبيق هو مع React ، فإننا نحتاج إلى القليل من التركيز على React.
نظرًا لأن كل المنطق مع تحميل البطاقات في مكوننا CardPage
، في الإجراء نفسه (مذهل!) ، لا يلزم اتخاذ أي إجراء: export function navigate(link) { return { type: NAVIGATE, path: link.pathname } }
إضافة المخفض سخيفة لهذه الحالة:
منذ الآن يمكن أن تتغير حالة تطبيقنا ، CardPage
نحتاج إلى إضافة طريقة componentDidUpdate
مماثلة لتلك التي أضفناها بالفعل componentWillMount
. الآن ، بعد تحديث الخصائص CardPage
(على سبيل المثال ، الخصائص cardSlug
أثناء التنقل) ، سيتم أيضًا طلب محتوى البطاقة من الواجهة الخلفية ( componentWillMount
لم يحدث ذلك إلا عند تهيئة المكون).حسنا ، docker-compose up --build frontend
ولدينا الملاحة العمل!
سوف يلاحظ القارئ اليقظ أن عنوان URL للصفحة لن يتغير عند التنقل بين البطاقات - حتى في لقطة الشاشة التي نراها Hello ، البطاقة العالمية على عنوان البطاقة التجريبية. تبعا لذلك ، انخفض أيضا إلى الأمام إلى الوراء الملاحة. دعونا نضيف بعض السحر الأسود مع التاريخ على الفور لإصلاحه!أبسط شيء يمكنك القيام به هو إضافة إلى العمل.navigate
تحديا history.pushState
. export function navigate(link) { history.pushState(null, "", link.href); return { type: NAVIGATE, path: link.pathname } }
الآن ، عند النقر فوق الارتباطات ، سيتغير عنوان URL في شريط عنوان المتصفح. ومع ذلك ، فإن زر الظهر كسر !لجعله يعمل ، نحن بحاجة إلى الاستماع إلى الحدث popstate
الكائن window
. علاوة على ذلك ، إذا كنا نريد في هذا الحدث القيام بالتنقل للخلف وكذلك للأمام (أي ، خلال dispatch(navigate(...))
) ، فعلينا navigate
إضافة إشارة "عدم pushState
" خاصة إلى الوظيفة (وإلا فسيتم كسر كل شيء أكثر!). بالإضافة إلى ذلك ، للتمييز بين حالات "دولنا" ، يجب أن نستخدم القدرة pushState
على حفظ البيانات الوصفية. هناك الكثير من السحر والتصحيح ، لذلك دعونا نصل إلى الشفرة! إليك ما سيبدو عليه التطبيق:
وإليك الإجراء التنقل:
الآن ستنجح القصة.حسنًا ، اللمسة الأخيرة: نظرًا لأن لدينا الآن إجراء navigate
، فلماذا لا نتخلى عن الكود الإضافي في العميل الذي يحسب الحالة الأولية؟ يمكننا فقط الاتصال انتقل إلى الموقع الحالي:
نسخ لصق دمرت!الواجهة الأمامية: التقديم من جانب الخادم
حان الوقت لدينا رقائق (في رأيي) الرئيسية - سهولة كبار المسئولين الاقتصاديين. حتى تتمكن محركات البحث من فهرسة المحتوى الخاص بنا ، والذي تم إنشاؤه ديناميكيًا بالكامل في مكونات React ، نحتاج إلى أن نكون قادرين على منحهم نتيجة تقديم React ، وكذلك تعلم كيفية جعل هذه النتيجة تفاعلية مرة أخرى.المخطط العام بسيط. أولاً: نحتاج إلى إدراج HTML الذي تم إنشاؤه بواسطة مكون React في قالب HTML الخاص بنا App
. سيتم عرض HTML هذا بواسطة محركات البحث (والمتصفح مع JS متوقف ، hehe). ثانياً: إضافة علامة إلى القالب <script>
الذي يحفظ في مكان ما (على سبيل المثال ، كائن window
) تفريغ حالة تم تقديم HTML منه. ثم يمكننا على الفور تهيئة طلبنا على جانب العميل مع هذه الحالة وإظهار ما هو مطلوب (يمكننا حتى استخدام الهيدراتإلى HTML الذي تم إنشاؤه ، حتى لا تعيد إنشاء شجرة DOM للتطبيق).لنبدأ بكتابة دالة تُرجع HTML المقدم والحالة النهائية.
أضف وسيطات ومنطقًا جديدًا إلى النموذج الذي تحدثنا عنه أعلاه:
يصبح خادم Express أكثر تعقيدًا:
لكن العميل أسهل:
بعد ذلك ، تحتاج إلى تنظيف أخطاء الأنظمة الأساسية مثل "لم يتم تعريف السجل". للقيام بذلك ، أضف وظيفة بسيطة (حتى الآن) في مكان ما utility.js
.
بعد ذلك ، سيكون هناك عدد معين من التغييرات الروتينية التي لن أحملها هنا (ولكن يمكن العثور عليها في الالتزام المقابل ). نتيجة لذلك ، سيتمكن تطبيق React الخاص بنا من العرض في المستعرض وعلى الخادم.إنه يعمل!
ولكن هناك ، كما يقولون ، تحذير واحد ...
التحميل؟ كل ما تراه Google على خدمة الأزياء الرائعة لدي هو LOADING؟!حسنًا ، يبدو أن كل تزامننا قد لعب ضدنا. نحتاج الآن إلى طريقة للسماح للخادم بفهم أن الاستجابة من الخلفية مع محتوى البطاقة تحتاج إلى الانتظار قبل تقديم تطبيق React إلى سلسلة وإرسالها إلى العميل. ومن المرغوب فيه أن تكون هذه الطريقة عامة إلى حد ما.يمكن أن يكون هناك العديد من الحلول. تتمثل إحدى الطرق في وصف ملف منفصل للمسارات التي يجب تأمين البيانات الخاصة بها ، والقيام بذلك قبل تقديم التطبيق ( مقالة ). هذا الحل له العديد من المزايا. إنه بسيط وصريح ويعمل.كتجربة (يجب أن يكون المحتوى الأصلي في المقالة على الأقل في مكان ما!) أقترح مخططًا آخر. دعونا في كل مرة نقوم فيها بتشغيل شيء غير متزامن ، يجب أن ننتظره ، أضف الوعد المناسب (على سبيل المثال ، الوعد الذي يعيد جلبه) في مكان ما في ولايتنا. لذلك سيكون لدينا مكان حيث يمكنك دائمًا التحقق مما إذا كان كل شيء قد تم تنزيله أم لا.إضافة اثنين من الإجراءات الجديدة.
سيتم استدعاء الأول عندما يتم تشغيل الجلب ، والثاني - في نهايته .then()
.الآن إضافة معالجتها إلى المخفض:
الآن سنقوم بتحسين العمل fetchCard
:
يبقى لإضافة initialState
الوعود إلى مجموعة فارغة وجعل الخادم ننتظر منهم جميعا! تصبح وظيفة التجسيد غير متزامنة وتأخذ الشكل التالي:
بسبب عدم render
التزامن المكتسب ، يكون معالج الطلب أكثر تعقيدًا أيضًا:
إت فويلا!
استنتاج
كما ترون ، فإن إنشاء تطبيق عالي التقنية ليس بهذه البساطة. ولكن ليس من الصعب جدا! يوجد التطبيق النهائي في المستودع على Github ، ومن الناحية النظرية ، فأنت بحاجة فقط إلى Docker لتشغيله.إذا كان المقال مطلوبًا ، فلن يتم التخلي عن هذا المستودع! سنكون قادرين على النظر إليه بشيء من المعرفة الأخرى الضرورية:- تسجيل ، رصد ، اختبار الحمل.
- اختبار ، CI ، CD.
- ميزات أكثر برودة مثل التفويض أو البحث عن النص الكامل.
- إعداد وتطوير بيئة الإنتاج.
شكرا لاهتمامكم!