กรณีศึกษา - ลากและวางการดาวน์โหลดใน Chrome

เกริ่นนำ

การลากและวาง (DnD) เป็นหนึ่งในคุณลักษณะที่ยอดเยี่ยมของ HTML 5 และใช้ได้ใน Firefox 3.5, Safari, Chrome และ IE เมื่อเร็วๆ นี้ Google ได้เปิดตัวฟีเจอร์ใหม่ที่ช่วยให้ผู้ใช้ Google Chrome ลากและวางไฟล์จากเบราว์เซอร์ลงในเดสก์ท็อปได้ ฟีเจอร์นี้ใช้สะดวกมาก แต่ยังไม่เป็นที่รู้จักอย่างกว้างขวางจนกระทั่ง Ryan Seddon โพสต์บทความเกี่ยวกับการค้นพบของการทำวิศวกรรมย้อนกลับกับฟีเจอร์ใหม่นี้

ที่ Box.net เราตื่นเต้นมากกับวิธีที่ความสามารถใหม่ๆ เหล่านี้ช่วยให้เราปรับปรุงโซลูชันการจัดการเนื้อหาระบบคลาวด์ รวมถึงมีส่วนร่วมในชุมชนนักพัฒนาซอฟต์แวร์ได้มากขึ้น เรายินดีที่จะแจ้งให้ทราบว่าได้ผสานรวม DnD Download ในผลิตภัณฑ์ของเราแล้ว ตอนนี้ผู้ใช้ Box ลากไฟล์จากเบราว์เซอร์ Chrome ไปยังเดสก์ท็อปได้โดยตรงเพื่อดาวน์โหลดและบันทึกไฟล์

ฉันอยากจะแบ่งปันวิธีการที่ฉันปรับปรุงหลายครั้งระหว่างการพัฒนาฟีเจอร์ใหม่นี้

ตรวจสอบการรองรับ API การลากและวาง

สิ่งแรกที่ต้องทำคือตรวจสอบว่าเบราว์เซอร์ของคุณรองรับการลากและวาง HTML5 อย่างสมบูรณ์หรือไม่ วิธีง่ายๆ ในการดำเนินการดังกล่าวคือการใช้ไลบรารีชื่อ Modernizr เพื่อตรวจสอบฟีเจอร์บางอย่าง ดังนี้

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

การทำซ้ำ 1

ผมลองใช้วิธีที่ Seddon พบใน Gmail ก่อน ผมเพิ่มแอตทริบิวต์ใหม่ ที่เรียกว่า "data-downloadurl" เพื่อ Anchor ลิงก์ของไฟล์ กระบวนการนี้ใช้แอตทริบิวต์ข้อมูลที่กำหนดเองของ 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>

ผมได้เพิ่มปลั๊กอิน jQuery ที่มีการตรวจจับฟีเจอร์เบราว์เซอร์เล็กน้อย โดยอิงจากplugin jQuery ที่ von Schorsch สร้างขึ้นซึ่งอิงตามบทความของ Seddon ไฮไลต์คือบรรทัดที่ฉันได้เพิ่มในเวอร์ชันของ 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 ใน IE จะสร้างข้อผิดพลาด JavaScript เนื่องจาก IE ใช้วิธี AttachEvent() ของตัวเอง เช่น e.dataTransfer นั้นไม่มีการกำหนดใน IE (ณ ตอนนี้) e.dataTransfer.constructor ส่งกลับ DataTransfer ใน Firefox (Mozilla) ในขณะที่เบราว์เซอร์ Webkit (Chrome และ Safari) จะติดตั้งใช้งานตัวสร้างคลิปบอร์ด ใน Safari นั้น e.dataTransfer.setData('DownloadURL','http://www.box.net') จะแสดงผลค่า "เท็จ" และ Chrome จะแสดงผลค่า "จริง" สำหรับคำสั่งนี้ การทดสอบทั้งหมดที่กล่าวถึงข้างต้นจะทำให้ฟีเจอร์นี้ใช้ได้กับ Chrome เท่านั้น คุณอาจโต้แย้งว่าเราสามารถทำตามขั้นตอนต่อไปนี้ได้

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

แต่ฉันต้องการใช้การตรวจหาฟีเจอร์เพื่อตรวจหาเบราว์เซอร์มากกว่า แต่ในทางเทคนิคแล้วตรวจไม่พบว่าการดาวน์โหลด DnD จะทำงานได้

ปัญหาของการทำซ้ำ 1

1) เนื่องจากขณะนี้เราเปิดใช้ DnD ในหน้าเว็บสำหรับการย้าย/คัดลอกไฟล์ระหว่างโฟลเดอร์ เราจึงต้องมีวิธีในการแยกแยะระหว่าง DnD Download กับ DnD ในหน้า ในทางเทคนิค เราไม่สามารถรวม การดำเนินการ 2 อย่างนี้เข้าด้วยกัน เราไม่สามารถคาดการณ์ได้ว่าผู้ใช้ต้องการย้ายไฟล์ไปยังโฟลเดอร์อื่นภายในบัญชี Box.net หรือลากไฟล์ไปยังเดสก์ท็อป การดำเนินการทั้ง 2 อย่างนี้แตกต่างกันโดยสิ้นเชิง ยิ่งไปกว่านั้น ยังไม่มีวิธีง่ายๆ ที่จะตรวจสอบว่าเคอร์เซอร์อยู่นอกหน้าต่างเบราว์เซอร์หรือไม่ คุณสามารถใช้ window.onmouseout (IE) และ document.onmouseout (เบราว์เซอร์อื่น) เพื่อแนบเหตุการณ์เมาส์เอาต์กับเอกสาร แล้วตรวจสอบว่า e.relatedTarget.nodeName == "HTML" (e คือเหตุการณ์เมาส์เอาต์หรือ window.event แบบใดก็ตามที่พร้อมใช้งาน) แต่ปัญหานี้ค่อนข้างยากเนื่องจากมีการแก้ไขฟองสบู่ เหตุการณ์อาจทริกเกอร์แบบสุ่มเมื่อคุณอยู่เหนือรูปภาพหรือเลเยอร์ โดยเฉพาะในเว็บแอปที่ซับซ้อน เช่น Box.net

