• #draw.io
  • #vue
  • #nuxt
  • #middleware
  • #実装
  • #アーキテクチャ
未分類

Draw.io動的埋め込み:採用した実装方法

概要

Draw.ioファイル(.drawio)をVueコンポーネント内で動的に表示するために、Nuxtサーバーミドルウェア方式を採用しました。

この方法により、.drawioファイルを編集してページをリロードするだけで、変更が即座に反映される真の「動的埋め込み」を実現しました。

最終的な実装構成

1. サーバーミドルウェア(Nuxt)

ファイル: apps/web/server/middleware/content-images.ts

import { defineEventHandler, setResponseHeaders, sendStream } from 'h3'
import { createReadStream, existsSync, statSync } from 'node:fs'
import { join } from 'node:path'

export default defineEventHandler(async (event) => {
  const url = event.node.req.url || ''

  // クエリパラメータを除去してパス名のみを取得
  const pathname = url.split('?')[0]

  // 画像と.drawioファイルをマッチング
  const match = pathname.match(/^\/(\d{4}-\d{2}-\d{2})\/([^/]+\.(png|jpg|jpeg|gif|webp|svg|drawio))$/i)
  if (!match) return

  const [, dateDir, filename, ext] = match
  const contentPath = join(process.cwd(), 'content', dateDir, filename)

  if (!existsSync(contentPath)) return
  if (!statSync(contentPath).isFile()) return

  const mimeTypes: Record<string, string> = {
    png: 'image/png',
    jpg: 'image/jpeg',
    jpeg: 'image/jpeg',
    gif: 'image/gif',
    webp: 'image/webp',
    svg: 'image/svg+xml',
    drawio: 'application/xml'
  }

  // 開発環境ではキャッシュ無効化
  const isDev = process.env.NODE_ENV === 'development'
  const cacheControl = (ext === 'drawio' && isDev)
    ? 'no-cache, no-store, must-revalidate'  // 開発環境では即座に反映
    : 'public, max-age=31536000'              // 本番環境では長期キャッシュ

  setResponseHeaders(event, {
    'Content-Type': mimeTypes[ext.toLowerCase()] || 'application/octet-stream',
    'Cache-Control': cacheControl
  })

  return sendStream(event, createReadStream(contentPath))
})

役割:

  • /YYYY-MM-DD/filename.drawio のURLパターンにマッチ
  • content/ディレクトリから実際のファイルを読み込み
  • 適切なMIMEタイプ(application/xml)で配信
  • 開発環境ではキャッシュ無効化、本番環境では長期キャッシュ

2. Vueコンポーネント

ファイル: apps/web/app/pages/blog/drawio-viewer-test.vue

<template>
  <div
    class="mxgraph"
    style="max-width:100%;border:1px solid #ccc;"
    :data-mxgraph="diagramData"
  ></div>
</template>

<script setup lang="ts">
const diagramData = ref('')

async function loadDiagram() {
  // キャッシュバスティング付きでfetch
  const response = await fetch(`/2025-11-18/freee-ai-system.drawio?t=${Date.now()}`, {
    cache: 'no-store'
  })

  const xml = await response.text()

  diagramData.value = JSON.stringify({
    highlight: '#0000ff',
    nav: true,
    resize: true,
    toolbar: 'zoom layers tags lightbox',
    edit: '_blank',
    xml: xml
  })

  loadViewerScript()
}

function loadViewerScript() {
  if (document.querySelector('script[src*="viewer-static.min.js"]')) {
    if (window.GraphViewer) {
      window.GraphViewer.processElements()
    }
    return
  }

  const script = document.createElement('script')
  script.src = 'https://viewer.diagrams.net/js/viewer-static.min.js'
  script.async = true
  document.body.appendChild(script)
}

onMounted(() => {
  loadDiagram()
})
</script>

役割:

  • サーバーミドルウェア経由で.drawioファイルを取得
  • XMLをviewer-static.min.js用のJSONに変換
  • data-mxgraph属性に設定して描画

3. ビルド時のファイルコピー

ファイル: apps/web/nuxt.config.ts

hooks: {
  'nitro:build:public-assets': async (nitro) => {
    const contentDir = resolve(__dirname, 'content');
    const publicDir = resolve(nitro.options.output.publicDir);

    async function copyImages(dir: string, relativePath = '') {
      const entries = await readdir(dir, { withFileTypes: true });

      for (const entry of entries) {
        const sourcePath = join(dir, entry.name);
        const currentRelativePath = join(relativePath, entry.name);

        if (entry.isDirectory()) {
          await copyImages(sourcePath, currentRelativePath);
        } else if (/\.(png|jpg|jpeg|gif|webp|svg|drawio)$/i.test(entry.name)) {
          // .drawioも含める
          const targetPath = join(publicDir, currentRelativePath);
          const targetDir = join(targetPath, '..');

          if (!existsSync(targetDir)) {
            await mkdir(targetDir, { recursive: true });
          }

          await copyFile(sourcePath, targetPath);
          console.log(`Copied file: ${currentRelativePath}`);
        }
      }
    }

    await copyImages(contentDir);
    console.log('✓ Content images copied to public directory');
  }
}

