صورة يومية: التمرين المعملي 4: إنشاء واجهة أمامية للويب

1. نظرة عامة

في هذا الدرس التطبيقي حول الترميز، ستنشئ واجهة أمامية على الويب في Google App Engine، ما يتيح للمستخدمين تحميل الصور من تطبيق الويب، بالإضافة إلى تصفّح الصور المحمَّلة والصور المصغّرة الخاصة بها.

21741cd63b425aeb.png

سيستخدم تطبيق الويب هذا إطار عمل CSS يُسمى Bulma للحصول على واجهة مستخدم جذابة، كما سيستخدم إطار عمل Vue.JS JavaScript للواجهة الأمامية من أجل طلب واجهة برمجة التطبيقات التي ستنشئها.

سيتألف هذا التطبيق من ثلاث علامات تبويب:

  • صفحة رئيسية تعرض الصور المصغّرة لجميع الصور التي تم تحميلها، بالإضافة إلى قائمة بالتصنيفات التي تصف الصورة (التصنيفات التي رصدتها Cloud Vision API في معمل سابق).
  • صفحة صورة مجمّعة تعرض الصورة المجمّعة التي تم إنشاؤها من أحدث 4 صور تم تحميلها
  • صفحة تحميل، حيث يمكن للمستخدمين تحميل صور جديدة

يبدو الواجهة الأمامية الناتجة على النحو التالي:

6a4d5e5603ba4b73.png

هذه الصفحات الثلاث هي صفحات HTML بسيطة:

  • تطلب صفحة الصفحة الرئيسية (index.html) رمز الخلفية Node App Engine للحصول على قائمة بالصور المصغّرة وتصنيفاتها، وذلك من خلال طلب AJAX إلى عنوان URL ‏/api/pictures. تستخدم الصفحة الرئيسية Vue.js لجلب هذه البيانات.
  • تشير صفحة الصورة المجمّعة (collage.html) إلى الصورة collage.png التي تجمع أحدث 4 صور.
  • تقدّم صفحة التحميل (upload.html) نموذجًا بسيطًا لتحميل صورة من خلال طلب POST إلى عنوان URL /api/pictures.

أهداف الدورة التعليمية

  • App Engine
  • Cloud Storage
  • Cloud Firestore

2. الإعداد والمتطلبات

إعداد البيئة بالسرعة التي تناسبك

  1. سجِّل الدخول إلى Google Cloud Console وأنشِئ مشروعًا جديدًا أو أعِد استخدام مشروع حالي. إذا لم يكن لديك حساب على Gmail أو Google Workspace، عليك إنشاء حساب.

b35bf95b8bf3d5d8.pnga99b7ace416376c4.pngbd84a6d3004737c5.png

  • اسم المشروع هو الاسم المعروض للمشاركين في هذا المشروع. وهي سلسلة من الأحرف لا تستخدمها Google APIs، ويمكنك تعديلها في أي وقت.
  • يجب أن يكون رقم تعريف المشروع فريدًا في جميع مشاريع Google Cloud، كما أنّه غير قابل للتغيير (لا يمكن تغييره بعد ضبطه). تنشئ Cloud Console تلقائيًا سلسلة فريدة، ولا يهمّك عادةً ما هي. في معظم دروس البرمجة، عليك الرجوع إلى رقم تعريف المشروع (ويتم تحديده عادةً على أنّه PROJECT_ID)، لذا إذا لم يعجبك، يمكنك إنشاء رقم آخر عشوائي، أو يمكنك تجربة رقمك الخاص ومعرفة ما إذا كان متاحًا. ثم يتم "تجميده" بعد إنشاء المشروع.
  • هناك قيمة ثالثة، وهي رقم المشروع الذي تستخدمه بعض واجهات برمجة التطبيقات. يمكنك الاطّلاع على مزيد من المعلومات حول هذه القيم الثلاث في المستندات.
  1. بعد ذلك، عليك تفعيل الفوترة في Cloud Console من أجل استخدام موارد/واجهات برمجة تطبيقات Cloud. لن تكلفك تجربة هذا الدرس البرمجي الكثير من المال، إن وُجدت تكلفة على الإطلاق. لإيقاف الموارد كي لا يتم تحصيل رسوم منك بعد هذا الدرس التطبيقي حول الترميز، اتّبِع أي تعليمات "تنظيف" واردة في نهاية الدرس. يمكن لمستخدمي Google Cloud الجدد الاستفادة من برنامج الفترة التجريبية المجانية بقيمة 300 دولار أمريكي.

