صورة يومية: التمرين المعملي 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.png

a99b7ace416376c4.png

bd84a6d3004737c5.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: مكتبة للتعامل مع عمليات تحميل الملفات بسهولة

الواجهة الأمامية السريعة

في بداية وحدة التحكّم 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

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