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

مقدمة

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

في Box.net، يسرّنا جدًا أنّ هذه الإمكانات الجديدة تتيح لنا تحسين حلّ إدارة المحتوى في السحابة الإلكترونية، بالإضافة إلى المساهمة بشكل أكبر في منتدى المطوّرين. يسرّني الإعلان عن دمج ميزة "تنزيل في وضع عدم الاتّصال بالإنترنت" في منتجنا. يمكن الآن لمستخدمي 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 يرصد بعض ميزات المتصفّح. تم تمييز السطور التي أضفتها إلى نسخة von Schorsch:

(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 في Internet Explorer إلى إنشاء خطأ JavaScript لأنّ Internet Explorer يستخدم طريقة attachEvent() الخاصة به. e.dataTransfer غير محدّد في Internet Explorer (اعتبارًا من الآن)، وe.dataTransfer.constructor يعرض DataTransfer في Firefox (Mozilla)، في حين أنّ متصفّحات Webkit (Chrome وSafari) تنفّذ أسلوب إنشاء Clipboard. في Safari، يعرض e.dataTransfer.setData('DownloadURL','http://www.box.net') القيمة false، ويعرض Chrome القيمة true لهذه العبارة. عند إجراء جميع الاختبارات المذكورة أعلاه، لن تتوفّر الميزة إلا لمتصفّح Chrome. قد تجادل بأنّه يمكنني ببساطة إجراء ما يلي:

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

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

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

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

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

التكرار 2

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

تم إيقاف مكوّن jQuery الإضافي في النسخة 1 الآن لأنّنا نحتاج إلى دمج عملية تنزيل DnD بشدّة مع عملية DnD على الصفحة. نستخدم إصدارًا معدَّلاً من المكوّن الإضافي Draggable في jQuery UI. داخل حدث 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 الذي تم إنشاؤه قبل عدة دقائق.

لا تعمل ميزة "تنزيل أثناء التشغيل" إلا على عناوين URL الفعلية التي تشير مباشرةً إلى مورد. في حال إجراء عملية إعادة توجيه، لا يكون حاليًا ذكيًا بما يكفي لاتّباع السلسلة (ويجب ألا يتّبع السلسلة مطلقًا لأسباب تتعلق بالأمان). لذلك، على الرغم من أنّ الرابط https://www.box.net/box_download_file?file_id=f_60466690 أعلاه سيتيح لك تنزيل الملف عند إدخاله في شريط العناوين في المتصفّح، لن يعمل مع ميزة "السحب والإفلات".

لتوضيح الاختلافات بين "عنوان 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 إلى الخادم ل retrieving the latest download URL of the file. ومع ذلك، لا يعمل.

تبيّن أنّه يجب أن تكون مكالمة متزامنة (أو كما أحب أن أُطلق عليها، 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 إلى برنامج المراسلة الفورية لنقله مباشرةً إلى صديقك… يتيح لك ذلك فرصًا لا حصر لها لزيادة إنتاجيتك.

سحب ملف إلى الطابعة
سحب ملف إلى الطابعة
سحب ملف إلى برنامج المراسلة الفورية
سحب ملف إلى برنامج المراسلة الفورية

ملاحظات وتحسينات مستقبلية

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

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

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

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

المراجع