بدء Cloud Shell

على الرغم من إمكانية تشغيل Google Cloud عن بُعد من الكمبيوتر المحمول، ستستخدم في هذا الدرس التطبيقي حول الترميز Google Cloud Shell، وهي بيئة سطر أوامر تعمل في السحابة الإلكترونية.

من Google Cloud Console، انقر على رمز Cloud Shell في شريط الأدوات أعلى يسار الصفحة:

55efc1aaa7a4d3ad.png

لن يستغرق توفير البيئة والاتصال بها سوى بضع لحظات. عند الانتهاء، من المفترض أن يظهر لك ما يلي:

7ffe5cbb04455448.png

يتم تحميل هذه الآلة الافتراضية مزوّدة بكل أدوات التطوير التي ستحتاج إليها. توفّر هذه الخدمة دليلًا منزليًا دائمًا بسعة 5 غيغابايت، وتعمل على Google Cloud، ما يؤدي إلى تحسين أداء الشبكة والمصادقة بشكل كبير. يمكن إكمال جميع المهام في هذا التمرين المعملي باستخدام متصفّح فقط.

3- تفعيل واجهات برمجة التطبيقات

تتطلّب خدمة App Engine استخدام واجهة برمجة التطبيقات Compute Engine API. تأكَّد من تفعيلها:

gcloud services enable compute.googleapis.com

من المفترض أن تظهر لك رسالة تفيد باكتمال العملية بنجاح:

Operation "operations/acf.5c5ef4f6-f734-455d-b2f0-ee70b5a17322" finished successfully.

4. استنساخ الرمز

يمكنك الاطّلاع على الرمز إذا لم يسبق لك ذلك:

git clone https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop

يمكنك بعد ذلك الانتقال إلى الدليل الذي يحتوي على الواجهة الأمامية:

cd serverless-photosharing-workshop/frontend

سيكون لديك تخطيط الملف التالي للواجهة الأمامية:

frontend
 |
 ├── index.js
 ├── package.json
 ├── app.yaml
 |
 ├── public
      |
      ├── index.html
      ├── collage.html
      ├── upload.html
      |
      ├── app.js
      ├── script.js
      ├── style.css

في جذر مشروعنا، لديك 3 ملفات:

  • يحتوي index.js على رمز Node.js
  • تحدّد السمة package.json تبعيات المكتبة
  • ‫app.yaml هو ملف الإعداد الخاص بـ Google App Engine

يحتوي مجلد public على الموارد الثابتة التالية:

  • index.html هي الصفحة التي تعرض كل الصور المصغّرة والتصنيفات
  • تعرض collage.html الصورة المجمّعة للصور الحديثة
  • يتضمّن upload.html نموذجًا لتحميل صور جديدة
  • تستخدم app.js مكتبة Vue.js لملء صفحة index.html بالبيانات.
  • تعالج script.js قائمة التنقّل ورمز "الهامبرغر" الخاص بها على الشاشات الصغيرة
  • تحدّد style.css بعض توجيهات CSS

5- استكشاف الرمز

الاعتمادية

يحدّد ملف package.json الاعتمادات المطلوبة للمكتبة:

{
  "name": "frontend",
  "version": "0.0.1",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "@google-cloud/firestore": "^3.4.1",
    "@google-cloud/storage": "^4.0.0",
    "express": "^4.16.4",
    "dayjs": "^1.8.22",
    "bluebird": "^3.5.0",
    "express-fileupload": "^1.1.6"
  }
}

يعتمد تطبيقنا على ما يلي:

  • ‫firestore: للوصول إلى Cloud Firestore باستخدام البيانات الوصفية للصور
  • مساحة التخزين: للوصول إلى Google Cloud Storage حيث يتم تخزين الصور
  • express: إطار عمل الويب لـ Node.js
  • dayjs: مكتبة صغيرة لعرض التواريخ بطريقة سهلة الاستخدام
  • ‫bluebird: مكتبة JavaScript للوعود
  • express-fileupload: مكتبة للتعامل مع عمليات تحميل الملفات بسهولة

الواجهة الأمامية Express

في بداية وحدة التحكّم index.js، ستحتاج إلى جميع العناصر التابعة المحدّدة في package.json سابقًا:

