• בלוג
  • כיצד לכתוב מקצר כתובות URL פשוט באמצעות הספריה Mojolicious ושפת פרל

כיצד לכתוב מקצר כתובות URL פשוט באמצעות הספריה Mojolicious ושפת פרל

09/03/2015

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

שפת הפיתוח היא פרל, זמן הלימוד קצר, ואני אמחיש באמצעות פיתוח URL Shortener פשוט בסביבה זו.

1. נתחיל ב HTML

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

See the Pen yyqWPM by ynonp (@ynonp) on CodePen.

2. בניית יישום Mojolicious

את הוראות ההתקנה של Mojolicious אתם יכולים למצוא באתר שלהם כאן:
http://mojolicio.us/

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

mojo generate lite_app my_tiny_url

הפקודה יוצרת קובץ אחד (כן, היישום כולו הוא קובץ אחד) וזה תוכנו:

#!/usr/bin/env perl
use Mojolicious::Lite;

# Documentation browser under "/perldoc"
plugin 'PODRenderer';

get '/' => sub {
  my $self = shift;
  $self->render('index');
};

app->start;
__DATA__

@@ index.html.ep
% layout 'default';
% title 'Welcome';
Welcome to the Mojolicious real-time web framework!

@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
  <head><title><%= title %></title></head>
  <body><%= content %></body>
</html>

הקובץ מורכב משני חלקים: בחלק הראשון נמצא קוד פרל ובחלק השני קבצי ה HTML של היישום. בניית היישום כקובץ אחד עובדת טוב כשמדובר בצד-שרת ליישומים קטנים או עבור ממשקי REST, ולכן מתאימה מעולה לאתגר שלנו. 

3. פיתוח הקוד עבור מקצר ה URL-ים

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

כדי לקצר URL-ים נוסיף עוד שני נתיבים, האחר שיקבל בקשות POST מהטופס ויוסיף את הנתיב לטבלת נתיבים מקוצרים (תשמר בתור Hash Map בצד השרת), והנתיב השני יהיה נתיב גנרי שיקבל כתובת קצרה, ישלוף את הכתובת הארוכה מתוך אותה ה Hash Map וישלח את הדפדפן לכתובת הארוכה. 

בניית נתיב POST דורשת קריאה לפונקציה post עם נתיב ופונקצייה לביצוע. אותה פונקציה לביצוע מקבלת בתור פרמטר ראשון אובייקט מידע מהסביבה באמצעותו אפשר לשלוף את פרטי הבקשה, פרמטרים, עוגיות, וגם להחליט כיצד להגיב. כך נראה הקוד:

post '/' => sub {
    my ($self) = @_;
    
    # fetch parameter url from request
    my $long = $self->param('url');
    
    # base64 encode
    chomp( my $short = b64_encode $idx );
    
    # remove trailing =
    $short =~ s/=*$//;
    $idx++;

    # add mapping to %urls hash
    $urls{$short} = $long;
    
	# render HTML passing the short URL we created and original one
	$self->render('index', result => $short, original => $long);
};

הקוד מתייחס למשתנה %urls שהוא אותו Hash שמחזיק את המיפויים, ובנוסף למשתנה $idx שהוא משתנה מספרי הגדל עם כל תוספת מיפוי. כדי לקצר את הנתיב אנו מקודדים את המספר ל Base64 ומורידים את הריפוד (קידוד Base64 מסתיים בתווי =, שאינם נחוצים לנו כאן מאחר ואין כוונה לפענח מחרוזת זו). את שני המשתנים וגם את הפונציה b64_encode נצטרך להגדיר בראש הקובץ. כך ייראה ראש הקובץ לאחר העדכון:

#!/usr/bin/env perl
use Mojolicious::Lite;
use Mojo::Util qw/b64_encode b64_decode/;


# Documentation browser under "/perldoc"
plugin 'PODRenderer';

my $idx = 0;
my %urls;

 

4. תוספת נתיב תרגום כתובת

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

כך נראה קוד הנתיב:

get '/:url' => sub {
	my ($self) = @_;

	# get URL from path
	my $url = $self->param('url');

	# throw an exception if no mapping is found for the URL
	die "Missing url: $url (all = @{[Dumper(\%urls)]}" if ! exists $urls{$url};

	# send an HTTP Redirect to the long URL
	$self->redirect_to($urls{$url});
};

 

