エンジニアの mizchi です。本記事は plaid advent calendar の 8日目になります。
フロントエンドに携わる人なら、フロントエンド開発にまつわるものは、ブラウザの中で開発が完結するはずだ、と考えたことがないでしょうか。僕は個人的にブラウザ上で開発が完結すべきであって、技術的にも不可能ではない、と思っています。これは開発環境と実行環境が同じであるべきという アラン・ケイ の Smalltalk と同じ発想です。
筆者はブラウザ上で Git/GitHub の操作ができる https://nedi.app/ というエディタを作成したり、そこから markdown のプレビューだけを高速化したり https://mdbuf.netlify.com/ というMarkdown プレビューツールを作ったり、最近は https://relaxed-franklin-8384b4.netlify.com/ (名前はまだない)という開発ツールを趣味半分仕事半分で作っています。PWA、オフラインキャッシュ、 WebWorker、node ツールチェインが発達した今なら、これが可能なのではないか? ということで、その環境を試作した話です。
ゴール: フロントエンドでフロントエンドをビルドする
ブラウザ上でフロントエンドが開発できると、何が嬉しいでしょうか。まず、ブラウザさえあればよいので、開発構築が容易になります。特にプログラミング教育では、いかに最初の一歩を踏ませるかが大事で、その上で視覚的なインパクトが伴うフロントエンドや、ゲームエンジンは特に入門向きだと思っています。(HTML/JS/CSS が入門向けの環境かどうかは、Web 開発以外の観点で議論の余地があると思いますが…)
また、開発者向けの実用的な側面でも、現代の GUI の開発は、ビルドに伴うファイル転送とプレビューなどの必要で発生している各種 IO への負荷が大きい問題を解決できる可能性があります。今までは書き出すしかなかったメモリ上の中間状態を引っこ抜いたり、プロセス的な距離が近くなるので開発ツールの効率を考え直したり、そのための差分検知のあり方が大きく変わるはずです。
ツールとプレビューがセットになった環境があると、動かせるコードのシェアが容易になります。ReactNative の開発者ならば、 https://snack.expo.io/ で簡単なコードのシェアをしたことがあるのではないでしょうか。
というわけで、フロントエンドでフロントエンドをビルドする、またはその開発ツールについて考えると、次のテーマが浮かび上がってきます。
- 永続層: ファイルシステム
- ツールチェインの解決
- ネイティブプラグイン
- エディタ
永続層: ファイルシステム
現実には永続化とファイルストレージの問題があります。手軽な永続層として使われる localStorage は単なる KVS、しかも同期ブロッキングな API なので、プログラミングで大量のデータを扱うには不向きで、 IndexedDb は非同期で高速ですが、 ホスト OS の空き容量サイズが一定値を下回ると消える可能性がある という挙動で実装されてあることが多く、現実に発動することは稀ですが、大容量を扱う可能性があるものとしては、完璧に信頼できるストレージではありません。
仮想ファイルシステムは、一旦は揮発する開発環境として扱う、という従来のブラウザツールによくあるポリシーを採用するのは不可能ではないのですが、 2019 年現在、 Native File System という API が提案され、Chrome で開発者フラグを建てると使うことができます。これに活路が見いだせるかも?と期待しています。
これは、限定的なホスト環境へのファイル読み書き機能です。この機能を使って、一旦はブラウザストレージに書き込んだものを、単一ファイルへの内部状態の書き出しを実装すれば、非効率とはいえ、ホスト環境のディスクへの永続化が可能です。まだ試していないですが、簡易なファイルソケットとして実装すれば、双方向の RPC も実装できるかもしれません。(となるとブラウザ単独で完結するという旨味もなくなるんですけどね…)
Native FileSystem 現在は単体ファイルの Read or Write, またはディレクトリの Read のパーミッションしかないのですが、 単体ファイルへの Read & Write や、ディレクトリへの Read & Write パーミッションがあると、使い勝手の良いブラウザ開発環境のストレージが実装できる可能性があります。
とはいえ、歴史的にはブラウザからのファイルシステムへのアクセスは、 ActiveX 経由の悪意あるディスクトラバースとして悪用された過去があるので、セキュリティ的に危険な機能です。慎重に議論されて実装される必要があるとは思っています。
参考:
- ブラウザーのストレージ制限と削除基準 - Web API | MDN https://developer.mozilla.org/ja/docs/Web/API/IndexedDB_API/Browser_storage_limits_and_eviction_criteria
- モダンブラウザのストレージ容量と調査方法まとめ - HTML5 Rocks
- Chrome(Canary) の Native File System API で ローカルファイルの読み書きをする - mizchi's blog
プリプロセス | バンドリングの課題
永続ストレージ問題は(問題はあるものの)解決されたとして、次に立ちふさがるのは現代のフロントエンドツールチェインです。
node.js 向けのツールチェインは、古くから isomorhic や universal javascript という概念があるのですが、多くはブラウザ向けに動くように調整された shim と一緒にビルドすることが可能です。
Universal / Isomorphic JavaScript について - Qiita
しかし、ここに標準ライブラリの net や fs といったモジュールを使うと話がややこしくなります。素朴な http 通信に限ったとしても、ブラウザはアクセス先に CORS の制約を受けます。このため、特にプロキシなどを用意しない場合、 CDN ぐらいしかアクセスできません。こうなると npm から tar.gz を落とす、といった、npm/yarn のツールチェインの入り口が閉じられてしまいます。
fs もブラウザ環境での永続層を想定しない結果として、 fs のよく使われるポリフィルは存在しません。一応、 https://github.com/jvilk/BrowserFS という IndexedDb をバックエンドにできるツールがあるのですが、あまり現代的ではない古い設計なのと、素朴な read & write を行うだけで、シーケンシャルなファイルアクセスができず、とても遅いです。
この結果、 React や Vue のような、フレームワークのランタイムは比較的に簡単に扱うことが可能なのですが、ブラウザでパースできない JSX や Vue SFC を扱うためのツールチェインをブラウザ側にもってくるのが、ひと手間かかります。具体的には、ユニバーサル性を謳うどのツールも、ツールチェインの依存のどこかで fs を無意識に依存のどこかで含んでいて、それらをモック化しつつビルドするのは、一手間かかります。
ライブラリのコアがブラウザでも動くことを謳いながら、ツールチェインのどこかで Isomorphism が失われるのは他の環境でもよく発生します。例えば eslint のプラグイン や textlint のプラグインは、外部ファイル参照の解決に fs を使いに行くものが多いので、ここをなんらかのインメモリキャッシュに差し替えたりしなければなりません。逆に言うと、それさえすれば動くものも多いです。
解決策: CDN
Proxy を設置しない場合、 CDN でフロントエンドのツールを引っ張ってくる必要があるのですが、幸いなことに便利な CDN がいくつかあります。
- https://www.jsdelivr.com/ npm のファイルへの CDN
- https://unpkg.com/ npm へのファイルへの commonjs でビルドしたものを返してくれる CDN
- https://www.pika.dev/cdn npm へのファイルのうち、 package.module で ESM でビルドしたものを返してくれる CDN
内部パスやカスタムローダーで特殊なファイルを解決する際は jsdelivr, module が指定されていたら pika.dev, それ以外は unpkg などの使い分けすると効率よく引っ張ってこれそうです。 npm は unpublish が特別な場合を除いてできないので、一度引っ張ってきたらローカルキャッシュに入れてしまっても構わないはず。
ネイティブプラグインの問題: WebAssembly は使えるか?
フロントエンドでフロントエンドを開発しようとすると次に問題になるのはネイティブプラグインです。有名所で、 node-sass などが C で書かれた libsass でコンパイルされたプラグインですね。
現代のフロントエンドは、 その為の開発ツールは基本的に JS で書かれてはいるのですが、またそうではないものも wasm で提供されるようになりはじめています。例えば 正規表現エンジンの oniguruma は Atom などで使われ、Sublime や Atom や VSCode で連綿と引き継がれてきた使われる共通の TextMate 仕様のコードハイライト定義で使われていたのですが、これが NeekSandhu/onigasm: Oniguruma regex library on the web using WebAssembly という wasm 版 oniguruma が提供されるようになったことで、 VSCode Online のウェブ版のビルドが実現されています。
とはいえビルドサイズが膨らむことが多く、PWA のオフラインキャッシュと組み合わせてロード時間を短縮する必要があったりします。
wasm のパッケージを登録できる https://wapm.io/ を眺めていると、想像していたより幅広いツールが日々ビルドされています。(使い物になるかはともかく…)
最近登録されたものを見てると、 wasi でファイルシステムが触れるようになったのが大きいですね。jq の wasm ビルドがあったり、極端なものだと、 wasm でビルドされた python があったりします。
とはいえ wasi も結局ブラウザで使うにはモックかインメモリキャッシュに差し替える必要があるので、これらはだいぶ辛そう…
というわけで、作ってみた
決めたこと
- 超巨大ファイルストレージは扱わない(超巨大ファイルを扱うための git 組み込みもしない)
- FS も複雑なものを扱わない想定でフラットに展開する
- ブラウザ上で動く rollup コンパイラでインメモリに展開し、ライブラリは CDN から引く
- エディタは vscode で実績がある monaco-editor で、 TypeScript を編集してフロントエンドのコードをプレビューする、という単目的なツール
できたのがこれ
URL: https://relaxed-franklin-8384b4.netlify.com/
ソースコード https://github.com/mizchi/bundle-on-browser
注意: 読み込みが遅いしバギーです。リロードで動かなかったりしたら、 DevTool > Application > Clear Site Data でキャッシュを破棄してください。
ドラッグアンドドロップできる可変レイアウトは https://github.com/mizchi/react-unite という昔作った Unity 風のレイアウトシステムを引っ張り出しました。タブが他のペーンに移動できたりして楽しいと思います。
lodash, preact, react, vue, svelte で動作確認をしています。(workspace タブの preset を見てみてください)
CDN の一回目の読み込みは遅いですが、二回目以降はキャッシュされて早いです。これによって、ブラウザ上でバンドルしてるにも関わらず、十分高速なプレビューが実現できています。
NativeFileSysetem が有効になっているならば、 Cmd+S で現在のファイルシステムのダンプを示す json を保存でき、Cmd+O でそれを読み込むことができます。
ブラウザ上のコンパイラの実装について
https://github.com/mizchi/web-compiler では、 ブラウザ上で動く rollup に、以下の機能を組み込んで、それ自体がブラウザで動くようにバンドラをコンパイルしました(ややこしい)
- ESM のバンドル
- typescript の読み込み(tsx の解決)
- npm のバージョンの解決
- CDN からのファイル実体の解決
- svelte の読み込み(なんとなく)
- terser による minify
その結果、4MB あって読み込みが重くなってしまったのですが、ブラウザでもサーバーでも、npm install @mizchi/web-compiler
で次のコードが動くようになりました。
import { compile } from "@mizchi/web-compiler";
const pkg = {
dependencies: {
"lodash.flatten": "*"
}
};
const code = `export default 1`;
const out = await compile({
entry: "/index.js",
files: {
"/index.js": code
},
pkg,
tsConfig: `{
compilerOptions: {
target: "es5"
}
}`
});
console.log(out);
ファイルシステム上の絶対パスと、そのエントリーと、解決すべきパッケージの dependencies が書かれた package.json と、その tsconfig.json です。これらを仮想 FS として、足りないものは CDN から引いています。
rollup のプラグインの規約がとてもわかり易いので、ハックしやすかったです。 rollup(...).generate()
はブラウザ環境で動くことを知っていれば、あとは何でもできると思います。
エディタ
VSCode で使われてる monaco-editor を使っています。
https://github.com/microsoft/monaco-editor
VSCode は electron(node) への依存が極力切り離されているので、ブラウザで起動可能なビルドが作れます。(https://github.com/microsoft/vscode/blob/master/scripts/code-web.js を参照)
試してみたところ、永続層が実装されていないのでインメモリで揮発してしまうのですが、ここを実装すると任意のバックエンドの VSCode が作れそうでした。
ここまでわかったので、さらに VSCode をハックして monaco 上で TypeScript の設定を読んだり、仮想 FS 上で TS の補完を扱ったりできたり、といったものを実装したのですが、それについては別途 12/9 の ginza.js で話す予定です。
未来
現状、フロントエンド開発すべてを Web に移す、といったことはまだ無理ですが、用途を絞れば、たとえば用途特化のオーサリング環境や学習用の開発環境などとしては十分実用的なものが作れると思います。
PWA によってオフラインツールが作りやすくなった今、みなさんも思い思いのエディタを自作してみてはいかがでしょうか。以上、フロントエンドでフロントエンドをビルドする話でした。