const express = require('express');
const fileUpload = require('express-fileupload');
const Firestore = require('@google-cloud/firestore');
const Promise = require("bluebird");
const {Storage} = require('@google-cloud/storage');
const storage = new Storage();
const path = require('path');
const dayjs = require('dayjs');
const relativeTime = require('dayjs/plugin/relativeTime')
dayjs.extend(relativeTime)

بعد ذلك، يتم إنشاء مثيل تطبيق Express.

يتم استخدام نوعَين من البرامج الوسيطة Express:

  • تشير المكالمة express.static() إلى أنّ الموارد الثابتة ستكون متاحة في الدليل الفرعي public.
  • ويضبط fileUpload() عملية تحميل الملفات بحيث لا يتجاوز حجم الملف 10 ميغابايت، وذلك لتحميل الملفات محليًا في نظام الملفات داخل الذاكرة في الدليل /tmp.
const app = express();
app.use(express.static('public'));
app.use(fileUpload({
    limits: { fileSize: 10 * 1024 * 1024 },
    useTempFiles : true,
    tempFileDir : '/tmp/'
}))

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

  • POST /api/pictures من خلال النموذج في upload.html، سيتم تحميل الصور عبر طلب POST
  • GET /api/pictures تعرض نقطة النهاية هذه مستند JSON يحتوي على قائمة بالصور وتصنيفاتها
  • GET /api/pictures/:name يعيد عنوان URL هذا التوجيه إلى موقع التخزين السحابي للصورة بالحجم الكامل
  • GET /api/thumbnails/:name يعيد توجيه عنوان URL هذا إلى موقع الصورة المصغّرة في السحابة الإلكترونية
  • GET /api/collage يعيد توجيه عنوان URL الأخير هذا إلى موقع التخزين السحابي لصورة الكولاج التي تم إنشاؤها

تحميل صورة

قبل استكشاف رمز Node.js لتحميل الصور، ألقِ نظرة سريعة على public/upload.html.

... 
<form method="POST" action="/api/pictures" enctype="multipart/form-data">
    ... 
    <input type="file" name="pictures">
    <button>Submit</button>
    ... 
</form>
... 

يشير عنصر النموذج إلى نقطة النهاية /api/pictures، مع طريقة POST لبروتوكول HTTP وتنسيق متعدد الأجزاء. على index.js الآن الاستجابة لنقطة النهاية والطريقة هذه، واستخراج الملفات:

app.post('/api/pictures', async (req, res) => {
    if (!req.files || Object.keys(req.files).length === 0) {
        console.log("No file uploaded");
        return res.status(400).send('No file was uploaded.');
    }
    console.log(`Receiving files ${JSON.stringify(req.files.pictures)}`);

    const pics = Array.isArray(req.files.pictures) ? req.files.pictures : [req.files.pictures];

    pics.forEach(async (pic) => {
        console.log('Storing file', pic.name);
        const newPicture = path.resolve('/tmp', pic.name);
        await pic.mv(newPicture);

        const pictureBucket = storage.bucket(process.env.BUCKET_PICTURES);
        await pictureBucket.upload(newPicture, { resumable: false });
    });


    res.redirect('/');
});

عليك أولاً التأكّد من أنّه يتم تحميل الملفات بالفعل. بعد ذلك، يمكنك تنزيل الملفات على جهازك باستخدام طريقة mv التي تأتي من وحدة Node لتحميل الملفات. بعد أن أصبحت الملفات متاحة على نظام الملفات المحلي، يمكنك تحميل الصور إلى حزمة Cloud Storage. أخيرًا، عليك إعادة توجيه المستخدم إلى الشاشة الرئيسية للتطبيق.

عرض الصور

حان الوقت لعرض صورك الجميلة!

في معالج /api/pictures، يمكنك البحث في مجموعة pictures في قاعدة بيانات Firestore لاسترداد جميع الصور (التي تم إنشاء صورها المصغّرة)، مرتّبة حسب تاريخ الإنشاء من الأحدث إلى الأقدم.

يمكنك إرسال كل صورة في مصفوفة JavaScript، مع اسمها والتصنيفات التي تصفها (المستمدة من Cloud Vision API) واللون السائد وتاريخ الإنشاء المناسب (باستخدام dayjs، يمكننا استخدام إزاحات الوقت النسبية، مثل "بعد 3 أيام").

