JavaScriptでBase64エンコード・デコード（UTF-8も）

# Base64
様々なところで使われているエンコード方式です。バイナリデータから64進数にエンコードします。URLエンコードよりもデータ効率がいいらしい。

| エンコード前 | URLエンコード | Base64エンコード |
| ---------- | --------- | --- |
| `https://qiita.com/` (18)| `https%3A%2F%2Fqiita.com%2F` (26) | `aHR0cHM6Ly9xaWl0YS5jb20v` (24) |
| `こんにちは` (15) | `%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF` (45) | `44GT44KT44Gr44Gh44Gv` (20) |


3バイトずつ4文字に変換します。

## 英文
マルチバイトでない文字列は`btoa`、`atob`を使ってエンコード・デコードできます。

```JavaScript
const text = "Hello, world!";

// エンコード
const encoded = btoa(text);
//=> "SGVsbG8sIHdvcmxkIQ=="

// デコード
const decoded = atob(encoded);
//=> "Hello, world!"
```

## 日本語など
JavaScriptの文字列はUTF-16なので、そのままではエンコードできません。
Base64はあくまでもバイト列のエンコードなので、バイト列に直してやる必要があります。

```JavaScript
btoa("テスト");
// Uncaught DOMException: Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range.
```

なので、URLエンコード等して全部1バイト文字にするか
※この方法では、UTF-8を直接エンコードしたようなものはデコードできません

```JavaScript
const text = "テスト";

// エンコード
const encoded = btoa(encodeURIComponent(text));
//=> "JUUzJTgzJTg2JUUzJTgyJUI5JUUzJTgzJTg4"


// デコード
const decoded = decodeURIComponent(atob(encoded));
//=> "テスト"
```

UTF-8などのバイト列に変換してからエンコードします。
`TextEncoder`でUTF-8にエンコードした結果の`ArrayBuffer`を`String.fromCharCode`に無理やり渡しているので、あまり良い方法ではないと思われます。
また、あまり長い文字列を渡すとエラーになるようです。[^2]

```JavaScript
const text = "テスト";

// エンコード
let utf8str = String.fromCharCode.apply(null, new TextEncoder().encode(text));
const encoded = btoa(utf8str);
//=> "44OG44K544OI"

// デコード
const decoded_utf8str = atob(encoded);
const decoded_array = new Uint8Array(Array.prototype.map.call(decoded_utf8str, c => c.charCodeAt()));
const decoded = new TextDecoder().decode(decoded_array);
//=> "テスト"
```

または、Blobを経由して変換する方法もあります。[^1]
この場合は非同期になります。

```JavaScript
const text = "テスト";

const encodedPromise = new Promise((resolve) => {
  const reader = new FileReader();
  reader.onload = () => {
    const offset = reader.result.indexOf(",") + 1;
    resolve(reader.result.slice(offset));
  };
  reader.readAsDataURL(new Blob([text]));
});
//=> Promise {<resolved>: "44OG44K544OI"}

const decodedPromise = encodedPromise
  .then(encoded => fetch("data:text/plain;charset=UTF-8;base64," + encoded))
  .then(response => response.text());
//=> Promise {<resolved>: "テスト"}
```

## Base64変換関数
ご利用はご自由にどうぞ。

```JavaScript
function base64Encode(...parts) {
  return new Promise(resolve => {
    const reader = new FileReader();
    reader.onload = () => {
      const offset = reader.result.indexOf(",") + 1;
      resolve(reader.result.slice(offset));
    };
    reader.readAsDataURL(new Blob(parts));
  });
}

function base64Decode(text, charset) {
  return fetch(`data:text/plain;charset=${charset};base64,` + text).then(response => response.text());
}

function base64DecodeAsBlob(text, type = "text/plain;charset=UTF-8") {
  return fetch(`data:${type};base64,` + text).then(response => response.blob());
} 
```

### つかいかた
```JavaScript
// Promise#then
base64Encode("こんにちは").then(encoded => { /* ... */ });

// await
const encoded = await base64Encode("こんにちは");

// ArrayBufferやBlobも使えます
const encodedFile = await base64Encode(inputFile.files[0]);
const decodedFile = await base64DecodeAsBlob(encodedFile, "image/jpeg");
```

[^1]: https://qiita.com/weal/items/1a2af81138cd8f49937d
[^2]: http://var.blog.jp/archives/54412592.html