役割:

  • 本番ビルド時にcontent/からpublic/へ.drawioファイルをコピー
  • Cloudflare Pagesなどの静的ホスティングで配信可能に

なぜこの方法を選んだか

検討した他の方法

1. iframe + Draw.ioビューアーURL

<iframe src="https://viewer.diagrams.net/?url=http://localhost:3000/file.drawio"></iframe>

却下理由:

  • ❌ CORSエラーが発生(Draw.ioビューアーはlocalhostのURLを読めない)
  • ❌ オンライン環境でしか動作しない

2. SVG/PNG自動変換

drawio --export --format svg input.drawio -o output.svg

却下理由:

  • ❌ ビルドプロセスが複雑化
  • ❌ インタラクティブ性がなくなる(ズーム、レイヤー切り替えなど)
  • ❌ 変換スクリプトの保守が必要

3. 静的ファイル配置(public/ディレクトリ)

public/
  diagrams/
    freee-ai-system.drawio

却下理由:

  • ❌ contentディレクトリと分離されて管理が煩雑
  • ❌ 日付ベースのディレクトリ構造が使えない
  • ❌ Gitでの履歴管理がしづらい

サーバーミドルウェア方式を選んだ理由

ファイル構造の統一性

  • 画像と同じcontent/YYYY-MM-DD/構造で管理できる
  • Markdownと図が同じディレクトリに配置できる

動的更新の実現

  • 開発環境でキャッシュを無効化できる
  • ファイル編集→リロードで即座に反映

本番環境でのパフォーマンス

  • 長期キャッシュでCDN配信を最適化
  • Cloudflare Pagesなどで高速配信

インタラクティブ性の維持

  • viewer-static.min.jsによりズーム、レイヤー表示などが使える
  • 編集ボタンでDraw.ioエディタを開ける

Gitでのバージョン管理

  • .drawioファイルはXMLテキストなので差分が見やすい
  • 変更履歴を追跡できる

データフロー

開発環境

┌─────────────────┐
│ Draw.ioエディタ │
│ file.drawio編集 │
└────────┬────────┘
         │ 保存
         ▼
┌─────────────────────────┐
│ content/2025-11-18/     │
│   freee-ai-system.drawio│
└────────┬────────────────┘
         │
         │ ブラウザがリロード
         ▼
┌──────────────────────────┐
│ ブラウザ                  │
│ fetch('/2025-11-18/      │
│   file.drawio?t=xxx')    │
│ cache: 'no-store'        │
└────────┬─────────────────┘
         │ HTTPリクエスト
         ▼
┌──────────────────────────┐
│ サーバーミドルウェア      │
│ content-images.ts        │
│ Cache-Control:           │
│   no-cache (dev)         │
└────────┬─────────────────┘
         │ XMLを返す
         ▼
┌──────────────────────────┐
│ viewer-static.min.js     │
│ 図を描画                  │
└──────────────────────────┘

本番環境(Cloudflare Pages)

┌─────────────────────┐
│ npm run build       │
│ Nitroフック実行     │
└────────┬────────────┘
         │ ファイルコピー
         ▼
┌─────────────────────┐
│ public/2025-11-18/  │
│   file.drawio       │
└────────┬────────────┘
         │
         │ wrangler pages deploy
         ▼
┌─────────────────────┐
│ Cloudflare Pages    │
│ CDN配信             │
│ Cache-Control:      │
│   max-age=31536000  │
└────────┬────────────┘
         │
         ▼
┌─────────────────────┐
│ ブラウザ            │
│ viewer-static表示   │
└─────────────────────┘

実装の利点

開発体験

  1. 編集→リロードのサイクルが高速
    • サーバー再起動不要
    • ビルド不要
    • SVG/PNG変換不要
  2. contentディレクトリに集約
    • Markdown、画像、図が同じ場所
    • 記事と図の関連が明確
  3. Gitでの管理が容易
    • XMLテキストなので差分が見やすい
    • コミット履歴で変更を追跡

本番環境

  1. 高速配信
    • CDNキャッシュ最適化
    • 静的アセットとして配信
  2. インタラクティブ
    • ズーム、レイヤー、編集ボタン
    • viewer-static.min.jsの全機能
  3. スケーラブル
    • 複数の図でも同じパターンで対応
    • ビルド時に自動コピー

注意点

開発環境

  • 環境変数: NODE_ENV=developmentが必要
  • キャッシュバスティング: ?t=${Date.now()}が必須

本番環境

  • ビルド前に確認: .drawioファイルがcontent/にあることを確認
  • デプロイ後の確認: public/にコピーされたかビルドログで確認

ブラウザ互換性

  • viewer-static.min.js: モダンブラウザのみ対応
  • IE11: 非対応

まとめ

サーバーミドルウェア方式により、以下を実現しました:

  • ✅ Draw.ioファイルを編集してリロードするだけで反映(開発環境)
  • ✅ contentディレクトリで一元管理
  • ✅ 本番環境で高速CDN配信
  • ✅ インタラクティブな図の表示
  • ✅ Gitでのバージョン管理

この方法は、開発体験と本番パフォーマンスの両立を実現する最適な選択であった。