app.get('/api/pictures', async (req, res) => {
    console.log('Retrieving list of pictures');

    const thumbnails = [];
    const pictureStore = new Firestore().collection('pictures');
    const snapshot = await pictureStore
        .where('thumbnail', '==', true)
        .orderBy('created', 'desc').get();

    if (snapshot.empty) {
        console.log('No pictures found');
    } else {
        snapshot.forEach(doc => {
            const pic = doc.data();
            thumbnails.push({
                name: doc.id,
                labels: pic.labels,
                color: pic.color,
                created: dayjs(pic.created.toDate()).fromNow()
            });
        });
    }
    console.table(thumbnails);
    res.send(thumbnails);
});

تعرض وحدة التحكّم هذه نتائج بالشكل التالي:

[
   {
      "name": "IMG_20180423_163745.jpg",
      "labels": [
         "Dish",
         "Food",
         "Cuisine",
         "Ingredient",
         "Orange chicken",
         "Produce",
         "Meat",
         "Staple food"
      ],
      "color": "#e78012",
      "created": "a day ago"
   },
   ...
]

يتم استهلاك بنية البيانات هذه من خلال مقتطف صغير من Vue.js من صفحة index.html. في ما يلي نسخة مبسطة من الترميز من تلك الصفحة:

<div id="app">
        <div class="container" id="app">
                <div id="picture-grid">
                        <div class="card" v-for="pic in pictures">
                                <div class="card-content">
                                        <div class="content">
                                                <div class="image-border" :style="{ 'border-color': pic.color }">
                                                        <a :href="'/api/pictures/' + pic.name">
                                                                <img :src="'/api/thumbnails/' + pic.name">
                                                        </a>
                                                </div>
                                                <a class="panel-block" v-for="label in pic.labels" :href="'/?q=' + label">
                                                        <span class="panel-icon">
                                                                <i class="fas fa-bookmark"></i> &nbsp;
                                                        </span>
                                                        {{ label }}
                                                </a>
                                        </div>
                                </div>
                        </div>
            </div>
        </div>
</div>

سيشير معرّف div إلى Vue.js بأنّه جزء من الترميز الذي سيتم عرضه ديناميكيًا. يتم تنفيذ التكرارات بفضل توجيهات v-for.

تتضمّن الصور حدودًا ملونة جميلة تتوافق مع اللون السائد في الصورة، كما تم رصده من خلال Cloud Vision API، ونشير إلى الصور المصغّرة والصور بملء الشاشة في مصادر الروابط والصور.

أخيرًا، ندرج التصنيفات التي تصف الصورة.

إليك رمز JavaScript لمقتطف Vue.js (في ملف public/app.js الذي تم استيراده في أسفل صفحة index.html):

var app = new Vue({
  el: '#app',
  data() {
    return { pictures: [] }
  },
  mounted() {
    axios
      .get('/api/pictures')
      .then(response => { this.pictures = response.data })
  }
})

يستخدم رمز Vue مكتبة Axios لإجراء طلب AJAX إلى نقطة النهاية /api/pictures. يتم بعد ذلك ربط البيانات التي تم إرجاعها برمز العرض في الترميز الذي رأيته سابقًا.

عرض الصور

من index.html، يمكن للمستخدمين عرض الصور المصغّرة للصور والنقر عليها لعرض الصور بالحجم الكامل، ومن collage.html، يمكن للمستخدمين عرض صورة collage.png.

في ترميز HTML الخاص بهذه الصفحات، تشير الصورة src والرابط href إلى نقاط النهاية الثلاث هذه، والتي تعيد التوجيه إلى مواقع Cloud Storage الخاصة بالصور والصور المصغّرة والصور المجمّعة. ليس عليك ترميز المسار بشكل ثابت في ترميز HTML.

app.get('/api/pictures/:name', async (req, res) => {
    res.redirect(`https://storage.cloud.google.com/${process.env.BUCKET_PICTURES}/${req.params.name}`);
});

app.get('/api/thumbnails/:name', async (req, res) => {
    res.redirect(`https://storage.cloud.google.com/${process.env.BUCKET_THUMBNAILS}/${req.params.name}`);
});

app.get('/api/collage', async (req, res) => {
    res.redirect(`https://storage.cloud.google.com/${process.env.BUCKET_THUMBNAILS}/collage.png`);
});

تشغيل تطبيق Node

بعد تحديد جميع نقاط النهاية، يصبح تطبيق Node.js جاهزًا للتشغيل. يستجيب تطبيق Express للمنفذ 8080 تلقائيًا، وهو جاهز للتعامل مع الطلبات الواردة.

const PORT = process.env.PORT || 8080;

