きっかけ
以下の記事で、RubyとかJavaScript(ES2017)をURLに埋め込んでポータビリティを実現する方法を紹介しました。
極小WebアプリをURLに埋め込んで超ポータブルなWebアプリを作りたい! - Qiita
そもそも、ポータビリティを完全に追求するならData URLがあります!今回はData URLでポータブルなWebページな実現とNipp1との違いを書きました。
Data URLの構造
data:[<mediatype>][;base64],<data>
MDNから引用しています。こちらにもっと詳しくまとまっています:データ URL | MDN
画像をData URLにするのはよく見かけます。画像以外に、<mediatype>
をtext/html
にすればHTMLが記述できるようになります。つまり、HTML内にCSSを書いたりJavaScriptを書いたりできるので、Webサーバもいらずに完璧にポータブルなWebページが作れます!
具体例
例があったほうが分かりやすいと思うので、具体例を少しあげます。
簡単な時計
簡易的に時計ページです

(クリックで開かないので、コピペして開きます)
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>
<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リスト

(クリックで開かないので、コピペして開きます)
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
サマー・ウォーズのワールドクロック

(クリックで開かないので、コピペして開きます)
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と同じところ
違うところ
Nippがやりたかったのはクライアントだけで完結するRuby環境なので、一番の違いと目的はこの点だと思います。
それ以外の点として、
- Data URL: Webサーバ不要
- Nipp: 静的にファイルをホストするためにWebサーバーが必要
- 補足:プログレッシブウェブアプリ(PWA)のキャッシュが効いたときに、Webサーバも経由しなくなる
- Data URL: コードの圧縮はしない2
- Nipp: コードを圧縮する(deflate or LZMA)
個人的な使わけとしては、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: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>
<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でプレビューできるようにしました)