מקרה לדוגמה – הורדת 'גרירה ושחרור' ב-Chrome

מבוא

גרירה ושחרור (DnD) היא אחת מהתכונות הנפלאות של HTML 5, והיא נתמכת ב-Firefox 3.5, ב-Safari, ב-Chrome וב-IE. לאחרונה Google השיקה תכונה חדשה שמאפשרת למשתמשי Google Chrome לגרור קבצים מהדפדפן ולשחרר אותם בשולחן העבודה. זו תכונה נוחה להפליא, אבל היא לא הייתה ידועה עד שריאן סדון פרסם מאמר על הגילויים של פיצ'ר ההנדסה הפוכה שפיתח.

אנחנו ב-Box.net נרגשים מאוד לראות איך היכולות החדשות האלה מאפשרות לנו לשפר את הפתרון שלנו לניהול תוכן בענן, וגם לתרום יותר לקהילת המפתחים. אני שמח לבשר ש-DnD Download שולבה במוצר שלנו. עכשיו משתמשי Box יכולים לגרור קבצים ישירות מדפדפן Chrome לשולחן העבודה שלהם כדי להוריד את הקובץ ולשמור אותו.

ברצוני לשתף אותך כיצד עברתי כמה איטרציות במהלך הפיתוח של תכונה חדשה זו.

חיפוש תמיכה ב-Drag and Drop

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

if (Modernizr.draganddrop) {
// Browser supports native HTML5 DnD.
} else {
// Fallback to a library solution.
}

איטרציה 1

ניסיתי תחילה את הגישה שסדון מצא ב-Gmail. הוספתי מאפיין חדש שנקרא 'data-downloadurl' לקישורי עוגן של קבצים. תהליך זה משתמש במאפייני נתונים מותאמים אישית של HTML5. ב-data-download URL, צריך לכלול את סוג ה-MIME של הקובץ, את שם קובץ היעד (שם הקובץ הרצוי של הקובץ שהורדתם) ואת כתובת ה-URL להורדת הקובץ. לכן הקוד מתווסף לתבנית ה-HTML:

<a href="#" class="dnd"
data-downloadurl="{$item.mime}:{$item.filename}:{$item.url}"></a>

שתפיק פלט כזה:

<a href="#" class="dnd" data-downloadurl=
"image/jpeg:Penguins.jpg:https://www.box.net/box_download_file?file_id=f66690"></a>

על סמך plugin של jQuery שפון שורש יצר, שמבוסס על המאמר של סדדון, הוספתי פלאגין של jQuery שמזהה קצת תכונות בדפדפן. השורות שהוספתי לגרסה של פון שורש מודגשות:

(function($) {

$.fn.extend({
dragout: function() {
var files = this;
if (files.length > 0) {
    $(files).each(function() {
    var url = (this.dataset && this.dataset.downloadurl) ||
                this.getAttribute("data-downloadurl");
    if (this.addEventListener) {
        this.addEventListener("dragstart", function(e) {
        if (e.dataTransfer && e.dataTransfer.constructor == Clipboard &&
            e.dataTransfer.setData('DownloadURL', 'http://www.box.net')) {
            e.dataTransfer.setData("DownloadURL", url);
        }
        },false);
    }
    });
}
}
});

})(jQuery);

הסיבה לכך היא שללא זיהוי הדפדפן קודם, ביצוע addEventListener() לרכיב HTML ב-IE ייצור שגיאת JavaScript כי IE משתמש בשיטה ייחודית התווספת event() . e.dataTransfer לא מוגדר ב-IE (נכון להיום), e.dataTransfer.constructor מחזיר את DataTransfer ב-Firefox (Mozilla), בעוד שדפדפני Webkit (Chrome ו-Safari) מטמיעים את מבנה הלוח. ב-Safari, e.dataTransfer.setData('DownloadURL','http://www.box.net') מחזירה false ו-Chrome מחזיר True להצהרה הזו. ביצוע כל הבדיקות שצוינו למעלה, משאיר את התכונה זמינה רק ב-Chrome. אני עשוי לטעון שאני יכול פשוט לבצע את הפעולות הבאות:

/chrome/.test( navigator.userAgent.toLowerCase() )

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

בעיות באיטרציה 1

