קוד צד השרת

ישומים רבים כוללים רכיבי צד שרת כדי לסנכרן בין מספר משתמשים או כדי לחשוף רק חלק מהמידע שנשמר לכל משתמש.

1. תפקיד צד-השרת

ביישום שרת/לקוח השרת משמש לסנכרן בין המשתמשים או לקבל החלטות שלא כדאי לקבל אותן על מכונת הלקוח, מטעמי אבטחה או ביצועים. הידע והחומרה העדיפה של השרת מאפשרים חסכון בזמן חישוב, מאחר ואפשר לשתף מידע שחושב למספר משתמשים.

ביישומים מסורתיים קוד צד השרת וצד הלקוח נכתבו בצורה מעורבבת בטכנולוגיה שנקראה בשם כללי שפת תבניות. הרעיון הוא שבתוך קובץ אחד אנו רושמים את קוד ה HTML שלנו וכשצריך להריץ קוד צד-שרת אנו ״בורחים״ מה HTML לשפה אחרת (למשל PHP). עמוד ה HTML שישלח ללקוח לא יישלח כפי שכתבנו אותו אלא יעבור פענוח: כל פקודה בשפת צד השרת תבוצע ותוחלף בתוצאת הפעלתה.

כך למשל נראה קוד PHP שמדפיס את התאריך הנוכחי על השרת במספר פורמטים:

<!DOCTYPE html>
<html>
<body>

<?php
echo "Today is " . date("Y/m/d") . "<br>";
echo "Today is " . date("Y.m.d") . "<br>";
echo "Today is " . date("Y-m-d") . "<br>";
echo "Today is " . date("l");
?>

</body>
</html>

כשהלקוח פונה לשרת ומבקש את קובץ ה HTML הקובץ שנשלח נראה כך:

<!DOCTYPE html>
<html>
<body>

Today is 2015/12/06<br>Today is 2015.12.06<br>Today is 2015-12-06<br>Today is Sunday
</body>
</html>

החסרון העיקרי של גישה זו היא שהצגת מידע נוסף מחייבת החלפה של עמוד ה HTML במלואו בעמוד חדש שמגיע מהשרת ולכן מעברי העמודים במערכת נתפשים כאיטיים. חסרון נוסף הוא שאם נרצה לקבל את המידע עבור יישום שאיננו לקוח Web, למשל עבור אפליקציית Mobile, נצטרך לכתוב קוד נוסף בצד השרת.

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

בדוגמת ה PHP שראינו, קל לדמיין קוד צד-שרת שבמקום לשלוח עמוד HTML מלא ישלח רק את מחרוזת התאריך בשלושת הפורמטים, וקוד צד-לקוח יציג את המחרוזות.

2. שרת REST פשוט

הגישה החדשה לפיתוח שרתים התאפיינה בהסתכלות על השרת כמיפוי: לקוח מבקש משאב באמצעות פועל ונתיב והשרת מחזיר את המידע בפורמט שנקרא JSON. הפורמט אגב נבחר כדי להקל על לקוחות המיושמים ב JavaScript והוא נראה בדיוק כמו הגדרת מערך או אובייקט ב JavaScript (ולכן שמו JSON, קיצור של JavaScript Object Notation).

הפועל נלקח מפרוטוקול HTTP, כאשר הפעלים העיקריים הינם GET, PUT, POST, DELETE. נתיב הינו פשוט מחרוזת שמייצגת משאב או מידע בצד השרת. ניקח לדוגמא יישום שמנהל אנשי קשר. ביישום זה הלקוח עשוי לפנות לשרת כדי לקבל מידע על אנשי הקשר השמורים, לקוח יכול ליצור איש קשר חדש ויכול לקבל מידע מפורט על איש קשר מסוים. תפקיד קוד צד השרת הוא לענות על הבקשות ולהחזיר מידע בלבד ללא תגיות HTML.

שפות תכנות רבות מאפשרות כתיבה של שרתי REST. אני בחרתי לבנות את הדוגמא בשפת JavaScript ובסביבת הרצה שנקראת Node.js. הסביבה מאפשרת פיתוח קוד צד-שרת ב JavaScript. קוד זה לא רץ בתוך דפדפן אלא מתוך שורת הפקודה:

var express = require('express');
var _ = require('underscore');
var app = express();
var bodyParser = require('body-parser')


app.use(express.static('client'));
app.use(bodyParser.json());

var contacts = [
  { id: 0, name: 'Joe',  email: 'joe@gmail.com' },
  { id: 1, name: 'Jane', email: 'jane@yahoo.com' },
  { id: 2, name: 'Bob',  email: 'bob@gmail.com' },
];

