Univer と Astro でブラウザ内 Excel 数式ジャッジを作る
ホーム / ブログ / Univer と Astro でブラウザ内 Excel 数式ジャッジを作る

Univer と Astro でブラウザ内 Excel 数式ジャッジを作る

· SheetDo · engineeringuniverastroexcel

Astro の静的生成、Univer の数式エンジン、複数ケースによるハードコード対策で、SheetDo がブラウザだけで Excel 数式を採点する仕組み。

多くのコーディングジャッジは、バックエンドのサンドボックスでコードを実行します。Excel の練習にはこのモデルが合いません。本物の表計算の感触、即時フィードバック、提出ごとの計算コストゼロが欲しいからです。

SheetDo では、数式の採点をすべてブラウザ内で行います。SEO 向けページには Astro SSG、グリッドと数式エンジンには Univer、複数ケースのチェックには小さな React アイランドを使います。

なぜ Astro + Univer か

スタックを決めた目標は三つです。

  1. SEO と速い初回描画 — 練習ページにはクロール可能な HTML と構造化データが必要。
  2. 本物の Excel 操作感 — 選択、数式バー、セル参照、Canvas グリッド。
  3. バックエンド計算ゼロ — 再計算はすべてクライアント側。

独自の数式パーサも書きませんでした。本物の Excel には範囲、絶対参照、動的配列、面倒な型変換が要ります。Univer の @univerjs/engine-formula がすでにそれを担っています。

Astro の静的シェル(SEO、コピー、JSON-LD、スケルトン)
        │  client:only="react"
        ▼
ExerciseRunner(React アイランド)
        │
        ├── SheetEditor(Univer グリッド + 数式エンジン)
        └── 複数ケース採点(データ注入 → 再計算 → 比較)

落とし穴 1:Univer と SSR は相性が悪い

Univer は window、document、Canvas を必要とします。Astro の SSR では落ちます。ランナーは client:only="react" でマウントし、アイランド起動中のレイアウトジャンプを防ぐため、同じ見た目の Astro スケルトンを slot="fallback" で出します。

<ExerciseRunner client:only="react" exercise={exercise} locale={locale}>
  <ExerciseRunnerSkeleton slot="fallback" exercise={exercise} locale={locale} />
</ExerciseRunner>

Vite 側の手当ても必要です。Univer のパッケージグラフは深く、古い optimizeDeps キャッシュが開発時の空白アイランドの原因になりました。astro.config.ts でインポート集合を固定し、@univerjs/* を ssr.noExternal にしています。

落とし穴 2:シートをロックしつつ、採点では書き換えたい

学習者にはフルのスプレッドシートが見えます。ロックがないと、元データを編集して入力をごまかし「合格」できてしまいます。

回答セル以外の編集は Univer のイベント(BeforeSheetEditStart、BeforeCommandExecute)でキャンセルし、採点用に短い プログラムからの退避ハッチ を開けます。

async function withEditableSheet(mutate) {
  allowProgrammaticMutateRef.current = true;
  try {
    return await mutate(sheet);
  } finally {
    allowProgrammaticMutateRef.current = false;
  }
}

ユーザーはロックされたセルを触れません。採点側はケースの書き換えと数式の適用ができます。

採点:複数ケースとハードコード対策

期待値が一つだけでは足りません。合計の演習で =1500 が通ってしまいます。Submit 時は次を行います。

  1. = で始まる数式であることを要求する。
  2. 必要なら関数キーワード(XLOOKUP、SUM など)を要求する。
  3. 各テストケースで:セルを注入 → ユーザー数式を適用 → 計算完了を待つ → 比較。
  4. 表示中のケースを復元し、ケースごとの合否を示す。
for (const tc of testCases) {
  await editor.updateCells({ ...baseCells, ...tc.cells });
  const value = await editor.applyFormula(target, formula);
  evaluations.push({ actualValue: value, expectedValue: tc.expectedValue });
}

微妙な点:Univer の計算は非同期です。setFormula のあと、セルを読む前に onCalculationResultApplied を待ちます。そうしないと古い値を採点してしまいます。

比較は浮動小数、通貨記号、カンマも許容します(valuesEqual、許容誤差 1e-9)。

演習バンクのヘッドレス CI

すべての JSON 演習に公式解があります。Vitest で UI なしの Univer(core + sheets + formula プラグインのみ)を起動し、各解を全ケースに通し、答えがずれたら CI を落とします。Playwright もブラウザも不要で、バンク全体が数秒で終わります。

得られたもの

バックエンドサンドボックスクライアント採点(Astro + Univer)
レイテンシ数百 ms 以上数十 ms
計算コスト提出数に比例ほぼ CDN のみ
オフライン弱い初回読み込み後は動く
SEOSPA の摩擦静的 HTML

要点: 重い表計算 UI は SSR から外す。文字列一致ではなく注入ケースで採点する。結果を読む前に必ず数式エンジンの完了を待つ。

公開先:sheetdo.com。

← ブログ