1) מכיוון שכרגע יש לנו אפשרות להעביר או להעתיק קבצים בין תיקיות באמצעות DnD בדף, אנחנו צריכים דרך להבחין בין 'הורדת DnD' לבין 'DnD' בדף. מבחינה טכנית, אנחנו לא יכולים לשלב את שתי הפעולות האלה. אנחנו לא יכולים לחזות אם משתמש ירצה להעביר קובץ לתיקייה אחרת בחשבון Box.net או לגרור אותו לשולחן העבודה. שתי הפעולות האלה שונות לחלוטין. בנוסף, אין דרך קלה לזהות אם הסמן נמצא מחוץ לחלון הדפדפן. אפשר להשתמש ב-window.onmouseout (IE) וב-document.onmouseout (בדפדפנים אחרים) כדי לצרף למסמך אירוע mouseout ולבדוק אם e.relatedTarget.nodeName == "HTML" (e הוא האירוע mouseout או window.event, בהתאם לזמינות). אבל זה די קשה עקב בועות של האירוע. האירוע עשוי להתרחש באופן אקראי כשעוברים מעל תמונה או שכבה, במיוחד באפליקציית אינטרנט מורכבת כמו Box.net.

2) אנחנו רוצים שהמשתמש יבצע פעולה כלשהי באופן מפורש כדי למנוע ממנו לגרור בטעות משהו לשולחן העבודה. יכול להיות שעורך של תיקיית Box יוכל להעלות קובץ הפעלה שמבצע פעולה לא רצויה במחשב של מי שיוריד אותו. אנחנו רוצים שהמשתמש יידע בדיוק מתי תתבצע הורדת קובץ לשולחן העבודה.

איטרציה 2

החלטנו להתנסות ב-control + גרירה (גרירת קובץ בעת לחיצה על מקש Ctrl של Windows). הפעולה הזו תואמת לפעולות שאנשים יכולים לבצע בשולחן העבודה של Windows כדי לשכפל קובץ. נדרשת גם עבודה נוספת (אבל לא שלב נוסף) מהמשתמש כדי למנוע הורדה בטעות של קבצים.

הפלאגין jQuery באיטרציה 1 ננטש כעת מכיוון שאנו צריכים לשלב היטב את 'הורדת DnD' עם ה-DnD בדף. אם זה מעניין אותך, אנחנו משתמשים בגרסה מתוקנת של הפלאגין Draggable בממשק המשתמש של jQuery. בתוך אירוע mousedown של רכיב יעד, מוסיפים את הקוד הבא:

// DnD to desktop when the Ctrl key is pressed while dragging
if (e.ctrlKey) {
var that = $(e.target);
// make sure it is not IE (attachEvent).
if (that[0].addEventListener) {
    that[0].addEventListener("dragstart",function(e) {
        // e.dataTransfer in Firefox uses the DataTransfer constructor
        // instead of Clipboard
        // make sure it's Chrome and not Safari (both webkit-based).
        // setData on DownloadURL returns true on Chrome, and false on Safari
        if (e.dataTransfer && e.dataTransfer.constructor == Clipboard &&
            e.dataTransfer.setData('DownloadURL','http://www.box.net')) {
        var url = (this.dataset && this.dataset.downloadurl) ||
                    this.getAttribute("data-downloadurl");
        e.dataTransfer.setData("DownloadURL", url);
        }
    }, false);
    return;
}
}

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

בעיות באיטרציה 2

מטעמי אבטחה, Box.net לא חושף כתובות URL קבועות לגישה ישירה לקבצים סטטיים. שם האחסון לא ייחודי ל-Box.net. בשירותי אחסון אונליין אסור לחשוף כתובות URL קבועות ללא שכבת אבטחה נוספת, שבודקת אם הקובץ ציבורי ואם המשתמש עם ההרשאות המתאימות מבקש את ההורדה הרצויה.

