Mach3.laBlog

SSIを手元の環境で再現してみる実験

この記事は賞味期限切れです。(更新から1年が経過しています)

SSI(サーバーサイドインクルード)を使用したページの制作時、 いちいち Apache を立ち上げて動作確認するのが面倒だと感じる事はないでしょうか。 今回は手元の環境で簡単な似非SSIを再現してみる実験の話です。

SSIを手元の環境で再現してみる実験

SSIについて

SSIそのものについては周知の事と思いますが、 HTMLのコメントノードを使ってサーバ側でコンテンツをインクルードする仕組みです。 例えば、HTML内に次のようなコメントを書いておきます。

<!--#include virtual="/common/header.html"-->

そうすると、サーバ側で自動的に /common/header.html をその部分に読み込んだHTMLを吐き出してくれます。

Apache等のSSIに対応したWebサーバで閲覧すれば良いだけの話ですが、 その為にApacheを用意するのもいささか面倒に感じる事もあるでしょう。

GruntでSSIしてみる

まずは Gruntgrunt-contrib-connect を使ってSSIを再現してみます。 middleware オプションを使ってサーバ側で置換してしまいます。

grunt.initConfig({
    connect: {
        dev: {
            options: {
                base: "html",
                port: 8080,
                keepalive: true,
                /* ↓ これを使います */
                middleware: function(){ ... }
            }
        }
    }
});

まず必要なモジュールを読み込んでおきます。

var fs = require("fs"),
    path = require("path"),
    url = require("url");

middleware の実装は以下のようにしました。

middleware: function(connect, options, middlewares){
    middlewares.unshift(function(req, res, next){
        var pattern, root, file, html;

        pattern = /<!--#include\s+?(virtual|file)="(.+?)".*?-->/g;
        root = options.base[0];
        file = path.join(root, url.parse(req.url).pathname).replace(/\/$/, "/index.html");

        if(/\.s?html$/.test(file) && fs.existsSync(file)){
            html = fs.readFileSync(file, "utf-8").replace(pattern, function(a, key, value){
                var inc = (key === "virtual") ? path.join(root, value)
                : path.join(file.replace(/[^\/]+$/, ""), value);
                return fs.existsSync(inc) ? fs.readFileSync(inc) : "";
            });
            return res.end(html);
        }
        return next();
    });
    return middlewares;
}
  1. 拡張子が .html あるいは .shtml の時だけ処理します
  2. SSIのパターンを見つけたら、該当ファイルを読み込んで差し替えます
  3. 該当ファイルがなければなにもせずに静的ファイルを吐き出します

試してませんが、Gulpでも同じ事が出来るでしょう。

なお、#include のみ処理し、#set#echo 等の他のコマンドは全スルーしております。 これらも使いたい場合は node-ssi 等を使ってmiddleware内でパースすると良いと思います。 (さすがに exec コマンドはサポートしてなさそうですが…)

SSIが見られるのは主に大きめのWebサイトのテンプレートですが、 経験上 #include 以外が使われているケースはほとんど見られません。

フロントでSSIしてみる

すでにSSIじゃなくなってますが、フロント側(JavaScript)でSSIを再現してみます。 むりやり名前をつけるならばFEI(フロントエンドインクルード)とかになるんでしょうか…。 とはいえ、XHRを使う都合上なんらかのサーバを通して確認する事になります。

先に申し上げておきますと、全くおすすめはできません。 特にJavaScriptも絡んだページ制作では、かなりの悪影響が予想されます。 当然本番で使用するのはもってのほかなので、ただの実験の記録として読み流してください。

パターン1: innerHTMLで強引に

ページ末尾でこのようなコードを実行してみました。

(function(){
    var body = document.getElementsByTagName("body").item(0);
    body.innerHTML.replace(
        /<!--#include.+?(?:file|virtual)="(.+?)".*?-->/g,
        function(key, value){
            var xhr = new XMLHttpRequest();
            xhr.open("GET", value);
            xhr.addEventListener("readystatechange", function(e){
                if(this.readyState === 4 && this.status < 400){
                    body.innerHTML = body.innerHTML.replace(key, this.responseText);
                }
            });
            xhr.send();
        }
    );
}());

innerHTML にはコメントも含まれているので、それらを強引に文字列置換してしまって差し替えています。 やさしさがかけらもない、ひどいコードです。 このコードの最も大きなデメリット(いや、デメリットしかありませんが)は、 要素に結びつけたJavaScriptが全く動かなくなる点でしょう。

document.getElementById("my-button")
.addEventListener("click", function(){
    console.log("click");
});

このような処理が書いてあったとしても、これは全く動きません。 innerHTML を書き換えると同時にDOMを破壊してしまい、上で参照している #my-button という要素とのつながりが断ち切られてしまう為です。

パターン2: DOMインターフェースを使って

ページの末尾で次のようなコードを実行します。

(function(){
    var each = function(list, callback){
        return Array.prototype.forEach.call(list, callback);
    };

    var parseSSI = function(nodes){
        each(nodes, function(node){
            var match, xhr;

            if(node.childNodes.length){ return parseSSI(node.childNodes); }
            if(node.nodeType !== 8){ return; }

            match = node.nodeValue.match(/#include.+?(?:virtual|file)="(.+?)"/);

            if(match){
                xhr = new XMLHttpRequest();
                xhr.open("GET", match[1]);
                xhr.addEventListener("readystatechange", function(){
                    if(this.readyState !== 4 || this.status >= 400){ return; }
                    var tmp = document.createElement("div");
                    tmp.innerHTML = this.responseText;
                    each(tmp.childNodes, function(child){
                        node.parentNode.insertBefore(child, node);
                    });
                });
                xhr.send();
            }
        });
    };

    parseSSI(document.getElementsByTagName("body"));
}());

body配下にぶら下がっている要素をすべて走査して、 コメントノード(nodeType === 8)だった場合にパターンと照合して処理します。

こちらのパターンはDOMを破壊しませんが、不完全なHTMLをモジュールとして読み込んだ場合に、それを補完してしまう欠点があります。 例えば次のような部品を読み込んだ場合。 (古き掲示板CGIのテンプレートを彷彿とさせる構成ですね)

<!-- header.html -->
<header>
    <h1>...</h1>
    <nav>...</nav>
</header>
<article>
<!-- /header.html -->

ここにコンテンツがはいります。

<!-- footer.html -->
</article>
<footer>...</footer>
<!-- /footer.html -->

不完全な要素が補完されてしまう為、想定と異なる構成になってしまい、当然見た目も崩れてしまうでしょう。

このような使い方をしなければ、SSIを模した形のモジュールライブラリとして使い様があるか…?とも考えましたが、 DOMを全走査しなければならない為、コスト的にあまり良策とは言い難いですね。 改善策はなくもなさそうですが、「そこまでやる必要がどこにあるのか」と考えてしまったのでそこで放棄いたしました。

まとめ

SSIの環境が必要になるケースは今後もまだなくなりはしないと思います。 そういった案件に限って1ページのみの制作という事もよくある話なので、 出来るだけ手元の環境でさくっと再現してしまいたいですね。

コメント

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です


*