SVGのWidget化のススメ。SVGにJavaScriptを埋め込むのがなぜ良いか?
前回に引き続き、SVG (Scalable Vector Graphics)の記事です。今回は、ズバリ。「SVGのWidget化」をテーマにします。
SVGファイル (.svg) にJavaScriptを埋め込めるのはご存知でしょうか?
SVGファイルを単なるスケーラブルな画像のフォーマットだと思っていたら、大間違い。HTML同様、<script>タグを使って、JavaScriptのプログラムを埋め込めるのです。ここでは、読者が、SVGファイルをテキストファイルであるということを理解している前提です。JpegやPNGのようなバイナリーフォーマットではないということです。
知らなかった方は、試しにSVGファイルをテキストエディターで開いてみましょう。HTML同様のタグの記号で定義された、マークアップ言語になっています。
といったところが前置きで、本題に進みましょう。
SVGのWidget化とは?
HTMLの中でSVGを表示するには、JpegやPNGのように<img>タグを使います。SVGファイルの<img>タグのsrc属性に指定すればよいです。ここまでは、普通の画像ファイルの扱いと同じ。
一方、SVGは<img>タグではなく、<object>タグを使って読み込むことができます。この時、ブラウザはSVGに含まれるJavaScriptを実行するようになります。
私が言っている「SVGのWidget化」とは、この仕組みを使って、パスなどの画像情報とJavaScriptのプログラムをひとまとまりにした状態で取り扱うやり方のことです。
具体例を示します。
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
...
<path id='path1' d='M 0 6.48843 C 0 2.90497 2.90497 0 6.48843 0 ...></path>
...
<script type="text/javascript">
// ここにプログラムが書かれる
let p1 = document.getElementById('path1')
...
</script>
</svg>
上のコードは、SVGファイルを概念的に表現してみたものです。「…」のところには色々な記述が入りますが、ここでは説明のため省略しています。
4行目は画像を構築するパスの1つのタグを表現しています。idで任意の名前を付けられるのはHTMLと同様です。8行目〜12行目が埋め込んだJavaScriptのコードです。SVGのDOMへのアクセスや、イベントのハンドリングなどのプログラムを記載すると、これだけで「インタラクティブな画像」ができます。
このように、SVGの画像要素と、JavaScriptのプログラムを1つのファイルに入れ込むことを、私は「SVGのWidget化」と呼んでいます。
SVGをWidget化するのがなぜ良いのか?
SVGのWidget化する利点は何でしょう?
SVG画像をJavaScriptで動かすにはいくつかのやり方があります。大別すると以下の3つのパターンです。例によって手描きです。
①のパターン(SVGとJSが別ファイル)
①のパータンは、<object>タグで外部のSVGファイルを読み込みますが、SVGを操作するJavaScriptのコードは、元のHTML(または、HTMLから読み込んだ外部JSファイルでも同様)。
この場合、SVGファイルのdocumentは読み込んだHTMLのdocumentとは異なるものとなります。ですので、SVGのDOM操作を実行するには、まず、HTMLのdocumentからSVGファイル内のdocumentを取り出す必要があります。
HTML内部の<object>タグのElementを見つけ、そのcontentDocumentプロパティにアクセスします。Element::contentDocumentがSVGのdocumentになります。
HTMLの内部のJavaScriptのコードはこんな感じになります。
<html>
...
<body>
...
<object id='object1' data='target.svg'>
...
<script>
document.addEventListener('DOMContentLoaded', () => {
let o = document.getElementById('object1')
let svgd = o.contentDocument
// svgdがSVGのドキュメント。
// ↓ここでSVG内部の要素のIDを指定してDOM操作
let p = svgd.getElementById('round_rect_1')
...
})
</script>
</body>
</html>
9行目でSVGを読み込んでいるobjectタグの要素を取り出します。10行目のcontentDocumentがSVGファイル内部のSVG DOMになります。SVGファイルの中では、操作したい要素にid=’round_rect_1’のようにIDを指定ておきます。
このパターンは、SVGのドキュメントを取り出すのに、上記のようなちょっとした手間がかかります。加えて、例えば、SVGの中のDOMの構成を変更した場合に、その影響が別ファイルであるHTMLの中のJavaScriptに及びます。したがって、プログラムのサイズが大きくなると、メンテナンスしにくくなるという問題があります。
②のパターン(HTMLにSVGを埋め込む)
2つめのパターンは、HTMLにSVGを埋め込んでしまう、というパターンです。SVGはHTMLの内部に以下のような形で記載することができます。
<html>
...
<body>
...
<div id='contents'>
...
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
...
<path id='round_rect_1' d='M 0 100 ...'>
</svg>
</div>
...
</body>
</html>
7行目から10行目がSVGのコンテンツです。この方法は、JavaScriptで扱う場合にも、通常のHTMLのDOM操作と同じように実行できるので便利です。
しかし、同じ画像を1つのページで複数の場所に表示したい場合はどうなるでしょう?
全く同じSVGのコードを複数ヶ所に記載する必要があります。SVGをちょっと変えたい、となっても複数ヶ所入れ替える必要が出てきてメンテナンス性が悪くなります。それだけなら、サーバー側のアプリで埋め込む、あるいは、WebpackのようなBundlerツールで埋め込む、などすれば「複数ヶ所」のメンテナンス性は課題にはならないでしょう。
ところが、今度はSVGドキュメントのIDが重複してしまうという問題が発生します。HTMLドキュメント内部で同じIDが複数ヶ所に使われると、getElementById()などのAPIで簡単にElementが取得できない、という課題を何らかの方法で解決する必要が出てくるわけです。
③のパターン(SVGのWidget化)
そこで出てくるのが、SVGファイルにJavaScriptのコードも埋め込んでしまう、という方法です。コードのイメージは、先の「SVGのWidget化とは?」で記載したとおりです。
この方法であれば、SVGの画像を修正した場合は、一緒に書かれているJavaScriptを修正することで間違いが減るでしょう。メンテナンスの問題は解消されます。また、SVGファイルそれぞれが独自のdocumentを持ちますので、1つのHTMLページに複数のSVGを読み込んだとしても、それぞれ別の名前空間になるでのID重複問題は発生しません。
こういうのを、オブジェクト指向プログラミングでは「カプセル化」と呼んでいます。つまり、1つのオブジェクトに処理をまとめ込んで外部からのアクセスを制限しようという考え方。オブジェクトと外の世界の分離をしっかりしましょう、ということです。
Widget化の実例
簡単な(しかも、ちょっと雑な)例を見てみましょう。
以下の四角はSVGをobjectタグで貼り付けたものです。ブルーの四角をクリックしてみると、箱が左上に行ったり、右下に行ったりすることが確認できますね。
これを読み込むのは以下のような1行のコードです。
<object type="image/svg+xml" data='/wp-content/uploads/svg-sample/sample.svg'></object>
JavaScriptのコードを読み込んだり、このページに埋め込んだりする必要はありません。SVGファイルの中のスクリプトのお陰でインタラクティブなコンテンツになっています。これがカプセル化の良いところです。単一のSVGファイルに必要なものを全てカプセル化したからこそ、こんな芸当ができるのです。
サンプルのSVGファイルをダウンロードするSVGの挙動を外から変える
ところで、Widget化したSVGファイルを1つのページの複数ヶ所に表示したい場合もあるでしょう。そして、表示する場所によって、ちょっと挙動を変えたい、という場合もあるかと思います。
そんな時は、アプリケーションの起動パラメータみたいな形で、SVG内部のJavaScriptコードに特別な値をセットしてみましょう。
SVGファイルを読み込むHTMLファイルの<object>タグで適当な属性を定義して使います。例えば、以下のようなコードです。
<object defaultid='1' type="image/svg+xml" data='/wp-content/uploads/svg-sample/sample.svg'></object>
上記のdefaultidという属性は特別に私が定義したパラメータの名前です。HTMLで特別の意図に使われるような属性の値でなければなんでもよいでしょう。
読み込まれるSVGファイルの内部のJavaScriptでは、以下のようにロードしたHTMLの<object>タグの属性にアクセスできます。
let defaultid = document.defaultView.frameElement.getAttribute('defaultid');
ここでいう、documentはSVGファイルのDOMです。その中の、defaultViewというのがHTMLがSVGを貼り付けているビューです。そのビューのframeElementがSVGファイルをロードした<object>タグのElementオブジェクトになります。先に定義した私のdefaultid属性にアクセスするには、<object>のElementのgetAttribute()メソッドを使えばよいのです。
このようにHTMLからパラメータを指定すると、それによってSVGファイル内部のJavaScriptで挙動を変えられます。
実際の動作をみてみましょう。
以下の2つの画像は、先ほどと同じSVGファイルを貼り付けたものです。ただし、<object>タグに渡すパラメータが異なります。その結果、最初に表示されたときのブルーの四角の表示位置が違うのですね。先程同様、ブルーの四角をクリックするとちゃんとそれぞれ動作します。もちろん、この記事のように1つのページで何個ロードしても、それぞれが正しく動作します。
SVGにJavaScriptを埋め込む際の難点
上記でみてきたように、Widget化は良いことづくめに見えます。しかしながら、難点もあります。私がやってみて感じた難点は以下のようなものです。
Firefox(v93.0 64bit Linux) だと、開発ツールを使ってデバッグできない
ChromeやEdgeは開発ツールを使ってデバッグできますが、Firefoxは使えませんでした。表示や動作は問題ありません。
Visual Studio Codeのシンタックスハイライタがない?
拡張機能の探し方がわるかったのか?SVGのエディターでJavaScriptコードのシンタックスハイライトができませんでした。したがって、コーディングの間違いなどが見つけにくいです。
Webpackのloaderが存在しない?
複雑なJavaScriptのプログラムをSVGに埋め込もうとすると、WebpackのようなBunlderツールが欲しくなるところです。しかし、ネット上で色々探してみたものそのようなloaderプラグインが存在しません。どなたか見つけたら連絡いただけると助かります。
まとめ
SVGファイルにJavaScriptのコードを埋め込んだ「SVGのWidget化」をオススメしました。単体でインタラクティブな動作をする画像ファイルをつくるにはもってこいです。カプセル化の利点は有効ですが、難点もいくつかあります。
利点と難点を理解して便利に使いたい実装方法です。