完璧にポータブルなWebページを作る方法 - Webサーバーもなしで実現

## きっかけ

以下の記事で、RubyとかJavaScript(ES2017)をURLに埋め込んでポータビリティを実現する方法を紹介しました。
[極小WebアプリをURLに埋め込んで超ポータブルなWebアプリを作りたい！ - Qiita](https://qiita.com/nwtgck/items/f6363a159a5edbfcc3b4)

そもそも、ポータビリティを完全に追求するなら**Data URL**があります！今回はData URLでポータブルなWebページな実現とNipp[^what_is_nipp]との違いを書きました。

[^what_is_nipp]: 上の記事で紹介されていたアプリケーションの名前

## Data URLの構造

```
data:[<mediatype>][;base64],<data>
```


MDNから引用しています。こちらにもっと詳しくまとまっています：[データ URL | MDN](https://developer.mozilla.org/ja/docs/data_URIs)

画像をData URLにするのはよく見かけます。画像以外に、`<mediatype>`を`text/html`にすればHTMLが記述できるようになります。つまり、HTML内にCSSを書いたりJavaScriptを書いたりできるので、Webサーバもいらずに完璧にポータブルなWebページが作れます！

## 具体例

例があったほうが分かりやすいと思うので、具体例を少しあげます。

### 簡単な時計

簡易的に時計ページです

![clock.mp4.opt.gif](https://qiita-image-store.s3.amazonaws.com/0/84451/aba5bcd3-14f3-f24b-c3ed-2511541f0cb1.gif)


(クリックで開かないので、コピペして開きます)
[data:text/html,%3Chtml%3E%0A%3Chead%3E%0A%3Ctitle%3EClock%3C/title%3E%0A%3C/head%3E%0A%3Cbody%3E%0A%3Cdiv%20id='clock'%3E%3C/div%3E%0A%3Cscript%3E%0AsetInterval(function()%7B%0A%20%20document.getElementById('clock').innerText%20=%20new%20Date();%0A%7D,%20500);%0A%3C/script%3E%0A%3C/body%3E%0A%3C/html%3E](data:text/html,%3Chtml%3E%0A%3Chead%3E%0A%3Ctitle%3EClock%3C/title%3E%0A%3C/head%3E%0A%3Cbody%3E%0A%3Cdiv%20id='clock'%3E%3C/div%3E%0A%3Cscript%3E%0AsetInterval(function()%7B%0A%20%20document.getElementById('clock').innerText%20=%20new%20Date();%0A%7D,%20500);%0A%3C/script%3E%0A%3C/body%3E%0A%3C/html%3E)



```html:中のコード
<html>
<head>
<title>Clock</title>
</head>
<body>
<div id='clock'></div>
<script>
setInterval(function(){
  document.getElementById('clock').innerText = new Date();
}, 500);
</script>
</body>
</html>
```

### VueのTodoリスト

![vue-todo-list.mp4.opt.gif](https://qiita-image-store.s3.amazonaws.com/0/84451/901e50c3-867b-140b-6d0c-52e88194f949.gif)


(クリックで開かないので、コピペして開きます)
<data:text/html,%3Chtml%3E%0A%3Chead%3E%0A%3Ctitle%3EVue%20Todo%20List%3C/title%3E%0A%3Cstyle%3E%0Abody%20%7B%0A%20%20background:%20#20262E;%0A%20%20padding:%2020px;%0A%20%20font-family:%20Helvetica;%0A%7D%0A%0A#app%20%7B%0A%20%20background:%20#fff;%0A%20%20border-radius:%204px;%0A%20%20padding:%2020px;%0A%20%20transition:%20all%200.2s;%0A%7D%0A%0Ali%20%7B%0A%20%20margin:%208px%200;%0A%7D%0A%0Ah2%20%7B%0A%20%20font-weight:%20bold;%0A%20%20margin-bottom:%2015px;%0A%7D%0A%0Adel%20%7B%0A%20%20color:%20rgba(0,%200,%200,%200.3);%0A%7D%0A%3C/style%3E%0A%3Cscript%20src=%22https://cdn.jsdelivr.net/npm/vue%22%3E%3C/script%3E%0A%3C/head%3E%0A%3Cbody%3E%0A%3Cdiv%20id=%22app%22%3E%0A%20%20%3Ch2%3ETodos:%3C/h2%3E%0A%20%20%3Col%3E%0A%20%20%20%20%3Cli%20v-for=%22todo%20in%20todos%22%3E%0A%20%20%20%20%20%20%3Clabel%3E%0A%20%20%20%20%20%20%20%20%3Cinput%20type=%22checkbox%22%0A%20%20%20%20%20%20%20%20%20%20v-on:change=%22toggle(todo)%22%0A%20%20%20%20%20%20%20%20%20%20v-bind:checked=%22todo.done%22%3E%0A%0A%20%20%20%20%20%20%20%20%3Cdel%20v-if=%22todo.done%22%3E%0A%20%20%20%20%20%20%20%20%20%20%7B%7B%20todo.text%20%7D%7D%0A%20%20%20%20%20%20%20%20%3C/del%3E%0A%20%20%20%20%20%20%20%20%3Cspan%20v-else%3E%0A%20%20%20%20%20%20%20%20%20%20%7B%7B%20todo.text%20%7D%7D%0A%20%20%20%20%20%20%20%20%3C/span%3E%0A%20%20%20%20%20%20%3C/label%3E%0A%20%20%20%20%3C/li%3E%0A%20%20%3C/ol%3E%0A%3C/div%3E%0A%3Cscript%3E%0Anew%20Vue(%7B%0A%20%20el:%20%22#app%22,%0A%20%20data:%20%7B%0A%20%20%20%20todos:%20%5B%0A%20%20%20%20%20%20%7B%20text:%20%22Learn%20JavaScript%22,%20done:%20false%20%7D,%0A%20%20%20%20%20%20%7B%20text:%20%22Learn%20Vue%22,%20done:%20false%20%7D,%0A%20%20%20%20%20%20%7B%20text:%20%22Play%20around%20in%20JSFiddle%22,%20done:%20true%20%7D,%0A%20%20%20%20%20%20%7B%20text:%20%22Build%20something%20awesome%22,%20done:%20true%20%7D%0A%20%20%20%20%5D%0A%20%20%7D,%0A%20%20methods:%20%7B%0A%20%20%20%20toggle:%20function(todo)%7B%0A%20%20%20%20%20%20todo.done%20=%20!todo.done%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D)%0A%3C/script%3E%0A%3C/body%3E%0A%3C/html%3E>


### サマー・ウォーズのワールドクロック

![world-clock.mp4.opt.gif](https://qiita-image-store.s3.amazonaws.com/0/84451/2adf320d-dd1b-d2ad-8f1a-e5b47f745948.gif)


(クリックで開かないので、コピペして開きます)
<data:text/html,%3C!--%20(original%20from%3A%20https%3A%2F%2Fshimz.me%2Fblog%2Fd3-js%2F4360)%20--%3E%0A%3Chtml%3E%0A%3Chead%3E%0A%20%20%3Ctitle%3EWorld%20Clock%3C%2Ftitle%3E%0A%20%20%3Cstyle%3E%0A%20%20%20%20html%2C%0A%20%20%20%20body%20%7B%0A%20%20%20%20%20%20margin%3A%200px%3B%0A%20%20%20%20%20%20padding%3A%200px%3B%0A%20%20%20%20%20%20width%3A%20100%25%3B%0A%20%20%20%20%20%20height%3A%20100%25%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20svg%20%7B%0A%20%20%20%20%20%20width%3A%20960px%3B%0A%20%20%20%20%20%20height%3A%20500px%3B%0A%20%20%20%20%7D%0A%20%20%3C%2Fstyle%3E%0A%20%20%3Cscript%20src%3D%22https%3A%2F%2Fcdnjs.cloudflare.com%2Fajax%2Flibs%2Fd3%2F3.5.17%2Fd3.min.js%22%3E%3C%2Fscript%3E%0A%20%20%3Cscript%20src%3D%22https%3A%2F%2Fcdnjs.cloudflare.com%2Fajax%2Flibs%2Fmoment.js%2F2.10.3%2Fmoment-with-locales.min.js%22%3E%3C%2Fscript%3E%0A%3C%2Fhead%3E%0A%0A%3Cbody%3E%0A%20%20%3Csvg%3E%3C%2Fsvg%3E%0A%3C%2Fbody%3E%0A%3Cscript%3E%0A%20%20const%20windowWidht%20%3D%20960%3B%0A%20%20const%20windowHeight%20%3D%20500%3B%0A%0A%20%20const%20svg%20%3D%20d3.select(%22svg%22)%3B%0A%0A%20%20const%20projection90%20%3D%20d3.geo.orthographic()%0A%20%20%20%20.scale(windowWidht%20%2F%204)%0A%20%20%20%20.rotate(%5B0%2C%200%2C%200%5D)%0A%20%20%20%20.translate(%5BwindowWidht%20%2F%202%2C%20windowHeight%20%2F%202%5D)%0A%20%20%20%20.clipAngle(90)%3B%0A%0A%20%20const%20projection180%20%3D%20d3.geo.orthographic()%0A%20%20%20%20.scale(windowWidht%20%2F%204)%0A%20%20%20%20.rotate(%5B0%2C%200%2C%200%5D)%0A%20%20%20%20.translate(%5BwindowWidht%20%2F%202%2C%20windowHeight%20%2F%202%5D)%0A%20%20%20%20.clipAngle(180)%3B%0A%0A%20%20d3.json(%22https%3A%2F%2Fgist.githubusercontent.com%2Fnwtgck%2F3d4efebf41e47fbdd30601ffb393c3a6%2Fraw%2F45ed39348f2b8cbaff60c6bae1d423286d8b77d4%2FLandmasses.geojson%22%2C%20geojson%20%3D%3E%20%7B%0A%0A%20%20%20%20const%20stage%20%3D%20svg.append(%22svg%3Ag%22)%3B%0A%0A%20%20%20%20stage.attr(%22transform%22%2C%20%60rotate(23.4%2C%20%24%7BwindowWidht%20%2F%202%7D%2C%20%20%24%7BwindowHeight%20%2F%202%7D)%60)%3B%0A%0A%20%20%20%20const%20backMap%20%3D%20stage.append(%22svg%3Apath%22)%0A%20%20%20%20%20%20.attr(%7B%0A%20%20%20%20%20%20%20%20%22fill-opacity%22%3A%201%2C%0A%20%20%20%20%20%20%20%20%22fill%22%3A%20%22%23EDE9F1%22%2C%0A%20%20%20%20%20%20%20%20%22stroke%22%3A%20%22none%22%2C%0A%20%20%20%20%20%20%7D)%3B%0A%0A%20%20%20%20const%20frontMap%20%3D%20stage.append(%22svg%3Apath%22)%0A%20%20%20%20%20%20.attr(%7B%0A%20%20%20%20%20%20%20%20%22fill-opacity%22%3A%201%2C%0A%20%20%20%20%20%20%20%20%22fill%22%3A%20%22%23FD81DB%22%2C%0A%20%20%20%20%20%20%20%20%22stroke%22%3A%20%22none%22%2C%0A%20%20%20%20%20%20%7D)%3B%0A%0A%20%20%20%20(()%20%3D%3E%20%7B%0A%20%20%20%20%20%20let%20i%20%3D%200%3B%0A%20%20%20%20%20%20(function%20update()%20%7B%0A%20%20%20%20%20%20%20%20i%20%3D%20i%20%2B%200.2%3B%0A%20%20%20%20%20%20%20%20projection90.rotate(%5Bi%2C%200%2C%200%5D)%3B%0A%20%20%20%20%20%20%20%20projection180.rotate(%5Bi%2C%200%2C%200%5D)%3B%0A%0A%20%20%20%20%20%20%20%20const%20frontPath%20%3D%20d3.geo.path().projection(projection90)%3B%0A%20%20%20%20%20%20%20%20const%20backPath%20%3D%20d3.geo.path().projection(projection180)%3B%0A%0A%20%20%20%20%20%20%20%20backMap.attr(%22d%22%2C%20backPath(geojson))%3B%0A%20%20%20%20%20%20%20%20frontMap.attr(%22d%22%2C%20frontPath(geojson))%3B%0A%0A%20%20%20%20%20%20%20%20setTimeout(update%2C%20100)%3B%0A%20%20%20%20%20%20%7D)()%3B%0A%20%20%20%20%7D)()%3B%0A%0A%20%20%20%20const%20marginLeft%20%3D%20windowWidht%20%2F%207%3B%0A%20%20%20%20const%20marginTop%20%3D%20windowHeight%20%2F%203%20%2B%20windowHeight%20%2F%2012%3B%0A%20%20%20%20const%20textY%20%3D%208%3B%0A%0A%20%20%20%20const%20clockGroup%20%3D%20svg.append(%22g%22)%0A%20%20%20%20%20%20.attr(%22transform%22%2C%20%60translate(%24%7B%5BmarginLeft%2C%20marginTop%5D%7D)%60)%3B%0A%0A%20%20%20%20const%20clockRect%20%3D%20clockGroup.append(%22rect%22)%0A%20%20%20%20%20%20.attr(%7B%0A%20%20%20%20%20%20%20%20%22width%22%3A%20%2270%25%22%2C%0A%20%20%20%20%20%20%20%20%22height%22%3A%20windowWidht%20%2F%2010%2C%0A%20%20%20%20%20%20%20%20%22fill%22%3A%20%22EDE9F1%22%2C%0A%20%20%20%20%20%20%20%20%22fill-opacity%22%3A%200.2%0A%20%20%20%20%20%20%7D)%3B%0A%0A%20%20%20%20const%20clockText%20%3D%20clockGroup.append(%22text%22)%0A%20%20%20%20%20%20.attr(%7B%0A%20%20%20%20%20%20%20%20%22x%22%3A%20%2210%22%2C%0A%20%20%20%20%20%20%20%20%22y%22%3A%20windowWidht%20%2F%2011%2C%0A%20%20%20%20%20%20%20%20%22font-size%22%3A%20110%2C%0A%20%20%20%20%20%20%20%20%22font-weight%22%3A%20%22bold%22%2C%0A%20%20%20%20%20%20%20%20%22font-family%22%3A%20%22arial%22%2C%0A%20%20%20%20%20%20%20%20%22line-height%22%3A%201.5%2C%0A%20%20%20%20%20%20%20%20%22letter-spacing%22%3A%205%2C%0A%20%20%20%20%20%20%20%20%22word-spacing%22%3A%205%0A%20%20%20%20%20%20%7D)%3B%0A%0A%20%20%20%20(function%20loop()%20%7B%0A%20%20%20%20%20%20clockText.text(moment().format(%27HH%3Amm%3Ass%3ASS%27))%3B%0A%20%20%20%20%20%20setTimeout(loop%2C%201)%3B%0A%20%20%20%20%7D)()%3B%0A%20%20%7D)%3B%0A%3C%2Fscript%3E%0A%3C%2Fbody%3E%0A%3C%2Fhtml%3>


SHIMIZUさんのブログから元になっています: https://shimz.me/blog/d3-js/4360
見た目がとてもいいので使わせもらいました。ありがとうございます！

## Nippと同じところ

* URLにコードを埋め込んでいる

## 違うところ

**Nippがやりたかったのはクライアントだけで完結するRuby環境**なので、一番の違いと目的はこの点だと思います。
それ以外の点として、

* Data URL: Webサーバ不要
* Nipp: 静的にファイルをホストするためにWebサーバーが必要
  - 補足：プログレッシブウェブアプリ(PWA)のキャッシュが効いたときに、Webサーバも経由しなくなる
* Data URL: コードの圧縮はしない[^data_uri_compression]
* Nipp: コードを圧縮する（deflate or LZMA）

[^data_uri_compression]: おそらく現在のところはないと思いますが、`;base64`部分に他の文字列を指定するような拡張は簡単にできそうな仕様なので、今後できるようになる可能性はあるかもしれません。


個人的な使わけとしては、NippはRubyを使いたいときにして、UIを完全に置き換える例で紹介した場合のものは、Data URLを使いたいかなと思ってます。

## おまけ: Data URLのエディタ

まず最初は、Data URL用のエディタはNippで簡単に作成しました。

Nipp: <https://nipp.cf/#Data_URL_Editor/es2017/S0hJLEm0KkmtKNHPKMnN0VGpTs1Lzk9JDQ3ydM7PLcjPS80r0SjW1CtKLchJTE7V0FfXT9dRUFc1MlfXrE0AAA==>

それで、このNippを使って、Nippを使わずにData URLだけで、使えるエディタを作成します。これで、**完璧にData URLのエディタ自体もData URL上で動きます**! Data URLがリアルタイム生成されて、リアルタイムにプレビューできます。

![Data URI Editor on Data URI](https://qiita-image-store.s3.amazonaws.com/0/84451/4435fd54-8fe0-56ed-a379-8676e771a1d4.gif)

<data:text/html,%3Chtml%3E%0A%20%20%3Chead%3E%0A%20%20%20%20%3Ctitle%3EData%20URI%20Editor%3C%2Ftitle%3E%0A%20%20%20%20%3Cstyle%3E%0A%20%20%20%20%20%20textarea%20%7B%0A%20%20%20%20%20%20%20%20width%3A%20100%25%3B%0A%20%20%20%20%20%20%20%20height%3A%2010em%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20iframe%20%7B%0A%20%20%20%20%20%20%20%20width%3A%20100%25%3B%0A%20%20%20%20%20%20%20%20height%3A%20100%25%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%3C%2Fstyle%3E%0A%20%20%3C%2Fhead%3E%0A%20%20%3Cbody%3E%0A%20%20%20%20%3Ctextarea%20id%3D%27editor%27%20placeholder%3D%22Input%20HTML%22%3E%3C%2Ftextarea%3E%0A%20%20%20%20%3Ctextarea%20id%3D%27data_uri%27%3E%3C%2Ftextarea%3E%0A%20%20%20%20%3Ciframe%20id%3D%22preview%22%3E%3C%2Fiframe%3E%0A%20%20%20%20%3Cscript%3E%0A%20%20%20%20%20%20var%20editor%20%20%3D%20document.getElementById(%27editor%27)%3B%0A%20%20%20%20%20%20var%20dataUri%20%3D%20document.getElementById(%27data_uri%27)%3B%0A%20%20%20%20%20%20var%20preview%20%3D%20document.getElementById(%27preview%27)%3B%0A%20%20%20%20%20%20function%20update()%7B%0A%20%20%20%20%20%20%20%20preview.src%20%3D%20dataUri.value%20%3D%20%22data%3Atext%2Fhtml%2C%22%20%2B%20encodeURIComponent(editor.value).replace(%2F%27%2Fg%2C%20%27%2527%27)%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20editor.onkeyup%20%3D%20update%3B%0A%20%20%20%20%20%20update()%3B%0A%20%20%20%20%3C%2Fscript%3E%0A%20%20%3C%2Fbody%3E%0A%3C%2Fhtml%3E>

このシンプルなエディタのコードは他のライブラリへの依存がないので、ポータブルです。

```html:コード
<html>
  <head>
    <title>Data URI Editor</title>
    <style>
      textarea {
        width: 100%;
        height: 10em;
      }

      iframe {
        width: 100%;
        height: 100%;
      }
    </style>
  </head>
  <body>
    <textarea id='editor' placeholder="Input HTML"></textarea>
    <textarea id='data_uri'></textarea>
    <iframe id="preview"></iframe>
    <script>
      var editor  = document.getElementById('editor');
      var dataUri = document.getElementById('data_uri');
      var preview = document.getElementById('preview');
      function update(){
        preview.src = dataUri.value = "data:text/html," + encodeURIComponent(editor.value).replace(/'/g, '%27');
      }
      editor.onkeyup = update;
      update();
    </script>
  </body>
</html>
```
（変更：iframeでプレビューできるようにしました）