👨‍💻

WebContainersを使わない独自アプローチで実現するブラウザ内のNode.js学習環境

2024/12/16に公開

はじめに

現代のフロントエンド技術の進化に伴い、ブラウザ内でサーバーサイドの処理を再現する試みが増えています。その中でもWebContainersCodeSandbox は、Node.js環境をブラウザ上で実現する代表的なソリューションです。しかし、それらの技術を利用せずに、ブラウザネイティブな形でNode.jsの機能を再現できないか?

この疑問を出発点に、独自の手法でこの課題に挑戦しました。

Progateの社内勉強会で発表した時を起点に僕の中でどうしてもこの環境を最後まで実現したいという思いが強くなり、実際に実装に取り組んでみました。

https://youtu.be/Gk_-DSLbRSE?si=CzdDZF6XWTR8Qstq

勉強会の発表時点ではまだコンセプトや基盤の実証が中心でしたが、その後、具体的な実装を進めて完成に近づいてきました。

この記事では、Progateで提供している学習プラットフォームの一部回収を目指し、
ブラウザ上でNode.jsに限りなく近い環境を構築する過程と技術的チャレンジについて詳述します。

Node.js学習環境の現状と課題

ProgateではNode.jsの学習環境を提供しており、現在の実装はDockerを活用してAWS上で動作しています。ユーザーがブラウザ上でコードを記述・実行すると、そのコードはサーバーサイドで評価され、結果が返却される仕組みです。このアプローチは安定して動作していますが、以下の課題があります。

  • 運用の負荷: サーバーインフラの管理、Node.jsバージョンの更新やセキュリティ対応が必要。
  • 柔軟性の欠如: サーバー依存のため、新しい環境への展開が容易ではない。
  • コスト面の問題: 接続数の増加への対応にコストがかかる。

なぜWebContainersを採用しなかったのか?

すでにブラウザーでNode.jsを動かすための技術として、WebContainersNodeboxといった技術が存在します。
WebContainersは確かに強力な技術ですが、以下の制約がプロジェクトにとって障壁となりました。

  • ブラウザ依存性: WebContainersは現在、Chrome系ブラウザでのみ動作します。ユーザーが多様なブラウザを使用する環境では、限定的な対応は課題となります。
  • 商用利用のコスト: エンタープライズ向けのライセンスが必要であり、長期的な運用においてコスト面での障壁があります。
  • カスタマイズ性の限界: WebContainersは高度に最適化されていますが、その分内部の仕組みに踏み込んでのカスタマイズが難しく、自社の特化したニーズに応える柔軟性が低いと判断しました。

デモページ

こちら実際に独自のアプローチで実現したNode.js学習基盤のデモページです。

以下のデモページでは、ブラウザ上でnpm installnpm start の流れを体験できます。

ejsexpresssqliteなどがブラウザーだけで動作することが確認できます!

特に注目いただきたいのは、右側のプレビューエリアです。エディター上でコードを変更すると、Express.jsのプレビューが即座に反映される仕組みです。実際にサーバーサイドの挙動を視覚的に確認でき、学習体験をより直感的なものにしています。

https://pub-6bc4a3e4095b41239156a48e59d19f54.r2.dev/index.html

実装のアプローチ

すべてのNode.jsの機能をブラウザで再現するような緻密なLow Level Emulationを行うことはリソース的にも難しいため、
Node.jsの大枠の機能を再現しつつ、概念的な機能の抽出を行う、High Level Emulationを目指しました。
Progateの学習環境に必要な機能に絞り込み、最低でもその学習に必要な機能だけは再現できるように、以下のアプローチを取りました。

  • requireの再現
  • npm installの再現
  • ファイルシステムの再現
  • Node.jsの標準モジュールの再現
  • シェルコマンドの再現

いくつかの技術的な課題に直面しましたが、この中でいくつかピックアップしてどのようなアプローチで解決したかを紹介します。

requireのしくみ

Node.jsのrequireメソッドを再現するため、Webpack内部で使用されているenhanced-resolveライブラリを改造しました。
依存解決後、esbuildでコードをバンドルし、ブラウザ上で評価します。

https://github.com/webpack/enhanced-resolve

https://esbuild.github.io/

このアプローチにより、ブラウザ上でのモジュール依存解決が可能となりました。

fsの仕組み

ファイルシステムの再現にはmemfsを採用しました。これはメモリ内で動作する仮想的なファイルシステムを提供し、Node.jsのfsモジュールを模倣します。

https://github.com/streamich/memfs

memfsというライブラリは、Node.jsのfsの仕組みを再現するためのライブラリです。
ブラウザーでそのまま動くのが特徴で、fsをかなり忠実に再現していて、実際に物理的にファイルを書き換えるのではなく、メモリ上でファイルを操作するファイルシステムを提供しています。

Node.jsの標準モジュール

以下のようなNode.js標準モジュールをブラウザで再現しました

  • pathstream: Polyfillを利用。
  • http: Service Workerを活用し、ブラウザのネットワークレイヤーで挙動を模倣。

特にhttp モジュールは、ネットワーク要求をハンドリングする複雑なロジックが必要でした。Service Workerを用いることでリクエストの再ルーティングを実現しました。

時には改造が必要

特定のモジュールやライブラリがブラウザ上でそのまま動作しない場合には、patch-packageというツールを活用して対応しました。このツールを用いることで、Node.jsモジュールに対する必要な修正を保持しつつ、npm install時に元の修正が消えない仕組みを構築しました。

https://www.npmjs.com/package/patch-package

シェルコマンドの再現

ブラウザ上に仮想シェル環境を構築し、cdlsrm といったコマンドを再現しました。これにより、Node.jsが内部で依存するOS的な操作の一部をエミュレート可能にしています。

課題と今後の展望

本環境の実現は一定の成果を挙げましたが、未解決の課題も残されています。

1. コアモジュールの再現

netcryptoperf_hooksといった一部のコアモジュールは、ブラウザでの再現が難しいため、未対応です。

2. パフォーマンス最適化

processごとに専用のWorkerを立ち上げるなどパフォーマンスの最適化をしたいなと考えています。

また、Node.jsのコードの実行をシミュレートするためにリアルタイムにブラウザでWebpackの内部で使われているenhanced-resolveを利用しています。

この技術だと、モジュールの解決が遅いためRspackというライブラリも使ってみたいと考えています。

RspackはRustで書かれており、enhanced-resolveと同等のモジュール解決機能を持ちつつ、高速性が期待されます。
これを利用することで、ブラウザ上でのモジュール依存解決速度を大幅に向上させられる可能性があります。

https://rspack.dev/guide/features/module-resolution

さいごに

このように、Progateではブラウザ技術を最大限に活用し、プログラミングの学習体験をよりよくするための取り組みを行っています。

もしこのような技術や挑戦に興味があり、「自分もこんな面白い技術に取り組みたい!」と思った方は、ぜひProgateで一緒に働いてみませんか。お待ちしています!

Progate Tech Blog

Discussion