תמונה יומית: שיעור Lab 4 – יצירת ממשק משתמש באינטרנט

1. סקירה כללית

ב-codelab הזה, תיצרו קצה קדמי (frontend) לאתר ב-Google App Engine, שיאפשר למשתמשים להעלות תמונות מאפליקציית האינטרנט, וגם לעיין בתמונות שהועלו ובמיניאטורות שלהן.

21741cd63b425aeb.png

באפליקציית האינטרנט הזו נשתמש ב-framework של CSS שנקרא Bulma, כדי ליצור ממשק משתמש שנראה טוב, וגם ב-framework של JavaScript frontend שנקרא Vue.JS, כדי לקרוא ל-API של האפליקציה שתבנו.

האפליקציה הזו תכלול שלוש כרטיסיות:

  • דף הבית שבו יוצגו התמונות הממוזערות של כל התמונות שהועלו, יחד עם רשימת התוויות שמתארות את התמונה (התוויות שזוהו על ידי Cloud Vision API במעבדה הקודמת).
  • דף קולאז' שבו יוצג הקולאז' שנוצר מ-4 התמונות האחרונות שהועלו.
  • דף העלאה, שבו משתמשים יכולים להעלות תמונות חדשות.

ממשק הקצה שיתקבל ייראה כך:

6a4d5e5603ba4b73.png

שלושת הדפים האלה הם דפי HTML פשוטים:

  • בדף הבית (index.html) מתבצעת קריאה לקוד ה-backend של Node App Engine כדי לקבל את רשימת התמונות הממוזערות והתוויות שלהן, באמצעות קריאת AJAX לכתובת ה-URL‏ /api/pictures. בדף הבית נעשה שימוש ב-Vue.js לאחזור הנתונים האלה.
  • העמוד collage (collage.html) מצביע על התמונה collage.png שכוללת את 4 התמונות האחרונות.
  • בדף upload (העלאה) (upload.html) יש טופס פשוט להעלאת תמונה באמצעות בקשת POST לכתובת ה-URL /api/pictures.

מה תלמדו

  • App Engine
  • Cloud Storage
  • Cloud Firestore

2. הגדרה ודרישות

הגדרת סביבה בקצב אישי

  1. נכנסים ל-מסוף Google Cloud ויוצרים פרויקט חדש או משתמשים בפרויקט קיים. אם עדיין אין לכם חשבון Gmail או Google Workspace, אתם צריכים ליצור חשבון.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • שם הפרויקט הוא השם המוצג של הפרויקט הזה למשתתפים. זו מחרוזת של תווים שלא נמצאת בשימוש ב-Google APIs, ואפשר לעדכן אותה בכל שלב.
  • מזהה הפרויקט חייב להיות ייחודי בכל הפרויקטים ב-Google Cloud, והוא קבוע (אי אפשר לשנות אותו אחרי שמגדירים אותו). מסוף Cloud יוצר באופן אוטומטי מחרוזת ייחודית. בדרך כלל לא צריך להתייחס אליה. ברוב סדנאות ה-Codelab, צריך להפנות למזהה הפרויקט (ובדרך כלל הוא מזוהה כ-PROJECT_ID), אז אם לא מוצא חן בעיניכם, אפשר ליצור מזהה אקראי אחר, או לנסות מזהה משלכם ולבדוק אם הוא זמין. אחרי שהפרויקט נוצר, הוא 'קפוא'.
  • יש ערך שלישי, מספר פרויקט, שחלק מממשקי ה-API משתמשים בו. במאמרי העזרה מפורט מידע נוסף על שלושת הערכים האלה.
  1. בשלב הבא, תצטרכו להפעיל את החיוב במסוף Cloud כדי להשתמש במשאבי Cloud או בממשקי API. העלות של התרגול הזה לא אמורה להיות גבוהה, ואולי אפילו לא תצטרכו לשלם בכלל. כדי לכבות את המשאבים ולא לחייב אתכם מעבר למה שמוסבר במדריך הזה, צריך לפעול לפי ההוראות לניקוי שמופיעות בסוף ה-Codelab. משתמשים חדשים ב-Google Cloud זכאים לתוכנית תקופת ניסיון בחינם בשווי 300$.

מפעילים את Cloud Shell

אפשר להפעיל את Google Cloud מרחוק מהמחשב הנייד, אבל ב-codelab הזה תשתמשו ב-Google Cloud Shell, סביבת שורת פקודה שפועלת בענן.

ב-מסוף Google Cloud, לוחצים על סמל Cloud Shell בסרגל הכלים שבפינה הימנית העליונה:

55efc1aaa7a4d3ad.png

יחלפו כמה רגעים עד שההקצאה והחיבור לסביבת העבודה יושלמו. בסיום התהליך, אמור להופיע משהו כזה:

7ffe5cbb04455448.png

המכונה הווירטואלית הזו כוללת את כל הכלים שדרושים למפתחים. יש בה ספריית בית בנפח מתמיד של 5GB והיא פועלת ב-Google Cloud, מה שמשפר מאוד את הביצועים והאימות ברשת. אפשר לבצע את כל העבודה ב-Lab הזה רק באמצעות דפדפן.

3. הפעלת ממשקי ה-API

‫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() מגדיר העלאת קבצים כדי להגביל את גודל הקובץ ל-10MB, כדי להעלות את הקבצים באופן מקומי במערכת הקבצים בזיכרון בספרייה /tmp.
const app = express();
app.use(express.static('public'));
app.use(fileUpload({
    limits: { fileSize: 10 * 1024 * 1024 },
    useTempFiles : true,
    tempFileDir : '/tmp/'
}))

בין המשאבים הסטטיים, יש קובצי HTML של דף הבית, דף הקולאז' ודף ההעלאה. הדפים האלה יקראו ל-API backend. ל-API הזה יהיו נקודות הקצה הבאות:

  • POST /api/pictures באמצעות הטופס בקובץ upload.html, התמונות יועלו באמצעות בקשת POST
  • GET /api/pictures נקודת הקצה הזו מחזירה מסמך JSON שמכיל את רשימת התמונות והתוויות שלהן
  • GET /api/pictures/:name כתובת ה-URL הזו מפנה למיקום ב-Cloud Storage של התמונה בגודל מלא
  • 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 שמגיעה ממודול Node להעלאת קבצים. עכשיו, כשהקבצים זמינים במערכת הקבצים המקומית, מעלים את התמונות לקטגוריה של Cloud Storage. לבסוף, מפנים את המשתמש בחזרה למסך הראשי של האפליקציה.

הצגת התמונות

הגיע הזמן להציג את התמונות היפות שלכם!

ב-handler‏ /api/pictures, בודקים את האוסף pictures במסד הנתונים של Firestore כדי לאחזר את כל התמונות (שנוצרו להן תמונות ממוזערות), לפי סדר יורד של תאריך היצירה.

אתם מעבירים כל תמונה למערך JavaScript, עם השם שלה, התוויות שמתארות אותה (מ-Cloud Vision API), הצבע הדומיננטי ותאריך היצירה (עם dayjs, אנחנו משתמשים בהזחות זמן יחסיות כמו "3 days from now").

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 שזה החלק של ה-markup שיוצג באופן דינמי. האיטרציות מתבצעות הודות להוראות 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 מפנים ל-3 נקודות הקצה האלה, שמפנות מחדש למיקומים ב-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

Deploy

מגדירים את האזור המועדף ב-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

השלבים הבאים