دراسة حالة - تنزيل السحب والإفلات في Chrome

مقدمة

يعد السحب والإفلات (DnD) واحدًا من العديد من الميزات الرائعة في HTML 5، ويتم دعمه في Firefox 3.5 وSafari وChrome وIE. طرحت Google مؤخرًا ميزة جديدة تتيح لمستخدمي Google Chrome سحب الملفات وإفلاتها من المتصفّح إلى سطح المكتب. وهي ميزة ملائمة للغاية، ولكنها لم تكن معروفة على نطاق واسع حتى نشر راين سيدون مقالة عن اكتشافات الهندسة العكسية على هذه الميزة الجديدة.

نحن في Box.net نحن متحمسون للغاية بشأن مساهمة هذه الإمكانات الجديدة في تحسين حل إدارة المحتوى على السحابة الإلكترونية، بالإضافة إلى المساهمة بشكل أكبر في منتدى المطوّرين. يسرّنا الإعلان عن دمج ميزة DnD Download في منتجنا. الآن، يمكن لمستخدمي Box سحب الملفات مباشرةً من متصفح Chrome إلى سطح المكتب لتنزيل الملف وحفظه.

أود أن أشارككم كيف مررت بالعديد من التكرارات أثناء تطوير هذه الميزة الجديدة.

البحث عن دعم لواجهة برمجة التطبيقات للسحب والإفلات

أول شيء يجب فعله هو التحقق من أن متصفحك يتيح بشكل كامل السحب والإفلات من خلال HTML5. ويمكن إجراء ذلك بسهولة من خلال استخدام مكتبة اسمها Modernizr للتحقّق من وجود ميزة معيَّنة:

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

التكرار 1

جربت لأول مرة الطريقة التي وجدها سيدون في Gmail. لقد أضفت سمة جديدة تسمى "data-downloadurl" لروابط ارتساء الملفات. تستخدم هذه العملية سمات البيانات المخصّصة في HTML5. في data-downloadurl، تحتاج إلى تضمين نوع 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 أنشأه فون شورش، وهو يستند إلى مقالة Seddon، أضفتُ مكوّن 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 يستخدم طريقة الملحق () الخاصة به. في Safari، يعرض e.dataTransfer.setData('DownloadURL','http://www.box.net') false، ويعرض Chrome true لهذه العبارة. يؤدي إجراء جميع الاختبارات المذكورة أعلاه إلى ترك الميزة متاحة فقط لمتصفِّح Chrome. يمكنك التصريح بأنّه يمكنني ببساطة القيام بما يلي:

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

ولكني أفضّل أن يتم اكتشاف الميزات بدلاً من اكتشاف المتصفح، على الرغم من أنّ هذا الإجراء لا يفيد من الناحية الفنية أنّ التنزيل سيؤدي إلى عدم اكتمال عملية التنزيل.

مشكلات التكرار 1

1) بما أنّنا نفعّل حاليًا ميزة DnD على الصفحة لنقل/نسخ الملفات بين المجلدات، نحتاج إلى طريقة للتمييز بين تنزيل DnD وDnD على الصفحة. من الناحية الفنية، لا يمكننا الجمع بين هذين الإجراءين. لا يمكننا التنبؤ بما إذا كان المستخدم يريد نقل ملف إلى مجلد آخر داخل حساب Box.net أو سحبه إلى سطح المكتب. هذان الإجراءان مختلفان تمامًا. علاوة على ذلك، ليست هناك طريقة سهلة لاكتشاف ما إذا كان المؤشر خارج نافذة المتصفح. يمكنك استخدام window.onmouseout (IE) وdocument.onmouseout (في متصفحات أخرى) لإرفاق حدث الماوس بالمستند، والتحقق مما إذا كان e.relatedTarget.nodeName == "HTML" (أي حدث الضغط على الأزرار window.event أو حدث window.event، أيهما متاح). إلا أن هذا الأمر صعب إلى حد ما بسبب فقاقيع الأحداث. قد يبدأ الحدث بشكل عشوائي عندما تقف فوق صورة أو طبقة، لا سيما في تطبيق ويب معقد مثل Box.net.

2) نريد أن يفعل المستخدم شيئًا ما بشكل صريح لمنعه من سحب شيء ما إلى سطح المكتب عن طريق الخطأ. من المحتمل أن يقوم محرر مجلد Box بتحميل ملف قابل للتنفيذ يفعل شيئًا غير مرغوب فيه على الكمبيوتر الخاص بالمستخدم الذي ينزّله. نريد أن يعرف المستخدم بالضبط متى سيتم تنزيل ملف إلى سطح المكتب.

التكرار 2

قررنا تجربة استخدام Control + السحب (سحب ملف عند الضغط على مفتاح Windows Ctrl). وهذا الإجراء متوافق مع ما يمكن للمستخدمين فعله على كمبيوتر سطح المكتب الذي يعمل بنظام Windows لإنشاء نسخة طبق الأصل من ملف. كما تتطلب جهدًا إضافيًا (ولكن ليس خطوة إضافية) من المستخدم لمنع تنزيل الملفات عن طريق الخطأ.

تم الآن التخلي عن المكوّن الإضافي jQuery في التكرار 1 لأنّنا نحتاج إلى دمج DnD Download (تنزيل DnD) بإحكام مع DnD على الصفحة. ولمن يهمه الأمر، نستخدم إصدارًا معدَّلاً من المكوّن الإضافي القابل للسحب في jQuery لواجهة المستخدم. داخل حدث الماوس لأسفل للعنصر المستهدف، نضع الرمز التالي:

// 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 Found"، ويعيد التوجيه إلى عنوان 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 يجب تنفيذها في الوقت الذي يتم فيه إرفاق أداة معالجة الحدث. وفقًا لواجهة برمجة التطبيقات في 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).

الأفكار والتحسينات المستقبلية

ولا يزال هذا الأمر أقل من الوضع المثالي، إذ إنّ الاتصال المتزامن يمكن أن يؤدي إلى قفل المتصفح لفترة وجيزة. ولا يساعد أيضًا HTML 5 Web Worker، إذ يجب أن يكون عامل الويب غير متزامن. يبدو أنه يجب تنفيذ setData في الوقت الذي يتم فيه إرفاق أداة معالجة الحدث.

في الواقع، الأداء مقبول جدًا. لا يسترد استدعاء Ajax (Sjax) المتزامن إلا سلسلة عنوان URL، والتي يجب أن تكون سريعة للغاية. فهو يأتي مع النفقات العامة في عنوان HTTP، والتي قد تتم معالجتها بواسطة WebSockets. ومع ذلك، إلى أن نرى استخدامًا أكبر لهذا النوع من التكنولوجيا، لا يستحق الأمر استخدام WebSockets لإرسال كل التحديثات الصغيرة إلى العميل.

آمل أيضًا أن تتم إضافة إمكانية تنزيل ملفات متعددة إلى واجهة برمجة التطبيقات في المستقبل. بالاقتران مع مربعات الاختيار المخصصة لتحديد ملفات متعددة على واجهة المستخدم، سيكون هذا أمرًا لا يصدق. بالإضافة إلى ذلك، سيكون من الأفضل تنزيل الملفات التي ينشئها العميل بهذه الطريقة، مثل الملفات النصية التي يتم إنشاؤها من نتيجة نموذج تم إرساله.

  • العمود dnd
  • إعادة ترتيب القائمة
  • إنشاء معرض صور
  • تصدير صورة لوحة

المراجع