フロントエンドでバイナリファイルを扱うためのBlobオブジェクトとFileオブジェクト

このエントリで紹介するBlobFileFileReaderHTML5で利用可能になったAPIで、ECMAScriptで定義されているわけではない。
そのため、Node.jsには存在せず、ブラウザ環境でのみ利用できる。

Blob

Blobは、バイナリデータを表すimmutableなオブジェクト。

const blob = new Blob(['<xml>foo</xml>'], {type: 'text/xml'});
console.log(blob); // Blob(14) {size: 14, type: "text/xml"}

第二引数で設定しているtypeで、MIMEを設定できる。
何も設定しなかった場合は空の文字列になる。

File

Fileはその名の通りファイルを表すオブジェクトで、Blobを継承している。

const file = new File(['<xml>foo</xml>'], 'example.xml', {type: 'text/xml'});
console.log(file); // name: "example.xml", type: "text/xml", size: 14, などのプロパティを持つ
console.log(file instanceof File); // true
console.log(file instanceof Blob); // true

new File()の第二引数で、ファイル名を指定する(必須)。それ以外のプロパティは第三引数のオブジェクトで指定するが、これはオプションであり必須ではない。

<input type="file">DnDイベントで、ローカルファイルをFileとして取得できる。

// ドロップで取得するケース
// e はdropイベントのイベントオブジェクト
e.dataTransfer.files
// <input type="file"> で取得するケース
// e はchangeイベントのイベントオブジェクト
e.target.files

filesはその名の通り、Fileの配列。

2018/1/25 追記

はてなブックマークで以下のコメントを頂きました。

id:daichirata filesはFileの配列ではなくFileList、配列として扱いたいならArray.fromとかしないと駄目なのでは

はい、仰る通りです。
配列のように添え字を使って各要素にアクセスできる、というだけで、配列ではありません。
{0: File, 1: File, ...}という形式のオブジェクトですね。
大変失礼しました。

const {files} = e.dataTransfer;
console.log(files); // FileList {0: File(6740), 1: File(14292), length: 2}
console.log(files[0]); // File(6740)

console.log(files[0] instanceof File); // true
console.log(files instanceof FileList); // true
console.log(Array.isArray(files)); // false

const array = Array.from(files);
console.log(array); // [File(6740), File(14292)]
console.log(Array.isArray(array)); // true
console.log(files[0] === array[0]); // true

追記終わり

FileReader

Blobオブジェクトの中身に直接アクセスすることは出来ない。当然、それを継承しているFileオブジェクトも同様である。
アクセスしたい場合はFileReaderを使う。

まず、FileReaderインスタンスを作成する。
次に、onloadを設定する。これは、読み込みが終わったときに呼び出されるコールバック関数。
そして、readAsXXXのメソッドを使ってBlobを読み込む。

const blob = new Blob(['<xml>foo</xml>'], {type: 'text/xml'});
console.log(blob); // Blob(14) {size: 14, type: "text/xml"}

const reader = new FileReader();
reader.onload = () => {
  console.log(reader.result);
};
reader.readAsText(blob); // <xml>foo</xml>
reader.readAsArrayBuffer(blob); // ArrayBuffer(14) {}
reader.readAsDataURL(blob); // data:text/xml;base64,PHhtbD5mb288L3htbD4=
reader.readAsBinaryString(blob); // <xml>foo</xml>

最後に使っているreadAsBinaryString()は、現在では非推奨になっている。

バイナリファイルをURLで表現する

DataURL

上記のサンプルでも使っているreadAsDataURL()は、BlobをDataURL形式で読み出すメソッド。

では、DataURLとは何か。

これは、データをURL(data:で始まる文字列)で表現するための仕組み。
バイナリファイルの場合はBase64という形式でエンコードする。

サンプルで出力されたdata:text/xml;base64,PHhtbD5mb288L3htbD4=をブラウザのアドレスバーに入れると、<xml>foo</xml>と表示されることを確認できる。

URLの代わりとして使えるため、例えば、img要素のsrc属性にDataURLを使うことも出来る。

データそのものをURLで表現しているため、Cookieなどに保存したり、サーバーに渡したりすることが出来る。

BlobURL

DataURLと似たような仕組みとして、BlobURLがある。
こちらは、blob:で始まる文字列。

URL.createObjectURL()Blobを渡すと作成される。

