タスクの動的実行

通常Javascriptでは、読み込むJavascriptのファイルをHTML文書内のscriptタグに記述し静的に呼び出す。
しかし、複数のタスクを組み合わせて動作させるようなユースケースでは、HTMLの読み込み時に利用するタスクが確定していないことが多いため、この呼び出し方法だけでは都合が悪い。
このように、時間軸に対して動的にJavascriptのファイルを読み込みたいことがあるが、動的に読み込む方法としては次の4通りある。

  1. PHP等のサーバサイドスクリプトでscriptタグを動的に生成する。
  2. クライアントサイドJavascriptでdocument.write()を用いてscriptタグを動的に生成する。
  3. クライアントサイドJavascriptでscript elementをdocument.createElement()を用いて生成する。
  4. Ajaxを用いてJavascript fileを読み込み、その中身をeval()関数で実行する。

どれも長所と短所があり、使いわける必要がある。

まず、1番の方法は従来型Webシステムでよく使われる方法である。下記のようにRubyやPHP等のサーバサイド言語で記述される。

<?php foreach($mods as $k=>&$v){ ?>
<script type="text/javascript" src="<?php echo $k; ?>"></script>
<?php } >

この方法ではJavascript読み込みの際にページ遷移が必要となるが、複数のタスクが並列して動作するシステムにおいては、ページ遷移を伴なわない実装が望ましいため、この方法はページを初期化する際を除いて望ましくない方法である。また、これはサーバサイドでの処理となるため、クライアントサイドで独立して動作させたい場合には都合がよくない。

2番目、3番目は、クライアント側のjavascriptにて動的にscriptタグを生成して、javascriptファイルを読む込む方法である。両者の違いはwindow.onloadの呼び出し前か後で使い分ける点のみである。どちらも、ブラウザのネイティブコードにて構文解釈されるため実行速度は高速である。下記のような実装になる。

SysU={
    jspaths_:{},
    systemout:function(s){document.write(s);},
    executeJs:function(code,sync){
        if(sync){
            try{eval(code);return true;}
            catch(e){return false;}
        }else if(this.header){
            this.header.appendChild(document.createTextNode("\n"));
            var sc=document.createElement("script");
            sc.type="text/javascript";
            try{sc.innerHTML=code;}catch(e){sc.text=code;}
            this.header.appendChild(sc);
        }else{
            this.systemout('<script type="text\/javascript">'+code+'<\/script>');
        }
        return true;
    },
    importJs:function(jspath){
        if(this.jspaths_[jspath]){return false;}
        if(this.header){
            this.header.appendChild(document.createTextNode("\n"));
            this.header.appendChild(
                this.extend(document.createElement("script"),
                {type:"text/javascript",src:jspath}));
        }else{
            this.systemout('<script type="text\/javascript" src="'+jspath+'"><\/script>');
        }
        this.jspaths_[jspath]=true;
        return true;
    },...
};

これは便利な方法ではあるが、複数のJavascriptファイルを読み込む際にファイルの読み込む順番が保証されないし、また、Javascriptファイルの読み込みを完了したことを知る手段が無いという欠点がある。読み込み順序が保証されないため、ターゲットのファイル内では関数とクラスとコンポーネントの定義のみにとどめるべきなど実装上の制約がいくつか存在する。
また、Javascriptファイルの読み込みを完了したことを知るため、ターゲットのファイルの最終行に、読み込み完了イベントを通知する仕組みを用意する必要もある。

例えば、ターゲットファイルの末尾に以下のようなコードを埋め込むことである。

...
Sys.send({'type':'eof'});

なお、TeaOSではTask定義関数ys.registerExecutable()を実行した際に終了通知イベントが発行される。

Sys.registerExecutable({	...});

この2番目、3番目の方法の欠点は、同期保証するために、ターゲットファイル(読み込むファイル)に加工する必要がある点である。ライブラリなど手を加えることが望ましくないコードの場合には、この方法は使えない。

4番目の方法、Ajaxを用いてJavascript fileを読み込み、その中身をeval()関数で実行するという方法は、2、3番目の方法と異なり、同期保証も、読み込み終了検知もできるが、実行速度は非常に遅い。また、DOMベースのデバッガであるFirebug等のデバッガツールではファイルの存在を認識できないため、デバッグが非常にやりにくい。この問題はブラウザの進化によりいずれ解決するかもしれないが。

以上の考察から、現状、2、3番目の方法と4番目の方法を混在させた実装が望ましいと考える。

なお、TeaOSでは、読み込み順序に依存しないタスク定義ファイルを「タスク・ファイル」、「タスク・ファイル」よりも前に読み込み完了しなければならないファイルを「ダイナミック・ライブラリ」と定義し、「ダイナミック・ライブラリ」と「タスク・ファイル」の組み合わせの定義を管理することでコンポーネント管理を行っている。 
同一の「ダイナミック・ライブラリ」を複数の「タスク・ファイル」が参照することもあり、また、1つの「タスク・ファイル」が複数回実行されることもあり、各ファイル毎にステートマシンを実装して、多重読み込み回避や同期を実現している。

カテゴリー: html5_multitask   パーマリンク

コメントを残す