DevTools Talk - Slides and Links

I presented on using Chrome Developer Tools at ComoRichWeb, a web developer user group in Columbia, MO on June 20th. Here are the slides and some other links:

Slides and Demo

Other Links Mentioned in the Talk

We had a good turnout last night, was glad to get a chance to share some front-end development tips with local developers.

FileReaderSync

I have been working on updating the FileReader.js JavaScript file reading library lately to help out with a few different projects. This library has provided a quick way to get started with reading files on a few different projects, like mothereffinganimatedgif.com and Instant Sprite.

One thing I noticed was that there is a FileReaderSync API, meant for loading files synchronously.

You might wonder why on earth would you want to load a file synchronously in your browser - that seems like it could block the entire UI until the file is loaded! It turns out you can't, at least not in the normal window context. FileReaderSync only exists inside of the context of a WebWorker:

Implementation

View a A working JS Fiddle using FileReaderSync{.btn}.

I also wrote about how to load web workers without a JavaScript file, but this technique works just fine using a normal external reference.

markup

<input type='file' id='files' multiple onchange='handleFileSelect()' />

page javascript

function processFiles(files, cb) {
    var syncWorker = new Worker('worker.js');
    syncWorker.onmessage = function(e) {
        cb(e.data.result);
    };

    Array.prototype.forEach.call(files, function(file) {
        syncWorker.postMessage(file);
    });
}

function handleFileSelect() {
    var files = document.getElementById('files').files;
    processFiles(files, function(src) {
        var img = new Image();
        img.src = src;
        document.body.appendChild(img);
    });
}

worker.js

self.addEventListener('message', function(e) {
    var data=e.data;
    try {
        var reader = new FileReaderSync();
        postMessage({
            result: reader.readAsDataURL(data)
        });
   } catch(e){
        postMessage({
            result:'error'
        });
   }
}, false);

The jsFiddle demo is a little more complicated than this, since it handles checking for support and an inline worker.

Gotchas

Something that was a little weird is that since you can't detect support from the main window, I need to spawn off a worker to post the message of whether it supports FileReaderSync. See a jsFiddle to detect FileReaderSync support. There may be a better way to do this, but I don't know of it.

This can be pretty complicated, but I have been tying it all into the filereader.js plugin, to make reading with FileReaderSync just an option along with the standard FileReader

Performance

It's hard for me to accurately measure the performance. On one hand, the FileReaderSync seems to load the images in a slower time per image (measured in milliseconds). I assume that this is due to the overhead and message passing with the worker, or possibly because it is a newer implementation.

However, on large images and videos, it definitely feels like the UI does not lock up as much when processing the files.

Purpose

I feel like maybe part of the point of this API is when you want to some heavy lifting with the file after it is loaded but still inside the worker, which isn't currently supported in FileReader.js. I could imagine ways this use case could be supported though (maybe by passing in a process() function as a string that the worker could call?

Check out the FileReader.js demo and see if you can tell a difference! I'd love to get any kinks worked out and get some feedback - I have been thinking of setting up a js-file-boilerplate project on Github to tie together a bunch of this functionality in a sample project.

Debugging Web Workers

While working on FileReader.js Web Worker file processing with FileReaderSync, I needed to figure out what was going on with the script inside a web worker. It can be very difficult to track down errors without standard developer tool support, like logging and debugging. Luckily, in Chrome Devtools, this is possible.

How to do it

It can be a little tricky to find, but click over to the 'Scripts' panel, and expand the 'Workers' accordion on the right. Then check the 'Pause on Start' checkbox and you will enter the debugging mode once the worker gets fired up. Here is a screenshot of the relevant checkbox:

Read Files In JavaScript

I was motivated to write a JavaScript plugin to make reading local files easier. I wanted this plugin to have no library dependancies so it could be accessible to anyone wanting to write an application that reads files. You can read the original blog entry about the motivation behind filereader.js for some more context.

Enter FileReader.js

See the full FileReader project page and demo on Github. I also keep an updated version FileReader.js annotated source code if you'd like to dig through the source.

Ways to Access Local Files in JavaScript