const blob = new Blob(['<xml>foo</xml>'], {type: 'text/xml'});
const url = URL.createObjectURL(blob);

基本的な使い方はDataURLと同じで、これもアドレスバーに入れると確認できる。

ただ、以下の違いがある。

BlobURLは必ずユニークな文字列になる。同一のBlobを渡しても、その都度、異なるBlobURLが生成される。

そして、これが最大の違いだが、BlobURLの場合は、それ自体がデータを表現しているわけではない。
データはあくまでもブラウザに保存されており、BlobURLはそれにアクセスするためのキーに過ぎない。
データの有効期間は、ブラウザを閉じるまで。

そのため、速度やメモリが、DataURLよりも効率的になる。
その反面、それ自体にデータが入っているわけではないので、BlobURLをサーバーなどに渡してもデータにはアクセスできない。

Chromeの場合、以下のURLで有効なBlobURLを確認できる。
chrome://blob-internals/

使われなくなったBlobURLは自動的に消去されるが、URL.revokeObjectURL()で明示的に消去するのが望ましいとされる。

URL.revokeObjectURL(url);

参考資料

『Real World HTTP』を読んだ

欲しいものリストから送って頂き、読んでいた。ようやく読了。

Webを支えるプロトコルであるHTTPについて、メソッドとはというところから始まり、HTTP/2に関する話題まで、幅広く網羅している。
主にクライアント側の視点で書かれている。

O'Reilly Japan - Real World HTTP

Go言語でサンプルを実装したり、curlを使って実際にどんなレスポンスが返ってくるか試したりしながら、進んでいく。

Goでのサンプルは実際に写経しながら進めた。その成果が下記のリポジトリ
全部やったわけではないが、TLS、チャンク、プロトコルアップグレード、OAuth2などを実装できたのはよかった。
curlについても少しは使えるようになった。どんなレスポンスが返ってくるのか見れるので、覚えると面白いと思う。

Webに関する基本的な知識を広げたくて、本書を読んだ。

JavaScriptでプログラミングの世界に入り、就職後も、SPAの保守や開発を行っている。
そのためサーバーやインフラの知識はすごく浅い。極端に言えば、用意してもらったAPIとやり取りしながらビューの構築を繰り返しているだけ。

職場の人たちがまさにそうなのだが、ちゃんとしたプログラマは、自分の専門外についてもそれなりに知識を持っている。
詳しくはなくても一通り理解しており、どんな話題であっても対応できる。
そして、詳しくはなくても概要は掴んでいるし理解があるから、必要に応じて調べたりキャッチアップできたりする。 自分にはこの、「基礎教養」みたいものが足りない。
この状況を何とかしたいと思っている。

「あとがき」に「最新のさまざまなウェブのトピックにキャッチアップできる基礎力が身につく本としてまとめました」とある本書は、上記のような問題意識を持っている自分にはちょうどよかった。
「HTTPに関する内容を一冊に集めることを目的」(「まえがき」)としているため、各トピックの内容に深く踏み込むことはないのだが、取り敢えずどんな用語や技術があるのかは分かった。
相変わらず「何となく知っている」という状態だが、その範囲や深さは広がったはず。気になったことがあったら本書を参照して、必要ならその内容を取っ掛かりにして自分で調べていけばいい。

ただ、全くの初心者には厳しい内容だと思う。
一応基礎の説明から入っているのだが、かなり駆け足で、それなりに知識がある読者を前提にしている。
正直なところ、自分もキツイ部分はあった。動画に関する部分は全く知識がなかったし、トランスポート層について理解している前提で話が進むのも厳しかった。
初心者ならまずは、『Webを支える技術』『Web API: The Good Parts』を読んだほうがいいと思う。

TLSについてのメモ