כשעוקבים אחרי "כתובת URL להורדה" (למשל https://www.box.net/box_download_file?file_id=f_60466690) של פריט, מתקבל קוד הסטטוס 302 נמצא ומפנה לכתובת URL אקראית (למשל https://www.box.net/dl/6045?a=1f1207a084&m=168299,11211&t=2&b=aca15820d924e3b) שהיא "כתובת ה-URL בפועל" של הקובץ. האתגר הוא שהתוקף שלו פג בכל כמה דקות, ולכן לא מעשי להציב אותו בפלט ה-HTML. היא עשויה להחזיר את השגיאה '404' כשהמשתמש מנסה להוריד את הקובץ מהקישור בפלט ה-HTML שנוצר לפני מספר דקות.

הורדת DnD פועלת רק בכתובות URL שמפנות ישירות למשאב. במקרה של הפניה אוטומטית, בשלב הזה המערכת לא חכמה מספיק כדי לעקוב אחרי השרשרת (והיא לא אמורה לעקוב אף פעם אחרי השרשרת מטעמי אבטחה). לכן, למרות שהקישור https://www.box.net/box_download_file?file_id=f_60466690 שלמעלה מאפשר להוריד את הקובץ כשמזינים אותו בסרגל המיקום של הדפדפן, הוא לא יעבוד עם DnD.

כדי להמחיש טוב יותר את ההבדלים בין 'כתובת URL בפועל' לבין 'כתובת URL להפניה אוטומטית', אפשר לעיין בצילומי המסך:

כתובת URL להפניה אוטומטית 302
כתובת URL להפניה אוטומטית 302
כתובת URL בפועל
כתובת ה-URL בפועל

איטרציה 3

בוא ננסה את Ajax.

שינינו מעט את הקוד באיטרציה הקודמת, והגענו למסקנה הבאה:

// DnD to desktop when the Ctrl key is pressed while dragging
if (e.ctrlKey) {
var that = $(e.target);
// make sure it is not IE (attachEvent).
if (that[0].addEventListener) {
that[0].addEventListener("dragstart", function(e) {
    // e.dataTransfer in Firefox uses the DataTransfer constructor
    // instead of Clipboard
    // make sure it's Chrome and not Safari (both webkit-based).
    // setData on DownloadURL returns true on Chrome, and false on Safari
    if (e.dataTransfer && e.dataTransfer.constructor == Clipboard &&
        e.dataTransfer.setData('DownloadURL', 'http://www.box.net')) {
    var url = (this.dataset && this.dataset.downloadurl) ||
                this.getAttribute("data-downloadurl");
    $.ajax({
        complete: function(data) {
        e.dataTransfer.setData("DownloadURL", data.responseText);
        },
        type:'GET',
        url: url
    });
    }
}, false);
return;
}
}

זה הגיוני. לאחר הגרירה, הוא מבצע מיד קריאת Ajax לשרת כדי לאחזר את כתובת ה-URL העדכנית ביותר להורדה של הקובץ. עם זאת, זה לא עובד.

מסתבר שזו צריכה להיות שיחה סנכרונית (או כמו שאני אוהב לקרוא לזה, Sjax). נראה שצריך לבצע את setData בזמן צירוף ה-event listener. על פי ה-API של jQuery, השורות המודגשות הופכות:

$.ajax({
async: false,
complete: function(data) {
e.dataTransfer.setData("DownloadURL", data.responseText);
},
type: 'GET',
url: url
});

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

בטוח יותר לבצע את הפעולות הבאות:

$.ajax({
async: false,
complete: function(data) {
e.dataTransfer.setData("DownloadURL", data.responseText);
},
error: function(xhr) {
if (xhr.status == 404) {
    xhr.abort();
}
},
type: 'GET',
timeout: 3000,
url: url
});

להדגמה של התכונה הזו, אל תהסס להעלות קובץ סטטי לחשבון Box.net. גוררים את סמל הקובץ החוצה לשולחן העבודה, תוך כדי לחיצה ארוכה על מקש Ctrl. אם אין לכם חשבון, יצירת חשבון נמשכת פחות מ-30 שניות.

התכונה הזו מאפשרת לכם להיות יצירתיים ולעשות הרבה דברים שאפשר. גרירה של תמונה לתיבת דו-שיח של מדפסת Windows תגרום להדפסת התמונה באופן מיידי. ניתן להעתיק שיר מ-Box לכונן של הטלפון הנייד, לגרור קובץ מ-Box ללקוח ה-IM על מנת להעביר אותו ישירות לחבר... כך מתקבלות אינספור אפשרויות להגדלת הפרודוקטיביות.

גרירת קובץ למדפסת
גרירת קובץ למדפסת.
גרירת קובץ ללקוח IM
גרירת קובץ ללקוח IM.

מחשבות ושיפורים עתידיים

זה עדיין לא אידיאלי, כי שיחה סינכרונית עלולה לנעול את הדפדפן לזמן קצר. גם ה-Web Worker של HTML 5 לא עוזר, כי עובד אינטרנט צריך להיות אסינכרוני. נראה שצריך לבצע את setData בזמן צירוף הקובץ event listener.

במציאות, הביצועים די סבירים. הקריאה הסינכרונית Ajax (Sjax) רק מאחזרת מחרוזת של כתובת URL, שאמורה להיות מהירה למדי. אכן יש תקורה גדולה בכותרת ה-HTTP, שאפשר לטפל בה על ידי WebSockets. עם זאת, עד שנזהה יותר שימוש בטכנולוגיה מסוג זה, לא כדאי להשתמש ב-WebSockets כדי לשלוח כל עדכון קטן ללקוח.

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

  • עמודה dnd
  • סידור מחדש של הרשימה
  • יצירת גלריית תמונות
  • ייצוא תמונה על קנבס

קובצי עזר