app.get('/contacts', function(req, res) {
  var data = contacts.map(function(item) {
    return _.pick(item, 'name', 'id');
  });

  res.send(data);
});

app.get('/contacts/:id', function(req, res) {
  var id = req.params.id;
  var item = _.findWhere(contacts, { id: Number(id) });
  res.send(item);
});

app.post('/contacts', function(req, res) {
  var name  = req.body.name;
  var email = req.body.email;
  var maxId = _.max(contacts, function(item) { return item.id; });
  var nextId = maxId.id + 1;

  contacts.push({ id: nextId, name: name, email: email });
  res.send('null');
  res.status(201).end();  
});

app.delete('/contacts/:id', function(req, res) {
  var id = req.params.id;
  var idx = _.findIndex(contacts, { id: id });
  if ( idx >= 0 ) {
    contacts.splice(1, 1);
  }
  res.send('null');
  res.status(200).end();
});

var port = process.env.BASKET_APP_PORT || 3030;
app.listen(port);


השרת ממתין לבקשות שמגיעות ברשת ומהווה מעין מיפוי בין פעולות לתוצאות, כאשר כל פעולה מאופיינת באמצעות פועל ונתיב. נתחיל  בפעולה GET /contacts שמוגדרת בבלוק הראשון: בכל פעם שלקוח פונה לפעולה זו הוא יקבל את רשימת השמות והמזהים של אנשי הקשר במערכת. ניתן לראות את התוצאה מהדפדפן באמצעות גלישה לכתובת:

http://localhost:3030/contacts

ובתגובה נקבל מערך JSON הכולל את הפרטים ונראה כך:

[{"name":"Joe","id":0},{"name":"Jane","id":1},{"name":"Bob","id":2}]

הפעולה בבלוק הבא, GET /contacts/:id היא יותר מורכבת. פעולה זו מתאימה למספר נתיבים, בפרט כל נתיב שמתחיל ב /contacts ומסתיים במילה נוספת. המילה האחרונה נקראת id והיא בעצם מזהה של איש קשר במערכת. בלוק זה יופעל כתוצאה מגלישה לנתיבים:

http://localhost:3030/contacts/1
http://localhost:3030/contacts/2

בתגובה לפניה כזו השרת יחפש את איש הקשר לפי שדה id ויחזיר את המידע המלא אודות אותו איש קשר שנראה כך:

{"id":1,"name":"Jane","email":"jane@yahoo.com"}

פעולות נוספות המוגדרות בבלוקים הבאים הינן POST ו DELETE עבור יצירת איש קשר חדש או מחיקת איש קשר. אותן כבר לא נוכל לבדוק באמצעות הקלדה בשורת הכתובת של הדפדפן מאחר והקלדה שם שולחת בקשת GET בלבד.

בפרק הבא נלמד לכתוב לקוח JavaScript שיתקשר עם שרת זה, ושם נשתמש גם בפעולות אלו.

אם אתם לא מכירים node.js חלק מהקוד עשוי להיראות מוזר ולא מובן. זכרו שהמטרה כאן היא לא ללמוד node.js אלא להבין כיצד פועל שרת REST. שרת כזה יכול להיות כתוב במגוון שפות והבחירה ב node.js נעשתה רק לשם הנוחות כדי להשאר עם שפת התכנות JavaScript שאנחנו כבר מכירים.


קוד השרת שהוצג במדריך:

var express = require('express');
var _ = require('underscore');
var app = express();
var bodyParser = require('body-parser')


app.use(express.static('client'));
app.use(bodyParser.json());

var contacts = [
  { id: 0, name: 'Joe',  email: 'joe@gmail.com' },
  { id: 1, name: 'Jane', email: 'jane@yahoo.com' },
  { id: 2, name: 'Bob',  email: 'bob@gmail.com' },
];

app.get('/contacts', function(req, res) {
  var data = contacts.map(function(item) {
    return _.pick(item, 'name', 'id');
  });

  res.send(data);
});

app.get('/contacts/:id', function(req, res) {
  var id = req.params.id;
  var item = _.findWhere(contacts, { id: Number(id) });
  res.send(item);
});

app.post('/contacts', function(req, res) {
  var name  = req.body.name;
  var email = req.body.email;
  var maxId = _.max(contacts, function(item) { return item.id; });
  var nextId = maxId.id + 1;

  contacts.push({ id: nextId, name: name, email: email });
  res.send('null');
  res.status(201).end();  
});

app.delete('/contacts/:id', function(req, res) {
  var id = req.params.id;
  var idx = _.findIndex(contacts, { id: id });
  if ( idx >= 0 ) {
    contacts.splice(1, 1);
  }
  res.send('null');
  res.status(200).end();
});

var port = process.env.BASKET_APP_PORT || 3030;
app.listen(port);