app.listen(PORT, () => {
    console.log(`Started web frontend service on port ${PORT}`);
    console.log(`- Pictures bucket = ${process.env.BUCKET_PICTURES}`);
    console.log(`- Thumbnails bucket = ${process.env.BUCKET_THUMBNAILS}`);
});

6. الاختبار محليًا

اختبِر الرمز برمجيًا على جهازك للتأكّد من أنّه يعمل قبل نشره على السحابة الإلكترونية.

عليك تصدير متغيرَي البيئة المرتبطَين بحزمتَي Cloud Storage:

export BUCKET_THUMBNAILS=thumbnails-${GOOGLE_CLOUD_PROJECT}
export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}

داخل المجلد frontend، ثبِّت تبعيات npm وابدأ الخادم:

npm install; npm start

إذا سارت الأمور على ما يرام، من المفترض أن يبدأ الخادم على المنفذ 8080:

Started web frontend service on port 8080
- Pictures bucket = uploaded-pictures-${GOOGLE_CLOUD_PROJECT}
- Thumbnails bucket = thumbnails-${GOOGLE_CLOUD_PROJECT}

ستظهر الأسماء الحقيقية للحِزم في هذه السجلات، ما يساعد في تصحيح الأخطاء.

من Cloud Shell، يمكنك استخدام ميزة "معاينة الويب" لتصفّح التطبيق الذي يتم تشغيله محليًا:

82fa3266d48c0d0a.png

استخدِم CTRL-C للخروج.

7. النشر على App Engine

تطبيقك جاهز للنشر.

ضبط App Engine

افحص ملف الإعداد app.yaml الخاص بـ App Engine:

runtime: nodejs16
env_variables:
  BUCKET_PICTURES: uploaded-pictures-GOOGLE_CLOUD_PROJECT
  BUCKET_THUMBNAILS: thumbnails-GOOGLE_CLOUD_PROJECT

يشير السطر الأول إلى أنّ وقت التشغيل يستند إلى Node.js 10. يتم تحديد متغيرَي بيئة للإشارة إلى الحزمتَين، إحداهما للصور الأصلية والأخرى للصور المصغّرة.

لاستبدال GOOGLE_CLOUD_PROJECT برقم تعريف مشروعك الفعلي، يمكنك تنفيذ الأمر التالي:

sed -i -e "s/GOOGLE_CLOUD_PROJECT/${GOOGLE_CLOUD_PROJECT}/" app.yaml

تفعيل

اضبط منطقتك المفضّلة في App Engine، وتأكَّد من استخدام المنطقة نفسها في المختبرات السابقة:

gcloud config set compute/region europe-west1

ونشرها:

gcloud app deploy

بعد دقيقة أو دقيقتَين، سيتم إعلامك بأنّ التطبيق يعرض عددًا كبيرًا من الزيارات:

Beginning deployment of service [default]...
╔════════════════════════════════════════════════════════════╗
╠═ Uploading 8 files to Google Cloud Storage                ═╣
╚════════════════════════════════════════════════════════════╝
File upload done.
Updating service [default]...done.
Setting traffic split for service [default]...done.
Deployed service [default] to [https://GOOGLE_CLOUD_PROJECT.appspot.com]
You can stream logs from the command line by running:
  $ gcloud app logs tail -s default
To view your application in the web browser run:
  $ gcloud app browse

يمكنك أيضًا الانتقال إلى قسم App Engine في Cloud Console للتأكّد من نشر التطبيق واستكشاف ميزات App Engine، مثل التحكم في الإصدارات وتقسيم الزيارات:

db0e196b00fceab1.png

8. اختبار التطبيق

للاختبار، انتقِل إلى عنوان URL التلقائي لتطبيق App Engine (https://<YOUR_PROJECT_ID>.appspot.com/) ويجب أن ترى واجهة المستخدم الأمامية تعمل.

6a4d5e5603ba4b73.png

9. التنظيف (اختياري)

إذا لم تكن تنوي الاحتفاظ بالتطبيق، يمكنك تنظيف الموارد لتوفير التكاليف ولتكون مواطنًا جيدًا للخدمات السحابية بشكل عام من خلال حذف المشروع بأكمله:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

10. تهانينا!

تهانينا! يربط تطبيق الويب Node.js المستضاف على App Engine جميع خدماتك معًا، ويتيح للمستخدمين تحميل الصور وعرضها.

المواضيع التي تناولناها

  • App Engine
  • Cloud Storage
  • Cloud Firestore

الخطوات التالية