2) เราอยากให้ผู้ใช้ดำเนินการบางอย่างอย่างชัดแจ้งเพื่อป้องกันไม่ให้ผู้ใช้ลากบางสิ่งไปยังเดสก์ท็อปโดยไม่ได้ตั้งใจ เอดิเตอร์ของโฟลเดอร์ Box อาจอัปโหลดไฟล์สั่งการซึ่งมีการทำงานที่ไม่พึงประสงค์บนคอมพิวเตอร์ของผู้ที่ดาวน์โหลดไฟล์นั้นได้ เราต้องการให้ผู้ใช้รู้อย่างแน่ชัดว่า ระบบจะดาวน์โหลดไฟล์ไปยังเดสก์ท็อปเมื่อใด

การทำซ้ำ 2

เราตัดสินใจทำการทดลองด้วยปุ่ม Control + ลาก (การลากไฟล์เมื่อกดปุ่ม Ctrl ของ Windows) การดำเนินการนี้จะสอดคล้องกับสิ่งที่ผู้ใช้ทำได้บนเดสก์ท็อป Windows เพื่อทำสำเนาไฟล์ นอกจากนี้ ผู้ใช้ยังต้องดำเนินการเพิ่มเติม (แต่ไม่ใช่ขั้นตอนเพิ่มเติม) เพื่อป้องกันไม่ให้ดาวน์โหลดไฟล์โดยไม่ตั้งใจ

ตอนนี้ระบบเลิกใช้ปลั๊กอิน jQuery ในการทำซ้ำ 1 เนื่องจากเราต้องผสานรวม DnD Download เข้ากับ DnD ในหน้าให้แน่นแฟ้นยิ่งขึ้น สำหรับผู้ที่สนใจ เราใช้ปลั๊กอินที่ลากได้ของ jQuery UI ที่แก้ไขแล้ว ภายในเหตุการณ์เมาส์ดาวน์ขององค์ประกอบเป้าหมาย เราจะใส่โค้ดต่อไปนี้

// 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) ของรายการ URL นี้จะแสดงรหัสสถานะ "302 Found" และเปลี่ยนเส้นทางไปยัง URL แบบสุ่ม (เช่น https://www.box.net/dl/6045?a=1f1207a084&m=168299,11211&t=2&b=aca15820d924e3b) ที่เป็น "URL จริง" ชั่วคราวของไฟล์ ความท้าทายคือ รหัสจะหมดอายุทุก 2-3 นาที ดังนั้นการใส่รหัสไว้ในเอาต์พุต HTML จึงใช้ไม่ได้ผล รายงานอาจแสดง "404" เมื่อผู้ใช้พยายามดาวน์โหลดไฟล์ที่ลิงก์ในเอาต์พุต HTML ที่สร้างขึ้นเมื่อไม่กี่นาทีที่ผ่านมา

DnD Download จะใช้ได้ใน 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 ในเวลาที่แนบ 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

ความคิดและการปรับปรุงในอนาคต

แต่วิธีนี้ถือว่ายังไม่ค่อยเหมาะสม เนื่องจากการเรียกใช้แบบพร้อมกันอาจล็อกเบราว์เซอร์เป็นระยะเวลาสั้นๆ HTML 5 Web Worker ไม่ได้ช่วยเช่นกัน เนื่องจาก Web Worker ต้องเป็นการทำงานไม่พร้อมกัน ดูเหมือนว่าจะต้องทำ setData ในเวลาที่มีการแนบ Listener เหตุการณ์

ในความเป็นจริง ประสิทธิภาพก็อยู่ในระดับที่ยอมรับได้ การเรียก Ajax (Sjax) แบบซิงโครนัส จะดึงสตริง URL เท่านั้น ซึ่งน่าจะทำได้ค่อนข้างรวดเร็ว โดยมาพร้อมกับโอเวอร์เฮด จำนวนมากในส่วนหัว HTTP ซึ่ง WebSocket อาจจัดการได้ อย่างไรก็ตาม จนกว่าเราจะเห็นมีการใช้เทคโนโลยีประเภทนี้มากขึ้น ก็ไม่คุ้มค่าที่จะใช้ WebSockets เพื่อส่งการอัปเดตเล็กๆ น้อยๆ ทั้งหมดไปยังไคลเอ็นต์

เราหวังว่าจะมีการเพิ่มความสามารถในการดาวน์โหลดหลายไฟล์ลงใน API ในอนาคต เมื่อใช้ร่วมกับช่องทำเครื่องหมายที่กำหนดเองเพื่อเลือกหลายๆ ไฟล์ในอินเทอร์เฟซผู้ใช้ คงจะน่าทึ่งมาก นอกจากนี้ จะยิ่งดีกว่านั้นอีก หากสามารถดาวน์โหลดไฟล์ที่ไคลเอ็นต์สร้างขึ้น เช่น ไฟล์ข้อความที่สร้างขึ้นจากผลลัพธ์ของแบบฟอร์มที่ส่ง ด้วยวิธีนี้

  • คอลัมน์ dnd
  • จัดเรียงรายการใหม่
  • การสร้างแกลเลอรีรูปภาพ
  • การส่งออกภาพพิมพ์แคนวาส

รายการอ้างอิง