EY-Office ブログ

BrowserPodを使いブラウザー上でReact+バックエンドを動かしてみた

ネット記事でBrowserPodを知りました、WebAssemblyを使いブラウザー上でNode.js等を動かせるツールです。BrowserPodのトップページを見るとUIフレームワークSvelteの開発環境が自分のブラウザー上で動き出します、開発環境はNode.js上で動作するViteです。
バックエンドや開発ツールとして使われるNode.jsがブラウザー上で動いています。凄いですね ❗

最初にReactの開発環境を動かそうとしたのですが、上手く行かずBrowserPodのTutorialを参考にExpressのバックエンド+Reactアプリをブラウザー上で動かしてみました。

BrowserPod Nano Banana Proが生成した画像を使っています

BrowserPodとは

BrowserPodに付いてAI(Claude Sonnet4.5)に要約してもらいました。

BrowserPodは、WebAssemblyベースのブラウザ内コンテナ技術で、完全なフルスタック開発環境をブラウザで実行できる革新的な技術です。Leaning Technologiesが開発しています。

主な特徴

ブラウザ内での開発環境 Node.jsプロジェクトをブラウザ内で、変更なしに実行でき、将来的にはPython、Rubyなどもサポート予定です。ローカルアプリのインストールやサーバー側のリソースプロビジョニングが不要です。

技術的な特徴

  • 複数のプログラミング言語に対応できる言語非依存設計
  • 複数プロセスの実行、永続的なブロックファイルシステム、パブリックHTTPエンドポイントを持つ
  • WebAssemblyとJavaScriptの組み合わせにコンパイルされたNode.jsを使用し、高性能な実行を実現

ユースケース

  • ブラウザベースのIDE
  • インタラクティブなドキュメントサイト
  • 教育環境
  • AIコーディングエージェントを安全なサンドボックス環境で実行

リリース予定と現状

BrowserPod Betaは2025年12月に一般提供される予定です。初期リリースはNode.js 22に焦点を当て、その後2026年にPythonとRubyのサポートを追加する計画です。

課題点

現在、Chrome、Edge、Braveなどのクロミウムベースのブラウザでのみ動作し、FirefoxとSafariには対応していないという制約があります。また、開発者コミュニティからはオープンソース化を求める声が多く上がっており、プロプライエタリライセンスモデルへの懸念も存在します。

BrowserPodは非商用利用では無料で使用できますが、商用利用には有料ライセンスが必要です。

また日本語の記事としてはWebブラウザ上にLinux/Node.sベースのWebアプリ開発環境をWebAssemblyで実装した「BrowserPod」発表。ブラウザ内サーバに別タブからアクセス可能がわかりやすいでしょうか。

チュートリアルをやってみる

Project作成

Quickstartのページある方法でProjectを作成します。

$ npm create browserpod-quickstart

> npx
> "create-browserpod-quickstart"

🚀 Create Browserpod Quickstart

✔ Select a template: › Web Server - A simple project running a web server
✔ Project name: … sample
✔ Browserpod API key (optional): … ********************************************************************

📁 Creating project in /Users/yy/tmp/browser-pod/sample...
✅ Project created successfully!

$ cd sample
$ npm install
$ npm run dev

ブラウザーでhttp://localhost:5173/をアクセスすると以下のような画面が表示されます。

コードの解説

作成されたプロジェクトは以下のような構造になっています、Viteを使ったWebアプリです。

.
├── .env
├── index.html
├── package-lock.json
├── package.json
├── public
│   └── project
│       ├── main.js
│       └── package.json
├── src
│   ├── main.js
│   └── utils.js
└── vite.config.js
index.html
  • ① 上の画面画像でHello World!と表示されている部分で、バックエンドをアクセスした結果が<iframe>内に表示されます
  • ② 上の画面画像で黒く表示されているコンソール部分でBrowserPodで動作しているコマンドが表示されています
  • ③ 次に説明するJavaScriptのコードが実行されます
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Hello BrowserPod</title>
  </head>
  <body>
    <div id="url">Waiting for portal...</div>
    <iframe id="portal"></iframe>                       <!--- ← ① -->
    <div class="console" id="console"></div>            <!--- ← ② -->
    <script type="module" src="/src/main.js"></script>  <!--- ← ③ -->
  </body>
</html>
/src/main.js

このJavaScriptはBrowserPodへのアプリをデプロイするコードです。

  • ① BrowserPodの初期化、BrowserPodを使うためのAPIキーを設定しています
  • ② コンソールの設置
  • <iframe>をクライアントに設定
  • ④ BrowserPod実行環境にpublic/projectにあるバックエンド環境をコピーしています
  • npm installの実行、BrowserPod実行環境にexpressをインストールしています
  • node main.jsでバックエンド・コード(public/project/main.js)をBrowserPod実行環境で実行

さらに詳しい説明は、コードは少し違いますがTutorialを見てください。

import { BrowserPod } from '@leaningtech/browserpod'
import { copyFile } from './utils'

// Initialize the Pod.                                              // ↓ ①
const pod = await BrowserPod.boot({apiKey:import.meta.env.VITE_BP_APIKEY});

// Create a Terminal
const terminalElem = document.getElementById("console");            // ← ②
document.body.appendChild(terminalElem);
const terminal = await pod.createDefaultTerminal(terminalElem);

// Hook the portal to preview the web page in an iframe
const portalIframe = document.getElementById("portal");             // ← ③
const urlDiv = document.getElementById("url");
pod.onPortal(({ url, port }) => {
  urlDiv.innerHTML = `Portal available at <a href="${url}">${url}</a> for local server listening on port ${port}`;
  portalIframe.src = url;
});