There are currently 3 ways to access a user's files. Not all browsers support all of the ways, and I will do my best to document some of the limitations and implementations of each.

Choose files from (or drag/drop them onto) a file input

FileReaderJS.setupInput(document.getElementById('file-input'), opts);

There is a way to bind to the change event on a file input, and access the files via input.files. Additionally, I bind to the drop event, and access the file collection via event.dataTransfer.files.

Drag/drop files onto any element

FileReaderJS.setupDrop(document.getElementById('dropzone'), opts);

This is a commonly requested way to access a users files – let them just drop them onto the page! This is how Instant Sprite and MotherEffingAnimatedGif use the FileReader.js plugin.

The drag and drop code is a little… difficult to work with.

There are at least two tricks to this drop event that might not be obvious at first glance.

The first is that we want to prevent the browser from redirecting if a user misses the drop zone. By binding the drop event on the body, and preventing default.

The next trick is that if the dragstart event happened on the body, this was caused by the user clicking and dragging an item on the page. We don't care about the results of the drag/drop event in that case. That is why there is a capturing dragstart and dragend events bound to the body, and this bit is checked on the events bound to the dropbox. We can avoid setting active CSS classes and processing the drop event if it was started on the body.

Capture the paste event on the document to access clipboard content.

FileReaderJS.setupClipboard(document.body, opts);

This is one of the coolest ideas, but flakiest implementations across browsers. I love the idea of being able to print screen or copy a file from my file browser and just paste it into the website.

Cool parts about Copy / Paste

You can print screen, or use the snipping tool in Windows and directly paste this into the screen. Try this out on the demo – it's pretty neat.

Issues with Copy / Paste from File Browsers
  • In OSX – I can copy/paste files out of the Finder, but it only gets the first one. And if the file is a non-image, then it actually pastes the thumbnail of the file type association (so a little PDF icon for a PDF, etc). There is also no filename that gets passed in (so I just call it “Clipboard 1”, “Clipboard 2”, etc.
  • In Windows – I can't seem to copy/paste files out of Explorer.

FileReaderSync

A neat part about using a plugin for handling the direct FileReader access is that we can transparently replace the technology behind the scenes, but keep your code the same. There is an option to set FileReaderJS.sync = true, and if your browser supports it – the files will be read inside of a WebWorker instead of inside the main event loop in your browser.

Load Web Workers Without A JavaScript File

Ever want to load a JavaScript Web Worker without specifying an external JavaScript file? There are a couple of different ways to do this by creating a blob and object URL - I wrapped this functionality up into a little plugin to share.

Here is the code. Or checkout out a working jsfiddle demo

// makeWorker is a little wrapper for generating web workers from strings
function makeWorker(script) {
    var URL = window.URL || window.webkitURL;
    var Blob = window.Blob;
    var Worker = window.Worker;

    if (!URL || !Blob || !Worker || !script) {
        return null;
    }

    var blob = new Blob([script]);
    var worker = new Worker(URL.createObjectURL(blob));
    return worker;
}
<div id='log'></div>

<script type='text/worker' id='worker-script'>
    self.addEventListener('message', function(e) {
        postMessage(e.data / 2);
    },false);
</script><script type='text/javascript'>

// Load a worker from a string, and manually initialize the worker
var inlineWorkerText =
    "self.addEventListener('message', function(e) { postMessage(e.data * 2); } ,false);";
var inlineWorker = makeWorker(inlineWorkerText);
inlineWorker.onmessage = function(e) {
    document.getElementById('log').innerHTML += '<br />Inline: ' + e.data;
};


// Load a worker from a script of type=text/worker, and use the getWorker helper
var scriptTagWorker = makeWorker(
    document.getElementById('worker-script').textContent
);

scriptTagWorker.onmessage = function(e) {
        document.getElementById('log').innerHTML += '<br />Script Tag: ' + e.data;
};

inlineWorker.postMessage(1);
inlineWorker.postMessage(2);
inlineWorker.postMessage(100);
scriptTagWorker.postMessage(1);
scriptTagWorker.postMessage(2);
scriptTagWorker.postMessage(100);

</script>