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

1. نظرة عامة

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

21741cd63b425aeb.png

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

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

  • صفحة رئيسية تعرض الصور المصغرة لجميع الصور التي تم تحميلها، بالإضافة إلى قائمة التصنيفات التي تصف الصورة (الصور التي اكتشفتها واجهة برمجة تطبيقات 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 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. تأكّد من أنّه مفعَّل:

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 من خلال البيانات الوصفية للصور،
  • storage: للوصول إلى 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 باستخدام طريقة HTTP POST وتنسيق متعدد الأجزاء. على 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 الواردة من وحدة "عقدة" تحميل الملفات. الآن وبعد أن أصبحت الملفات متاحة على نظام الملفات على الجهاز، يمكنك تحميل الصور إلى حزمة 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 Firestore

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