5. עדכון טופס ה HTML

בחלק האחרון ניקח את קובץ ה HTML שכתבנו ונשתול אותו בתוך היישום בחלק האחרון, במקום ה HTML המופיע שם. לקובץ זה נוסיף את שורת התוצאה במידה והגענו אליו דרך נתיב ה POST. הדרך של Mojolicious להוסיף מידע דינמי לתוך טופס היא באמצעות שפה הנקראת epl, שנראית כמו כל שפת Server Side Templates אחרת מלבד העובדה שהיא מבוססת פרל. כך ייראה עמוד ה HTML לאחר העדכון:

<!DOCTYPE html>
<html>
  <head>
	<title><%= title %></title>
	<style>
	form {
		width: 400px;
	}
	.control-group {
		margin: 20px;  
	}

	.control-group label {
		display:inline-block;
		min-width: 150px;  
	}

	input {
		width:140px;
	}

	input[type="submit"] {
		float:right;
		margin-right:80px;
	}
	</style>
	</head>
  <body>
			<form method="POST" action="/">
				<div class="control-group">
					<label for="long-url">Full Address</label>
					<input type="text" name="url" id="long-url" value="<%= defined(stash('original')) && stash('original') %>" />
				</div>
				<div class="control-group">
					<label for="short-url">Short URL (result)</label>
					<input type="text" id="short-url" readonly=yes value="<%= defined(stash('result')) && stash('result') %>" />
				</div>
				<input type="submit" value="Shorten" />
		</form>
	</body>
</html>

 

6. הקוד בפעולה

את הקוד אנו יכולים להריץ באמצעות הפקודה:

 perl my_tiny_url.pl daemon 

Mojolicious כבר כולל שרת המקשיב על פורט 3000 ומגיש את העמוד שלנו. למען השלמות הנה קוד היישום המלא כקובץ אחד בשפת פרל:

#!/usr/bin/env perl
use Mojolicious::Lite;
use Mojo::Util qw/b64_encode b64_decode/;


# Documentation browser under "/perldoc"
plugin 'PODRenderer';

my $idx = 0;
my %urls;

get '/' => sub {
  my $self = shift;
  $self->render('index');
};

post '/' => sub {
	my ($self) = @_;

	# fetch parameter url from request
	my $long = $self->param('url');

	# base64 encode
	chomp( my $short = b64_encode $idx );

	# remove trailing =
	$short =~ s/=*$//;
	$idx++;

	# add mapping to %urls hash
	$urls{$short} = $long;

	# render HTML passing the short URL we created and original one
	$self->render('index', result => $short, original => $long);
};

get '/:url' => sub {
	my ($self) = @_;

	# get URL from path
	my $url = $self->param('url');

	# throw an exception if no mapping is found for the URL
	die "Missing url: $url" if ! exists $urls{$url};

	# send an HTTP Redirect to the long URL
	$self->redirect_to($urls{$url});
};


app->start;
__DATA__

@@ index.html.ep
% layout 'default';
% title 'Welcome';
Welcome to the Mojolicious real-time web framework!

@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
  <head>
	<title><%= title %></title>
	<style>
	form {
		width: 400px;
	}
	.control-group {
		margin: 20px;  
	}

	.control-group label {
		display:inline-block;
		min-width: 150px;  
	}

	input {
		width:140px;
	}

	input[type="submit"] {
		float:right;
		margin-right:80px;
	}
	</style>
	</head>
  <body>
			<form method="POST" action="/">
				<div class="control-group">
					<label for="long-url">Full Address</label>
					<input type="text" name="url" id="long-url" value="<%= defined(stash('original')) && stash('original') %>" />
				</div>
				<div class="control-group">
					<label for="short-url">Short URL (result)</label>
					<input type="text" id="short-url" readonly=yes value="<%= defined(stash('result')) && stash('result') %>" />
				</div>
				<input type="submit" value="Shorten" />
		</form>
	</body>
</html>

 

7. שמירת המידע באופן קבוע בבסיס נתונים

