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

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

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

21741cd63b425aeb.png

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

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

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

החזית שתתקבל תיראה כך:

6a4d5e5603ba4b73.png

3 הדפים האלה הם דפי 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 ויוצרים פרויקט חדש או עושים שימוש חוזר בפרויקט קיים. אם אין לכם עדיין חשבון Gmail או חשבון Google Workspace, עליכם ליצור חשבון.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

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

הפעלת Cloud Shell

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

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

55efc1aaa7a4d3ad.png

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

7ffe5cbb04455448.png

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

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

ל-App Engine נדרש ממשק API של 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

ברמה הבסיסית (root) של הפרויקט שלנו יש 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: ה-framework של האינטרנט עבור 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. ה-API הזה יכלול את נקודות הקצה הבאות:

  • 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 שמגיעה ממודול ה-Node להעלאת קבצים. עכשיו, כשהקבצים זמינים במערכת הקבצים המקומית, אפשר להעלות את התמונות לקטגוריה של Cloud Storage. לבסוף, מפנים את המשתמש חזרה למסך הראשי של האפליקציה.

רישום התמונות

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

ב-handler של /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 מפנים ל-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

פריסה

פשוט מגדירים את האזור המועדף ל-App Engine, ומקפידים להשתמש באותו אזור בשיעורי ה-Lab הקודמים:

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 כדי לראות אם האפליקציה פרוסה ולגלות תכונות של App Engine כמו ניהול גרסאות ופיצול תנועה:

db0e196b00fceab1.png

8. בדיקת האפליקציה

כדי לבדוק זאת, יש לעבור לכתובת ה-URL של האפליקציה (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

השלבים הבאים