未分類
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表示 │
└─────────────────────┘
実装の利点
開発体験
- 編集→リロードのサイクルが高速
- サーバー再起動不要
- ビルド不要
- SVG/PNG変換不要
- contentディレクトリに集約
- Markdown、画像、図が同じ場所
- 記事と図の関連が明確
- Gitでの管理が容易
- XMLテキストなので差分が見やすい
- コミット履歴で変更を追跡
本番環境
- 高速配信
- CDNキャッシュ最適化
- 静的アセットとして配信
- インタラクティブ
- ズーム、レイヤー、編集ボタン
- viewer-static.min.jsの全機能
- スケーラブル
- 複数の図でも同じパターンで対応
- ビルド時に自動コピー
注意点
開発環境
- 環境変数:
NODE_ENV=developmentが必要 - キャッシュバスティング:
?t=${Date.now()}が必須
本番環境
- ビルド前に確認:
.drawioファイルがcontent/にあることを確認 - デプロイ後の確認:
public/にコピーされたかビルドログで確認
ブラウザ互換性
- viewer-static.min.js: モダンブラウザのみ対応
- IE11: 非対応
まとめ
サーバーミドルウェア方式により、以下を実現しました:
- ✅ Draw.ioファイルを編集してリロードするだけで反映(開発環境)
- ✅ contentディレクトリで一元管理
- ✅ 本番環境で高速CDN配信
- ✅ インタラクティブな図の表示
- ✅ Gitでのバージョン管理
この方法は、開発体験と本番パフォーマンスの両立を実現する最適な選択であった。