יש לנו מקצר URL-ים עובד, אך המשתמשים עלולים להתרגז בכל פעם שנפעיל מחדש את השרת והם יאבדו את כל הכתובות שקוצרו (שהרי שמרנו את כל המידע במשתנה בזכרון). מאחר ואנחנו בפרל קל מאוד לעבור לשמור בבסיס נתונים באמצעות הוספת שאילתות ה SQL המתאימות עבור הוספת כתובת ושליפת כתובת שמורה. אנו נשתמש בבסיס נתונים SQLite שנשמר כקובץ. כדי לבנות את בסיס הנתונים הפעילו את הפקודות הבאות משורת הפקודה:

sqlite3 tinyurl.db
CREATE TABLE urls (long string, short string);

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

#!/usr/bin/env perl
use Mojolicious::Lite;
use Mojo::Util qw/b64_encode b64_decode/;
use DBI;


my $database = 'tinyurl.db';
my $data_source = "dbi:SQLite:dbname=$database";
my $dbh = DBI->connect(
	$data_source,
	undef,
	undef,
	{
		RaiseError => 1,
		PrintError => 0,
		AutoCommit => 1,
	}
);
my $add_sth = $dbh->prepare("INSERT INTO urls (short, long) VALUES(?,?)");
my $fetch_sth = $dbh->prepare("SELECT short,long FROM urls WHERE short = ?");

my ($idx) = $dbh->selectrow_array("SELECT count(*) FROM urls");

בשלב שני נעדכן את הפקודות בנתיבים. בנתיב הוספת קיצור יש להשתמש בפקודת ה INSERT שיצרנו במקום הוספה ל Hash Map:

post '/' => sub {
	my ($self) = @_;

	# fetch parameter url from request
	my $long = $self->param('url');

	# base64 encode
	chomp( my $short = b64_encode $idx );

	# remove trailing =
	$short =~ s/=*$//;
	$idx++;

	# add mapping to %urls hash
	$add_sth->execute($short, $long);

	# render HTML passing the short URL we created and original one
	$self->render('index', result => $short, original => $long);
};

ובנתיב שליפת קיצור יש להשתמש בשאילתת השליפה שבנינו:

get '/:url' => sub {
	my ($self) = @_;

	# get URL from path
	my $url = $self->param('url');

	$fetch_sth->execute($url);
	my $data_ref = $fetch_sth->fetchrow_hashref;

	# throw an exception if no mapping is found for the URL
	die "Missing url: $url" if ! $data_ref;

	# send an HTTP Redirect to the long URL
	$self->redirect_to($data_ref->{long});
};


קוד התוכנית המלא זמין בקישור הבא:
https://gist.github.com/ynonp/d7542ac943a0a7e9214d

8. לסיכום

הספרייה Mojolicious מאפשרת לכתוב יישומי צד-שרת וממשקי REST בקלות יחסית ובקובץ אחד בלבד אותו ניתן להפיץ לחברים. הספריה תומכת גם ביישומים מורכבים יותר ומאפשרת להפריד את קבצי ה HTML לתיקייה נפרדת, ואף לפצל את תוכנית הפרל לקבצים ותיקיות נפרדים במודל MVC עבור יישומים מורכבים. הספריה מגיעה עם תמיכה מלאה ב Web Sockets ולא תלויה באף ספריה אחרת כדי לעבוד. 

ספריית Mojolicious לא המציאה את קוד השרת הרזה. למעשה, אנו מוצאים ספריות קוד צד שרת המספקות ממשק דומה בהרבה שפות, בין המפורסמות תמצאו את Sinatra של רובי, Express של Node.js, הספרייה Spark של Java, ננסי הכתובה בשפת C# וכמובן Flask בשפת פייתון. ספריות אלו לא מתאימות לכל יישום, אבל ככל שהמורכבות בצד הלקוח עולה, נחמד לגלות שאפשר לכתוב קוד צד-שרת פשוט שיעשה את העבודה בכל שפת תכנות שנבחר.

אפשר לקרוא עוד על Mojolicious באתר הבית של הפרויקט:
http://mojolicio.us/

ובקישור הבא תמצאו מדריך צעד-אחר-צעד לבניית אפליקצייה מלאה באמצעות פרל ו Mojolicious:
http://www.oliverguenther.de/2014/04/applications-with-mojolicious-part-one-introduction/