関心が強い部分なので、メモを記録しておく。

  • TLSはHTTPに依存しない。様々な形式のデータを双方向に流せる。
  • 既存のプロトコルに通信経路の安全性をプラスした新たなプロトコルを作り出せる汎用的な仕組み
  • 共通鍵は、送受信者が同じ鍵を用いる。同じ鍵を使って、暗号化と復号化を行う。だから鍵を共有している必要がある。
  • 公開鍵方式は、公開鍵と秘密鍵がある。公開鍵で暗号化した情報を送信。受信者が、秘密鍵を使って復元する。
  • TLSでの暗号化について
    • 通信ごとに一度だけ使う共通鍵を生成
    • 公開鍵方式で、共通鍵を渡す
    • その後は、共通鍵でやり取りする
  • TLSには3つのステップがある。ハンドシェイクプロトコル、レコードプロトコル、再接続時の高速なハンドシェイクプロトコル
  • ブラウザはサーバーから、SSLサーバー証明書を取得する。そこからスタートする。
    • SSLサーバー証明書
      • X.509というフォーマットで書かれており、RFCで仕様が策定されている。
      • 主体者、発行者、公開鍵、有効期限などが書かれている。
      • 証明書には発行者のデジタル署名がある。その発行者の証明書を取得することで、署名の信頼性を確認できる。さらにその証明書の発行者の証明書を取得する。これを繰り返し、大本を辿っていく。すると必ず、最終的に、発行者と主体者が同一の証明書が出てくる。この場合の発行者を、ルート認証局と呼ぶ。ルート認証局の信頼性を確認できれば、そこから展開されている一連の証明書も保証される。
      • ではどうやってルート認証局の信頼性を確認するのか。OSやブラウザには予め、信頼できる認証局の証明書がリストされている。これと照合することで、確認する。
      • 信頼が確認されなかった、発行者と主体者が同一の証明書のことを、通称「オレオレ証明書」と呼ぶ。
      • この一連の仕組みは、Chromeのアドレスバーの左のアイコンで確認できる。
    • 上記の、公開鍵を保証する仕組みを、公開鍵基盤という
  • 証明書の信頼が保証されたら、共通鍵の交換を行う。これは、鍵交換専用アルゴリズムを使って行われる
    • 鍵交換専用アルゴリズムでは、サーバーとクライアントがそれぞれに鍵の種を生成し、それを交換。それぞれで共通鍵を作る。鍵の種を交換する際に、公開鍵を使う。
    • 通信時の暗号化には、共通鍵とは別の鍵も使う。
  • 通信の高速化のために様々な工夫が行われており、QUICという通信方式もその一種。
  • 「サーバーを認証し、鍵を交換して通信を行う」というのが、TLSの骨格。
    • それは変えずに、アルゴリズムの組み合わせをリスト化してそこからクライアントとサーバーに選ばせることで、柔軟性を持たせている。
    • このアルゴリズムのセットを暗号スイートと呼ぶ
  • ALPNという機能によって、アプリケーション層のプロトコルHTTP/1.1HTTP/2など)を選択できる。ファイルタイプや言語のコンテントネゴシエーションのように、クライアントからリストを送り、サーバーがその中から一つを選んで返す。
  • TLSではプロキシサーバーに干渉されることがないため、クライアントとサーバーの間で調整できれば、互換性のないプロトコルも利用できる。

誤植リスト

誤植が多かった。

正誤表が用意されているのはいいのだが、他にもまだまだある。

見つけたものを全部記録しているわけではないのだが、一部メモしておいたので、書いておく。
誤植を見つけたらオライリーにメールしてくれとのことだったので、しておいた。

例9-5のように、本書の通りに記述しても動かないサンプルがある。

74ページ

RFC3986に準拠した変換

RFC1866に準拠した変換

85ページ

$ curl -H "Content-Type=image/jpeg" -d "@image.jpeg" http://localhost:18888

$ curl -H "Content-Type: image/jpeg" -d "@image.jpeg" http://localhost:18888

171ページ

例6−10が間違っている。

まず、冒頭に書くべきpackage mainがない。
それから、コメントも間違っている。

// handleは変わらないので省略

// handlerは変わらないので省略

174ページ

例6-13のimportが書かれていない。
iotimeといった、これまで出てきていないパッケージを読み込まないと動かないのだから、記載が必須。

import (
  "io"
  "time"
  "fmt"
  "log"
  "net/http"
)

200ページ

4章で紹介したComet

5章で紹介したComet

257ページ

例9−5。このままだとエラーが出て動かない。

this.primes = stream([]);

this.primes = m.stream([]);

279ページ

サーバーからHTTPでの接続を要請したいときには、

サーバーからHTTPSでの接続を要請したいときには、

308ページ

例11−1。

var redirectURL = "https://localhost:18888"

var redirectURL = "http://localhost:18888"

そもそもredirectURLはこのサンプルでは使ってないから、値は何でもいいのだが。
さらに言えば、この変数は不要。

311ページ

例11−3。

"io/ioutil"をimportしておかないと動かない。