// Copy our project files
await pod.createDirectory("/project");                              // ← ④
await copyFile(pod, "project/main.js");
await copyFile(pod, "project/package.json");

// Install dependencies                                             // ↓ ⑤
await pod.run("npm", ["install"], {echo:true, terminal:terminal, cwd: "/project"});
// Run the web server                                               // ↓ ⑥
await pod.run("node", ["main.js"], {echo:true, terminal:terminal, cwd: "/project"});
public/project/main.js

Expressを使った簡単なバックエンドです、GET /Hello World!を戻します。

const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

いつものジャンケンアプリ

いつものジャンケンアプリを作ってみました、対戦結果はバックエンドのExpressで管理します。上で説明したコードを改造して作りました。

プロジェクトの環境は以下のようになっています。

  • ① Reactのコードはbuildされたものを使いました
  • ② main.jsが2つあるのはわかりにくいのでserver.jsとdeploy.jsにリネームしました。
├── .env
├── .gitignore
├── index.html
├── package-lock.json
├── package.json
├── public
│   └── project
│       ├── app
│       │   ├── assets                   ← ①
│       │   │   ├── index-CUt-QHxW.css
│       │   │   └── index-D0PgqC8W.js
│       │   └── index.html
│       ├── package.json
│       └── server.js                    ← ②
├── src
│   ├── deploy.js                        ← ②
│   └── utils.js
└── vite.config.js
src/deploy.js

projectのコピーや最後のサーバー起動を変更しました。

・・・  前半は同じ  ・・・

// Copy our project files
await pod.createDirectory("/project");
await pod.createDirectory("/project/app");
await pod.createDirectory("/project/app/assets");
await copyFile(pod, "project/server.js");
await copyFile(pod, "project/package.json");
await copyFile(pod, "project/app/index.html");
await copyFile(pod, "project/app/assets/index-D0PgqC8W.js");
await copyFile(pod, "project/app/assets/index-CUt-QHxW.css");

// Install dependencies
await pod.run("npm", ["install"], {echo:true, terminal:terminal, cwd: "/project"});
// Run the web server
await pod.run("node", ["server.js"], {echo:true, terminal:terminal, cwd: "/project"});
public/project/server.js

バックエンドのExpressサーバーです。

  • ① 対戦結果を格納する変数
  • GET /パス名app/パス名のファイルを戻します
  • GET /api/scoresで対戦結果のJSONを戻します
  • POST /api/scoreで対戦結果を受け取りscores変数の先頭に追加します
const express = require('express')
const path = require('path')

const app = express()
const port = 3000
let scores = []                         // ← ①
                                        // ↓ ②
app.use('/', express.static(path.join(__dirname, 'app')));

app.get('/api/scores', (req, res) => {  // ← ③
  res.json(scores);
});
                                        // ↓ ④
app.post('/api/score', express.json(), (req, res) => {
  console.log('--  POST', req.body);
  scores.unshift(req.body);
  res.status(201).send();
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
});
public/app/index.html

Reactアプリ起動用のHTMLです

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>vite_react1</title>
    <script type="module" crossorigin src="/assets/index-D0PgqC8W.js"></script>
    <link rel="stylesheet" crossorigin href="/assets/index-CUt-QHxW.css">
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
ジャンケンアプリのコード

ジャンケンアプリのメインのコンポーネントです

  • ① 今回は開発はMac上で行い、buildしたものをBrowserPod(ブラウザー)で実行します
  • ② 対戦結果のState
  • ③ アプリ表示時にuseEffectで、バックエンドから対戦結果をfetchでGETします
  • ④ ジャンケンボタンを押したさいには、ジャンケン結果をバックエンドにfetchでPOSTします
    • ここで、表示用Stateも更新しています
  • JyankenBox, ScoreListコンポーネントはこのブログに良く出てくるコードなので省略します
import { useEffect, useState } from 'react'
import { judge, randomHand, type Score, type Te, type ServerScore } from './libs/jyanken';
import JyankenBox from './components/JyankenBox';
import ScoreList from './components/ScoreList';
                                                               // ↓ ①
const SERVER_URL = import.meta.env.DEV ? 'http://localhost:3000/api' : '/api';
const serverDataToScore = (serverScore: ServerScore) => (
  {...serverScore, matchDate: new Date(serverScore.matchDate)} as Score
);

export default function App() {
  const [scores, setScores] = useState<Score[]>([]);           // ← ②

  useEffect(() => {                                            // ← ③
    (async() => {
      const res = await fetch(`${SERVER_URL}/scores`);
      const data: ServerScore[] = await res.json();
      setScores(data.map((data) => serverDataToScore(data)));
    })();
  }, []);

  const pon = async (humanHand: Te) => {                       // ← ④
    const computerHand = randomHand();
    const score: Score = {
      human: humanHand,
      computer: computerHand,
      judgment: judge(humanHand, computerHand),
      matchDate: new Date()
    };

    await fetch(`${SERVER_URL}/score`, {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify(score),
    });
    setScores([score, ...scores]);
  };

  return (
    <div className="mx-2 md:mx-8 md:w-1/2">
      <h1 className="my-6 text-center text-2xl font-bold">
        じゃんけん ポン!
      </h1>
      <JyankenBox pon={pon} />
      <ScoreList scores={scores} />
    </div>
  );
}

まとめ

BrowserPodを使ってフロントエンドだけでなく、バックエンドもブラウザー上で動かす事ができました。👏

「で、何が嬉しいの?」と聞かれると少し困りますが、これはブラウザーの上だけで動いているのだと考えると凄い世界になったものだと感嘆します。

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